"""
Markdown -> HTML, for notes on the minisite. Stdlib only, and deliberately small.
A notebook renders markdown itself, so notes go into it as written. The minisite has
no renderer, and adding a dependency to turn a few paragraphs into HTML would
break the rule that the structural path is the stdlib. So this covers what notes
are actually written with:
# headings paragraphs - bullet / 1. numbered lists
**bold** *italic* `code` ```fenced code```
[links](url)  | pipe | tables |
> quotes ---
Anything it does not recognise is **escaped, not passed through** — a note is
text, and raw HTML in it is shown rather than executed.
"""
import re
from html import escape
def _url(raw: str) -> str:
"""An attribute-safe URL. The text around it is already escaped without
quotes, so a `"` here would close the attribute; and a `javascript:` link is
refused outright — a note is text, not a place to run code."""
if re.match(r"^\s*(javascript|vbscript|data):", raw, re.I):
return "#"
return raw.replace('"', """)
INLINE = [
(re.compile(r"!\[([^\]]*)\]\(([^)\s]+)\)"), lambda m, img: img(m.group(1), m.group(2))),
(re.compile(r"\[([^\]]+)\]\(([^)\s]+)\)"),
lambda m, img: f'{m.group(1)}'),
(re.compile(r"\*\*(.+?)\*\*"), lambda m, img: f"{m.group(1)}"),
(re.compile(r"(?{m.group(1)}"),
]
def _inline(text: str, image) -> str:
# Code spans first and protected, so nothing inside them is formatted.
spans: list[str] = []
def keep(m):
spans.append(f"{escape(m.group(1))}")
return f"\x00{len(spans) - 1}\x00"
text = re.sub(r"`([^`]+)`", keep, text)
text = escape(text, quote=False)
for pattern, render in INLINE:
text = pattern.sub(lambda m: render(m, image), text)
return re.sub(r"\x00(\d+)\x00", lambda m: spans[int(m.group(1))], text)
def to_html(text: str, *, image=None) -> str:
"""Render a note. `image(alt, src) -> html` lets the caller embed local images."""
image = image or (lambda alt, src: f'')
lines = text.replace("\r\n", "\n").split("\n")
out: list[str] = []
i = 0
def para(buf):
if buf:
out.append(f"
{_inline(' '.join(s.strip() for s in buf), image)}
") buf: list[str] = [] while i < len(lines): line = lines[i] stripped = line.strip() if stripped.startswith("```"): para(buf); buf = [] code = [] i += 1 while i < len(lines) and not lines[i].strip().startswith("```"): code.append(lines[i]) i += 1 out.append(f"{escape(chr(10).join(code))}")
i += 1
continue
if not stripped:
para(buf); buf = []
i += 1
continue
m = re.match(r"^(#{1,6})\s+(.*)$", stripped)
if m:
para(buf); buf = []
level = len(m.group(1))
out.append(f"{to_html(chr(10).join(quote), image=image)}") continue if re.match(r"^([-*+]|\d+[.)])\s+", stripped): para(buf); buf = [] ordered = bool(re.match(r"^\d", stripped)) items = [] while i < len(lines) and re.match(r"^\s*([-*+]|\d+[.)])\s+", lines[i]): items.append(re.sub(r"^\s*([-*+]|\d+[.)])\s+", "", lines[i])) i += 1 tag = "ol" if ordered else "ul" out.append(f"<{tag}>" + "".join(f"
| {_inline(h, image)} | " for h in head) + "
|---|
| {_inline(c, image)} | " for c in r) + "