"""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/.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
' f'{lang} render error: {e}' f'
\n' ) last_end = match.end() result.append(text[last_end:]) return ''.join(result)