|
| 1 | +"""MkDocs build hooks for browser-stable course rendering.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import html as html_module |
| 6 | +import re |
| 7 | +from pathlib import Path |
| 8 | +from urllib.parse import unquote, urlsplit |
| 9 | + |
| 10 | + |
| 11 | +TEACHING_FIGURE_RE = re.compile( |
| 12 | + r'<img\b' |
| 13 | + r'(?=[^>]*\bclass="[^"]*\bae-figure\b[^"]*")' |
| 14 | + r'(?=[^>]*\bsrc="(?P<src>[^"]+\.svg)")' |
| 15 | + r"[^>]*/?>" |
| 16 | +) |
| 17 | +SVG_OPEN_RE = re.compile(r"<svg\b(?P<attributes>[^>]*)>", re.IGNORECASE) |
| 18 | +CLASS_RE = re.compile(r'\bclass="(?P<classes>[^"]*)"', re.IGNORECASE) |
| 19 | +ID_RE = re.compile(r'\bid="(?P<id>[^"]+)"') |
| 20 | + |
| 21 | + |
| 22 | +def _prefix_svg_ids(svg: str, prefix: str) -> str: |
| 23 | + """Keep marker and gradient IDs unique after several SVGs are inlined.""" |
| 24 | + |
| 25 | + identifiers = ID_RE.findall(svg) |
| 26 | + for identifier in identifiers: |
| 27 | + replacement = f"{prefix}{identifier}" |
| 28 | + svg = svg.replace(f'id="{identifier}"', f'id="{replacement}"') |
| 29 | + svg = svg.replace(f"url(#{identifier})", f"url(#{replacement})") |
| 30 | + svg = svg.replace(f'href="#{identifier}"', f'href="#{replacement}"') |
| 31 | + svg = svg.replace(f"xlink:href=\"#{identifier}\"", f'xlink:href="#{replacement}"') |
| 32 | + return svg |
| 33 | + |
| 34 | + |
| 35 | +def _add_figure_class(svg: str) -> str: |
| 36 | + def replace(match: re.Match[str]) -> str: |
| 37 | + attributes = match.group("attributes") |
| 38 | + class_match = CLASS_RE.search(attributes) |
| 39 | + if class_match: |
| 40 | + classes = class_match.group("classes").split() |
| 41 | + if "ae-figure" not in classes: |
| 42 | + classes.append("ae-figure") |
| 43 | + attributes = CLASS_RE.sub( |
| 44 | + f'class="{" ".join(classes)}"', |
| 45 | + attributes, |
| 46 | + count=1, |
| 47 | + ) |
| 48 | + else: |
| 49 | + attributes = f' class="ae-figure"{attributes}' |
| 50 | + return f"<svg{attributes}>" |
| 51 | + |
| 52 | + return SVG_OPEN_RE.sub(replace, svg, count=1) |
| 53 | + |
| 54 | + |
| 55 | +def on_page_content(html: str, page, config, files) -> str: |
| 56 | + """Inline teaching SVGs so the selected site palette controls their colors.""" |
| 57 | + |
| 58 | + figure_dir = (Path(config.docs_dir) / "assets" / "figures").resolve() |
| 59 | + |
| 60 | + def replace(match: re.Match[str]) -> str: |
| 61 | + source = html_module.unescape(match.group("src")) |
| 62 | + source_path = unquote(urlsplit(source).path) |
| 63 | + filename = Path(source_path).name |
| 64 | + figure_path = (figure_dir / filename).resolve() |
| 65 | + |
| 66 | + if figure_path.parent != figure_dir or not figure_path.is_file(): |
| 67 | + return match.group(0) |
| 68 | + |
| 69 | + svg = figure_path.read_text(encoding="utf-8") |
| 70 | + prefix = f"ae-{figure_path.stem}-" |
| 71 | + return _add_figure_class(_prefix_svg_ids(svg, prefix)) |
| 72 | + |
| 73 | + return TEACHING_FIGURE_RE.sub(replace, html) |
0 commit comments