122 lines
3.1 KiB
Python
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()
|