from datetime import datetime import os import sys import shutil from collections import defaultdict import markdown import frontmatter from pathlib import Path from logger import logger from config import config class ContentItem: def render_content(self, categories, target_file = False): if target_file: self.target_file = Path(target_file) logger.debug(f"Rendering {self.source_file} to {self.target_file}") if hasattr(config, "footer"): footer_data = config.footer else: footer_data = '' try: if self.image_file and Path(self.image_file).exists(): self.image_file = Path(self.image_file) image_targetfile = Path(f"{config.output_dir}/{config.static_dir}/images/{self.source_file.stem}.jpg") logger.debug(f"Copying {self.image_file} to {image_targetfile}") shutil.copyfile(self.image_file, image_targetfile) self.image_file = f"{self.image_file.stem}.jpg" except Exception as e: logger.error(f"Couldn't render image file {self.image_file}: {e}") try: if self.audio_file and Path(self.audio_file).exists(): self.audio_file = Path(self.audio_file) audio_targetfile = Path(f"{config.output_dir}/{config.static_dir}/audio/{self.audio_file.name}") logger.debug(f"Copying {self.audio_file} to {audio_targetfile}") shutil.copyfile(self.audio_file, audio_targetfile) self.audio_file = f"{self.audio_file.stem}.mp3" except Exception as e: logger.error(f"Couldn't render audio file {self.audio_file}: {e}") try: if self.css_file and Path(self.css_file).exists(): self.css_file = Path(self.css_file) css_targetfile = Path(f"{config.output_dir}/{config.static_dir}/css/{self.css_file.name}") logger.debug(f"Copying {self.css_file} to {css_targetfile}") shutil.copyfile(self.css_file, css_targetfile) except Exception as e: logger.error(f"Couldn't render CSS file {self.css_file}: {e}") try: if self.js_file and self.js_file.exists(): self.js_file = Path(self.js_file) js_targetfile = Path(f"{config.output_dir}/{config.static_dir}/js/{self.js_file.name}") logger.debug(f"Copying {self.js_file} to {js_targetfile}") shutil.copyfile(self.js_file, js_targetfile) except Exception as e: logger.error(f"Couldn't render CSS file {self.js_file}: {e}") try: with self.target_file.open("w", encoding="utf-8") as f: f.write(config.content_item_template.render(content_item = self, page_title = f"{config.site_name}: {self.title}", parent_path = '../', categories = categories, footer_data = footer_data)) except Exception as e: logger.error(f"Couldn't render content file: {e}") def parse_content(self): logger.debug(f"Parsing file {self.source_file}") try: self.source_file = Path(self.source_file) self.parent_dir = self.source_file.parent # Most likely './content' self.slug = self.source_file.stem self.target_file = Path(f"{config.output_dir}/{self.source_file.parent}/{self.source_file.stem}.html") self.data = frontmatter.load(self.source_file) self.preview = self.data.content.replace('\n', '
')[:300] self.title = self.data.get("title", self.slug) self.omit_second_title = self.data.get("omit_second_title", False) self.date = self.data.get("date", "2000-01-01T00:00:00+03:00") self.categories = [c for c in self.data.get("categories", []) if c != 'default'] self.hidden = self.data.get("hidden", str(False)) self.data.content = self.data.content.replace('\n', ' \n') # For markdown newline rendering self.html = markdown.markdown(self.data.content) cover_image_path = f"{self.source_file.parent}/{self.source_file.stem}.jpg" self.image_file = cover_image_path if Path(cover_image_path).exists() else "" audio_filepath = f"{self.source_file.parent}/{self.source_file.stem}.mp3" self.audio_file = audio_filepath if Path(audio_filepath).exists() else "" css_filepath = f"{self.source_file.parent}/{self.source_file.stem}.css" self.css_file = css_filepath if Path(css_filepath).exists() else "" js_filepath = f"{self.source_file.parent}/{self.source_file.stem}.js" self.js_file = js_filepath if Path(js_filepath).exists() else "" except Exception as e: logger.error(f"Parser error, {e}") def create_content(self): with open(self.source_file, mode="w", encoding="utf-8") as f: f.writelines([ "---\n", f"title: {self.source_file.stem}\n", f"date: '{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}'\n", "description: ''\n", "author: ''\n", "categories: ['default']\n", "hidden: False\n", "omit_second_title: False\n", "---\n", "\n\n\n" ]) def __init__(self, file): self.source_file = file class Site: def __init__(self): self.output_dir = Path(config.output_dir) self.content_dir = Path(config.content_dir) self.static_dir = Path(config.static_dir) self.templates_dir = Path(config.templates_dir) self.images_dir = Path(f"{config.static_dir}/images") self.css_dir = Path(f"{config.static_dir}/css") self.js_dir = Path(f"{config.static_dir}/js") self.output_dir = Path(config.output_dir) self.content_items = [] self.categories = defaultdict(list) def init_site(self): # Exit if current directory not empty if os.path.isdir('.') and os.listdir('.'): logger.error("Current directory is not empty.") sys.exit(1) logger.info("Initializing new site") config.create_default_config() # Create directories for subdir in [self.content_dir]: os.makedirs(subdir, exist_ok=True) # Copy default theme shutil.copytree(f"{config.base_dir}/themes/default", "themes/default", dirs_exist_ok=True) def get_content_items(self): logger.debug("Getting content items") self.get_content_items = [] logger.debug(f"Scanning {Path(config.content_dir)}") for md_file in Path(config.content_dir).glob("*.md"): content_item = ContentItem(md_file) content_item.parse_content() self.content_items.append(content_item) self.content_items.sort(key=lambda i: (-datetime.fromisoformat(i.date).timestamp(), i.title)) def map_categories(self): for content_item in self.content_items: for category in content_item.categories: if not category == "default": self.categories[category].append(content_item.slug) def build(self): # Recreate the output dir if exists if self.output_dir.exists(): shutil.rmtree(self.output_dir) self.output_dir.mkdir() # Create public subdirs subdirs = ["categories", "content", "static"] for subdir in subdirs: subdir = self.output_dir / subdir subdir.mkdir(parents=True, exist_ok=True) # Copy theme's static dir to 'public/static shutil.copytree(f"{config.base_dir}/themes/{config.theme}/static", f"{self.output_dir}/static", dirs_exist_ok=True) # Get content items self.get_content_items() # Build categories map self.map_categories() # Render the content files for content_item in self.content_items: content_item.render_content(categories = self.categories) # Render the about file about_content = ContentItem(Path(f'themes/{config.theme}/content/about.md')) about_content.parse_content() about_content.render_content(categories = self.categories, target_file='public/static/about.html') # Render the index file if hasattr(config, "footer"): footer_data = config.footer else: footer_data = '' visible_content_items = [c for c in self.content_items if c.data.get("hidden") != True] with (self.output_dir / "index.html").open("w", encoding="utf-8") as f: f.write(config.index_template.render(page_title = config.site_name, content_items=visible_content_items, categories = self.categories, footer_data = footer_data)) # Render the categories indices if hasattr(config, "footer"): footer_data = config.footer else: footer_data = '' visible_content_items = [c for c in self.content_items if c.data.get("hidden") != True] for category in self.categories: category_index = Path(f"{self.output_dir}/categories/{category}.html") category_items = [i for i in visible_content_items if category in i.data.get("categories", "default")] with (category_index).open(mode="w", encoding="utf-8") as f: f.write(config.index_template.render(page_title=f"{config.site_name}: [ {category} ]", content_items=category_items, categories = self.categories, parent_path = '../', footer_data = footer_data)) logger.info(f"Created {len(self.content_items)} content items.")