from collections.abc import Iterator, Callable
from pathlib import Path
from dataclasses import dataclass
import click
import os
from urllib.parse import quote
from string import capwords
import shutil
import tempfile
import re
from markdown2 import markdown
LOG_REGEX = re.compile("(20[0-9]{2})-([0-9]{2})-([0-9]{2})-([A-z\\-]+)\\.md")
VIDEO_REGEX = re.compile("^!v\((.+?)\)$", flags=re.M)
AUDIO_REGEX = re.compile("^!a\((.+?)\)$")
FILES_URL = "https://files.0xf7.com/api/public/dl/NR1j-os8"
@dataclass
class Page:
title: str
url: str
entries: list[Path]
output: Path
@staticmethod
def create(source: Path, entries: list[Path], site_url: str, output_dir: Path):
title = capwords(source.stem.replace("-", " "))
url = f"{site_url}/{quote(source.stem)}"
output = output_dir / source.stem / "index.html"
return Page(title, url, entries, output)
@staticmethod
def order(entries: list[Path]) -> list[Path]:
log_entries = []
normal_entries = []
for e in entries:
if LOG_REGEX.fullmatch(e.name):
log_entries.append(e)
else:
normal_entries.append(e)
return sorted(normal_entries, key=lambda v: v.name) + sorted(log_entries, reverse=True, key=lambda v: v.name)
def renderEntries(page: Page) -> Iterator[str]:
""" Preprocess and feed the page entries to the markdown engine """
for entry in page.entries:
with open(entry, "r") as f:
raw = f.read()
def videoTag(match: re.Match) -> str:
return f""
def audioTag(match: re.Match) -> str:
return f""
with_videos = re.sub(VIDEO_REGEX, videoTag, raw)
with_audio = re.sub(AUDIO_REGEX, audioTag, with_videos)
yield markdown(with_audio, extras=["fenced-code-blocks", "strike", "tables"])
def renderPageCallback(template_file: Path, stylesheet_url: str) -> Callable[[Page, str], None]:
""" Callback to process the provided page metadata and output the final page to the filesystem """
from jinja2 import Environment, FileSystemLoader, select_autoescape
env = Environment(
loader=FileSystemLoader(template_file.parent),
autoescape=select_autoescape()
)
template = env.get_template(template_file.name)
def render(page: Page, site_title: str, navigation: list[dict[str, str]]) -> None:
print(f"Rendering {page.output}")
args = {"site_title": site_title, "stylesheet_url": stylesheet_url, "navigation": navigation, "entries": renderEntries(page)}
content = template.render(args)
os.makedirs(page.output.parent, exist_ok=True)
with open(page.output, "w") as f:
f.write(content)
return render
def collectPages(site_root: Path, site_url: str, output_dir: Path) -> Iterator[Page]:
""" Walk the site root collecting all the entries and organizing them into pages """
for f in os.listdir(site_root):
page_path = site_root / f
# Directories are rendered as one page with multiple entries
if os.path.isdir(page_path):
yield Page.create(page_path, Page.order(list(page_path.glob("**/*.md"))), site_url, output_dir)
# Single .md files are rendered as single-entry pages
elif page_path.suffix == ".md":
yield Page.create(page_path, [page_path], site_url, output_dir)
def generateSite(site_root: Path, base_url: str, output_dir: Path, render_page: Callable[[Page, str], None]) -> None:
""" Generate the site located at the specified root """
print(f"Generating site rooted at {site_root}")
site_url = f"{base_url}/{quote(site_root.stem)}"
site_dir = output_dir / site_root.stem
site_title = capwords(site_root.stem.replace("-", " "))
pages = sorted([p for p in collectPages(site_root, site_url, site_dir)], key=lambda v: v.title)
navigation = [{"text": p.title, "url": p.url} for p in pages]
for page in pages:
render_page(page, site_title, navigation)
home = Page("Home", site_url, [], site_dir / "index.html")
render_page(home, site_title, navigation)
def generateSites(config: dict[str, str]) -> None:
""" Generate all sites according to the specified config """
docs_root = Path(config["docs_root"])
output_dir = Path(config["output_dir"])
template_file = Path(config["template_file"])
css_file = Path(config["css_file"])
base_url = config["base_url"]
stylesheet_url = f"{base_url}/{css_file.name}"
render_page = renderPageCallback(template_file, stylesheet_url)
with tempfile.TemporaryDirectory() as tmpdir:
for f in os.listdir(docs_root):
site = docs_root / f
if os.path.isdir(site):
generateSite(site, base_url, Path(tmpdir), render_page)
shutil.copyfile(css_file, Path(tmpdir) / css_file.name)
if output_dir.exists():
shutil.rmtree(output_dir)
shutil.move(tmpdir, output_dir)
@click.command()
@click.argument("config_file", nargs=1, type=click.Path())
def main(config_file):
with open(config_file, "rb") as f:
import tomllib
config = tomllib.load(f)
generateSites(config)
if __name__ == '__main__':
main()