Files
git.stella-ops.org/scripts/render_docs.py
master f98cea3bcf Add Authority Advisory AI and API Lifecycle Configuration
- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings.
- Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations.
- Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration.
- Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options.
- Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations.
- Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client.
- Updated project file to include necessary package references for the new Issuer Directory Client library.
2025-11-02 13:50:25 +02:00

275 lines
8.3 KiB
Python

#!/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
import subprocess
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 = [
"markdown.extensions.fenced_code",
"markdown.extensions.codehilite",
"markdown.extensions.tables",
"markdown.extensions.toc",
"markdown.extensions.def_list",
"markdown.extensions.admonition",
]
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>{title}</title>
<style>
:root {{
color-scheme: light dark;
font-family: system-ui, -apple-system, Segoe UI, sans-serif;
line-height: 1.6;
}}
body {{
margin: 2.5rem auto;
padding: 0 1.5rem;
max-width: 70ch;
background: var(--background, #1118270d);
}}
pre {{
overflow: auto;
padding: 1rem;
background: #11182714;
border-radius: 0.5rem;
}}
code {{
font-family: SFMono-Regular, Consolas, 'Liberation Mono', monospace;
font-size: 0.95em;
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}}
th, td {{
border: 1px solid #4b5563;
padding: 0.5rem;
text-align: left;
}}
a {{
color: #2563eb;
}}
footer {{
margin-top: 3rem;
font-size: 0.85rem;
color: #6b7280;
}}
</style>
</head>
<body>
<main>
{body}
</main>
<footer>
<p>Generated on {generated_at} UTC · Source: {source}</p>
</footer>
</body>
</html>
"""
@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" <li><a href='{entry.output.as_posix()}'>{entry.title}</a>" f" · <code>{entry.source.as_posix()}</code></li>"
for entry in sorted(entries, key=lambda e: e.title.lower())
)
html = f"""<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>Stella Ops Documentation Index</title>
<style>
body {{
margin: 2.5rem auto;
padding: 0 1.5rem;
max-width: 70ch;
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
line-height: 1.6;
}}
h1 {{ font-size: 2.25rem; margin-bottom: 1rem; }}
ul {{ list-style: none; padding: 0; }}
li {{ margin-bottom: 0.75rem; }}
code {{ background: #11182714; padding: 0.2rem 0.35rem; border-radius: 0.35rem; }}
</style>
</head>
<body>
<h1>Stella Ops Documentation</h1>
<p>Generated on {generated_at} UTC</p>
<ul>
{items}
</ul>
</body>
</html>
"""
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 run_attestor_validation(repo_root: Path) -> None:
"""Execute the attestor schema + SDK validation prior to rendering docs."""
logging.info("Running attestor payload validation (npm run docs:attestor:validate)")
result = subprocess.run(
["npm", "run", "docs:attestor:validate"],
cwd=repo_root,
check=False,
)
if result.returncode != 0:
raise RuntimeError("Attestor payload validation failed; aborting docs render.")
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()
repo_root = Path(__file__).resolve().parents[1]
if not source_root.exists():
logging.error("Source directory %s does not exist", source_root)
return os.EX_NOINPUT
try:
run_attestor_validation(repo_root)
except RuntimeError as exc:
logging.error("%s", exc)
return os.EX_DATAERR
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())