markdown.py
python
sha256:c04414e7e3b812ff02f351d9792c79837b7841133a864f98fcaeff53a2a4afad
feat(KD-6b): MuseHub vault UI per frozen §KD-6a
Human
minor
⚠ breaking
12 days ago
| 1 | """Knowtation note markdown renderer — wikilinks, frontmatter panel, sanitisation (§KD-6a.A(a)).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import html as html_mod |
| 6 | import re |
| 7 | from typing import Any |
| 8 | |
| 9 | from musehub.api.routes.musehub.jinja2_filters import _markdown |
| 10 | |
| 11 | _FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL) |
| 12 | _WIKILINK_RE = re.compile(r"\[\[([^\]|#]+)(?:#([^\]|]+))?(?:\|([^\]]+))?\]\]") |
| 13 | |
| 14 | |
| 15 | def split_frontmatter(raw: str) -> tuple[dict[str, Any], str]: |
| 16 | """Return (frontmatter dict, body markdown) from a note string.""" |
| 17 | m = _FRONTMATTER_RE.match(raw) |
| 18 | if not m: |
| 19 | return {}, raw |
| 20 | import yaml |
| 21 | |
| 22 | try: |
| 23 | fm = yaml.safe_load(m.group(1)) or {} |
| 24 | except Exception: |
| 25 | fm = {} |
| 26 | if not isinstance(fm, dict): |
| 27 | fm = {} |
| 28 | body = raw[m.end() :] |
| 29 | return fm, body |
| 30 | |
| 31 | |
| 32 | def _resolve_wikilink( |
| 33 | target: str, |
| 34 | section: str | None, |
| 35 | label: str | None, |
| 36 | *, |
| 37 | owner: str, |
| 38 | repo_slug: str, |
| 39 | note_paths: frozenset[str], |
| 40 | ) -> str: |
| 41 | """Render one wikilink as a safe anchor or broken-link span.""" |
| 42 | target = target.strip().replace("\\", "/").lstrip("/") |
| 43 | if not target.endswith((".md", ".markdown", ".mdx")): |
| 44 | target = f"{target}.md" |
| 45 | href = f"/{owner}/{repo_slug}/note/{target}" |
| 46 | if section: |
| 47 | slug = re.sub(r"[^\w\s-]", "", section.lower()) |
| 48 | slug = re.sub(r"[\s_]+", "-", slug).strip("-") |
| 49 | if slug: |
| 50 | href = f"{href}#{slug}" |
| 51 | display = html_mod.escape(label or target.rsplit("/", 1)[-1]) |
| 52 | if target in note_paths or any(p.endswith("/" + target) for p in note_paths): |
| 53 | return f'<a class="vault-wikilink" href="{html_mod.escape(href, quote=True)}">{display}</a>' |
| 54 | return f'<span class="vault-wikilink broken" title="Note not found">{display}</span>' |
| 55 | |
| 56 | |
| 57 | def render_wikilinks( |
| 58 | text: str, |
| 59 | *, |
| 60 | owner: str, |
| 61 | repo_slug: str, |
| 62 | note_paths: frozenset[str], |
| 63 | ) -> str: |
| 64 | """Replace ``[[wikilinks]]`` with resolved anchors before markdown pass.""" |
| 65 | |
| 66 | def _repl(m: re.Match[str]) -> str: |
| 67 | return _resolve_wikilink( |
| 68 | m.group(1), |
| 69 | m.group(2), |
| 70 | m.group(3), |
| 71 | owner=owner, |
| 72 | repo_slug=repo_slug, |
| 73 | note_paths=note_paths, |
| 74 | ) |
| 75 | |
| 76 | return _WIKILINK_RE.sub(_repl, text) |
| 77 | |
| 78 | |
| 79 | def frontmatter_panel_html(fm: dict[str, Any]) -> str: |
| 80 | """Render frontmatter as a metadata panel — never injected into script context.""" |
| 81 | if not fm: |
| 82 | return "" |
| 83 | rows: list[str] = [] |
| 84 | for key in sorted(fm.keys()): |
| 85 | val = fm[key] |
| 86 | if isinstance(val, (list, tuple)): |
| 87 | display = ", ".join(html_mod.escape(str(v)) for v in val) |
| 88 | else: |
| 89 | display = html_mod.escape(str(val)) |
| 90 | rows.append( |
| 91 | f'<dt class="vault-fm-key">{html_mod.escape(str(key))}</dt>' |
| 92 | f'<dd class="vault-fm-val">{display}</dd>' |
| 93 | ) |
| 94 | return ( |
| 95 | '<aside class="vault-frontmatter-panel" aria-label="Note metadata">' |
| 96 | "<dl class=\"vault-fm-dl\">" + "".join(rows) + "</dl></aside>" |
| 97 | ) |
| 98 | |
| 99 | |
| 100 | def render_note_html( |
| 101 | raw: str, |
| 102 | *, |
| 103 | owner: str, |
| 104 | repo_slug: str, |
| 105 | note_paths: frozenset[str] | None = None, |
| 106 | ) -> tuple[dict[str, Any], str]: |
| 107 | """Full pipeline: frontmatter split → wikilinks → sanitised markdown HTML.""" |
| 108 | paths = note_paths or frozenset() |
| 109 | fm, body = split_frontmatter(raw) |
| 110 | body_linked = render_wikilinks(body, owner=owner, repo_slug=repo_slug, note_paths=paths) |
| 111 | note_base = f"/{owner}/{repo_slug}/note" |
| 112 | body_html = _markdown(body_linked, base_url=note_base) |
| 113 | return fm, body_html |
File History
1 commit
sha256:c04414e7e3b812ff02f351d9792c79837b7841133a864f98fcaeff53a2a4afad
feat(KD-6b): MuseHub vault UI per frozen §KD-6a
Human
minor
⚠
12 days ago