vantage-md 0.5.8 → 0.5.9
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.
- package/dist/index.cjs +185 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +185 -20
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +250 -45
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts.map +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +251 -46
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/react.cjs
CHANGED
|
@@ -51,6 +51,21 @@ let remark_rehype = require("remark-rehype");
|
|
|
51
51
|
remark_rehype = __toESM(remark_rehype, 1);
|
|
52
52
|
let rehype_stringify = require("rehype-stringify");
|
|
53
53
|
rehype_stringify = __toESM(rehype_stringify, 1);
|
|
54
|
+
//#region src/rehypeVantageAnchors.ts
|
|
55
|
+
/** What `rehypeVantageDirectives` stamps, in hast property form. */
|
|
56
|
+
const OQ_ID_PROPERTY$1 = "dataVantageOqId";
|
|
57
|
+
function rehypeVantageAnchors() {
|
|
58
|
+
return (tree) => {
|
|
59
|
+
(0, unist_util_visit.visit)(tree, "element", (node) => {
|
|
60
|
+
const carried = node.properties?.[OQ_ID_PROPERTY$1];
|
|
61
|
+
if (typeof carried !== "string" || carried === "") return;
|
|
62
|
+
delete node.properties[OQ_ID_PROPERTY$1];
|
|
63
|
+
if (typeof node.properties.id === "string" && node.properties.id !== "") return;
|
|
64
|
+
node.properties.id = carried;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
54
69
|
//#region src/rehypeSourceLines.ts
|
|
55
70
|
/**
|
|
56
71
|
* Tags that get a `data-source-line`.
|
|
@@ -277,12 +292,17 @@ const VANTAGE_RUNS = [
|
|
|
277
292
|
"only"
|
|
278
293
|
];
|
|
279
294
|
/**
|
|
280
|
-
* The tags a `section`/`block` directive may
|
|
295
|
+
* The tags a `section`/`block` directive may **target**.
|
|
281
296
|
*
|
|
282
|
-
* Deliberately `rehypeSourceLines`'s `BLOCK_TAGS`: a
|
|
283
|
-
* be a block with a `data-source-line`, so the styling surface and the
|
|
284
|
-
* surface coincide. It also keeps an inline directive from stamping the
|
|
285
|
-
* that happens to follow it inside a paragraph.
|
|
297
|
+
* Deliberately `rehypeSourceLines`'s `BLOCK_TAGS`: a directive's target should
|
|
298
|
+
* also be a block with a `data-source-line`, so the styling surface and the
|
|
299
|
+
* anchor surface coincide. It also keeps an inline directive from stamping the
|
|
300
|
+
* `<em>` that happens to follow it inside a paragraph.
|
|
301
|
+
*
|
|
302
|
+
* It does **not** bound a `section`'s range. Every element in the span is
|
|
303
|
+
* stamped, on the tag list or not, because a member only has to be a box in the
|
|
304
|
+
* flow for the section's vertical rule to cross it — see `styleRange` in
|
|
305
|
+
* `rehypeVantageDirectives.ts` for the hole that restricting the range left.
|
|
286
306
|
*
|
|
287
307
|
* It lives here rather than in the plugin because the CLI checker has to answer
|
|
288
308
|
* "will this directive stamp anything?" from an mdast tree with no hast in
|
|
@@ -332,6 +352,23 @@ const VANTAGE_ANCHOR_TARGETS = [
|
|
|
332
352
|
"table"
|
|
333
353
|
];
|
|
334
354
|
VANTAGE_ANCHOR_TARGETS.filter((tag) => tag !== "pre" && tag !== "table");
|
|
355
|
+
/**
|
|
356
|
+
* The shape of an `oq` directive's `id`: `OQ-` then an optional short uppercase
|
|
357
|
+
* prefix then digits. `OQ-9`, `OQ-TP6` and `OQ-A03` are ids; `OQ-foo`, `OQ-tp6`
|
|
358
|
+
* and a bare `OQ6` are not.
|
|
359
|
+
*
|
|
360
|
+
* The prefix is what keeps ids distinct once one document references another's
|
|
361
|
+
* questions — `trust-paths.md`'s `OQ-4` and a design sketch's `OQ-4` are
|
|
362
|
+
* different questions, and a bare number cannot say which one a cross-document
|
|
363
|
+
* reference means. It is optional because most documents never leave their own
|
|
364
|
+
* file, and requiring it everywhere would fire on every single-doc sketch.
|
|
365
|
+
*
|
|
366
|
+
* Three consumers read it from here and none of them may re-spell it: the
|
|
367
|
+
* plugin that stamps the anchor, the sanitiser that allowlists the value, and
|
|
368
|
+
* the checker's `vantage/oq-id-format`. A fourth copy is how the checker starts
|
|
369
|
+
* calling a working anchor malformed.
|
|
370
|
+
*/
|
|
371
|
+
const VANTAGE_OQ_ID = /^OQ-(?:[A-Z][A-Z0-9]{0,5})?[0-9]+$/;
|
|
335
372
|
const STYLE_KEYS = {
|
|
336
373
|
tone: VANTAGE_TONES,
|
|
337
374
|
emphasis: VANTAGE_EMPHASIS,
|
|
@@ -464,11 +501,13 @@ function parseVantageDirective(comment) {
|
|
|
464
501
|
//#endregion
|
|
465
502
|
//#region src/rehypeVantageDirectives.ts
|
|
466
503
|
/**
|
|
467
|
-
* What a `section`/`block` and an `oq` directive may
|
|
504
|
+
* What a `section`/`block` and an `oq` directive may **target**.
|
|
468
505
|
*
|
|
469
506
|
* Both lists live in `vantageDirectives.ts`, with the reasoning for each tag,
|
|
470
507
|
* because the CLI checker resolves the same question over mdast and must reach
|
|
471
508
|
* the same answer (D5).
|
|
509
|
+
*
|
|
510
|
+
* Neither list bounds a `section`'s range: see `styleRange`.
|
|
472
511
|
*/
|
|
473
512
|
const STYLE_TARGET_TAGS = new Set(VANTAGE_STYLE_TARGETS);
|
|
474
513
|
const ANCHOR_TARGET_TAGS = new Set(VANTAGE_ANCHOR_TARGETS);
|
|
@@ -515,6 +554,18 @@ const RUN_PROPERTY = "dataVantageRun";
|
|
|
515
554
|
const OQ_PROPERTY = "dataVantageOq";
|
|
516
555
|
const LEANING_PROPERTY = "dataVantageLeaning";
|
|
517
556
|
/**
|
|
557
|
+
* The id, carried as a `data-` attribute rather than written straight to `id`.
|
|
558
|
+
*
|
|
559
|
+
* This plugin runs *before* `rehypeSanitize` — it has to, it reads comments and
|
|
560
|
+
* the sanitiser deletes them — and the sanitiser's default schema clobbers `id`
|
|
561
|
+
* with the prefix `user-content-`. A bare `id` set here would reach the page as
|
|
562
|
+
* `user-content-OQ-4`, every `#OQ-4` link in every document would land nowhere,
|
|
563
|
+
* and nothing would error. `rehypeVantageAnchors` promotes this to a real `id`
|
|
564
|
+
* on the other side of the sanitiser, which is the same reason `rehypeSlug` is
|
|
565
|
+
* registered there (`pipeline.ts`).
|
|
566
|
+
*/
|
|
567
|
+
const OQ_ID_PROPERTY = "dataVantageOqId";
|
|
568
|
+
/**
|
|
518
569
|
* The three properties `collapsed=true` stamps across a section.
|
|
519
570
|
*
|
|
520
571
|
* The heading takes a *different* attribute from the blocks it hides, and that
|
|
@@ -587,6 +638,22 @@ function accepts(name, key, value) {
|
|
|
587
638
|
* `section` before anything else degrades to that one block, and `block` is
|
|
588
639
|
* always that one block. A heading nested inside a stamped `blockquote` or
|
|
589
640
|
* `li` does not end the section: the walk never descends.
|
|
641
|
+
*
|
|
642
|
+
* **Every element in the span, not only a `VANTAGE_STYLE_TARGETS` one.** That
|
|
643
|
+
* list gates the *target* and nothing else. Restricting the range to it as well
|
|
644
|
+
* used to leave a raw-HTML `<figure>`, `<dl>` or `<details>` unstamped between
|
|
645
|
+
* two stamped paragraphs — and the section's one continuous vertical rule is
|
|
646
|
+
* drawn per member, so an unstamped member is a hole the height of the block
|
|
647
|
+
* plus its margins. Measured over the real stylesheet: 44px for a one-line
|
|
648
|
+
* `<figure>`, against the 40px a neighbour can bleed upward, and arbitrarily
|
|
649
|
+
* large for anything taller. `collapsed=true` had the same shape of bug the
|
|
650
|
+
* other way round — it hid the paragraphs and left the figure on the page.
|
|
651
|
+
*
|
|
652
|
+
* The two lists answering different questions is the point, not an oversight:
|
|
653
|
+
* a *target* must be a block a review anchor can name, because a directive
|
|
654
|
+
* pointing at something unanchorable is a directive with no addressable effect.
|
|
655
|
+
* A *member* only has to be a box in the flow, because all it does is carry the
|
|
656
|
+
* run's tone across itself.
|
|
590
657
|
*/
|
|
591
658
|
function styleRange(children, targetIndex, name) {
|
|
592
659
|
const range = [targetIndex];
|
|
@@ -596,7 +663,7 @@ function styleRange(children, targetIndex, name) {
|
|
|
596
663
|
const node = children[i];
|
|
597
664
|
const nodeDepth = headingDepth(node);
|
|
598
665
|
if (nodeDepth !== void 0 && nodeDepth <= depth) break;
|
|
599
|
-
if (node.type === "element"
|
|
666
|
+
if (node.type === "element") range.push(i);
|
|
600
667
|
}
|
|
601
668
|
return range;
|
|
602
669
|
}
|
|
@@ -655,6 +722,8 @@ function stampStyle(children, targetIndex, name, pairs, state) {
|
|
|
655
722
|
}
|
|
656
723
|
function stampOq(target, pairs) {
|
|
657
724
|
setProperty(target, OQ_PROPERTY, "true");
|
|
725
|
+
const id = pairs.get("id");
|
|
726
|
+
if (id !== void 0 && id !== "") setProperty(target, OQ_ID_PROPERTY, id);
|
|
658
727
|
const leaning = pairs.get("leaning");
|
|
659
728
|
if (leaning === void 0) return;
|
|
660
729
|
const text = leaning.replace(/\s+/g, " ").trim().slice(0, MAX_LEANING);
|
|
@@ -960,6 +1029,7 @@ const sanitizeSchema = {
|
|
|
960
1029
|
["dataVantageCollapseToggle", COLLAPSE_GROUP_ID],
|
|
961
1030
|
["dataVantageRun", ...VANTAGE_RUNS],
|
|
962
1031
|
["dataVantageOq", "true"],
|
|
1032
|
+
["dataVantageOqId", VANTAGE_OQ_ID],
|
|
963
1033
|
["dataVantageAlert", ...VANTAGE_ALERTS],
|
|
964
1034
|
"dataVantageLeaning"
|
|
965
1035
|
],
|
|
@@ -1009,6 +1079,7 @@ function buildRehypePlugins(options = {}) {
|
|
|
1009
1079
|
plugins.push(rehypeVantageAlerts);
|
|
1010
1080
|
plugins.push(rehypeVantageDirectives);
|
|
1011
1081
|
if (sanitize) plugins.push([rehype_sanitize.default, sanitizeSchema]);
|
|
1082
|
+
plugins.push(rehypeVantageAnchors);
|
|
1012
1083
|
plugins.push(rehype_slug.default);
|
|
1013
1084
|
if (highlight) plugins.push(rehype_highlight.default);
|
|
1014
1085
|
if (math) plugins.push(rehypeCaptureMathStamps, rehype_katex.default, rehypeRestoreMathStamps);
|
|
@@ -1131,27 +1202,92 @@ function errorPosition(error) {
|
|
|
1131
1202
|
return { message };
|
|
1132
1203
|
}
|
|
1133
1204
|
//#endregion
|
|
1205
|
+
//#region src/mermaidTheme.ts
|
|
1206
|
+
/** Whether the document is asking for the dark palette right now. */
|
|
1207
|
+
function currentMermaidTheme() {
|
|
1208
|
+
return typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "default";
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Theme variables per theme. Mermaid derives most of its palette from these, so
|
|
1212
|
+
* the set is deliberately small: the surfaces, the ink, and the lines.
|
|
1213
|
+
*/
|
|
1214
|
+
const THEME_VARIABLES = {
|
|
1215
|
+
dark: {
|
|
1216
|
+
background: "#1d293d",
|
|
1217
|
+
mainBkg: "#314158",
|
|
1218
|
+
nodeBorder: "#90a1b9",
|
|
1219
|
+
nodeTextColor: "#f1f5f9",
|
|
1220
|
+
lineColor: "#90a1b9",
|
|
1221
|
+
textColor: "#e2e8f0",
|
|
1222
|
+
edgeLabelBackground: "#1d293d"
|
|
1223
|
+
},
|
|
1224
|
+
default: {
|
|
1225
|
+
background: "#f8fafc",
|
|
1226
|
+
mainBkg: "#f1f5f9",
|
|
1227
|
+
nodeBorder: "#62748e",
|
|
1228
|
+
nodeTextColor: "#0f172b",
|
|
1229
|
+
lineColor: "#62748e",
|
|
1230
|
+
textColor: "#1d293d",
|
|
1231
|
+
edgeLabelBackground: "#f8fafc"
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
function mermaidThemeVariables(theme) {
|
|
1235
|
+
return THEME_VARIABLES[theme];
|
|
1236
|
+
}
|
|
1237
|
+
//#endregion
|
|
1134
1238
|
//#region src/mermaidCache.ts
|
|
1135
1239
|
const svgCache = /* @__PURE__ */ new Map();
|
|
1240
|
+
const cacheKey = (code, theme) => `${theme}${code}`;
|
|
1241
|
+
/** The SVG for this fence in the theme the page is currently asking for. */
|
|
1242
|
+
function getCachedSvg(code, theme = currentMermaidTheme()) {
|
|
1243
|
+
return svgCache.get(cacheKey(code, theme));
|
|
1244
|
+
}
|
|
1245
|
+
function setCachedSvg(code, svg, theme = currentMermaidTheme()) {
|
|
1246
|
+
svgCache.set(cacheKey(code, theme), svg);
|
|
1247
|
+
}
|
|
1136
1248
|
//#endregion
|
|
1137
1249
|
//#region src/mermaidLoader.ts
|
|
1138
1250
|
let mermaidInstance = null;
|
|
1139
1251
|
let mermaidLoading = null;
|
|
1140
|
-
|
|
1252
|
+
/** The theme the loaded instance was last configured for, `null` until loaded. */
|
|
1253
|
+
let configuredTheme = null;
|
|
1254
|
+
function configure(m, theme) {
|
|
1255
|
+
m.initialize({
|
|
1256
|
+
startOnLoad: false,
|
|
1257
|
+
theme,
|
|
1258
|
+
themeVariables: mermaidThemeVariables(theme),
|
|
1259
|
+
securityLevel: "strict",
|
|
1260
|
+
suppressErrorRendering: true
|
|
1261
|
+
});
|
|
1262
|
+
configuredTheme = theme;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* The mermaid module, configured for the theme the page is asking for *now*.
|
|
1266
|
+
*
|
|
1267
|
+
* Re-configuring on a theme change is the point. `initialize` used to run once,
|
|
1268
|
+
* on first import, so every diagram rendered after a light/dark switch still
|
|
1269
|
+
* came out in the palette the session started in — a white slab of a flowchart
|
|
1270
|
+
* on the dark page, or a black one on the light page. `initialize` merges into
|
|
1271
|
+
* mermaid's global config, so calling it again is how the next `render` picks
|
|
1272
|
+
* the new palette up; the cache is keyed by theme so the old SVGs are not
|
|
1273
|
+
* served instead (`mermaidCache.ts`).
|
|
1274
|
+
*/
|
|
1141
1275
|
async function getMermaid() {
|
|
1142
|
-
|
|
1276
|
+
const theme = currentMermaidTheme();
|
|
1277
|
+
if (mermaidInstance) {
|
|
1278
|
+
if (configuredTheme !== theme) configure(mermaidInstance, theme);
|
|
1279
|
+
return mermaidInstance;
|
|
1280
|
+
}
|
|
1143
1281
|
if (!mermaidLoading) mermaidLoading = import("mermaid").then((mod) => {
|
|
1144
1282
|
const m = mod.default;
|
|
1145
|
-
m
|
|
1146
|
-
startOnLoad: false,
|
|
1147
|
-
theme: isDark() ? "dark" : "default",
|
|
1148
|
-
securityLevel: "strict",
|
|
1149
|
-
suppressErrorRendering: true
|
|
1150
|
-
});
|
|
1283
|
+
configure(m, currentMermaidTheme());
|
|
1151
1284
|
mermaidInstance = m;
|
|
1152
1285
|
return m;
|
|
1153
1286
|
});
|
|
1154
|
-
|
|
1287
|
+
const loaded = await mermaidLoading;
|
|
1288
|
+
const wanted = currentMermaidTheme();
|
|
1289
|
+
if (configuredTheme !== wanted) configure(loaded, wanted);
|
|
1290
|
+
return loaded;
|
|
1155
1291
|
}
|
|
1156
1292
|
//#endregion
|
|
1157
1293
|
//#region src/MermaidDiagram.tsx
|
|
@@ -1204,7 +1340,7 @@ const MaximizeIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
|
1204
1340
|
strokeWidth: "2",
|
|
1205
1341
|
strokeLinecap: "round",
|
|
1206
1342
|
strokeLinejoin: "round",
|
|
1207
|
-
className: "w-4 h-4 text-gray-600",
|
|
1343
|
+
className: "w-4 h-4 text-gray-600 dark:text-slate-200",
|
|
1208
1344
|
children: [
|
|
1209
1345
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "15 3 21 3 21 9" }),
|
|
1210
1346
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "9 21 3 21 3 15" }),
|
|
@@ -1365,32 +1501,32 @@ function ZoomableSvg({ svg }) {
|
|
|
1365
1501
|
dangerouslySetInnerHTML: { __html: svg }
|
|
1366
1502
|
})
|
|
1367
1503
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1368
|
-
className: "absolute bottom-4 right-4 flex items-center gap-1 rounded-lg bg-white/95 shadow-md border border-gray-200 px-1.5 py-1",
|
|
1504
|
+
className: "absolute bottom-4 right-4 flex items-center gap-1 rounded-lg bg-white/95 dark:bg-slate-800/95 shadow-md border border-gray-200 dark:border-slate-600 px-1.5 py-1",
|
|
1369
1505
|
children: [
|
|
1370
1506
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1371
1507
|
onClick: () => zoomBy(1 / 1.3),
|
|
1372
|
-
className: "p-1.5 rounded hover:bg-gray-100 text-gray-600",
|
|
1508
|
+
className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-600 dark:text-slate-200",
|
|
1373
1509
|
"aria-label": "Zoom out",
|
|
1374
1510
|
title: "Zoom out",
|
|
1375
1511
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinusIcon, {})
|
|
1376
1512
|
}),
|
|
1377
1513
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1378
1514
|
onClick: reset,
|
|
1379
|
-
className: "px-2 py-1 text-xs font-medium rounded hover:bg-gray-100 text-gray-600 min-w-[3rem]",
|
|
1515
|
+
className: "px-2 py-1 text-xs font-medium rounded hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-600 dark:text-slate-200 min-w-[3rem]",
|
|
1380
1516
|
title: "Reset zoom (or double-click)",
|
|
1381
1517
|
children: [Math.round(scale * 100), "%"]
|
|
1382
1518
|
}),
|
|
1383
1519
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1384
1520
|
onClick: () => zoomBy(1.3),
|
|
1385
|
-
className: "p-1.5 rounded hover:bg-gray-100 text-gray-600",
|
|
1521
|
+
className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-600 dark:text-slate-200",
|
|
1386
1522
|
"aria-label": "Zoom in",
|
|
1387
1523
|
title: "Zoom in",
|
|
1388
1524
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {})
|
|
1389
1525
|
}),
|
|
1390
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "w-px h-5 bg-gray-200 mx-0.5" }),
|
|
1526
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "w-px h-5 bg-gray-200 dark:bg-slate-600 mx-0.5" }),
|
|
1391
1527
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1392
1528
|
onClick: reset,
|
|
1393
|
-
className: "p-1.5 rounded hover:bg-gray-100 text-gray-600",
|
|
1529
|
+
className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 text-gray-600 dark:text-slate-200",
|
|
1394
1530
|
"aria-label": "Reset view",
|
|
1395
1531
|
title: "Reset view",
|
|
1396
1532
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetIcon, {})
|
|
@@ -1428,38 +1564,74 @@ function DiagramModal({ isOpen, onClose, children }) {
|
|
|
1428
1564
|
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4",
|
|
1429
1565
|
onClick: onClose,
|
|
1430
1566
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1431
|
-
className: "bg-white rounded-lg shadow-xl w-[96vw] h-[94vh] flex flex-col overflow-hidden",
|
|
1567
|
+
className: "bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 rounded-lg shadow-xl w-[96vw] h-[94vh] flex flex-col overflow-hidden",
|
|
1432
1568
|
role: "dialog",
|
|
1433
1569
|
"aria-modal": "true",
|
|
1434
1570
|
onClick: (e) => e.stopPropagation(),
|
|
1435
1571
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1436
|
-
className: "flex items-center justify-between p-4 border-b shrink-0",
|
|
1572
|
+
className: "flex items-center justify-between p-4 border-b border-gray-200 dark:border-slate-700 shrink-0",
|
|
1437
1573
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1438
1574
|
className: "text-lg font-semibold",
|
|
1439
1575
|
children: "Mermaid Diagram"
|
|
1440
1576
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1441
1577
|
onClick: onClose,
|
|
1442
|
-
className: "p-1 hover:bg-gray-100 rounded-full transition-colors",
|
|
1578
|
+
className: "p-1 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-full transition-colors",
|
|
1443
1579
|
"aria-label": "Close modal",
|
|
1444
1580
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, {})
|
|
1445
1581
|
})]
|
|
1446
1582
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1447
|
-
className: "flex-1 overflow-hidden bg-gray-50",
|
|
1583
|
+
className: "flex-1 overflow-hidden bg-gray-50 dark:bg-slate-800",
|
|
1448
1584
|
children
|
|
1449
1585
|
})]
|
|
1450
1586
|
})
|
|
1451
1587
|
});
|
|
1452
1588
|
}
|
|
1589
|
+
/**
|
|
1590
|
+
* The palette the page is asking for, re-read whenever `<html>`'s class list
|
|
1591
|
+
* changes.
|
|
1592
|
+
*
|
|
1593
|
+
* A diagram is an SVG baked at render time, so unlike everything else on the
|
|
1594
|
+
* page it does not restyle when the theme flips — it has to be drawn again. The
|
|
1595
|
+
* observer is what notices; the effect below is what redraws. Without it a
|
|
1596
|
+
* session that started in light mode kept white boxes with black ink on the
|
|
1597
|
+
* dark page for as long as it lived.
|
|
1598
|
+
*
|
|
1599
|
+
* `useSyncExternalStore` rather than state plus an effect: the class list *is*
|
|
1600
|
+
* an external store, and reading it in an effect would both miss a flip that
|
|
1601
|
+
* landed before the effect ran and set state synchronously inside one.
|
|
1602
|
+
*/
|
|
1603
|
+
function subscribeToTheme(onChange) {
|
|
1604
|
+
if (typeof MutationObserver === "undefined") return () => {};
|
|
1605
|
+
const observer = new MutationObserver(onChange);
|
|
1606
|
+
observer.observe(document.documentElement, {
|
|
1607
|
+
attributes: true,
|
|
1608
|
+
attributeFilter: ["class"]
|
|
1609
|
+
});
|
|
1610
|
+
return () => observer.disconnect();
|
|
1611
|
+
}
|
|
1612
|
+
/** Server render has no `<html>` to read, and no diagram to draw either. */
|
|
1613
|
+
const serverTheme = () => "default";
|
|
1614
|
+
function useMermaidTheme() {
|
|
1615
|
+
return (0, react.useSyncExternalStore)(subscribeToTheme, currentMermaidTheme, serverTheme);
|
|
1616
|
+
}
|
|
1453
1617
|
const MermaidDiagramInner = ({ code }) => {
|
|
1454
|
-
const
|
|
1455
|
-
const [svg, setSvg] = (0, react.useState)(() => svgCache.get(code) || "");
|
|
1456
|
-
const [errorMessage, setErrorMessage] = (0, react.useState)(null);
|
|
1618
|
+
const theme = useMermaidTheme();
|
|
1457
1619
|
const [showSource, setShowSource] = (0, react.useState)(false);
|
|
1458
1620
|
const [isModalOpen, setIsModalOpen] = (0, react.useState)(false);
|
|
1459
|
-
const [isLoading, setIsLoading] = (0, react.useState)(!hasCached);
|
|
1460
1621
|
const [minHeight, setMinHeight] = (0, react.useState)("auto");
|
|
1461
1622
|
const containerRef = (0, react.useRef)(null);
|
|
1462
1623
|
const lastHeightRef = (0, react.useRef)(null);
|
|
1624
|
+
/**
|
|
1625
|
+
* What this render produced, and which (code, theme) it was for.
|
|
1626
|
+
*
|
|
1627
|
+
* Keyed rather than plain, and read past the cache rather than instead of it,
|
|
1628
|
+
* because a theme flip has to invalidate both: the cache is a miss in the new
|
|
1629
|
+
* palette, and last render's SVG is the wrong palette. Deriving `svg` here
|
|
1630
|
+
* rather than mirroring the cache into state is also what keeps a cache hit
|
|
1631
|
+
* from being a `setState` inside an effect.
|
|
1632
|
+
*/
|
|
1633
|
+
const [rendered, setRendered] = (0, react.useState)(null);
|
|
1634
|
+
const [failure, setFailure] = (0, react.useState)(null);
|
|
1463
1635
|
const stableId = (0, react.useMemo)(() => {
|
|
1464
1636
|
let hash = 0;
|
|
1465
1637
|
for (let i = 0; i < code.length; i++) {
|
|
@@ -1469,9 +1641,14 @@ const MermaidDiagramInner = ({ code }) => {
|
|
|
1469
1641
|
}
|
|
1470
1642
|
return `mermaid-${Math.abs(hash).toString(36)}`;
|
|
1471
1643
|
}, [code]);
|
|
1644
|
+
const attempt = `${theme} ${stableId}`;
|
|
1645
|
+
const svg = getCachedSvg(code, theme) ?? (rendered?.key === attempt ? rendered.svg : "");
|
|
1646
|
+
const errorMessage = failure?.key === attempt ? failure.message : null;
|
|
1647
|
+
const isLoading = svg === "" && errorMessage === null;
|
|
1472
1648
|
(0, react.useEffect)(() => {
|
|
1473
|
-
if (
|
|
1649
|
+
if (getCachedSvg(code, theme) !== void 0) return;
|
|
1474
1650
|
let mounted = true;
|
|
1651
|
+
const key = `${theme} ${stableId}`;
|
|
1475
1652
|
const renderDiagram = async () => {
|
|
1476
1653
|
try {
|
|
1477
1654
|
if (containerRef.current) {
|
|
@@ -1483,17 +1660,18 @@ const MermaidDiagramInner = ({ code }) => {
|
|
|
1483
1660
|
const id = `${stableId}-${Date.now()}`;
|
|
1484
1661
|
const { svg: renderedSvg } = await m.render(id, code);
|
|
1485
1662
|
if (mounted) {
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1663
|
+
setCachedSvg(code, renderedSvg, theme);
|
|
1664
|
+
setRendered({
|
|
1665
|
+
key,
|
|
1666
|
+
svg: renderedSvg
|
|
1667
|
+
});
|
|
1490
1668
|
}
|
|
1491
1669
|
} catch (err) {
|
|
1492
1670
|
console.error("Mermaid render error:", err);
|
|
1493
|
-
if (mounted) {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1671
|
+
if (mounted) setFailure({
|
|
1672
|
+
key,
|
|
1673
|
+
message: extractErrorMessage(err)
|
|
1674
|
+
});
|
|
1497
1675
|
}
|
|
1498
1676
|
};
|
|
1499
1677
|
renderDiagram();
|
|
@@ -1503,7 +1681,7 @@ const MermaidDiagramInner = ({ code }) => {
|
|
|
1503
1681
|
}, [
|
|
1504
1682
|
code,
|
|
1505
1683
|
stableId,
|
|
1506
|
-
|
|
1684
|
+
theme
|
|
1507
1685
|
]);
|
|
1508
1686
|
if (errorMessage) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1509
1687
|
"data-testid": "mermaid-container",
|
|
@@ -1543,7 +1721,7 @@ const MermaidDiagramInner = ({ code }) => {
|
|
|
1543
1721
|
dangerouslySetInnerHTML: { __html: svg }
|
|
1544
1722
|
}), svg && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1545
1723
|
onClick: () => setIsModalOpen(true),
|
|
1546
|
-
className: "absolute top-2 right-2 p-2 bg-white/90 shadow-sm border rounded-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50",
|
|
1724
|
+
className: "absolute top-2 right-2 p-2 bg-white/90 dark:bg-slate-800/90 shadow-sm border border-gray-200 dark:border-slate-600 rounded-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 dark:hover:bg-slate-700",
|
|
1547
1725
|
"aria-label": "Maximize diagram",
|
|
1548
1726
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MaximizeIcon, {})
|
|
1549
1727
|
})]
|
|
@@ -2203,18 +2381,20 @@ async function renderMermaidBlocks(container, options = {}) {
|
|
|
2203
2381
|
const { className = "mermaid", onError } = options;
|
|
2204
2382
|
const codeBlocks = container.querySelectorAll("pre > code.language-mermaid, pre > code[class*=\"language-mermaid\"]");
|
|
2205
2383
|
if (codeBlocks.length === 0) return;
|
|
2206
|
-
|
|
2384
|
+
let loading;
|
|
2385
|
+
const mermaidOnce = () => loading ??= getMermaid();
|
|
2207
2386
|
const renderPromises = Array.from(codeBlocks).map(async (codeEl) => {
|
|
2208
2387
|
const preEl = codeEl.parentElement;
|
|
2209
2388
|
if (!preEl) return;
|
|
2210
2389
|
const code = codeEl.textContent || "";
|
|
2211
2390
|
if (!code.trim()) return;
|
|
2212
|
-
const cached =
|
|
2391
|
+
const cached = getCachedSvg(code);
|
|
2213
2392
|
if (cached) {
|
|
2214
2393
|
replaceWithSvg(preEl, cached, className);
|
|
2215
2394
|
return;
|
|
2216
2395
|
}
|
|
2217
2396
|
try {
|
|
2397
|
+
const mermaid = await mermaidOnce();
|
|
2218
2398
|
let hash = 0;
|
|
2219
2399
|
for (let i = 0; i < code.length; i++) {
|
|
2220
2400
|
hash = (hash << 5) - hash + code.charCodeAt(i);
|
|
@@ -2222,7 +2402,7 @@ async function renderMermaidBlocks(container, options = {}) {
|
|
|
2222
2402
|
}
|
|
2223
2403
|
const id = `mermaid-${Math.abs(hash).toString(36)}-${Date.now()}`;
|
|
2224
2404
|
const { svg } = await mermaid.render(id, code);
|
|
2225
|
-
|
|
2405
|
+
setCachedSvg(code, svg);
|
|
2226
2406
|
replaceWithSvg(preEl, svg, className);
|
|
2227
2407
|
} catch (err) {
|
|
2228
2408
|
if (onError) onError(code, err instanceof Error ? err : new Error(String(err)));
|
|
@@ -2230,9 +2410,34 @@ async function renderMermaidBlocks(container, options = {}) {
|
|
|
2230
2410
|
});
|
|
2231
2411
|
await Promise.all(renderPromises);
|
|
2232
2412
|
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Attributes the wrapper inherits from the `<pre>` it replaces.
|
|
2415
|
+
*
|
|
2416
|
+
* The splice is the same shape of problem `rehypeVantageMathStamps` solves for
|
|
2417
|
+
* KaTeX: the pipeline stamped the fence, and swapping the element out throws
|
|
2418
|
+
* the stamps away. A mermaid diagram inside a toned section then drew no slice
|
|
2419
|
+
* of the section's vertical rule, leaving a hole as tall as the diagram; a
|
|
2420
|
+
* collapsed section left the diagram visible under a closed heading; and a
|
|
2421
|
+
* `#L` anchor pointing at the fence resolved to nothing.
|
|
2422
|
+
*
|
|
2423
|
+
* Named individually rather than copied wholesale: `class` is the caller's
|
|
2424
|
+
* (`className`), and `id` would be duplicated onto a second element.
|
|
2425
|
+
*/
|
|
2426
|
+
const CARRIED_ATTRIBUTES = [
|
|
2427
|
+
"data-source-line",
|
|
2428
|
+
"data-vantage-tone",
|
|
2429
|
+
"data-vantage-emphasis",
|
|
2430
|
+
"data-vantage-run",
|
|
2431
|
+
"data-vantage-collapsed",
|
|
2432
|
+
"data-vantage-collapse-group"
|
|
2433
|
+
];
|
|
2233
2434
|
function replaceWithSvg(preEl, svg, className) {
|
|
2234
2435
|
const wrapper = document.createElement("div");
|
|
2235
2436
|
wrapper.className = className;
|
|
2437
|
+
for (const name of CARRIED_ATTRIBUTES) {
|
|
2438
|
+
const value = preEl.getAttribute(name);
|
|
2439
|
+
if (value !== null) wrapper.setAttribute(name, value);
|
|
2440
|
+
}
|
|
2236
2441
|
wrapper.innerHTML = svg;
|
|
2237
2442
|
preEl.replaceWith(wrapper);
|
|
2238
2443
|
}
|