rbpu_datasheet/doc_builder/codeblock_extension.py

77 lines
2.1 KiB
Python
Raw Permalink Normal View History

2026-07-19 20:56:15 +08:00
"""Code block renderer — standalone pre-markdown step.
Usage in build pipeline:
md_text = render_code_blocks(md_text, project_dir)
html = markdown.markdown(md_text, extensions=[...])
Syntax in chapters:
```schemdraw
import schemdraw
...
```
Runs BEFORE markdown parsing, so avoids priority conflicts with
fenced_code / codehilite extensions.
Renders live in doc_builder/renderers/<tag>.py.
Each exports: render(code_text, project_dir) -> HTML string.
"""
import importlib.util
import re
from pathlib import Path
from html import unescape as html_unescape
CODE_BLOCK_RE = re.compile(
r'(?:^|\n)```(\w+)\s*\n(.*?)\n```(?:\n|$)',
re.DOTALL,
)
PASSTHROUGH = {'mermaid', 'wavedrom', 'python', 'bash', 'json', 'yaml', 'ascii'}
def _load_renderer(lang, project_dir):
rp = project_dir / 'doc_builder' / 'renderers' / f'{lang}.py'
if not rp.exists():
return None
spec = importlib.util.spec_from_file_location(
f'render_{lang}', str(rp)
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return getattr(mod, 'render', None)
def render_code_blocks(text, project_dir):
"""Find ```lang blocks, dispatch to renderers, replace inline."""
result = []
last_end = 0
for match in CODE_BLOCK_RE.finditer(text):
lang = match.group(1)
code = match.group(2)
result.append(text[last_end:match.start()])
if lang in PASSTHROUGH:
result.append(match.group(0))
else:
renderer = _load_renderer(lang, project_dir)
if renderer is None:
result.append(match.group(0))
else:
try:
code_text = html_unescape(code.strip())
html = renderer(code_text, project_dir)
result.append(f'\n{html}\n')
except Exception as e:
result.append(
f'\n<div class="warning">'
f'{lang} render error: {e}'
f'</div>\n'
)
last_end = match.end()
result.append(text[last_end:])
return ''.join(result)