77 lines
2.1 KiB
Python
77 lines
2.1 KiB
Python
|
|
"""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)
|