From 24c5792b986895fa6f99e6ed44f91eba62b065c2 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Mon, 10 Aug 2026 20:52:42 +1000 Subject: [PATCH 1/3] fix(docs): repair 1.9.4 publication erratum --- .github/workflows/documentation.yml | 7 ++++--- docs/RELEASE_NOTES_1.9.4.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 55d36da..66e36ff 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -4,9 +4,9 @@ on: workflow_dispatch: inputs: release_ref: - description: Exact 1.9.4 tag to publish + description: Source ref for a 1.9.4 documentation rebuild required: true - default: v1.9.4 + default: main release: types: [published] @@ -113,7 +113,8 @@ jobs: run: | gh release upload "${{ github.event.release.tag_name }}" \ artifacts/mathlib-fp-docs-1.9.4.zip \ - artifacts/mathlib-fp-docs-1.9.4.zip.sha256 --clobber + artifacts/mathlib-fp-docs-1.9.4.zip.sha256 --clobber \ + --repo "$GITHUB_REPOSITORY" deploy: if: github.event_name != 'release' || github.event.release.tag_name == 'v1.9.4' diff --git a/docs/RELEASE_NOTES_1.9.4.md b/docs/RELEASE_NOTES_1.9.4.md index 125e572..be40502 100644 --- a/docs/RELEASE_NOTES_1.9.4.md +++ b/docs/RELEASE_NOTES_1.9.4.md @@ -1,4 +1,4 @@ -# Release notes: 1.9.4 +# mathlib-fp v1.9.4 ## Numerical trust closure From 91d3c780b5c40edf5b59d44c501808502dda7288 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Mon, 10 Aug 2026 21:03:37 +1000 Subject: [PATCH 2/3] fix(docs): render Markdown tables as HTML --- tools/build_docs.py | 106 +++++++++++++++++++++++++++++++++++---- tools/test_build_docs.py | 25 ++++++++- 2 files changed, 119 insertions(+), 12 deletions(-) diff --git a/tools/build_docs.py b/tools/build_docs.py index 1828551..1cfe73b 100644 --- a/tools/build_docs.py +++ b/tools/build_docs.py @@ -68,7 +68,23 @@ def close_list() -> None: output.append("") in_list = False - for line in source.splitlines(): + def table_cells(line: str) -> list[str] | None: + value = line.strip() + if not value.startswith("|"): + return None + if value.endswith("|"): + value = value[:-1] + return [cell.strip() for cell in value[1:].split("|")] + + def is_table_divider(cells: list[str] | None, width: int) -> bool: + return bool(cells) and len(cells) == width and all( + re.fullmatch(r":?-{3,}:?", cell) for cell in cells + ) + + lines = source.splitlines() + line_index = 0 + while line_index < len(lines): + line = lines[line_index] if line.startswith("```"): flush_paragraph() close_list() @@ -80,10 +96,12 @@ def close_list() -> None: f'
'
                 )
             in_code = not in_code
+            line_index += 1
             continue
         if in_code:
             output.append(html.escape(line) + "\n")
             plain.append(line)
+            line_index += 1
             continue
         heading = re.match(r"^(#{1,6})\s+(.+)$", line)
         if heading:
@@ -107,14 +125,51 @@ def close_list() -> None:
         elif not line.strip():
             flush_paragraph()
             close_list()
+        elif (
+            (header_cells := table_cells(line)) is not None
+            and line_index + 1 < len(lines)
+            and is_table_divider(
+                table_cells(lines[line_index + 1]), len(header_cells)
+            )
+        ):
+            flush_paragraph()
+            close_list()
+            label = html.escape(
+                f"Scrollable table: {header_cells[0]}", quote=True
+            )
+            output.append(
+                f'
' + ) + output.extend( + f'' + for cell in header_cells + ) + output.append("") + plain.extend(header_cells) + line_index += 2 + while line_index < len(lines): + row_cells = table_cells(lines[line_index]) + if row_cells is None or len(row_cells) != len(header_cells): + break + output.append("") + output.extend( + f"" + for cell in row_cells + ) + output.append("") + plain.extend(row_cells) + line_index += 1 + output.append("
{inline(cell, link_resolver)}
{inline(cell, link_resolver)}
") + continue elif line.startswith("|"): - # Preserve Markdown tables readably; source remains canonical. flush_paragraph() close_list() output.append(f'
{html.escape(line)}
') plain.append(line) else: paragraph.append(line.strip()) + line_index += 1 flush_paragraph() close_list() if in_code: @@ -269,12 +324,21 @@ def write_site_index(site_root: Path, versions: dict[str, object]) -> None: mathlib-fp documentation — current release {html.escape(current)} - +

mathlib-fp documentation

-

Current release: {html.escape(current)}

+

Current release: {html.escape(current)}

Choose a release below. Each version keeps the content generated from that release's tagged documentation; publishing a newer release does not replace it.

-
    {''.join(items)}
""" +
    {''.join(items)}
""" site_root.mkdir(parents=True, exist_ok=True) (site_root / "index.html").write_text(page, encoding="utf-8") (site_root / ".nojekyll").write_text("", encoding="utf-8") @@ -377,12 +441,32 @@ def main() -> int: assets = output / "assets" assets.mkdir(exist_ok=True) (assets / "site.css").write_text( - "body{font:16px/1.55 system-ui;max-width:72rem;margin:auto;padding:1rem}" - "header{display:flex;gap:1.5rem;align-items:center;flex-wrap:wrap;" - "border-bottom:1px solid #bbb}" - "pre{overflow:auto;background:#f5f5f5;padding:.7rem}" - "code{background:#f5f5f5}#results a{display:block}" - ".table-source{margin:0;padding:.2rem .7rem}", + """:root{color-scheme:light;--ink:#172033;--muted:#536178;--line:#d9e0ea; +--surface:#fff;--canvas:#f4f7fb;--accent:#126b8d;--accent-dark:#0d4057; +--code:#f1f5f9}*{box-sizing:border-box}body{margin:0;background:var(--canvas); +color:var(--ink);font:16px/1.65 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif} +header{display:flex;gap:1rem 1.5rem;align-items:center;flex-wrap:wrap;background:var(--accent-dark); +padding:.85rem max(1.25rem,calc((100vw - 76rem)/2));color:#e8f4f8;box-shadow:0 1px 0 #082c3c} +header>a{color:#fff;font-weight:700;text-decoration:none;letter-spacing:.01em}header label{font-size:.9rem; +font-weight:600}header input{width:min(17rem,50vw);margin-left:.45rem;border:1px solid #9cb4bf; +border-radius:.25rem;padding:.4rem .55rem;font:inherit}header nav{font-size:.86rem}header nav a{color:#d7f2fb} +main,#results{max-width:76rem;margin:0 auto}main{min-height:calc(100vh - 4rem);background:var(--surface); +padding:2.5rem max(1.25rem,calc((100vw - 72rem)/2)) 4rem;box-shadow:0 1px 3px #17203312} +#results{padding:0 max(1.25rem,calc((100vw - 72rem)/2));background:var(--surface)}#results a{display:block; +padding:.4rem 0;color:var(--accent)}h1,h2,h3{line-height:1.2;color:#102a43}h1{font-size:clamp(2rem,4vw,2.8rem); +letter-spacing:-.025em;margin:0 0 1.5rem}h2{font-size:1.55rem;margin:2.5rem 0 1rem;padding-top:.4rem} +h3{font-size:1.2rem;margin:1.8rem 0 .7rem}a{color:var(--accent);text-decoration-thickness:.08em; +text-underline-offset:.14em}a:hover{color:#074e6b}p,li{max-width:76ch}ul{padding-left:1.35rem}pre{overflow:auto; +padding:1rem 1.15rem;background:var(--code);border:1px solid var(--line);border-radius:.35rem;line-height:1.5} +code{background:var(--code);padding:.1em .28em;border-radius:.2rem;font-family:ui-monospace,SFMono-Regular,Consolas,monospace} +pre code{padding:0;background:transparent}.table-wrap{overflow-x:auto;margin:1.25rem 0;border:1px solid var(--line); +border-radius:.4rem;background:var(--surface)}table{width:100%;min-width:42rem;border-collapse:collapse;font-size:.94rem} +th,td{padding:.75rem .85rem;text-align:left;vertical-align:top;border-bottom:1px solid var(--line)}th{background:#e8f1f5; +color:#163a4b;font-size:.82rem;letter-spacing:.02em;text-transform:uppercase}tbody tr:nth-child(even){background:#f8fafc} +tbody tr:last-child td{border-bottom:0}.table-wrap:focus-visible{outline:3px solid #64b5d2;outline-offset:2px} +blockquote{margin:1.25rem 0;padding:.25rem 1rem;border-left:4px solid #74a9be;background:#f3f8fa;color:#354a5f} +@media(max-width:640px){header{padding:.75rem 1rem}header input{width:13rem}main{padding:1.75rem 1rem 3rem} +h1{font-size:2rem}table{min-width:34rem}th,td{padding:.6rem .7rem}}""", encoding="utf-8", ) (assets / "search.js").write_text( diff --git a/tools/test_build_docs.py b/tools/test_build_docs.py index 38695f3..975e9f9 100644 --- a/tools/test_build_docs.py +++ b/tools/test_build_docs.py @@ -10,7 +10,12 @@ import zipfile from pathlib import Path -from build_docs import load_versions, write_offline_archive, write_site_index +from build_docs import ( + load_versions, + markdown_to_html, + write_offline_archive, + write_site_index, +) class DocumentationBuildTests(unittest.TestCase): @@ -54,6 +59,7 @@ def test_landing_page_identifies_current_and_links_older_release(self) -> None: page = (site / "index.html").read_text(encoding="utf-8") self.assertIn("Current release: 1.9.3", page) self.assertIn('href="1.9.0/index.html"', page) + self.assertIn('class="release-list"', page) def test_offline_archive_is_deterministic_and_self_identifying(self) -> None: site = self.root / "site" @@ -73,6 +79,23 @@ def test_offline_archive_is_deterministic_and_self_identifying(self) -> None: "mathlib-fp-docs-1.9.3/release.json", archive.namelist() ) + def test_markdown_table_renders_as_a_semantic_html_table(self) -> None: + source = ( + "| Unit family | Domain |\n" + "| --- | --- |\n" + "| [MathBase](MathBase.md) | Shared numerics |\n" + ) + + body, plain = markdown_to_html(source) + + self.assertIn("", body) + self.assertIn("", body) + self.assertIn('', body) + self.assertIn('MathBase', body) + self.assertIn("", body) + self.assertNotIn("table-source", body) + self.assertIn("Shared numerics", plain) + if __name__ == "__main__": unittest.main() From 847efc17f2fb0ad28261537a8d2a5c787c32b987 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Mon, 10 Aug 2026 21:06:55 +1000 Subject: [PATCH 3/3] fix(docs): preserve wrapped list items --- tools/build_docs.py | 17 +++++++++++++++-- tools/test_build_docs.py | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/tools/build_docs.py b/tools/build_docs.py index 1cfe73b..8c70981 100644 --- a/tools/build_docs.py +++ b/tools/build_docs.py @@ -52,6 +52,7 @@ def markdown_to_html(source: str, link_resolver=None) -> tuple[str, str]: output: list[str] = [] plain: list[str] = [] paragraph: list[str] = [] + list_item: list[str] = [] in_code = False in_list = False @@ -62,9 +63,17 @@ def flush_paragraph() -> None: plain.append(text) paragraph.clear() + def flush_list_item() -> None: + if list_item: + text = " ".join(list_item) + output.append(f"
  • {inline(text, link_resolver)}
  • ") + plain.append(text) + list_item.clear() + def close_list() -> None: nonlocal in_list if in_list: + flush_list_item() output.append("") in_list = False @@ -119,9 +128,12 @@ def is_table_divider(cells: list[str] | None, width: int) -> bool: if not in_list: output.append("
      ") in_list = True + else: + flush_list_item() item = re.sub(r"^[-*]\s+", "", line) - output.append(f"
    • {inline(item, link_resolver)}
    • ") - plain.append(item) + list_item.append(item) + elif in_list and re.match(r"^\s{2,}\S", line): + list_item.append(line.strip()) elif not line.strip(): flush_paragraph() close_list() @@ -168,6 +180,7 @@ def is_table_divider(cells: list[str] | None, width: int) -> bool: output.append(f'
      {html.escape(line)}
      ') plain.append(line) else: + close_list() paragraph.append(line.strip()) line_index += 1 flush_paragraph() diff --git a/tools/test_build_docs.py b/tools/test_build_docs.py index 975e9f9..26b256d 100644 --- a/tools/test_build_docs.py +++ b/tools/test_build_docs.py @@ -96,6 +96,23 @@ def test_markdown_table_renders_as_a_semantic_html_table(self) -> None: self.assertNotIn("table-source", body) self.assertIn("Shared numerics", plain) + def test_wrapped_markdown_list_item_stays_inside_list_item(self) -> None: + source = ( + "- Release notes — stable-family\n" + " numerical evidence and offline validation.\n" + "- Qualification report — release gate.\n" + ) + + body, plain = markdown_to_html(source) + + self.assertIn( + "
    • Release notes — stable-family numerical evidence and offline " + "validation.
    • ", + body, + ) + self.assertNotIn("

      numerical evidence", body) + self.assertIn("Release notes — stable-family numerical evidence", plain) + if __name__ == "__main__": unittest.main()

    Unit familyShared numerics