"""One document, written twice: Markdown and a self-contained HTML page. The report is built as a list of blocks (heading, paragraph, bullet list, table) and each writer turns the same list into its format, so the two can never say different things. """ import html CSS = """ :root { --bg:#fff; --fg:#1d1d1f; --mute:#6e6e73; --line:#d9d9de; --head:#f3f3f5; --pass:#1a7f37; --fail:#c62828; --warn:#9a6700; --passbg:#e8f5ec; --failbg:#fdecea; --series1:#2a78d6; --good:#0ca30c; --critical:#d03b3b; --warning:#fab219; --neutral:#898781; --grid:#e1e0d9; --axis:#c3c2b7; } @media (prefers-color-scheme: dark) { :root { --bg:#161618; --fg:#e8e8ea; --mute:#9a9aa0; --line:#3a3a3e; --head:#232326; --pass:#5cc97a; --fail:#ff7b72; --warn:#e3b341; --passbg:#16301f; --failbg:#3a1a1a; --series1:#3987e5; --grid:#2c2c2a; --axis:#383835; } } figure { margin:12px 0 20px; max-width:900px; } figcaption { color:var(--mute); font-size:13px; } svg.plot { width:100%; height:auto; display:block; } svg.plot text { fill:var(--mute); font:11px system-ui, sans-serif; } svg.plot .grid { stroke:var(--grid); stroke-width:1; } svg.plot .ref { stroke:var(--axis); stroke-width:2; } svg.plot circle:hover, svg.plot rect:hover { stroke:var(--fg); stroke-width:2; } body { background:var(--bg); color:var(--fg); margin:0 auto; max-width:1400px; padding:16px; font:14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; } h1 { font-size:22px; margin:8px 0 4px; } h2 { font-size:18px; margin:28px 0 8px; } h3 { font-size:15px; margin:20px 0 6px; } p, li { max-width:900px; } .mute { color:var(--mute); } .wrap { overflow-x:auto; margin:8px 0 16px; } table { border-collapse:collapse; font-variant-numeric:tabular-nums; } th, td { border-bottom:1px solid var(--line); padding:4px 8px; text-align:left; white-space:nowrap; } th { background:var(--head); position:sticky; top:0; } td.num, th.num { text-align:right; } td.pass { color:var(--pass); font-weight:600; } td.fail { color:var(--fail); font-weight:600; } td.warn { color:var(--warn); font-weight:600; } tr.fail td { background:var(--failbg); } code { font:12px ui-monospace, Menlo, monospace; } """ class Doc: def __init__(self, title): self.title = title self.blocks = [] def h(self, level, text): self.blocks.append(("h", level, text)) def p(self, text): self.blocks.append(("p", text)) def ul(self, items): self.blocks.append(("ul", list(items))) def svg(self, markup, caption): """An inline SVG plot (HTML only; the Markdown names it and points at the HTML).""" self.blocks.append(("svg", markup, caption)) def table(self, head, rows, num=(), row_class=None, cell_class=None): """head: column titles; rows: lists of str; num: indices of right-aligned columns; row_class(row) and cell_class(i, value) give optional CSS classes (HTML only).""" self.blocks.append(("table", head, rows, set(num), row_class, cell_class)) def markdown(self): out = [f"# {self.title}", ""] for b in self.blocks: if b[0] == "h": out += ["#" * b[1] + " " + b[2], ""] elif b[0] == "p": out += [b[1], ""] elif b[0] == "ul": out += ["- " + x for x in b[1]] + [""] elif b[0] == "svg": out += [f"_Plot in report.html: {b[2]}_", ""] else: _, head, rows, num, _, _ = b if not rows: out += ["(none)", ""] continue out.append("| " + " | ".join(head) + " |") out.append("|" + "|".join("--:" if i in num else ":--" for i in range(len(head))) + "|") for r in rows: out.append("| " + " | ".join(str(x).replace("|", "/") for x in r) + " |") out.append("") return "\n".join(out) def html(self): e = html.escape out = ["", '
', '', f"{e(b[1])}
") elif b[0] == "ul": out.append("(none)
') continue t = ['| {e(h)} | ' if i in num else f"{e(h)} | " for i, h in enumerate(head)] t.append("
|---|---|
| {e(str(x))} | ") t.append("