| 1 | #!/usr/bin/env python3 |
| 2 | """Build a self-contained HTML rendering of an Ontology Semantic Atlas. |
| 3 | |
| 4 | Usage: build_atlas_html.py <project-dir> <mermaid-bundle.js> [--atlas documentation/semantic-atlas.md] [--out documentation/semantic-atlas.html] |
| 5 | |
| 6 | The bundle is Mermaid + @mermaid-js/layout-elk (registered) compiled to a single IIFE that sets |
| 7 | window.mermaid (see scripts/build_mermaid_bundle.sh) — so every ```mermaid block, all of which carry |
| 8 | the `config: layout: elk` frontmatter, is laid out by ELK with no CDN, plugin or viewer support needed. |
| 9 | Blocks that fail to render are replaced in-page by a visible RENDER ERROR box, and the page exposes |
| 10 | window.__report = {total, ok, errors:[{index, message}]} for headless checks. |
| 11 | |
| 12 | Requires the `markdown` package (pip install markdown). |
| 13 | """ |
| 14 | from __future__ import annotations |
| 15 | import argparse, re |
| 16 | from pathlib import Path |
| 17 | import markdown |
| 18 | |
| 19 | ap = argparse.ArgumentParser() |
| 20 | ap.add_argument("project_dir", type=Path); ap.add_argument("bundle", type=Path) |
| 21 | ap.add_argument("--atlas", type=Path, default=Path("documentation/semantic-atlas.md")) |
| 22 | ap.add_argument("--out", type=Path, default=Path("documentation/semantic-atlas.html")) |
| 23 | a = ap.parse_args() |
| 24 | proj = a.project_dir.resolve() |
| 25 | atlas = a.atlas if a.atlas.is_absolute() else proj / a.atlas |
| 26 | out = a.out if a.out.is_absolute() else proj / a.out |
| 27 | md_src = atlas.read_text(encoding="utf-8") |
| 28 | body = markdown.markdown(md_src, extensions=["tables", "fenced_code", "toc"]) |
| 29 | # python-markdown emits <pre><code class="language-mermaid">…escaped…</code></pre>; keep the escaped text, |
| 30 | # Mermaid decodes entities from a <pre> element's textContent. |
| 31 | pat = re.compile(r'<pre><code class="language-mermaid">(.*?)</code></pre>', re.S) |
| 32 | n = 0 |
| 33 | def repl(m): |
| 34 | global n; n += 1 |
| 35 | return f'<pre class="mermaid" data-idx="{n-1}">{m.group(1)}</pre>' |
| 36 | body = pat.sub(repl, body) |
| 37 | assert n == md_src.count("```mermaid"), (n, md_src.count("```mermaid")) |
| 38 | title_m = re.search(r"^# (.+)$", md_src, re.M) |
| 39 | title = title_m.group(1) if title_m else "Ontology Semantic Atlas" |
| 40 | css = """ |
| 41 | body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;max-width:1400px;margin:0 auto;padding:1rem 2rem;color:#1a1a1a;background:#fff;line-height:1.45} |
| 42 | table{border-collapse:collapse;font-size:.85rem;margin:1rem 0;display:block;overflow-x:auto} |
| 43 | th,td{border:1px solid #ccc;padding:.3rem .5rem;text-align:left;vertical-align:top} th{background:#f3f3f3} |
| 44 | pre.mermaid{background:transparent;overflow-x:auto;margin:1rem 0} pre.mermaid svg{max-width:100%;height:auto} |
| 45 | pre.render-error{background:#fff0f0;border:2px solid #c00;padding:1rem;white-space:pre-wrap;font-size:.8rem} |
| 46 | code{background:#f4f4f4;padding:0 .2em;border-radius:3px;font-size:.9em} pre:not(.mermaid) code{background:none} |
| 47 | h1,h2,h3{margin-top:2rem} |
| 48 | #status{position:fixed;top:0;right:0;background:#222;color:#fff;font:12px monospace;padding:.3rem .6rem;z-index:9} |
| 49 | """ |
| 50 | js = """ |
| 51 | (async()=>{ |
| 52 | const st=document.getElementById('status'); |
| 53 | mermaid.initialize({startOnLoad:false, securityLevel:'strict', maxTextSize:900000, maxEdges:5000}); |
| 54 | const nodes=[...document.querySelectorAll('pre.mermaid')]; |
| 55 | const report={total:nodes.length, ok:0, errors:[]}; window.__report=report; |
| 56 | for(const el of nodes){ |
| 57 | const idx=el.dataset.idx, src=el.textContent; |
| 58 | try{ const {svg}=await mermaid.render('atlas-svg-'+idx, src); el.innerHTML=svg; report.ok++; } |
| 59 | catch(e){ const p=document.createElement('pre'); p.className='render-error'; |
| 60 | p.textContent='RENDER ERROR (block '+idx+'): '+(e&&e.message||e)+'\\n\\n'+src; |
| 61 | el.replaceWith(p); report.errors.push({index:+idx, message:String(e&&e.message||e)}); } |
| 62 | st.textContent='rendered '+(report.ok+report.errors.length)+'/'+report.total+(report.errors.length?' errors: '+report.errors.length:''); |
| 63 | } |
| 64 | window.__done=true; |
| 65 | })(); |
| 66 | """ |
| 67 | doc = f"""<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> |
| 68 | <title>{title}</title><style>{css}</style> |
| 69 | <script>{a.bundle.read_text(encoding="utf-8")}</script> |
| 70 | </head><body><div id="status">rendering…</div> |
| 71 | {body} |
| 72 | <script>{js}</script></body></html>""" |
| 73 | out.parent.mkdir(parents=True, exist_ok=True) |
| 74 | out.write_text(doc, encoding="utf-8") |
| 75 | print(f"Wrote {out} — {n} mermaid blocks, {out.stat().st_size/1e6:.1f} MB") |