From 83d175b7b069bc3bbc0d600c2fab4d082e04b521 Mon Sep 17 00:00:00 2001 From: Eddy Pedroni Date: Tue, 8 Apr 2025 17:01:55 +0200 Subject: Python implementation with better multi-entry page support --- godocs.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 godocs.py (limited to 'godocs.py') diff --git a/godocs.py b/godocs.py new file mode 100644 index 0000000..554fae9 --- /dev/null +++ b/godocs.py @@ -0,0 +1,122 @@ +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 + +DRY = False + +@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) + +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(), + extensions=["jinja_markdown.MarkdownExtension"] + ) + template = env.get_template(template_file.name) + + def entries(page: Page) -> Iterator[str]: + for entry in page.entries: + with open(entry, "r") as f: + yield f.read() + + 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": entries(page)} + content = template.render(args) + if DRY: + print(content) + else: + 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, 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 = [p for p in collectPages(site_root, site_url, site_dir)] + 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()) +@click.option("--dry/--no-dry", default=False, help="Instead of generating the sites, print every step to stdout") +def main(config_file, dry): + global DRY + DRY = dry + + with open(config_file, "rb") as f: + import tomllib + config = tomllib.load(f) + + generateSites(config) + +if __name__ == '__main__': + main() -- cgit v1.2.3