Skip to content

Build_gallery

FieldValue
TypeSkill Resource
Source~/.copilot/skills/technical-writing/references/mermaid/gallery/build/build_gallery.py
DescriptionNot specified

Source Content

#!/usr/bin/env python3
"""Generate the self-contained gallery from diagrams.py + harvested svgs.json.
Emits two files:
gallery.html full standalone document (repo + SendUserFile)
gallery_artifact.html body-only fragment for the Artifact tool
"""
import html
import json
from pathlib import Path
from diagrams import GROUPS, DIAGRAMS
HERE = Path(__file__).parent
SVGS = json.loads((HERE / "svgs.json").read_text(encoding="utf-8"))
FA_CDN = ("https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7/"
"css/all.min.css")
def fontawesome_style() -> str:
"""Self-host FontAwesome Free so the inline `<i class="fas fa-…">` icons in the
harvested SVGs paint in a standalone file with no network. Reads a vendored
`fontawesome/` folder (css/all.min.css + webfonts/*.woff2) and base64-inlines
the woff2 fonts. Returns "" when no vendored copy is present — the caller then
falls back to the CDN <link> (works online; the standalone doc still renders)."""
import base64
import re
fa = HERE / "fontawesome"
css_path = fa / "css" / "all.min.css"
if not css_path.exists():
return ""
css = css_path.read_text(encoding="utf-8")
def inline(match) -> str:
rel = match.group(1).strip("'\"")
font = (fa / "css" / rel).resolve()
if not font.exists():
return match.group(0)
b64 = base64.b64encode(font.read_bytes()).decode()
return f'url(data:font/woff2;base64,{b64}) format("woff2")'
css = re.sub(r'url\((\.\./webfonts/[^)]+\.woff2)\)\s*format\("woff2"\)', inline, css)
# drop non-woff2 fallbacks to keep the embed lean
css = re.sub(r',?\s*url\([^)]+\.(?:woff|ttf|eot|svg)[^)]*\)\s*format\([^)]+\)', "", css)
return f"<style>{css}</style>"
FA_EMBED = fontawesome_style()
FA_HEAD = FA_EMBED or f'<link rel="stylesheet" href="{FA_CDN}">'
def esc(s: str) -> str:
return html.escape(s, quote=False)
CSS = """
:root{
--ink:#1b2431; --muted:#586372; --faint:#8a93a1;
--paper:#eef0f3; --card:#ffffff;
--line:#dcdfe4; --line-2:#c6cbd3;
--accent:#235e7a; --accent-soft:#e6eef2; --accent-ink:#163d51;
--good:#2f6f4e; --good-soft:#e8f1ec;
--caution:#9c5a37; --caution-soft:#f4ebe4;
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
--sans:-apple-system,"Segoe UI",system-ui,Roboto,Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);
line-height:1.6;-webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums;}
.wrap{max-width:1000px;margin:0 auto;padding:0 24px 96px;}
/* ── header ── */
.masthead{padding:56px 0 28px;border-bottom:1px solid var(--line-2);margin-bottom:8px;}
.eyebrow{font-family:var(--mono);font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;
color:var(--accent);font-weight:600;}
h1{font-size:clamp(2rem,4.4vw,2.9rem);line-height:1.05;letter-spacing:-.02em;font-weight:700;
margin:.5rem 0 .4rem;text-wrap:balance;}
.dek{font-size:1.12rem;color:var(--muted);max-width:60ch;margin:0;}
.how{margin:22px 0 0;max-width:66ch;color:var(--ink);}
.how p{margin:.5rem 0;}
.legend{display:flex;flex-wrap:wrap;gap:14px 22px;margin-top:18px;font-size:.85rem;color:var(--muted);}
.legend b{color:var(--ink);font-weight:600;}
/* ── quick-pick index ── */
.index{margin:30px 0 8px;border:1px solid var(--line);border-radius:10px;background:var(--card);overflow:hidden;}
.index h2{margin:0;padding:14px 18px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;
font-family:var(--mono);color:var(--accent-ink);background:var(--accent-soft);border-bottom:1px solid var(--line);}
.index-scroll{overflow-x:auto;}
table.pick{border-collapse:collapse;width:100%;font-size:.92rem;}
table.pick th,table.pick td{text-align:left;padding:9px 18px;border-bottom:1px solid var(--line);vertical-align:top;}
table.pick th{font-family:var(--mono);font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);font-weight:600;}
table.pick tr:last-child td{border-bottom:none;}
table.pick a{color:var(--accent);text-decoration:none;font-weight:600;white-space:nowrap;}
table.pick a:hover{text-decoration:underline;}
table.pick .k{font-family:var(--mono);font-size:.78rem;color:var(--muted);}
/* ── phase section ── */
.phase{margin:56px 0 0;}
.phase-head{display:flex;align-items:baseline;gap:14px;padding-bottom:6px;border-bottom:2px solid var(--ink);}
.phase-num{font-family:var(--mono);font-size:.82rem;font-weight:700;color:var(--accent);letter-spacing:.06em;}
.phase-head h2{font-size:1.5rem;letter-spacing:-.01em;margin:0;font-weight:680;}
.phase-note{color:var(--muted);margin:.6rem 0 0;max-width:70ch;}
/* ── card ── */
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;margin-top:22px;
padding:22px 24px 24px;scroll-margin-top:16px;}
.card-top{display:flex;flex-wrap:wrap;align-items:center;gap:10px 12px;}
.card-top h3{font-size:1.22rem;margin:0;font-weight:660;letter-spacing:-.01em;}
.badge{font-family:var(--mono);font-size:.72rem;padding:3px 8px;border-radius:6px;line-height:1;font-weight:600;}
.badge.type{background:var(--accent-soft);color:var(--accent-ink);}
.badge.live{background:var(--good-soft);color:var(--good);}
.badge.static{background:#eef0f3;color:var(--muted);border:1px solid var(--line);}
.lead{margin:14px 0 0;font-size:1.06rem;color:var(--ink);padding-left:14px;border-left:3px solid var(--accent);}
.uses{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:18px 0 4px;}
.uses h4{font-family:var(--mono);font-size:.7rem;letter-spacing:.1em;text-transform:uppercase;margin:0 0 6px;font-weight:600;}
.uses .col.good h4{color:var(--good);}
.uses .col.avoid h4{color:var(--caution);}
.uses ul{margin:0;padding-left:18px;font-size:.93rem;color:var(--muted);}
.uses li{margin:3px 0;}
.uses li::marker{color:var(--line-2);}
.well{margin:20px 0 0;border:1px solid var(--line);border-radius:8px;background:#fbfbfc;padding:18px;}
.well-scroll{overflow-x:auto;}
.well-scroll svg{max-width:100%;height:auto;display:block;margin:0 auto;}
.notice{margin:12px 0 0;font-size:.95rem;color:var(--accent-ink);background:var(--accent-soft);
border-radius:8px;padding:11px 14px;}
.notice b{font-family:var(--mono);font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;
color:var(--accent);display:block;margin-bottom:2px;font-weight:700;}
details.src{margin:16px 0 0;border-top:1px solid var(--line);padding-top:12px;}
details.src summary{font-family:var(--mono);font-size:.76rem;color:var(--muted);cursor:pointer;
letter-spacing:.04em;text-transform:uppercase;list-style:none;}
details.src summary::-webkit-details-marker{display:none;}
details.src summary::before{content:"› ";color:var(--accent);}
details.src[open] summary::before{content:"⌄ ";}
pre{margin:12px 0 0;overflow-x:auto;background:#1b2431;color:#e7ebf0;border-radius:8px;
padding:14px 16px;font-family:var(--mono);font-size:.82rem;line-height:1.55;}
footer{margin-top:64px;padding-top:20px;border-top:1px solid var(--line-2);color:var(--faint);font-size:.85rem;}
footer code{font-family:var(--mono);color:var(--muted);}
a:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px;}
@media (max-width:640px){
.uses{grid-template-columns:1fr;}
.wrap{padding:0 16px 64px;}
}
@media print{
body{background:#fff;}
.card,.index{break-inside:avoid;border-color:#000;}
.notice{background:#fff;border:1px solid #000;color:#000;}
pre{background:#fff;color:#000;border:1px solid #000;}
details.src{display:block;}
details.src[open]summary::before,details.src summary::before{content:"";}
}
"""
def card_html(d: dict) -> str:
svg = SVGS.get(d["id"], "<p>[not rendered]</p>")
live = ('<span class="badge live">Inline icons ✓</span>' if d["icons"]
else '<span class="badge static">Notation-only</span>')
when = "".join(f"<li>{esc(x)}</li>" for x in d["when"])
avoid = "".join(f"<li>{esc(x)}</li>" for x in d["avoid"])
code = esc(d["code"])
return f"""
<article class="card" id="{d['id']}">
<div class="card-top">
<h3>{esc(d['title'])}</h3>
<span class="badge type">{esc(d['keyword'])}</span>
{live}
</div>
<p class="lead">{esc(d['reader_q'])}</p>
<div class="uses">
<div class="col good"><h4>Reach for it when</h4><ul>{when}</ul></div>
<div class="col avoid"><h4>Not the right tool when</h4><ul>{avoid}</ul></div>
</div>
<div class="well"><div class="well-scroll">{svg}</div></div>
<p class="notice"><b>What to notice</b>{esc(d['notice'])}</p>
<details class="src"><summary>Mermaid source</summary><pre>{code}</pre></details>
</article>"""
def index_rows() -> str:
rows = []
for d in DIAGRAMS:
rows.append(
f'<tr><td>{esc(d["reader_q"])}</td>'
f'<td><a href="#{d["id"]}">{esc(d["title"])}</a></td>'
f'<td class="k">{esc(d["keyword"])}</td></tr>'
)
return "".join(rows)
def build_inner() -> str:
parts = []
parts.append('<div class="wrap">')
parts.append(f"<style>{CSS}</style>")
# masthead
parts.append("""
<header class="masthead">
<div class="eyebrow">Mermaid · for digital service teams</div>
<h1>The diagram catalog</h1>
<p class="dek">Sixteen diagram types, each matched to the question it answers — so a service team can pick the right picture, and every diagram of that kind comes out looking the same way twice.</p>
<div class="how">
<p>Read it like a sample book. Every card names the one reader question the diagram answers, when to reach for it, when to reach for something else, and what a good one looks like rendered — using a single worked example (a parking-permit service) so the styles are comparable across types.</p>
<p>Point at a card and say &ldquo;that&rsquo;s the look we want.&rdquo; That agreement is what keeps diagrams consistent whether a person or the model draws the next one.</p>
</div>
<div class="legend">
<span><b>Inline icons ✓</b> this type paints inline FontAwesome icons</span>
<span><b>Notation-only</b> its own notation carries the meaning — labels stay plain</span>
</div>
</header>""")
# quick-pick index
parts.append(f"""
<section class="index" aria-label="Quick pick">
<h2>If you need to show&hellip; reach for</h2>
<div class="index-scroll">
<table class="pick">
<thead><tr><th>To answer this question</th><th>Reach for</th><th>Type</th></tr></thead>
<tbody>{index_rows()}</tbody>
</table>
</div>
</section>""")
# phases
for n, (gid, gtitle, gnote) in enumerate(GROUPS, 1):
cards = "".join(card_html(d) for d in DIAGRAMS if d["group"] == gid)
parts.append(f"""
<section class="phase">
<div class="phase-head"><span class="phase-num">{n:02d}</span><h2>{esc(gtitle)}</h2></div>
<p class="phase-note">{esc(gnote)}</p>
{cards}
</section>""")
parts.append("""
<footer>
<p>One worked example, sixteen ways to draw it. Generated from the <code>mermaid</code> skill&rsquo;s
<code>service-team-diagrams.md</code> reference — every diagram here is lint-clean and copy-ready.</p>
</footer>
</div>""")
return "".join(parts)
INNER = build_inner()
DOC = (
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
"<title>The diagram catalog — Mermaid for digital service teams</title>"
+ FA_HEAD +
"</head><body>" + INNER + "</body></html>"
)
# The Artifact fragment can't reach a CDN (strict CSP), so it only shows icons
# when FontAwesome is vendored and embedded; otherwise the labels still read.
ARTIFACT_TITLE = "<title>The diagram catalog — Mermaid for digital service teams</title>\n"
(HERE / "gallery.html").write_text(DOC, encoding="utf-8")
(HERE / "gallery_artifact.html").write_text(ARTIFACT_TITLE + FA_EMBED + INNER, encoding="utf-8")
print("wrote gallery.html and gallery_artifact.html")
print("standalone size:", len((HERE / 'gallery.html').read_text()), "bytes")