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