#!/usr/bin/env python3 """Render Markdown documentation under docs/ into a static HTML bundle. The script converts every Markdown file into a standalone HTML document, mirroring the original folder structure under the output directory. A `manifest.json` file is also produced to list the generated documents and surface basic metadata (title, source path, output path). Usage: python scripts/render_docs.py --source docs --output build/docs-site Dependencies: pip install markdown pygments """ from __future__ import annotations import argparse import json import logging import os import shutil from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Iterable, List import markdown # Enable fenced code blocks, tables, and definition lists. These cover the # Markdown constructs heavily used across the documentation set. MD_EXTENSIONS = [ "fenced_code", "codehilite", "tables", "toc", "def_list", "admonition", ] HTML_TEMPLATE = """ {title}
{body}
""" @dataclass class DocEntry: source: Path output: Path title: str def to_manifest(self) -> dict[str, str]: return { "source": self.source.as_posix(), "output": self.output.as_posix(), "title": self.title, } def discover_markdown_files(source_root: Path) -> Iterable[Path]: for path in source_root.rglob("*.md"): if path.is_file(): yield path def read_title(markdown_text: str, fallback: str) -> str: for raw_line in markdown_text.splitlines(): line = raw_line.strip() if line.startswith("#"): return line.lstrip("#").strip() or fallback return fallback def convert_markdown(path: Path, source_root: Path, output_root: Path) -> DocEntry: relative = path.relative_to(source_root) output_path = output_root / relative.with_suffix(".html") output_path.parent.mkdir(parents=True, exist_ok=True) text = path.read_text(encoding="utf-8") html_body = markdown.markdown(text, extensions=MD_EXTENSIONS) title = read_title(text, fallback=relative.stem.replace("_", " ")) generated_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") output_path.write_text( HTML_TEMPLATE.format( title=title, body=html_body, generated_at=generated_at, source=relative.as_posix(), ), encoding="utf-8", ) return DocEntry(source=relative, output=output_path.relative_to(output_root), title=title) def copy_static_assets(source_root: Path, output_root: Path) -> None: for path in source_root.rglob("*"): if path.is_dir() or path.suffix.lower() == ".md": # Skip Markdown (already rendered separately). continue relative = path.relative_to(source_root) destination = output_root / relative destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(path.read_bytes()) logging.info("Copied asset %s", relative) def write_manifest(entries: Iterable[DocEntry], output_root: Path) -> None: manifest_path = output_root / "manifest.json" manifest = [entry.to_manifest() for entry in entries] manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8") logging.info("Wrote manifest with %d entries", len(manifest)) def write_index(entries: List[DocEntry], output_root: Path) -> None: index_path = output_root / "index.html" generated_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") items = "\n".join( f"
  • {entry.title}" f" · {entry.source.as_posix()}
  • " for entry in sorted(entries, key=lambda e: e.title.lower()) ) html = f""" Stella Ops Documentation Index

    Stella Ops Documentation

    Generated on {generated_at} UTC

    """ index_path.write_text(html, encoding="utf-8") logging.info("Wrote HTML index with %d entries", len(entries)) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Render documentation bundle") parser.add_argument("--source", default="docs", type=Path, help="Directory containing Markdown sources") parser.add_argument("--output", default=Path("build/docs-site"), type=Path, help="Directory for rendered output") parser.add_argument("--clean", action="store_true", help="Remove the output directory before rendering") return parser.parse_args() def main() -> int: logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") args = parse_args() source_root: Path = args.source.resolve() output_root: Path = args.output.resolve() if not source_root.exists(): logging.error("Source directory %s does not exist", source_root) return os.EX_NOINPUT if args.clean and output_root.exists(): logging.info("Cleaning existing output directory %s", output_root) shutil.rmtree(output_root) output_root.mkdir(parents=True, exist_ok=True) entries: List[DocEntry] = [] for md_file in discover_markdown_files(source_root): entry = convert_markdown(md_file, source_root, output_root) entries.append(entry) logging.info("Rendered %s -> %s", entry.source, entry.output) write_manifest(entries, output_root) write_index(entries, output_root) copy_static_assets(source_root, output_root) logging.info("Documentation bundle available at %s", output_root) return os.EX_OK if __name__ == "__main__": raise SystemExit(main())