Files
hydrogen/classes.py
2025-06-18 14:00:00 +03:00

206 lines
9.6 KiB
Python

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
#from jinja_env import env, content_item_template, index_template
from jinja2 import Environment, FileSystemLoader
# Get the script base_dir and check for templates dir
if Path(f'themes/{config.theme}/templates').exists():
base_dir = "."
elif getattr(sys, 'frozen', False):
# Running fron Pyinstaller-packed binary
base_dir = Path(sys._MEIPASS)
else:
# Running as a plain Python app
base_dir = Path(__file__).resolve().parent
# Prepare template rendering engine
if Path(f"themes/{config.theme}/templates").exists():
# Use the templates from the initialized site instance and the installed theme
jinja_env_dir = f"themes/{config.theme}/templates"
logger.debug(f"Using locally initialized templates from {jinja_env_dir}")
else:
# Use default templates from the default theme shipped with the app
# i.e. PyInstaller-packed single executable
logger.debug("Using shipped default temlpates.")
jinja_env_dir = f"{base_dir}/themes/default/templates"
env = Environment(loader=FileSystemLoader(f"{jinja_env_dir}"))
env.globals['header_image'] = f"{base_dir}/themes/{config.theme}/static/images/header.jpg"
index_template = env.get_template("index.html")
content_item_template = env.get_template("content_item.html")
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 self.image_file.exists():
image_targetfile = Path(f"{config.output_dir}/{config.static_dir}/images/{self.image_file.name}")
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"
if self.css_file and self.css_file.exists():
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)
if self.js_file and self.js_file.exists():
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)
with self.target_file.open("w", encoding="utf-8") as f:
f.write(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"Renderer: {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', '<br>')[: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 = Path(f"{self.source_file.parent}/{self.source_file.stem}.jpg")
self.image_file = cover_image_path if cover_image_path.exists() else ""
css_filepath = Path(f"{self.source_file.parent}/{self.source_file.stem}.css")
self.css_file = css_filepath if css_filepath.exists() else ""
js_filepath = Path(f"{self.source_file.parent}/{self.source_file.stem}.js")
self.js_file = js_filepath if js_filepath.exists() else ""
logger.debug(f"CCC {self.source_file.parts} : {self.source_file.parent} / {self.source_file.stem}.jpg")
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"{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"{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(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")]
with (category_index).open(mode="w", encoding="utf-8") as f:
f.write(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.")