jekyll-theme-zer0 1.28.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1143 -13
- data/_data/README.md +2 -0
- data/_data/ai.yml +5 -3
- data/_data/ai_pricing.yml +36 -0
- data/_data/backlog.yml +507 -2
- data/_data/consumers.yml +157 -9
- data/_data/features.yml +303 -20
- data/_data/feedback_types.yml +17 -12
- data/_data/i18n/fr.yml +12 -7
- data/_data/i18n/manifest.yml +39 -12
- data/_data/ingredient_densities.yml +122 -0
- data/_data/landing.yml +5 -2
- data/_data/navigation/main.yml +16 -0
- data/_data/navigation/quickstart.yml +4 -0
- data/_data/recipe_courses.yml +64 -0
- data/_data/site_builder.yml +874 -0
- data/_data/theme-manifest.yml +160 -124
- data/_data/ui-text.yml +26 -0
- data/_includes/README.md +26 -2
- data/_includes/analytics/posthog.html +2 -2
- data/_includes/components/admin-links.html +2 -2
- data/_includes/components/admin-tabs.html +2 -2
- data/_includes/components/ai-chat.html +14 -11
- data/_includes/components/analytics-dashboard.html +8 -8
- data/_includes/components/author-bio.html +1 -1
- data/_includes/components/author-card.html +10 -2
- data/_includes/components/author-eeat.html +4 -4
- data/_includes/components/background-customizer.html +10 -10
- data/_includes/components/background-image.html +114 -0
- data/_includes/components/background-settings.html +28 -15
- data/_includes/components/collection-manager.html +5 -5
- data/_includes/components/component-showcase.html +13 -13
- data/_includes/components/config-editor.html +12 -12
- data/_includes/components/config-viewer.html +8 -8
- data/_includes/components/cookie-consent.html +15 -15
- data/_includes/components/cta-button.html +7 -2
- data/_includes/components/dev-shortcuts.html +7 -7
- data/_includes/components/env-dashboard.html +8 -8
- data/_includes/components/env-switcher.html +9 -9
- data/_includes/components/feature-card.html +2 -2
- data/_includes/components/halfmoon.html +2 -2
- data/_includes/components/info-section.html +42 -37
- data/_includes/components/js-cdn.html +15 -15
- data/_includes/components/language-toggle.html +168 -21
- data/_includes/components/mermaid.html +72 -435
- data/_includes/components/nanobar.html +5 -5
- data/_includes/components/nav-editor.html +2 -2
- data/_includes/components/nav-export.html +2 -2
- data/_includes/components/nav-overview.html +2 -2
- data/_includes/components/page-feedback.html +45 -30
- data/_includes/components/page-views-init.html +55 -0
- data/_includes/components/page-views.html +33 -0
- data/_includes/components/post-card.html +22 -22
- data/_includes/components/post-type-badge.html +2 -2
- data/_includes/components/powered-by.html +2 -2
- data/_includes/components/preview-image.html +6 -0
- data/_includes/components/quick-index.html +2 -2
- data/_includes/components/recipe-card.html +67 -0
- data/_includes/components/recipe-duration.html +50 -0
- data/_includes/components/recipe-grams.html +58 -0
- data/_includes/components/recipe-index.html +96 -0
- data/_includes/components/recipe-ingredients.html +90 -0
- data/_includes/components/recipe-meta.html +96 -0
- data/_includes/components/recipe-nutrition.html +57 -0
- data/_includes/components/recipe-qty.html +73 -0
- data/_includes/components/recipe-ratio.html +151 -0
- data/_includes/components/recipe-scaler.html +73 -0
- data/_includes/components/recipe-steps.html +86 -0
- data/_includes/components/recipe-temp.html +45 -0
- data/_includes/components/search-modal.html +29 -4
- data/_includes/components/searchbar.html +2 -2
- data/_includes/components/shortcuts-modal.html +3 -0
- data/_includes/components/svg-background.html +2 -2
- data/_includes/components/theme-customizer.html +2 -2
- data/_includes/components/theme-info.html +14 -7
- data/_includes/components/theme-preview-gallery.html +22 -22
- data/_includes/content/giscus.html +2 -2
- data/_includes/content/intro.html +8 -8
- data/_includes/content/jsonld-faq.html +2 -2
- data/_includes/content/jsonld-software.html +24 -5
- data/_includes/content/seo.html +4 -4
- data/_includes/content/sitemap.html +27 -27
- data/_includes/content/toc.html +183 -183
- data/_includes/core/branding.html +6 -6
- data/_includes/core/console-capture.html +32 -74
- data/_includes/core/favicon.html +49 -7
- data/_includes/core/footer-fabs.html +17 -3
- data/_includes/core/footer.html +49 -34
- data/_includes/core/head.html +110 -86
- data/_includes/core/header.html +76 -54
- data/_includes/docs/bootstrap-docs.html +8 -8
- data/_includes/landing/landing-install-cards.html +2 -2
- data/_includes/landing/landing-quick-links.html +1 -1
- data/_includes/navigation/admin-nav.html +2 -2
- data/_includes/navigation/nav-tree.html +8 -8
- data/_includes/navigation/navbar.html +12 -12
- data/_includes/navigation/section-sidebar.html +109 -27
- data/_includes/navigation/sidebar-config.html +36 -2
- data/_includes/navigation/sidebar-left.html +17 -16
- data/_includes/navigation/sidebar-right.html +8 -7
- data/_includes/obsidian/full-graph.html +2 -2
- data/_includes/setup/claude-session.html +72 -0
- data/_includes/setup/prereq-checklist.html +90 -0
- data/_includes/setup/wizard.html +924 -222
- data/_includes/stats/stats-categories.html +8 -8
- data/_includes/stats/stats-header.html +14 -14
- data/_includes/stats/stats-metrics.html +14 -14
- data/_includes/stats/stats-no-data.html +12 -12
- data/_includes/stats/stats-overview.html +6 -6
- data/_includes/stats/stats-tags.html +8 -8
- data/_layouts/404.html +38 -24
- data/_layouts/README.md +2 -0
- data/_layouts/admin.html +24 -24
- data/_layouts/article.html +43 -33
- data/_layouts/author.html +20 -20
- data/_layouts/authors.html +2 -2
- data/_layouts/book-abc.html +12 -12
- data/_layouts/book-story.html +15 -15
- data/_layouts/book.html +12 -12
- data/_layouts/collection.html +33 -33
- data/_layouts/cookbook.html +88 -0
- data/_layouts/default.html +31 -28
- data/_layouts/home.html +23 -23
- data/_layouts/index.html +10 -10
- data/_layouts/landing.html +17 -17
- data/_layouts/news.html +44 -44
- data/_layouts/note.html +38 -38
- data/_layouts/notebook.html +34 -34
- data/_layouts/recipe.html +274 -0
- data/_layouts/root.html +92 -55
- data/_layouts/section.html +62 -33
- data/_layouts/setup.html +3 -3
- data/_layouts/sitemap-collection.html +49 -49
- data/_layouts/stats.html +40 -40
- data/_layouts/tag.html +12 -12
- data/_layouts/welcome.html +21 -21
- data/_sass/components/_callout.scss +1 -1
- data/_sass/components/_footer.scss +37 -1
- data/_sass/components/_mermaid.scss +375 -0
- data/_sass/components/_page-views.scss +36 -0
- data/_sass/components/_recipe.scss +506 -0
- data/_sass/components/_setup-wizard.scss +764 -0
- data/_sass/components/_ui-enhancements.scss +6 -6
- data/_sass/core/_navbar.scss +261 -46
- data/_sass/layouts/_landing.scss +2 -2
- data/_sass/layouts/_navbar-extras.scss +14 -4
- data/_sass/tokens/_color.scss +6 -0
- data/_sass/tokens/_index.scss +2 -0
- data/_sass/tokens/_radius.scss +21 -0
- data/_sass/tokens/_typography.scss +4 -0
- data/_sass/utilities/_focus.scss +14 -0
- data/assets/css/main.scss +4 -0
- data/assets/js/ai-chat.js +47 -5
- data/assets/js/fleet-feedback-capture.js +124 -0
- data/assets/js/fleet-feedback.js +853 -0
- data/assets/js/mermaid-diagrams.js +1267 -0
- data/assets/js/modules/navigation/config.js +9 -6
- data/assets/js/modules/navigation/navbar.js +55 -0
- data/assets/js/modules/navigation/scroll-spy.js +315 -80
- data/assets/js/modules/theme/appearance.js +8 -2
- data/assets/js/obsidian-wiki-links.js +8 -3
- data/assets/js/page-feedback.js +125 -192
- data/assets/js/page-views.js +372 -0
- data/assets/js/recipe-scaler.js +501 -0
- data/assets/js/search-modal.js +36 -0
- data/assets/js/setup-wizard.js +2279 -226
- data/assets/js/site-builder.js +1834 -0
- data/assets/js/ui-enhancements.js +11 -3
- data/scripts/README.md +44 -0
- data/scripts/ai/README.md +38 -0
- data/scripts/ai/api_call.rb +124 -0
- data/scripts/ai/usage.rb +314 -0
- data/scripts/ai/usage_report.rb +225 -0
- data/scripts/bin/audit-consumer +39 -7
- data/scripts/bin/giscus-discussions +213 -14
- data/scripts/bin/manifest +35 -12
- data/scripts/ci/agent_review_result.py +164 -0
- data/scripts/ci/test_agent_review_result.py +172 -0
- data/scripts/ci/test_visual_evidence_autogen.py +341 -0
- data/scripts/ci/visual_evidence_autogen.py +1060 -0
- data/scripts/content-review.rb +20 -1
- data/scripts/design-system-check.rb +170 -0
- data/scripts/lib/audit.sh +42 -2
- data/scripts/lint-liquid-raw.rb +137 -0
- data/scripts/test/integration/mermaid +22 -8
- data/scripts/test/lib/run_tests.sh +3 -1
- data/scripts/test/lib/test_agent_review_result.sh +27 -0
- data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
- data/scripts/translate.rb +94 -16
- metadata +48 -2
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
// Feature: ZER0-013
|
|
2
|
+
/*
|
|
3
|
+
* ===================================================================
|
|
4
|
+
* mermaid-diagrams.js — Mermaid diagram figures (render + chrome)
|
|
5
|
+
* ===================================================================
|
|
6
|
+
*
|
|
7
|
+
* File: assets/js/mermaid-diagrams.js
|
|
8
|
+
* Purpose: Turn every ```mermaid fence (and legacy <div class="mermaid">)
|
|
9
|
+
* into an accessible <figure> with a rendered SVG, a toolbar
|
|
10
|
+
* (zoom, fullscreen, copy source, download SVG), a caption taken
|
|
11
|
+
* from the diagram's accTitle / title, and a graceful error state
|
|
12
|
+
* that keeps the diagram source on the page instead of losing it.
|
|
13
|
+
*
|
|
14
|
+
* Loaded by _includes/components/mermaid.html — only on pages that opt in
|
|
15
|
+
* with `mermaid: true` — after the vendored mermaid.min.js. Both scripts
|
|
16
|
+
* are `defer`, so this runs once the DOM is parsed but BEFORE
|
|
17
|
+
* DOMContentLoaded: fences are converted before code-copy.js decorates
|
|
18
|
+
* <pre> blocks, so a diagram never gets a copy button and a line gutter.
|
|
19
|
+
*
|
|
20
|
+
* Reads one JSON block injected by the include:
|
|
21
|
+
* #mermaidConfig — securityLevel, toolbar/fullscreen/download flags, labels
|
|
22
|
+
*
|
|
23
|
+
* Theming
|
|
24
|
+
* No colour is hard-coded. The palette is derived from the live design
|
|
25
|
+
* tokens (--zer0-color-*, --bs-*) every time a diagram is rendered, so
|
|
26
|
+
* diagrams follow the active colour mode (light / dark / wizard), the
|
|
27
|
+
* selected skin, and any `theme_color` or Appearance-panel override — and
|
|
28
|
+
* re-render when any of those change. "Dark mode" is decided by the page
|
|
29
|
+
* background's luminance, not by the name of an attribute value, so a
|
|
30
|
+
* dark skin on a light mode still gets legible ink.
|
|
31
|
+
*
|
|
32
|
+
* Re-rendering
|
|
33
|
+
* Mermaid marks a node `data-processed` and replaces its text with the
|
|
34
|
+
* SVG, so the source is gone after the first render. Every source is kept
|
|
35
|
+
* here (WeakMap keyed by figure) and each render goes through
|
|
36
|
+
* mermaid.render(), so a theme switch re-renders from the original text.
|
|
37
|
+
*
|
|
38
|
+
* Public API (also what the Playwright spec drives):
|
|
39
|
+
* window.zer0Mermaid.renderAll(root?) -> Promise<figure[]> convert + render fences under root
|
|
40
|
+
* window.zer0Mermaid.render(el, src?) -> Promise<figure> render one element (or replace it with src)
|
|
41
|
+
* window.zer0Mermaid.refresh() -> Promise<figure[]> re-derive the palette, re-render every figure
|
|
42
|
+
* window.zer0Mermaid.getSource(figure) -> string | null
|
|
43
|
+
* window.zer0Mermaid.palette() -> { dark, vars } the derived Mermaid themeVariables
|
|
44
|
+
* window.zer0Mermaid.figures() -> figure[]
|
|
45
|
+
* Events
|
|
46
|
+
* `zer0:diagram-rendered` on each figure (bubbles): { ok, type, error }
|
|
47
|
+
* `zer0:diagrams-ready` on document after a renderAll/refresh: { count, failed }
|
|
48
|
+
* ===================================================================
|
|
49
|
+
*/
|
|
50
|
+
(function () {
|
|
51
|
+
"use strict";
|
|
52
|
+
|
|
53
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------
|
|
56
|
+
// Configuration
|
|
57
|
+
// ---------------------------------------------------------------------
|
|
58
|
+
function readJSON(id, fallback) {
|
|
59
|
+
var el = document.getElementById(id);
|
|
60
|
+
if (!el) return fallback;
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(el.textContent);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var CONFIG = readJSON("mermaidConfig", null) || {};
|
|
69
|
+
var LABELS = Object.assign(
|
|
70
|
+
{
|
|
71
|
+
diagram: "Diagram",
|
|
72
|
+
tools: "Diagram tools",
|
|
73
|
+
zoomIn: "Zoom in",
|
|
74
|
+
zoomOut: "Zoom out",
|
|
75
|
+
zoomReset: "Reset zoom",
|
|
76
|
+
fullscreen: "View fullscreen",
|
|
77
|
+
close: "Close fullscreen view",
|
|
78
|
+
copy: "Copy diagram source",
|
|
79
|
+
copied: "Copied",
|
|
80
|
+
copyFailed: "Copy failed",
|
|
81
|
+
download: "Download as SVG",
|
|
82
|
+
errorTitle: "This diagram could not be rendered",
|
|
83
|
+
errorHint: "Check the syntax in the Mermaid Live Editor. The source is shown below so nothing is lost.",
|
|
84
|
+
showSource: "Diagram source",
|
|
85
|
+
loading: "Loading diagram",
|
|
86
|
+
},
|
|
87
|
+
CONFIG.labels || {}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
var SHOW_TOOLBAR = CONFIG.toolbar !== false;
|
|
91
|
+
var ALLOW_FULLSCREEN = CONFIG.fullscreen !== false;
|
|
92
|
+
var ALLOW_DOWNLOAD = CONFIG.download !== false;
|
|
93
|
+
var SECURITY_LEVEL = CONFIG.securityLevel || "strict";
|
|
94
|
+
var SUPPORTS_DIALOG = typeof HTMLDialogElement !== "undefined" &&
|
|
95
|
+
typeof HTMLDialogElement.prototype.showModal === "function";
|
|
96
|
+
|
|
97
|
+
var ZOOM_MIN = 0.5;
|
|
98
|
+
var ZOOM_MAX = 4;
|
|
99
|
+
var ZOOM_STEP = 1.25;
|
|
100
|
+
var EPSILON = 0.001;
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------
|
|
103
|
+
// Diagram-type detection (for accessible names + the figure's data attr)
|
|
104
|
+
// ---------------------------------------------------------------------
|
|
105
|
+
var TYPES = [
|
|
106
|
+
[/^(graph|flowchart)\b/i, "flowchart", "Flowchart"],
|
|
107
|
+
[/^sequenceDiagram\b/i, "sequence", "Sequence diagram"],
|
|
108
|
+
[/^classDiagram\b/i, "class", "Class diagram"],
|
|
109
|
+
[/^stateDiagram\b/i, "state", "State diagram"],
|
|
110
|
+
[/^erDiagram\b/i, "er", "Entity relationship diagram"],
|
|
111
|
+
[/^gantt\b/i, "gantt", "Gantt chart"],
|
|
112
|
+
[/^pie\b/i, "pie", "Pie chart"],
|
|
113
|
+
[/^gitGraph\b/i, "git", "Git graph"],
|
|
114
|
+
[/^journey\b/i, "journey", "User journey"],
|
|
115
|
+
[/^mindmap\b/i, "mindmap", "Mind map"],
|
|
116
|
+
[/^timeline\b/i, "timeline", "Timeline"],
|
|
117
|
+
[/^quadrantChart\b/i, "quadrant", "Quadrant chart"],
|
|
118
|
+
[/^requirementDiagram\b/i, "requirement", "Requirement diagram"],
|
|
119
|
+
[/^C4(Context|Container|Component|Dynamic|Deployment)\b/i, "c4", "C4 diagram"],
|
|
120
|
+
[/^sankey(-beta)?\b/i, "sankey", "Sankey diagram"],
|
|
121
|
+
[/^xychart(-beta)?\b/i, "xychart", "XY chart"],
|
|
122
|
+
[/^block(-beta)?\b/i, "block", "Block diagram"],
|
|
123
|
+
[/^zenuml\b/i, "zenuml", "ZenUML sequence diagram"],
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
function parseMeta(source) {
|
|
127
|
+
var text = String(source || "").replace(/\r\n?/g, "\n").trim();
|
|
128
|
+
var title = null;
|
|
129
|
+
var description = null;
|
|
130
|
+
|
|
131
|
+
// Optional YAML front matter (mermaid ≥ 10): `---\ntitle: X\n---`
|
|
132
|
+
var fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
|
|
133
|
+
if (fm) {
|
|
134
|
+
var t = fm[1].match(/^\s*title\s*:\s*(.+?)\s*$/m);
|
|
135
|
+
if (t) title = t[1].replace(/^["']|["']$/g, "");
|
|
136
|
+
}
|
|
137
|
+
// Accessibility directives win over the front-matter title.
|
|
138
|
+
var acc = text.match(/^\s*accTitle\s*:\s*(.+?)\s*$/m);
|
|
139
|
+
if (acc) title = acc[1];
|
|
140
|
+
var accDescr = text.match(/^\s*accDescr\s*:\s*(.+?)\s*$/m);
|
|
141
|
+
if (accDescr) description = accDescr[1];
|
|
142
|
+
|
|
143
|
+
var body = fm ? text.slice(fm[0].length) : text;
|
|
144
|
+
var lines = body.split("\n");
|
|
145
|
+
var first = "";
|
|
146
|
+
for (var i = 0; i < lines.length; i++) {
|
|
147
|
+
var line = lines[i].trim();
|
|
148
|
+
if (line && line.indexOf("%%") !== 0) { first = line; break; }
|
|
149
|
+
}
|
|
150
|
+
var type = "unknown";
|
|
151
|
+
var typeLabel = LABELS.diagram;
|
|
152
|
+
for (var j = 0; j < TYPES.length; j++) {
|
|
153
|
+
if (TYPES[j][0].test(first)) { type = TYPES[j][1]; typeLabel = TYPES[j][2]; break; }
|
|
154
|
+
}
|
|
155
|
+
return { source: text, title: title, description: description, type: type, typeLabel: typeLabel };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------
|
|
159
|
+
// Colour helpers — tokens → concrete hex the way the browser resolves them
|
|
160
|
+
// ---------------------------------------------------------------------
|
|
161
|
+
var probeEl = null;
|
|
162
|
+
var canvasCtx = null;
|
|
163
|
+
|
|
164
|
+
function clamp01(v) { return Math.max(0, Math.min(1, v)); }
|
|
165
|
+
|
|
166
|
+
function hexToRgb(hex) {
|
|
167
|
+
var m = String(hex).trim().match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
168
|
+
if (!m) return null;
|
|
169
|
+
var h = m[1];
|
|
170
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
171
|
+
return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function rgbToHex(c) {
|
|
175
|
+
var to = function (v) { return ("0" + Math.round(Math.max(0, Math.min(255, v))).toString(16)).slice(-2); };
|
|
176
|
+
return "#" + to(c.r) + to(c.g) + to(c.b);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Parse `rgb()` / `rgba()` with either comma or space syntax.
|
|
180
|
+
function parseRgbString(str) {
|
|
181
|
+
var m = String(str).match(/rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)/i);
|
|
182
|
+
if (!m) return null;
|
|
183
|
+
var a = 1;
|
|
184
|
+
if (m[4] !== undefined) a = m[4].slice(-1) === "%" ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
|
|
185
|
+
return { r: +m[1], g: +m[2], b: +m[3], a: isNaN(a) ? 1 : a };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Normalise any CSS colour string (hex, rgb, hsl, color-mix() output,
|
|
189
|
+
// color(srgb …)) to {r,g,b,a}. A 2D canvas is the one API every browser
|
|
190
|
+
// ships that turns any parsable colour into a canonical serialisation.
|
|
191
|
+
function toRgba(color) {
|
|
192
|
+
var direct = parseRgbString(color);
|
|
193
|
+
if (direct) return direct;
|
|
194
|
+
var hex = hexToRgb(color);
|
|
195
|
+
if (hex) { hex.a = 1; return hex; }
|
|
196
|
+
try {
|
|
197
|
+
if (!canvasCtx) canvasCtx = document.createElement("canvas").getContext("2d");
|
|
198
|
+
if (canvasCtx) {
|
|
199
|
+
canvasCtx.fillStyle = "#000000";
|
|
200
|
+
canvasCtx.fillStyle = color;
|
|
201
|
+
var out = canvasCtx.fillStyle;
|
|
202
|
+
var h2 = hexToRgb(out);
|
|
203
|
+
if (h2) { h2.a = 1; return h2; }
|
|
204
|
+
var p2 = parseRgbString(out);
|
|
205
|
+
if (p2) return p2;
|
|
206
|
+
}
|
|
207
|
+
} catch (err) { /* fall through */ }
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Composite a translucent colour over an opaque background.
|
|
212
|
+
function over(c, bg) {
|
|
213
|
+
if (!c) return null;
|
|
214
|
+
var a = c.a === undefined ? 1 : c.a;
|
|
215
|
+
if (a >= 1 || !bg) return { r: c.r, g: c.g, b: c.b };
|
|
216
|
+
return { r: c.r * a + bg.r * (1 - a), g: c.g * a + bg.g * (1 - a), b: c.b * a + bg.b * (1 - a) };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Resolve `var(--token, fallback)` exactly as the cascade sees it right now.
|
|
220
|
+
function tokenColor(token, fallback, bgRgb) {
|
|
221
|
+
if (!probeEl) {
|
|
222
|
+
probeEl = document.createElement("span");
|
|
223
|
+
probeEl.setAttribute("aria-hidden", "true");
|
|
224
|
+
probeEl.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;visibility:hidden";
|
|
225
|
+
(document.body || document.documentElement).appendChild(probeEl);
|
|
226
|
+
}
|
|
227
|
+
probeEl.style.color = "";
|
|
228
|
+
probeEl.style.color = "var(" + token + ", " + fallback + ")";
|
|
229
|
+
var resolved = null;
|
|
230
|
+
try { resolved = toRgba(getComputedStyle(probeEl).color); } catch (err) { /* ignore */ }
|
|
231
|
+
if (!resolved) resolved = toRgba(fallback);
|
|
232
|
+
return rgbToHex(over(resolved, bgRgb));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function mix(hexA, hexB, weightA) {
|
|
236
|
+
var a = hexToRgb(hexA), b = hexToRgb(hexB);
|
|
237
|
+
if (!a || !b) return hexA;
|
|
238
|
+
var w = clamp01(weightA);
|
|
239
|
+
return rgbToHex({ r: a.r * w + b.r * (1 - w), g: a.g * w + b.g * (1 - w), b: a.b * w + b.b * (1 - w) });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function luminance(hex) {
|
|
243
|
+
var c = hexToRgb(hex);
|
|
244
|
+
if (!c) return 1;
|
|
245
|
+
var lin = function (v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
246
|
+
return 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function rgbToHsl(c) {
|
|
250
|
+
var r = c.r / 255, g = c.g / 255, b = c.b / 255;
|
|
251
|
+
var max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
252
|
+
var h = 0, s = 0, l = (max + min) / 2;
|
|
253
|
+
if (max !== min) {
|
|
254
|
+
var d = max - min;
|
|
255
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
256
|
+
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
257
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
258
|
+
else h = (r - g) / d + 4;
|
|
259
|
+
h *= 60;
|
|
260
|
+
}
|
|
261
|
+
return { h: h, s: s, l: l };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function hslToHex(h, s, l) {
|
|
265
|
+
h = ((h % 360) + 360) % 360;
|
|
266
|
+
var c = (1 - Math.abs(2 * l - 1)) * s;
|
|
267
|
+
var x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
268
|
+
var m = l - c / 2;
|
|
269
|
+
var r = 0, g = 0, b = 0;
|
|
270
|
+
if (h < 60) { r = c; g = x; } else if (h < 120) { r = x; g = c; } else if (h < 180) { g = c; b = x; }
|
|
271
|
+
else if (h < 240) { g = x; b = c; } else if (h < 300) { r = x; b = c; } else { r = c; b = x; }
|
|
272
|
+
return rgbToHex({ r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Text colour that reads on a given fill: dark ink above the luminance
|
|
276
|
+
// where black and white contrast equally (~0.18), white below it. A light
|
|
277
|
+
// brand such as the "air" skin's #6fa8dc (L≈0.37) needs dark text — white
|
|
278
|
+
// on it is only 2.5:1.
|
|
279
|
+
function inkOn(fillHex, ink) {
|
|
280
|
+
return luminance(fillHex) > 0.18 ? (luminance(ink) < 0.4 ? ink : "#1b1f22") : "#ffffff";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function contrastRatio(a, b) {
|
|
284
|
+
var la = luminance(a), lb = luminance(b);
|
|
285
|
+
var hi = Math.max(la, lb), lo = Math.min(la, lb);
|
|
286
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Nudge `hex` toward `towards` until it reaches `min` contrast against `bg`.
|
|
290
|
+
// Guards palettes where the brand colour is (nearly) the page background —
|
|
291
|
+
// wizard mode paints the body in the brand blue, so blue nodes on a blue
|
|
292
|
+
// page would vanish.
|
|
293
|
+
function ensureContrast(hex, bg, min, towards) {
|
|
294
|
+
var out = hex;
|
|
295
|
+
for (var i = 0; i < 12 && contrastRatio(out, bg) < min; i++) out = mix(towards, out, 0.15);
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// A 12-colour categorical series that starts from the brand hue and fans
|
|
300
|
+
// out around the wheel at a saturation/lightness that stays legible on the
|
|
301
|
+
// current background (pie slices, git branches, mindmap branches, x-y plots).
|
|
302
|
+
function buildSeries(primary, accent, dark) {
|
|
303
|
+
var base = rgbToHsl(hexToRgb(primary));
|
|
304
|
+
var acc = rgbToHsl(hexToRgb(accent));
|
|
305
|
+
var L = dark ? 0.62 : 0.44;
|
|
306
|
+
var S = Math.max(0.5, Math.min(0.8, base.s || 0.6));
|
|
307
|
+
var offsets = [0, 150, 60, 210, 300, 30, 180, 270, 90, 240, 330, 120];
|
|
308
|
+
var out = [];
|
|
309
|
+
for (var i = 0; i < offsets.length; i++) out.push(hslToHex(base.h + offsets[i], S, L));
|
|
310
|
+
// The accent hue is series #2 so brand pairs stay together.
|
|
311
|
+
if (acc.s > 0.2) out[1] = hslToHex(acc.h, Math.max(S, Math.min(0.85, acc.s)), L);
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------
|
|
316
|
+
// Palette → Mermaid `base` theme variables
|
|
317
|
+
// ---------------------------------------------------------------------
|
|
318
|
+
function palette() {
|
|
319
|
+
var bgHex = tokenColor("--bs-body-bg", "#ffffff", null);
|
|
320
|
+
var bgRgb = hexToRgb(bgHex);
|
|
321
|
+
var dark = luminance(bgHex) < 0.35;
|
|
322
|
+
|
|
323
|
+
var ink = tokenColor("--bs-body-color", dark ? "#dee2e6" : "#212529", bgRgb);
|
|
324
|
+
var inkMuted = tokenColor("--bs-secondary-color", dark ? "#adb5bd" : "#6c757d", bgRgb);
|
|
325
|
+
// The skin mixin writes both --bs-primary and --zer0-color-primary, but a
|
|
326
|
+
// site-level `theme_color.main` re-pins the latter at :root after main.css,
|
|
327
|
+
// so only --bs-primary reliably follows the active skin — it is what the
|
|
328
|
+
// buttons and links the reader sees are painted with.
|
|
329
|
+
var primary = tokenColor("--bs-primary", "var(--zer0-color-primary, #007bff)", bgRgb);
|
|
330
|
+
var accent = tokenColor("--zer0-color-accent", "#ffe484", bgRgb);
|
|
331
|
+
// Keep the brand usable as a border/line colour on this background.
|
|
332
|
+
primary = ensureContrast(primary, bgHex, 2.2, ink);
|
|
333
|
+
accent = ensureContrast(accent, bgHex, 1.6, ink);
|
|
334
|
+
var surface = tokenColor("--zer0-color-bg-elevated", dark ? "#2b3035" : "#f8f9fa", bgRgb);
|
|
335
|
+
var surface2 = tokenColor("--zer0-color-bg-muted", dark ? "#343a40" : "#e9ecef", bgRgb);
|
|
336
|
+
var border = tokenColor("--zer0-color-border", dark ? "#495057" : "#dee2e6", bgRgb);
|
|
337
|
+
var success = tokenColor("--zer0-color-success", "#198754", bgRgb);
|
|
338
|
+
var info = tokenColor("--zer0-color-info", "#0dcaf0", bgRgb);
|
|
339
|
+
var warning = tokenColor("--zer0-color-warning", "#ffc107", bgRgb);
|
|
340
|
+
var danger = tokenColor("--zer0-color-danger", "#dc3545", bgRgb);
|
|
341
|
+
var link = tokenColor("--zer0-color-link", primary, bgRgb);
|
|
342
|
+
|
|
343
|
+
var fontFamily = "";
|
|
344
|
+
try { fontFamily = getComputedStyle(document.body).fontFamily; } catch (err) { /* ignore */ }
|
|
345
|
+
if (!fontFamily) fontFamily = "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif";
|
|
346
|
+
|
|
347
|
+
// Tints: a colour laid over the page background. Dark mode needs less
|
|
348
|
+
// colour because the ink is light. A pale brand (a light-blue skin) needs
|
|
349
|
+
// MORE of itself before the tint reads as a surface, so the weight grows
|
|
350
|
+
// until the fill separates from the page — that keeps the hue instead of
|
|
351
|
+
// greying it. When the brand IS the page colour (wizard mode) no weight
|
|
352
|
+
// helps, so fall back to nudging toward the ink.
|
|
353
|
+
var tintTo = function (hex, minRatio) {
|
|
354
|
+
var w = dark ? 0.3 : 0.14;
|
|
355
|
+
var out = mix(hex, bgHex, w);
|
|
356
|
+
while (contrastRatio(out, bgHex) < minRatio && w < 0.6) {
|
|
357
|
+
w += 0.06;
|
|
358
|
+
out = mix(hex, bgHex, w);
|
|
359
|
+
}
|
|
360
|
+
return ensureContrast(out, bgHex, minRatio, ink);
|
|
361
|
+
};
|
|
362
|
+
var tint = function (hex) { return tintTo(hex, 1.12); };
|
|
363
|
+
var tintStrong = function (hex) { return ensureContrast(mix(hex, bgHex, dark ? 0.45 : 0.32), bgHex, 1.25, ink); };
|
|
364
|
+
var deepen = function (hex) { return mix(hex, dark ? "#ffffff" : "#000000", dark ? 0.85 : 0.8); };
|
|
365
|
+
|
|
366
|
+
var series = buildSeries(primary, accent, dark);
|
|
367
|
+
var accentDeep = deepen(accent);
|
|
368
|
+
var nodeFill = tintTo(primary, 1.15);
|
|
369
|
+
var nodeFill2 = tintTo(accent, 1.15);
|
|
370
|
+
var onPrimary = inkOn(primary, ink);
|
|
371
|
+
var edgeLabelBg = mix(surface, bgHex, 0.6);
|
|
372
|
+
|
|
373
|
+
var vars = {
|
|
374
|
+
darkMode: dark,
|
|
375
|
+
background: bgHex,
|
|
376
|
+
fontFamily: fontFamily,
|
|
377
|
+
fontSize: "14px",
|
|
378
|
+
|
|
379
|
+
// Core trio (most diagram types derive from these)
|
|
380
|
+
primaryColor: nodeFill,
|
|
381
|
+
primaryTextColor: ink,
|
|
382
|
+
primaryBorderColor: primary,
|
|
383
|
+
secondaryColor: nodeFill2,
|
|
384
|
+
secondaryTextColor: ink,
|
|
385
|
+
secondaryBorderColor: accentDeep,
|
|
386
|
+
tertiaryColor: surface,
|
|
387
|
+
tertiaryTextColor: ink,
|
|
388
|
+
tertiaryBorderColor: border,
|
|
389
|
+
|
|
390
|
+
lineColor: inkMuted,
|
|
391
|
+
textColor: ink,
|
|
392
|
+
mainBkg: nodeFill,
|
|
393
|
+
secondBkg: nodeFill2,
|
|
394
|
+
mainContrastColor: ink,
|
|
395
|
+
darkTextColor: ink,
|
|
396
|
+
border1: primary,
|
|
397
|
+
border2: border,
|
|
398
|
+
arrowheadColor: inkMuted,
|
|
399
|
+
labelBackground: edgeLabelBg,
|
|
400
|
+
labelColor: ink,
|
|
401
|
+
titleColor: ink,
|
|
402
|
+
|
|
403
|
+
// Flowchart
|
|
404
|
+
nodeBkg: nodeFill,
|
|
405
|
+
nodeBorder: primary,
|
|
406
|
+
nodeTextColor: ink,
|
|
407
|
+
clusterBkg: mix(surface, bgHex, 0.7),
|
|
408
|
+
clusterBorder: border,
|
|
409
|
+
defaultLinkColor: inkMuted,
|
|
410
|
+
edgeLabelBackground: edgeLabelBg,
|
|
411
|
+
|
|
412
|
+
// Notes
|
|
413
|
+
noteBkgColor: tintStrong(warning),
|
|
414
|
+
noteTextColor: ink,
|
|
415
|
+
noteBorderColor: deepen(warning),
|
|
416
|
+
|
|
417
|
+
// Sequence
|
|
418
|
+
actorBkg: nodeFill,
|
|
419
|
+
actorBorder: primary,
|
|
420
|
+
actorTextColor: ink,
|
|
421
|
+
actorLineColor: inkMuted,
|
|
422
|
+
signalColor: ink,
|
|
423
|
+
signalTextColor: ink,
|
|
424
|
+
labelBoxBkgColor: surface,
|
|
425
|
+
labelBoxBorderColor: border,
|
|
426
|
+
labelTextColor: ink,
|
|
427
|
+
loopTextColor: ink,
|
|
428
|
+
activationBkgColor: tintStrong(primary),
|
|
429
|
+
activationBorderColor: primary,
|
|
430
|
+
sequenceNumberColor: onPrimary,
|
|
431
|
+
|
|
432
|
+
// Gantt
|
|
433
|
+
sectionBkgColor: mix(primary, bgHex, dark ? 0.16 : 0.08),
|
|
434
|
+
altSectionBkgColor: bgHex,
|
|
435
|
+
sectionBkgColor2: mix(accent, bgHex, dark ? 0.16 : 0.12),
|
|
436
|
+
excludeBkgColor: surface2,
|
|
437
|
+
taskBorderColor: deepen(primary),
|
|
438
|
+
taskBkgColor: primary,
|
|
439
|
+
taskTextColor: onPrimary,
|
|
440
|
+
taskTextLightColor: onPrimary,
|
|
441
|
+
taskTextDarkColor: ink,
|
|
442
|
+
taskTextOutsideColor: ink,
|
|
443
|
+
taskTextClickableColor: link,
|
|
444
|
+
activeTaskBorderColor: accentDeep,
|
|
445
|
+
activeTaskBkgColor: tintStrong(accent),
|
|
446
|
+
gridColor: border,
|
|
447
|
+
doneTaskBkgColor: surface2,
|
|
448
|
+
doneTaskBorderColor: border,
|
|
449
|
+
critBorderColor: danger,
|
|
450
|
+
critBkgColor: tintStrong(danger),
|
|
451
|
+
todayLineColor: danger,
|
|
452
|
+
|
|
453
|
+
// Class / state / ER
|
|
454
|
+
classText: ink,
|
|
455
|
+
transitionColor: inkMuted,
|
|
456
|
+
transitionLabelColor: ink,
|
|
457
|
+
stateLabelColor: ink,
|
|
458
|
+
stateBkg: nodeFill,
|
|
459
|
+
labelBackgroundColor: bgHex,
|
|
460
|
+
compositeBackground: surface,
|
|
461
|
+
altBackground: surface2,
|
|
462
|
+
compositeTitleBackground: tintStrong(primary),
|
|
463
|
+
compositeBorder: border,
|
|
464
|
+
innerEndBackground: primary,
|
|
465
|
+
specialStateColor: ink,
|
|
466
|
+
attributeBackgroundColorOdd: bgHex,
|
|
467
|
+
attributeBackgroundColorEven: surface,
|
|
468
|
+
|
|
469
|
+
// Requirement / C4 / errors
|
|
470
|
+
requirementBackground: nodeFill,
|
|
471
|
+
requirementBorderColor: primary,
|
|
472
|
+
requirementTextColor: ink,
|
|
473
|
+
relationColor: inkMuted,
|
|
474
|
+
relationLabelBackground: bgHex,
|
|
475
|
+
relationLabelColor: ink,
|
|
476
|
+
personBorder: primary,
|
|
477
|
+
personBkg: nodeFill,
|
|
478
|
+
errorBkgColor: tintStrong(danger),
|
|
479
|
+
errorTextColor: ink,
|
|
480
|
+
|
|
481
|
+
// Pie
|
|
482
|
+
pieTitleTextColor: ink,
|
|
483
|
+
pieSectionTextColor: "#ffffff",
|
|
484
|
+
pieLegendTextColor: ink,
|
|
485
|
+
pieStrokeColor: bgHex,
|
|
486
|
+
pieOuterStrokeColor: border,
|
|
487
|
+
pieOpacity: 0.9,
|
|
488
|
+
|
|
489
|
+
// Git graph
|
|
490
|
+
commitLabelColor: ink,
|
|
491
|
+
commitLabelBackground: surface2,
|
|
492
|
+
tagLabelColor: ink,
|
|
493
|
+
tagLabelBackground: tintStrong(accent),
|
|
494
|
+
tagLabelBorder: accentDeep,
|
|
495
|
+
|
|
496
|
+
// Quadrant
|
|
497
|
+
quadrant1Fill: tint(series[0]),
|
|
498
|
+
quadrant2Fill: tint(series[1]),
|
|
499
|
+
quadrant3Fill: tint(series[2]),
|
|
500
|
+
quadrant4Fill: tint(series[3]),
|
|
501
|
+
quadrant1TextFill: ink,
|
|
502
|
+
quadrant2TextFill: ink,
|
|
503
|
+
quadrant3TextFill: ink,
|
|
504
|
+
quadrant4TextFill: ink,
|
|
505
|
+
quadrantPointFill: primary,
|
|
506
|
+
quadrantPointTextFill: ink,
|
|
507
|
+
quadrantXAxisTextFill: ink,
|
|
508
|
+
quadrantYAxisTextFill: ink,
|
|
509
|
+
quadrantInternalBorderStrokeFill: border,
|
|
510
|
+
quadrantExternalBorderStrokeFill: border,
|
|
511
|
+
quadrantTitleFill: ink,
|
|
512
|
+
|
|
513
|
+
// XY chart
|
|
514
|
+
xyChart: {
|
|
515
|
+
backgroundColor: bgHex,
|
|
516
|
+
titleColor: ink,
|
|
517
|
+
xAxisLabelColor: ink,
|
|
518
|
+
xAxisTitleColor: ink,
|
|
519
|
+
xAxisTickColor: inkMuted,
|
|
520
|
+
xAxisLineColor: border,
|
|
521
|
+
yAxisLabelColor: ink,
|
|
522
|
+
yAxisTitleColor: ink,
|
|
523
|
+
yAxisTickColor: inkMuted,
|
|
524
|
+
yAxisLineColor: border,
|
|
525
|
+
plotColorPalette: series.join(","),
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// Indexed series: pie slices, git branches, journey/mindmap/timeline scales.
|
|
530
|
+
for (var i = 0; i < 12; i++) {
|
|
531
|
+
vars["pie" + (i + 1)] = series[i];
|
|
532
|
+
vars["cScale" + i] = tintStrong(series[i]);
|
|
533
|
+
vars["cScalePeer" + i] = series[i];
|
|
534
|
+
vars["cScaleLabel" + i] = ink;
|
|
535
|
+
vars["cScaleInv" + i] = series[i];
|
|
536
|
+
if (i < 8) {
|
|
537
|
+
vars["git" + i] = series[i];
|
|
538
|
+
vars["gitInv" + i] = inkOn(series[i], ink);
|
|
539
|
+
vars["gitBranchLabel" + i] = inkOn(series[i], ink);
|
|
540
|
+
vars["fillType" + i] = tintStrong(series[i]);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return { dark: dark, vars: vars, fontFamily: fontFamily, background: bgHex };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function buildMermaidConfig(p) {
|
|
548
|
+
return {
|
|
549
|
+
startOnLoad: false,
|
|
550
|
+
securityLevel: SECURITY_LEVEL,
|
|
551
|
+
theme: "base",
|
|
552
|
+
themeVariables: p.vars,
|
|
553
|
+
fontFamily: p.fontFamily,
|
|
554
|
+
// Parse/render failures are surfaced in the figure itself (see
|
|
555
|
+
// showError); Mermaid's own console logging would only duplicate them.
|
|
556
|
+
logLevel: "fatal",
|
|
557
|
+
flowchart: {
|
|
558
|
+
useMaxWidth: true,
|
|
559
|
+
// SVG labels avoid foreignObject clipping/position bugs with theme CSS
|
|
560
|
+
htmlLabels: false,
|
|
561
|
+
curve: "basis",
|
|
562
|
+
padding: 12,
|
|
563
|
+
nodeSpacing: 40,
|
|
564
|
+
rankSpacing: 48,
|
|
565
|
+
},
|
|
566
|
+
sequence: {
|
|
567
|
+
useMaxWidth: true,
|
|
568
|
+
mirrorActors: true,
|
|
569
|
+
diagramMarginX: 24,
|
|
570
|
+
diagramMarginY: 12,
|
|
571
|
+
actorMargin: 48,
|
|
572
|
+
messageMargin: 32,
|
|
573
|
+
boxMargin: 10,
|
|
574
|
+
noteMargin: 10,
|
|
575
|
+
},
|
|
576
|
+
gantt: {
|
|
577
|
+
useMaxWidth: true,
|
|
578
|
+
titleTopMargin: 24,
|
|
579
|
+
barHeight: 22,
|
|
580
|
+
barGap: 6,
|
|
581
|
+
topPadding: 48,
|
|
582
|
+
leftPadding: 96,
|
|
583
|
+
gridLineStartPadding: 36,
|
|
584
|
+
fontSize: 12,
|
|
585
|
+
numberSectionStyles: 4,
|
|
586
|
+
axisFormat: "%Y-%m-%d",
|
|
587
|
+
},
|
|
588
|
+
class: { useMaxWidth: true },
|
|
589
|
+
state: { useMaxWidth: true },
|
|
590
|
+
er: { useMaxWidth: true },
|
|
591
|
+
pie: { useMaxWidth: true },
|
|
592
|
+
journey: { useMaxWidth: true },
|
|
593
|
+
gitGraph: { useMaxWidth: true },
|
|
594
|
+
mindmap: { useMaxWidth: true },
|
|
595
|
+
timeline: { useMaxWidth: true },
|
|
596
|
+
requirement: { useMaxWidth: true },
|
|
597
|
+
quadrantChart: { useMaxWidth: true },
|
|
598
|
+
xyChart: { useMaxWidth: true },
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
var lastPaletteKey = null;
|
|
603
|
+
var lastPalette = null;
|
|
604
|
+
|
|
605
|
+
function initMermaid(force) {
|
|
606
|
+
var p = palette();
|
|
607
|
+
var key = JSON.stringify(p.vars) + "|" + p.fontFamily;
|
|
608
|
+
if (!force && key === lastPaletteKey) return false;
|
|
609
|
+
lastPaletteKey = key;
|
|
610
|
+
lastPalette = p;
|
|
611
|
+
window.mermaid.initialize(buildMermaidConfig(p));
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ---------------------------------------------------------------------
|
|
616
|
+
// DOM helpers
|
|
617
|
+
// ---------------------------------------------------------------------
|
|
618
|
+
function el(tag, className, attrs) {
|
|
619
|
+
var node = document.createElement(tag);
|
|
620
|
+
if (className) node.className = className;
|
|
621
|
+
if (attrs) Object.keys(attrs).forEach(function (k) { node.setAttribute(k, attrs[k]); });
|
|
622
|
+
return node;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function icon(name) {
|
|
626
|
+
var i = el("i", "bi " + name, { "aria-hidden": "true" });
|
|
627
|
+
return i;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function button(iconName, label, onClick) {
|
|
631
|
+
var b = el("button", "zer0-diagram__btn", { type: "button", "aria-label": label, title: label });
|
|
632
|
+
b.appendChild(icon(iconName));
|
|
633
|
+
b.addEventListener("click", function (e) {
|
|
634
|
+
e.preventDefault();
|
|
635
|
+
e.stopPropagation();
|
|
636
|
+
onClick(b, e);
|
|
637
|
+
});
|
|
638
|
+
return b;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function fire(target, name, detail) {
|
|
642
|
+
try {
|
|
643
|
+
target.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: detail || {} }));
|
|
644
|
+
} catch (err) { /* ignore */ }
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function slugify(text) {
|
|
648
|
+
return String(text || "diagram").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "diagram";
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ---------------------------------------------------------------------
|
|
652
|
+
// Registry
|
|
653
|
+
// ---------------------------------------------------------------------
|
|
654
|
+
var registry = new WeakMap(); // figure -> { meta, scope }
|
|
655
|
+
var allFigures = [];
|
|
656
|
+
var counter = 0;
|
|
657
|
+
|
|
658
|
+
function nextId() {
|
|
659
|
+
counter += 1;
|
|
660
|
+
return "zer0-diagram-" + counter + "-" + Date.now().toString(36);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ---------------------------------------------------------------------
|
|
664
|
+
// Zoom — implemented by sizing the SVG itself (real layout, so the
|
|
665
|
+
// scrollable viewport, centring and print all keep working; no transforms).
|
|
666
|
+
// ---------------------------------------------------------------------
|
|
667
|
+
function currentZoom(canvas) {
|
|
668
|
+
var z = parseFloat(canvas.getAttribute("data-zoom"));
|
|
669
|
+
return isNaN(z) ? 1 : z;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function fitWidth(svg) {
|
|
673
|
+
// Width the SVG occupies at zoom 1 (mermaid's max-width or the viewport).
|
|
674
|
+
var rect = svg.getBoundingClientRect();
|
|
675
|
+
return rect.width || parseFloat(svg.style.maxWidth) || (svg.viewBox && svg.viewBox.baseVal.width) || 0;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function setZoom(scope, zoom) {
|
|
679
|
+
var canvas = scope.canvas;
|
|
680
|
+
var svg = canvas.querySelector("svg");
|
|
681
|
+
zoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoom));
|
|
682
|
+
var isOne = Math.abs(zoom - 1) < EPSILON;
|
|
683
|
+
|
|
684
|
+
if (svg) {
|
|
685
|
+
if (isOne) {
|
|
686
|
+
svg.style.width = "";
|
|
687
|
+
svg.style.height = "";
|
|
688
|
+
if (svg.getAttribute("data-zer0-max-width")) svg.style.maxWidth = svg.getAttribute("data-zer0-max-width");
|
|
689
|
+
canvas.removeAttribute("data-zoom");
|
|
690
|
+
// Measure the fitted size only once we are back at 1:1.
|
|
691
|
+
svg.setAttribute("data-zer0-fit-width", String(fitWidth(svg)));
|
|
692
|
+
var h = svg.getBoundingClientRect().height;
|
|
693
|
+
if (h) svg.setAttribute("data-zer0-fit-height", String(h));
|
|
694
|
+
} else {
|
|
695
|
+
var base = parseFloat(svg.getAttribute("data-zer0-fit-width"));
|
|
696
|
+
if (!base) {
|
|
697
|
+
// First zoom without a 1:1 measurement: reset, measure, continue.
|
|
698
|
+
setZoom(scope, 1);
|
|
699
|
+
base = parseFloat(svg.getAttribute("data-zer0-fit-width")) || fitWidth(svg);
|
|
700
|
+
}
|
|
701
|
+
var baseH = parseFloat(svg.getAttribute("data-zer0-fit-height"));
|
|
702
|
+
svg.style.maxWidth = "none";
|
|
703
|
+
svg.style.width = Math.round(base * zoom) + "px";
|
|
704
|
+
if (svg.hasAttribute("height") && baseH) svg.style.height = Math.round(baseH * zoom) + "px";
|
|
705
|
+
canvas.setAttribute("data-zoom", zoom.toFixed(3));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
scope.viewport.classList.toggle("is-zoomed", !isOne);
|
|
710
|
+
if (scope.level) {
|
|
711
|
+
scope.level.textContent = Math.round(zoom * 100) + "%";
|
|
712
|
+
scope.level.hidden = isOne;
|
|
713
|
+
}
|
|
714
|
+
if (scope.buttons) {
|
|
715
|
+
if (scope.buttons.zoomIn) scope.buttons.zoomIn.disabled = zoom >= ZOOM_MAX - EPSILON;
|
|
716
|
+
if (scope.buttons.zoomOut) scope.buttons.zoomOut.disabled = zoom <= ZOOM_MIN + EPSILON;
|
|
717
|
+
if (scope.buttons.reset) scope.buttons.reset.disabled = isOne;
|
|
718
|
+
}
|
|
719
|
+
return zoom;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function zoomBy(scope, factor) {
|
|
723
|
+
return setZoom(scope, currentZoom(scope.canvas) * factor);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Drag-to-pan for mouse users once the diagram overflows its viewport.
|
|
727
|
+
function enablePan(viewport) {
|
|
728
|
+
var drag = null;
|
|
729
|
+
viewport.addEventListener("pointerdown", function (e) {
|
|
730
|
+
if (e.pointerType !== "mouse" || e.button !== 0) return;
|
|
731
|
+
if (e.target.closest("a, button")) return;
|
|
732
|
+
var canScroll = viewport.scrollWidth > viewport.clientWidth + 1 || viewport.scrollHeight > viewport.clientHeight + 1;
|
|
733
|
+
if (!canScroll) return;
|
|
734
|
+
drag = { x: e.clientX, y: e.clientY, left: viewport.scrollLeft, top: viewport.scrollTop, moved: false };
|
|
735
|
+
viewport.classList.add("is-panning");
|
|
736
|
+
try { viewport.setPointerCapture(e.pointerId); } catch (err) { /* ignore */ }
|
|
737
|
+
});
|
|
738
|
+
viewport.addEventListener("pointermove", function (e) {
|
|
739
|
+
if (!drag) return;
|
|
740
|
+
var dx = e.clientX - drag.x, dy = e.clientY - drag.y;
|
|
741
|
+
if (Math.abs(dx) + Math.abs(dy) > 2) drag.moved = true;
|
|
742
|
+
viewport.scrollLeft = drag.left - dx;
|
|
743
|
+
viewport.scrollTop = drag.top - dy;
|
|
744
|
+
});
|
|
745
|
+
var end = function (e) {
|
|
746
|
+
if (!drag) return;
|
|
747
|
+
drag = null;
|
|
748
|
+
viewport.classList.remove("is-panning");
|
|
749
|
+
try { viewport.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ }
|
|
750
|
+
};
|
|
751
|
+
viewport.addEventListener("pointerup", end);
|
|
752
|
+
viewport.addEventListener("pointercancel", end);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function enableWheelZoom(viewport, scope) {
|
|
756
|
+
viewport.addEventListener("wheel", function (e) {
|
|
757
|
+
if (!(e.ctrlKey || e.metaKey)) return; // plain wheel keeps scrolling the page
|
|
758
|
+
e.preventDefault();
|
|
759
|
+
zoomBy(scope, e.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP);
|
|
760
|
+
}, { passive: false });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function enableKeys(viewport, scope) {
|
|
764
|
+
viewport.addEventListener("keydown", function (e) {
|
|
765
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
766
|
+
var handled = true;
|
|
767
|
+
switch (e.key) {
|
|
768
|
+
case "+": case "=": zoomBy(scope, ZOOM_STEP); break;
|
|
769
|
+
case "-": case "_": zoomBy(scope, 1 / ZOOM_STEP); break;
|
|
770
|
+
case "0": setZoom(scope, 1); break;
|
|
771
|
+
case "f": case "F":
|
|
772
|
+
if (scope.buttons && scope.buttons.fullscreen && !scope.buttons.fullscreen.disabled) scope.buttons.fullscreen.click();
|
|
773
|
+
else handled = false;
|
|
774
|
+
break;
|
|
775
|
+
default: handled = false;
|
|
776
|
+
}
|
|
777
|
+
if (handled) e.preventDefault();
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ---------------------------------------------------------------------
|
|
782
|
+
// Toolbar actions
|
|
783
|
+
// ---------------------------------------------------------------------
|
|
784
|
+
function announce(scope, text) {
|
|
785
|
+
if (!scope.status) return;
|
|
786
|
+
scope.status.textContent = "";
|
|
787
|
+
// Re-insert on the next tick so identical messages are announced again.
|
|
788
|
+
setTimeout(function () { scope.status.textContent = text; }, 30);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function copySource(btn, scope) {
|
|
792
|
+
var entry = registry.get(scope.figure);
|
|
793
|
+
var text = entry ? entry.meta.source : "";
|
|
794
|
+
var done = function (ok) {
|
|
795
|
+
var i = btn.querySelector("i");
|
|
796
|
+
if (i) i.className = "bi " + (ok ? "bi-check-lg" : "bi-x-lg");
|
|
797
|
+
btn.classList.toggle("is-success", ok);
|
|
798
|
+
btn.classList.toggle("is-failure", !ok);
|
|
799
|
+
announce(scope, ok ? LABELS.copied : LABELS.copyFailed);
|
|
800
|
+
setTimeout(function () {
|
|
801
|
+
if (i) i.className = "bi bi-clipboard";
|
|
802
|
+
btn.classList.remove("is-success", "is-failure");
|
|
803
|
+
}, 2000);
|
|
804
|
+
};
|
|
805
|
+
if (!navigator.clipboard || !navigator.clipboard.writeText) { done(false); return; }
|
|
806
|
+
navigator.clipboard.writeText(text).then(function () { done(true); }, function () { done(false); });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function downloadSvg(scope) {
|
|
810
|
+
var svg = scope.canvas.querySelector("svg");
|
|
811
|
+
var entry = registry.get(scope.figure);
|
|
812
|
+
if (!svg || !entry) return;
|
|
813
|
+
var clone = svg.cloneNode(true);
|
|
814
|
+
var vb = svg.viewBox && svg.viewBox.baseVal;
|
|
815
|
+
if (vb && vb.width && vb.height) {
|
|
816
|
+
clone.setAttribute("width", String(Math.round(vb.width)));
|
|
817
|
+
clone.setAttribute("height", String(Math.round(vb.height)));
|
|
818
|
+
}
|
|
819
|
+
clone.removeAttribute("style");
|
|
820
|
+
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
821
|
+
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
822
|
+
// Diagrams render on a transparent canvas; bake the page surface in so a
|
|
823
|
+
// dark-mode export is not white-on-nothing when opened elsewhere.
|
|
824
|
+
if (lastPalette) clone.style.backgroundColor = lastPalette.background;
|
|
825
|
+
var xml = new XMLSerializer().serializeToString(clone);
|
|
826
|
+
var blob = new Blob(['<?xml version="1.0" encoding="UTF-8"?>\n' + xml], { type: "image/svg+xml;charset=utf-8" });
|
|
827
|
+
var url = URL.createObjectURL(blob);
|
|
828
|
+
var a = el("a", null, { href: url, download: slugify(entry.meta.title || entry.meta.typeLabel) + ".svg" });
|
|
829
|
+
a.style.display = "none";
|
|
830
|
+
document.body.appendChild(a);
|
|
831
|
+
a.click();
|
|
832
|
+
setTimeout(function () { a.remove(); URL.revokeObjectURL(url); }, 1000);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Build a toolbar bound to a mutable scope ({ figure, canvas, viewport }).
|
|
836
|
+
// The lightbox reuses one toolbar for whichever figure it is showing.
|
|
837
|
+
function buildToolbar(scope, opts) {
|
|
838
|
+
var bar = el("div", "zer0-diagram__toolbar", { role: "toolbar", "aria-label": LABELS.tools });
|
|
839
|
+
var buttons = {};
|
|
840
|
+
|
|
841
|
+
buttons.zoomOut = button("bi-zoom-out", LABELS.zoomOut, function () { zoomBy(scope, 1 / ZOOM_STEP); });
|
|
842
|
+
var level = el("span", "zer0-diagram__zoom", { "aria-live": "polite", "aria-atomic": "true" });
|
|
843
|
+
level.hidden = true;
|
|
844
|
+
buttons.zoomIn = button("bi-zoom-in", LABELS.zoomIn, function () { zoomBy(scope, ZOOM_STEP); });
|
|
845
|
+
buttons.reset = button("bi-arrow-counterclockwise", LABELS.zoomReset, function () { setZoom(scope, 1); });
|
|
846
|
+
buttons.reset.disabled = true;
|
|
847
|
+
|
|
848
|
+
bar.appendChild(buttons.zoomOut);
|
|
849
|
+
bar.appendChild(level);
|
|
850
|
+
bar.appendChild(buttons.zoomIn);
|
|
851
|
+
bar.appendChild(buttons.reset);
|
|
852
|
+
bar.appendChild(el("span", "zer0-diagram__sep", { "aria-hidden": "true" }));
|
|
853
|
+
|
|
854
|
+
buttons.copy = button("bi-clipboard", LABELS.copy, function (b) { copySource(b, scope); });
|
|
855
|
+
bar.appendChild(buttons.copy);
|
|
856
|
+
|
|
857
|
+
if (ALLOW_DOWNLOAD) {
|
|
858
|
+
buttons.download = button("bi-download", LABELS.download, function () { downloadSvg(scope); });
|
|
859
|
+
bar.appendChild(buttons.download);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (opts && opts.inLightbox) {
|
|
863
|
+
buttons.close = button("bi-x-lg", LABELS.close, function () { closeLightbox(); });
|
|
864
|
+
buttons.close.classList.add("zer0-diagram__btn--close");
|
|
865
|
+
bar.appendChild(buttons.close);
|
|
866
|
+
} else if (ALLOW_FULLSCREEN && SUPPORTS_DIALOG) {
|
|
867
|
+
buttons.fullscreen = button("bi-arrows-fullscreen", LABELS.fullscreen, function () { openLightbox(scope.figure); });
|
|
868
|
+
bar.appendChild(buttons.fullscreen);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
var status = el("span", "visually-hidden", { "aria-live": "polite", "aria-atomic": "true" });
|
|
872
|
+
bar.appendChild(status);
|
|
873
|
+
|
|
874
|
+
scope.level = level;
|
|
875
|
+
scope.buttons = buttons;
|
|
876
|
+
scope.status = status;
|
|
877
|
+
return bar;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// state: "loading" | "rendered" | "error". Only a rendered SVG can be
|
|
881
|
+
// zoomed, exported or shown fullscreen; copying the source always works.
|
|
882
|
+
function setToolbarState(scope, state) {
|
|
883
|
+
var b = scope.buttons;
|
|
884
|
+
if (!b) return;
|
|
885
|
+
var rendered = state === "rendered";
|
|
886
|
+
["zoomIn", "zoomOut", "download", "fullscreen"].forEach(function (k) {
|
|
887
|
+
if (b[k]) b[k].disabled = !rendered;
|
|
888
|
+
});
|
|
889
|
+
if (b.reset) b.reset.disabled = true;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ---------------------------------------------------------------------
|
|
893
|
+
// Figure construction
|
|
894
|
+
// ---------------------------------------------------------------------
|
|
895
|
+
function makeFigure(meta) {
|
|
896
|
+
var figure = el("figure", "zer0-diagram is-loading", { "data-zer0-diagram": meta.type });
|
|
897
|
+
figure.setAttribute("aria-busy", "true");
|
|
898
|
+
|
|
899
|
+
var viewport = el("div", "zer0-diagram__viewport", { tabindex: "0", role: "region" });
|
|
900
|
+
viewport.setAttribute("aria-label", meta.title || meta.typeLabel);
|
|
901
|
+
var canvas = el("div", "zer0-diagram__canvas");
|
|
902
|
+
var skeleton = el("div", "zer0-diagram__skeleton skeleton", { "aria-hidden": "true" });
|
|
903
|
+
canvas.appendChild(skeleton);
|
|
904
|
+
var loading = el("span", "visually-hidden");
|
|
905
|
+
loading.textContent = LABELS.loading;
|
|
906
|
+
canvas.appendChild(loading);
|
|
907
|
+
viewport.appendChild(canvas);
|
|
908
|
+
|
|
909
|
+
var scope = { figure: figure, canvas: canvas, viewport: viewport };
|
|
910
|
+
if (SHOW_TOOLBAR) {
|
|
911
|
+
figure.appendChild(buildToolbar(scope));
|
|
912
|
+
enableWheelZoom(viewport, scope);
|
|
913
|
+
enableKeys(viewport, scope);
|
|
914
|
+
}
|
|
915
|
+
enablePan(viewport);
|
|
916
|
+
figure.appendChild(viewport);
|
|
917
|
+
|
|
918
|
+
var caption = el("figcaption", "zer0-diagram__caption");
|
|
919
|
+
if (meta.title) caption.textContent = meta.title; else caption.hidden = true;
|
|
920
|
+
figure.appendChild(caption);
|
|
921
|
+
|
|
922
|
+
registry.set(figure, { meta: meta, scope: scope });
|
|
923
|
+
allFigures.push(figure);
|
|
924
|
+
return figure;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// The element to swap out for a fence: the outermost wrapper Jekyll (or
|
|
928
|
+
// code-copy.js, if it somehow ran first) put around the <pre>.
|
|
929
|
+
function fenceWrapper(pre) {
|
|
930
|
+
return pre.closest(".highlighter-rouge") || pre.closest(".code-block-body") || pre;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function collect(root) {
|
|
934
|
+
root = root || document;
|
|
935
|
+
var found = [];
|
|
936
|
+
|
|
937
|
+
var codes = root.querySelectorAll("pre > code.language-mermaid");
|
|
938
|
+
Array.prototype.forEach.call(codes, function (code) {
|
|
939
|
+
var pre = code.parentNode;
|
|
940
|
+
if (!pre || !pre.isConnected) return;
|
|
941
|
+
var figure = makeFigure(parseMeta(code.textContent));
|
|
942
|
+
fenceWrapper(pre).replaceWith(figure);
|
|
943
|
+
found.push(figure);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
var pres = root.querySelectorAll('pre[data-language="mermaid"]');
|
|
947
|
+
Array.prototype.forEach.call(pres, function (pre) {
|
|
948
|
+
if (!pre.isConnected) return;
|
|
949
|
+
var figure = makeFigure(parseMeta(pre.textContent));
|
|
950
|
+
fenceWrapper(pre).replaceWith(figure);
|
|
951
|
+
found.push(figure);
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
var divs = root.querySelectorAll("div.mermaid:not([data-processed])");
|
|
955
|
+
Array.prototype.forEach.call(divs, function (div) {
|
|
956
|
+
if (!div.isConnected || div.closest(".zer0-diagram")) return;
|
|
957
|
+
var figure = makeFigure(parseMeta(div.textContent));
|
|
958
|
+
div.replaceWith(figure);
|
|
959
|
+
found.push(figure);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
return found;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// ---------------------------------------------------------------------
|
|
966
|
+
// Rendering
|
|
967
|
+
// ---------------------------------------------------------------------
|
|
968
|
+
function decorateSvg(svg, meta) {
|
|
969
|
+
svg.classList.add("zer0-diagram__svg");
|
|
970
|
+
if (!svg.hasAttribute("aria-labelledby") && !svg.hasAttribute("aria-label")) {
|
|
971
|
+
svg.setAttribute("aria-label", meta.title || meta.typeLabel);
|
|
972
|
+
}
|
|
973
|
+
if (!svg.hasAttribute("role")) svg.setAttribute("role", "img");
|
|
974
|
+
if (svg.style.maxWidth) svg.setAttribute("data-zer0-max-width", svg.style.maxWidth);
|
|
975
|
+
svg.removeAttribute("data-zer0-fit-width");
|
|
976
|
+
svg.removeAttribute("data-zer0-fit-height");
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function showError(figure, entry, err) {
|
|
980
|
+
var scope = entry.scope;
|
|
981
|
+
var message = String((err && (err.message || err.str)) || err || "Unknown error").split("\n").slice(0, 4).join("\n");
|
|
982
|
+
|
|
983
|
+
scope.canvas.innerHTML = "";
|
|
984
|
+
var box = el("div", "zer0-diagram__error");
|
|
985
|
+
box.appendChild(icon("bi-exclamation-triangle-fill"));
|
|
986
|
+
var body = el("div", "zer0-diagram__error-body");
|
|
987
|
+
var title = el("p", "zer0-diagram__error-title");
|
|
988
|
+
title.textContent = LABELS.errorTitle;
|
|
989
|
+
body.appendChild(title);
|
|
990
|
+
// Plain <pre> (no <code> child) on purpose: code-copy.js decorates every
|
|
991
|
+
// `pre code` on DOMContentLoaded with a copy button and a line gutter, and
|
|
992
|
+
// an error card that renders before that pass would pick both up. The
|
|
993
|
+
// toolbar's copy control already covers the source.
|
|
994
|
+
var pre = el("pre", "zer0-diagram__error-message");
|
|
995
|
+
pre.textContent = message;
|
|
996
|
+
body.appendChild(pre);
|
|
997
|
+
var hint = el("p", "zer0-diagram__error-hint");
|
|
998
|
+
hint.textContent = LABELS.errorHint;
|
|
999
|
+
body.appendChild(hint);
|
|
1000
|
+
box.appendChild(body);
|
|
1001
|
+
scope.canvas.appendChild(box);
|
|
1002
|
+
|
|
1003
|
+
var details = el("details", "zer0-diagram__source");
|
|
1004
|
+
details.open = true;
|
|
1005
|
+
var summary = el("summary");
|
|
1006
|
+
summary.textContent = LABELS.showSource;
|
|
1007
|
+
details.appendChild(summary);
|
|
1008
|
+
var srcPre = el("pre", "zer0-diagram__source-text");
|
|
1009
|
+
srcPre.textContent = entry.meta.source;
|
|
1010
|
+
details.appendChild(srcPre);
|
|
1011
|
+
scope.canvas.appendChild(details);
|
|
1012
|
+
|
|
1013
|
+
figure.classList.add("has-error");
|
|
1014
|
+
figure.classList.remove("is-rendered");
|
|
1015
|
+
setToolbarState(scope, "error");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function renderFigure(figure) {
|
|
1019
|
+
var entry = registry.get(figure);
|
|
1020
|
+
if (!entry || !window.mermaid) return Promise.resolve(figure);
|
|
1021
|
+
var scope = entry.scope;
|
|
1022
|
+
var meta = entry.meta;
|
|
1023
|
+
|
|
1024
|
+
figure.classList.add("is-loading");
|
|
1025
|
+
figure.classList.remove("has-error");
|
|
1026
|
+
figure.setAttribute("aria-busy", "true");
|
|
1027
|
+
setToolbarState(scope, "loading");
|
|
1028
|
+
|
|
1029
|
+
var id = nextId();
|
|
1030
|
+
return Promise.resolve()
|
|
1031
|
+
.then(function () { return window.mermaid.parse(meta.source); })
|
|
1032
|
+
.then(function () { return window.mermaid.render(id, meta.source, scope.canvas); })
|
|
1033
|
+
.then(function (result) {
|
|
1034
|
+
scope.canvas.innerHTML = result && result.svg ? result.svg : "";
|
|
1035
|
+
var svg = scope.canvas.querySelector("svg");
|
|
1036
|
+
if (!svg) throw new Error("Mermaid returned no SVG");
|
|
1037
|
+
decorateSvg(svg, meta);
|
|
1038
|
+
if (result.bindFunctions) {
|
|
1039
|
+
try { result.bindFunctions(scope.canvas); } catch (err) { /* interactive bindings are optional */ }
|
|
1040
|
+
}
|
|
1041
|
+
figure.classList.add("is-rendered");
|
|
1042
|
+
setToolbarState(scope, "rendered");
|
|
1043
|
+
setZoom(scope, 1);
|
|
1044
|
+
fire(figure, "zer0:diagram-rendered", { ok: true, type: meta.type });
|
|
1045
|
+
return figure;
|
|
1046
|
+
})
|
|
1047
|
+
.catch(function (err) {
|
|
1048
|
+
// Mermaid can leave its temporary render container behind on failure.
|
|
1049
|
+
["d" + id, id].forEach(function (leftover) {
|
|
1050
|
+
var node = document.getElementById(leftover);
|
|
1051
|
+
if (node && !scope.canvas.contains(node)) node.remove();
|
|
1052
|
+
});
|
|
1053
|
+
showError(figure, entry, err);
|
|
1054
|
+
fire(figure, "zer0:diagram-rendered", { ok: false, type: meta.type, error: String((err && err.message) || err) });
|
|
1055
|
+
return figure;
|
|
1056
|
+
})
|
|
1057
|
+
.then(function () {
|
|
1058
|
+
figure.classList.remove("is-loading");
|
|
1059
|
+
figure.removeAttribute("aria-busy");
|
|
1060
|
+
return figure;
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function renderMany(figures) {
|
|
1065
|
+
var failed = 0;
|
|
1066
|
+
return Promise.all(figures.map(function (f) {
|
|
1067
|
+
return renderFigure(f).then(function (fig) {
|
|
1068
|
+
if (fig.classList.contains("has-error")) failed += 1;
|
|
1069
|
+
return fig;
|
|
1070
|
+
});
|
|
1071
|
+
})).then(function (done) {
|
|
1072
|
+
fire(document, "zer0:diagrams-ready", { count: done.length, failed: failed });
|
|
1073
|
+
return done;
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ---------------------------------------------------------------------
|
|
1078
|
+
// Fullscreen lightbox (<dialog>) — the figure's canvas is MOVED into the
|
|
1079
|
+
// dialog and back, so ids, bound click handlers and zoom state stay unique.
|
|
1080
|
+
// ---------------------------------------------------------------------
|
|
1081
|
+
var lightbox = null;
|
|
1082
|
+
|
|
1083
|
+
function ensureLightbox() {
|
|
1084
|
+
if (lightbox) return lightbox;
|
|
1085
|
+
var dlg = el("dialog", "zer0-diagram-lightbox");
|
|
1086
|
+
var inner = el("div", "zer0-diagram-lightbox__inner");
|
|
1087
|
+
var bar = el("div", "zer0-diagram-lightbox__bar");
|
|
1088
|
+
var title = el("p", "zer0-diagram-lightbox__title", { id: "zer0-diagram-lightbox-title" });
|
|
1089
|
+
bar.appendChild(title);
|
|
1090
|
+
var viewport = el("div", "zer0-diagram__viewport zer0-diagram-lightbox__viewport", { tabindex: "0", role: "region" });
|
|
1091
|
+
var scope = { figure: null, canvas: null, viewport: viewport };
|
|
1092
|
+
var toolbar = buildToolbar(scope, { inLightbox: true });
|
|
1093
|
+
toolbar.classList.add("zer0-diagram__toolbar--static");
|
|
1094
|
+
bar.appendChild(toolbar);
|
|
1095
|
+
inner.appendChild(bar);
|
|
1096
|
+
inner.appendChild(viewport);
|
|
1097
|
+
dlg.appendChild(inner);
|
|
1098
|
+
dlg.setAttribute("aria-labelledby", "zer0-diagram-lightbox-title");
|
|
1099
|
+
|
|
1100
|
+
enablePan(viewport);
|
|
1101
|
+
enableWheelZoom(viewport, scope);
|
|
1102
|
+
enableKeys(viewport, scope);
|
|
1103
|
+
|
|
1104
|
+
dlg.addEventListener("click", function (e) {
|
|
1105
|
+
if (e.target === dlg) dlg.close(); // backdrop click
|
|
1106
|
+
});
|
|
1107
|
+
dlg.addEventListener("close", restoreFromLightbox);
|
|
1108
|
+
document.body.appendChild(dlg);
|
|
1109
|
+
|
|
1110
|
+
lightbox = { dialog: dlg, title: title, viewport: viewport, scope: scope, current: null };
|
|
1111
|
+
return lightbox;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function openLightbox(figure) {
|
|
1115
|
+
if (!SUPPORTS_DIALOG) return;
|
|
1116
|
+
var entry = registry.get(figure);
|
|
1117
|
+
if (!entry || !figure.classList.contains("is-rendered")) return;
|
|
1118
|
+
var lb = ensureLightbox();
|
|
1119
|
+
if (lb.current) restoreFromLightbox();
|
|
1120
|
+
|
|
1121
|
+
var canvas = entry.scope.canvas;
|
|
1122
|
+
var placeholder = document.createComment("zer0-diagram canvas (shown fullscreen)");
|
|
1123
|
+
// Keep the figure's height so the page does not jump while the canvas is away.
|
|
1124
|
+
entry.scope.viewport.style.minHeight = entry.scope.viewport.getBoundingClientRect().height + "px";
|
|
1125
|
+
canvas.parentNode.insertBefore(placeholder, canvas);
|
|
1126
|
+
|
|
1127
|
+
lb.current = { figure: figure, canvas: canvas, placeholder: placeholder, opener: document.activeElement, zoom: currentZoom(canvas) };
|
|
1128
|
+
lb.scope.figure = figure;
|
|
1129
|
+
lb.scope.canvas = canvas;
|
|
1130
|
+
lb.title.textContent = entry.meta.title || entry.meta.typeLabel;
|
|
1131
|
+
lb.viewport.setAttribute("aria-label", entry.meta.title || entry.meta.typeLabel);
|
|
1132
|
+
lb.viewport.appendChild(canvas);
|
|
1133
|
+
|
|
1134
|
+
figure.classList.add("is-in-lightbox");
|
|
1135
|
+
document.documentElement.classList.add("zer0-diagram-lightbox-open");
|
|
1136
|
+
lb.dialog.showModal();
|
|
1137
|
+
setToolbarState(lb.scope, "rendered");
|
|
1138
|
+
setZoom(lb.scope, 1);
|
|
1139
|
+
fire(figure, "zer0:diagram-fullscreen", { open: true });
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function restoreFromLightbox() {
|
|
1143
|
+
var lb = lightbox;
|
|
1144
|
+
if (!lb || !lb.current) return;
|
|
1145
|
+
var cur = lb.current;
|
|
1146
|
+
lb.current = null;
|
|
1147
|
+
cur.placeholder.replaceWith(cur.canvas);
|
|
1148
|
+
cur.placeholder = null;
|
|
1149
|
+
var entry = registry.get(cur.figure);
|
|
1150
|
+
if (entry) {
|
|
1151
|
+
entry.scope.viewport.style.minHeight = "";
|
|
1152
|
+
setZoom(entry.scope, cur.zoom || 1);
|
|
1153
|
+
}
|
|
1154
|
+
cur.figure.classList.remove("is-in-lightbox");
|
|
1155
|
+
document.documentElement.classList.remove("zer0-diagram-lightbox-open");
|
|
1156
|
+
lb.scope.figure = null;
|
|
1157
|
+
lb.scope.canvas = null;
|
|
1158
|
+
if (cur.opener && typeof cur.opener.focus === "function" && cur.opener.isConnected) {
|
|
1159
|
+
try { cur.opener.focus({ preventScroll: true }); } catch (err) { /* ignore */ }
|
|
1160
|
+
}
|
|
1161
|
+
fire(cur.figure, "zer0:diagram-fullscreen", { open: false });
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function closeLightbox() {
|
|
1165
|
+
if (lightbox && lightbox.dialog.open) lightbox.dialog.close();
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// ---------------------------------------------------------------------
|
|
1169
|
+
// Theme / skin / token changes → re-render from source
|
|
1170
|
+
// ---------------------------------------------------------------------
|
|
1171
|
+
var refreshTimer = null;
|
|
1172
|
+
|
|
1173
|
+
function refresh(force) {
|
|
1174
|
+
if (!window.mermaid) return Promise.resolve([]);
|
|
1175
|
+
var changed = initMermaid(force);
|
|
1176
|
+
if (!changed && !force) return Promise.resolve(allFigures.slice());
|
|
1177
|
+
// A re-render replaces the canvas contents; leave the lightbox first so
|
|
1178
|
+
// its zoom/toolbar state cannot go stale.
|
|
1179
|
+
if (lightbox && lightbox.current) closeLightbox();
|
|
1180
|
+
return renderMany(allFigures.filter(function (f) { return f.isConnected; }));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function scheduleRefresh() {
|
|
1184
|
+
clearTimeout(refreshTimer);
|
|
1185
|
+
refreshTimer = setTimeout(function () { refresh(false); }, 80);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function watchTheme() {
|
|
1189
|
+
if (typeof MutationObserver === "function") {
|
|
1190
|
+
var observer = new MutationObserver(scheduleRefresh);
|
|
1191
|
+
// `style` catches the Appearance panel writing token overrides onto
|
|
1192
|
+
// <html>. `class` is deliberately NOT watched: the lightbox toggles a
|
|
1193
|
+
// class there, and the palette comparison in refresh() is what decides
|
|
1194
|
+
// whether anything actually changed.
|
|
1195
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-bs-theme", "data-theme-skin", "style"] });
|
|
1196
|
+
if (document.body) observer.observe(document.body, { attributes: true, attributeFilter: ["data-bs-theme"] });
|
|
1197
|
+
}
|
|
1198
|
+
document.addEventListener("zer0:skin-change", scheduleRefresh);
|
|
1199
|
+
if (window.matchMedia) {
|
|
1200
|
+
var mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
1201
|
+
if (mq.addEventListener) mq.addEventListener("change", scheduleRefresh);
|
|
1202
|
+
else if (mq.addListener) mq.addListener(scheduleRefresh);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// ---------------------------------------------------------------------
|
|
1207
|
+
// Public API + boot
|
|
1208
|
+
// ---------------------------------------------------------------------
|
|
1209
|
+
function renderAll(root) {
|
|
1210
|
+
if (!window.mermaid) return Promise.resolve([]);
|
|
1211
|
+
initMermaid(false);
|
|
1212
|
+
var figures = collect(root || document);
|
|
1213
|
+
return renderMany(figures);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function render(target, source) {
|
|
1217
|
+
if (!window.mermaid) return Promise.resolve(null);
|
|
1218
|
+
initMermaid(false);
|
|
1219
|
+
var figure;
|
|
1220
|
+
if (target && registry.has(target)) {
|
|
1221
|
+
figure = target;
|
|
1222
|
+
if (typeof source === "string") {
|
|
1223
|
+
var entry = registry.get(figure);
|
|
1224
|
+
entry.meta = parseMeta(source);
|
|
1225
|
+
var caption = figure.querySelector(".zer0-diagram__caption");
|
|
1226
|
+
if (caption) { caption.textContent = entry.meta.title || ""; caption.hidden = !entry.meta.title; }
|
|
1227
|
+
figure.setAttribute("data-zer0-diagram", entry.meta.type);
|
|
1228
|
+
entry.scope.viewport.setAttribute("aria-label", entry.meta.title || entry.meta.typeLabel);
|
|
1229
|
+
}
|
|
1230
|
+
} else if (target && target.nodeType === 1) {
|
|
1231
|
+
figure = makeFigure(parseMeta(typeof source === "string" ? source : target.textContent));
|
|
1232
|
+
target.replaceWith(figure);
|
|
1233
|
+
} else {
|
|
1234
|
+
return Promise.resolve(null);
|
|
1235
|
+
}
|
|
1236
|
+
return renderFigure(figure);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
window.zer0Mermaid = {
|
|
1240
|
+
version: "3.0.0",
|
|
1241
|
+
config: CONFIG,
|
|
1242
|
+
labels: LABELS,
|
|
1243
|
+
renderAll: renderAll,
|
|
1244
|
+
render: render,
|
|
1245
|
+
refresh: function () { return refresh(true); },
|
|
1246
|
+
getSource: function (figure) { var e = registry.get(figure); return e ? e.meta.source : null; },
|
|
1247
|
+
palette: function () { return palette(); },
|
|
1248
|
+
figures: function () { return allFigures.filter(function (f) { return f.isConnected; }); },
|
|
1249
|
+
setZoom: function (figure, zoom) { var e = registry.get(figure); return e ? setZoom(e.scope, zoom) : null; },
|
|
1250
|
+
openFullscreen: openLightbox,
|
|
1251
|
+
closeFullscreen: closeLightbox,
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
function boot() {
|
|
1255
|
+
if (!window.mermaid || typeof window.mermaid.render !== "function") {
|
|
1256
|
+
// Vendor bundle missing: leave the fences as readable code blocks.
|
|
1257
|
+
document.documentElement.classList.add("zer0-diagram-unavailable");
|
|
1258
|
+
if (window.console && console.warn) console.warn("[zer0-mermaid] mermaid.min.js did not load; diagrams left as source.");
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
renderAll(document);
|
|
1262
|
+
watchTheme();
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
|
1266
|
+
else boot();
|
|
1267
|
+
})();
|