KPN/scripts/render_readme.py
2026-05-12 21:23:33 +02:00

122 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""
Render README.md from README.md.in by expanding @snippet directives.
Directive format (in README.md.in):
<!-- @snippet path/to/file.cpp snippet_name -->
Snippet tags (in C++ source files):
// [snippet: snippet_name]
...content...
// [/snippet: snippet_name]
In Python files use # instead of //.
"""
import re
import sys
import textwrap
from pathlib import Path
ROOT = Path(__file__).parent.parent
TEMPLATE = ROOT / "README.md.in"
OUTPUT = ROOT / "README.md"
DIRECTIVE_RE = re.compile(r'<!--\s*@snippet\s+(\S+)\s+(\S+)\s*-->')
LANG_MAP = {'.cpp': 'cpp', '.hpp': 'cpp', '.h': 'cpp', '.py': 'python', '.c': 'c'}
def comment_prefix(path: Path) -> str:
return '#' if path.suffix == '.py' else '//'
def extract_snippet(file_path: Path, name: str) -> str:
prefix = comment_prefix(file_path)
open_tag = f'{prefix} [snippet: {name}]'
close_tag = f'{prefix} [/snippet: {name}]'
text = file_path.read_text()
lines = text.splitlines()
in_snippet = False
found = False
result = []
for line in lines:
stripped = line.strip()
if stripped == open_tag:
if in_snippet:
raise ValueError(f"Nested open tag for '{name}' in {file_path}")
in_snippet = True
found = True
continue
if stripped == close_tag:
if not in_snippet:
raise ValueError(f"Unexpected close tag for '{name}' in {file_path}")
in_snippet = False
continue
if in_snippet:
result.append(line)
if not found:
raise ValueError(f"Snippet '{name}' not found in {file_path}")
if in_snippet:
raise ValueError(f"Unclosed snippet '{name}' in {file_path}")
# Strip leading/trailing blank lines
while result and not result[0].strip():
result.pop(0)
while result and not result[-1].strip():
result.pop()
# Dedent common leading whitespace
content = textwrap.dedent('\n'.join(result))
return content
def lang_for(file_path: Path) -> str:
return LANG_MAP.get(file_path.suffix, '')
def render(template: str) -> str:
errors = []
def replace(m: re.Match) -> str:
rel_path = m.group(1)
name = m.group(2)
file_path = ROOT / rel_path
if not file_path.exists():
errors.append(f"File not found: {file_path}")
return m.group(0)
try:
content = extract_snippet(file_path, name)
except ValueError as e:
errors.append(str(e))
return m.group(0)
lang = lang_for(file_path)
return f'```{lang}\n{content}\n```'
result = DIRECTIVE_RE.sub(replace, template)
if errors:
for err in errors:
print(f"ERROR: {err}", file=sys.stderr)
sys.exit(1)
return result
def main() -> None:
if not TEMPLATE.exists():
print(f"Error: {TEMPLATE} not found", file=sys.stderr)
sys.exit(1)
rendered = render(TEMPLATE.read_text())
OUTPUT.write_text(rendered)
print(f"Rendered {TEMPLATE.name}{OUTPUT.name}")
if __name__ == '__main__':
main()