gabriel / musehub public
markdown.py python
113 lines 3.5 KB
Raw
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