vantage-md 0.5.7 → 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 +196 -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 +196 -20
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +261 -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 +262 -46
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/react.cjs
CHANGED
|
@@ -51,7 +51,31 @@ 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
|
|
70
|
+
/**
|
|
71
|
+
* Tags that get a `data-source-line`.
|
|
72
|
+
*
|
|
73
|
+
* `td`/`th` are here for review mode: a comment anchors to the cell it was
|
|
74
|
+
* written on, so the cell needs a line of its own to be found again. Every cell
|
|
75
|
+
* in a row reports the *row's* start line — a GFM row is one source line — so a
|
|
76
|
+
* line no longer names at most one anchorable element, and whatever resolves an
|
|
77
|
+
* anchor has to break the tie by block hash (`useReviewHighlights`).
|
|
78
|
+
*/
|
|
55
79
|
const BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
56
80
|
"p",
|
|
57
81
|
"h1",
|
|
@@ -64,6 +88,8 @@ const BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
|
64
88
|
"blockquote",
|
|
65
89
|
"pre",
|
|
66
90
|
"table",
|
|
91
|
+
"td",
|
|
92
|
+
"th",
|
|
67
93
|
"tr",
|
|
68
94
|
"ul",
|
|
69
95
|
"ol",
|
|
@@ -266,12 +292,17 @@ const VANTAGE_RUNS = [
|
|
|
266
292
|
"only"
|
|
267
293
|
];
|
|
268
294
|
/**
|
|
269
|
-
* The tags a `section`/`block` directive may
|
|
295
|
+
* The tags a `section`/`block` directive may **target**.
|
|
270
296
|
*
|
|
271
|
-
* Deliberately `rehypeSourceLines`'s `BLOCK_TAGS`: a
|
|
272
|
-
* be a block with a `data-source-line`, so the styling surface and the
|
|
273
|
-
* surface coincide. It also keeps an inline directive from stamping the
|
|
274
|
-
* 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.
|
|
275
306
|
*
|
|
276
307
|
* It lives here rather than in the plugin because the CLI checker has to answer
|
|
277
308
|
* "will this directive stamp anything?" from an mdast tree with no hast in
|
|
@@ -321,6 +352,23 @@ const VANTAGE_ANCHOR_TARGETS = [
|
|
|
321
352
|
"table"
|
|
322
353
|
];
|
|
323
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]+$/;
|
|
324
372
|
const STYLE_KEYS = {
|
|
325
373
|
tone: VANTAGE_TONES,
|
|
326
374
|
emphasis: VANTAGE_EMPHASIS,
|
|
@@ -453,11 +501,13 @@ function parseVantageDirective(comment) {
|
|
|
453
501
|
//#endregion
|
|
454
502
|
//#region src/rehypeVantageDirectives.ts
|
|
455
503
|
/**
|
|
456
|
-
* What a `section`/`block` and an `oq` directive may
|
|
504
|
+
* What a `section`/`block` and an `oq` directive may **target**.
|
|
457
505
|
*
|
|
458
506
|
* Both lists live in `vantageDirectives.ts`, with the reasoning for each tag,
|
|
459
507
|
* because the CLI checker resolves the same question over mdast and must reach
|
|
460
508
|
* the same answer (D5).
|
|
509
|
+
*
|
|
510
|
+
* Neither list bounds a `section`'s range: see `styleRange`.
|
|
461
511
|
*/
|
|
462
512
|
const STYLE_TARGET_TAGS = new Set(VANTAGE_STYLE_TARGETS);
|
|
463
513
|
const ANCHOR_TARGET_TAGS = new Set(VANTAGE_ANCHOR_TARGETS);
|
|
@@ -504,6 +554,18 @@ const RUN_PROPERTY = "dataVantageRun";
|
|
|
504
554
|
const OQ_PROPERTY = "dataVantageOq";
|
|
505
555
|
const LEANING_PROPERTY = "dataVantageLeaning";
|
|
506
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
|
+
/**
|
|
507
569
|
* The three properties `collapsed=true` stamps across a section.
|
|
508
570
|
*
|
|
509
571
|
* The heading takes a *different* attribute from the blocks it hides, and that
|
|
@@ -576,6 +638,22 @@ function accepts(name, key, value) {
|
|
|
576
638
|
* `section` before anything else degrades to that one block, and `block` is
|
|
577
639
|
* always that one block. A heading nested inside a stamped `blockquote` or
|
|
578
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.
|
|
579
657
|
*/
|
|
580
658
|
function styleRange(children, targetIndex, name) {
|
|
581
659
|
const range = [targetIndex];
|
|
@@ -585,7 +663,7 @@ function styleRange(children, targetIndex, name) {
|
|
|
585
663
|
const node = children[i];
|
|
586
664
|
const nodeDepth = headingDepth(node);
|
|
587
665
|
if (nodeDepth !== void 0 && nodeDepth <= depth) break;
|
|
588
|
-
if (node.type === "element"
|
|
666
|
+
if (node.type === "element") range.push(i);
|
|
589
667
|
}
|
|
590
668
|
return range;
|
|
591
669
|
}
|
|
@@ -644,6 +722,8 @@ function stampStyle(children, targetIndex, name, pairs, state) {
|
|
|
644
722
|
}
|
|
645
723
|
function stampOq(target, pairs) {
|
|
646
724
|
setProperty(target, OQ_PROPERTY, "true");
|
|
725
|
+
const id = pairs.get("id");
|
|
726
|
+
if (id !== void 0 && id !== "") setProperty(target, OQ_ID_PROPERTY, id);
|
|
647
727
|
const leaning = pairs.get("leaning");
|
|
648
728
|
if (leaning === void 0) return;
|
|
649
729
|
const text = leaning.replace(/\s+/g, " ").trim().slice(0, MAX_LEANING);
|
|
@@ -949,6 +1029,7 @@ const sanitizeSchema = {
|
|
|
949
1029
|
["dataVantageCollapseToggle", COLLAPSE_GROUP_ID],
|
|
950
1030
|
["dataVantageRun", ...VANTAGE_RUNS],
|
|
951
1031
|
["dataVantageOq", "true"],
|
|
1032
|
+
["dataVantageOqId", VANTAGE_OQ_ID],
|
|
952
1033
|
["dataVantageAlert", ...VANTAGE_ALERTS],
|
|
953
1034
|
"dataVantageLeaning"
|
|
954
1035
|
],
|
|
@@ -998,6 +1079,7 @@ function buildRehypePlugins(options = {}) {
|
|
|
998
1079
|
plugins.push(rehypeVantageAlerts);
|
|
999
1080
|
plugins.push(rehypeVantageDirectives);
|
|
1000
1081
|
if (sanitize) plugins.push([rehype_sanitize.default, sanitizeSchema]);
|
|
1082
|
+
plugins.push(rehypeVantageAnchors);
|
|
1001
1083
|
plugins.push(rehype_slug.default);
|
|
1002
1084
|
if (highlight) plugins.push(rehype_highlight.default);
|
|
1003
1085
|
if (math) plugins.push(rehypeCaptureMathStamps, rehype_katex.default, rehypeRestoreMathStamps);
|
|
@@ -1120,27 +1202,92 @@ function errorPosition(error) {
|
|
|
1120
1202
|
return { message };
|
|
1121
1203
|
}
|
|
1122
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
|
|
1123
1238
|
//#region src/mermaidCache.ts
|
|
1124
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
|
+
}
|
|
1125
1248
|
//#endregion
|
|
1126
1249
|
//#region src/mermaidLoader.ts
|
|
1127
1250
|
let mermaidInstance = null;
|
|
1128
1251
|
let mermaidLoading = null;
|
|
1129
|
-
|
|
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
|
+
*/
|
|
1130
1275
|
async function getMermaid() {
|
|
1131
|
-
|
|
1276
|
+
const theme = currentMermaidTheme();
|
|
1277
|
+
if (mermaidInstance) {
|
|
1278
|
+
if (configuredTheme !== theme) configure(mermaidInstance, theme);
|
|
1279
|
+
return mermaidInstance;
|
|
1280
|
+
}
|
|
1132
1281
|
if (!mermaidLoading) mermaidLoading = import("mermaid").then((mod) => {
|
|
1133
1282
|
const m = mod.default;
|
|
1134
|
-
m
|
|
1135
|
-
startOnLoad: false,
|
|
1136
|
-
theme: isDark() ? "dark" : "default",
|
|
1137
|
-
securityLevel: "strict",
|
|
1138
|
-
suppressErrorRendering: true
|
|
1139
|
-
});
|
|
1283
|
+
configure(m, currentMermaidTheme());
|
|
1140
1284
|
mermaidInstance = m;
|
|
1141
1285
|
return m;
|
|
1142
1286
|
});
|
|
1143
|
-
|
|
1287
|
+
const loaded = await mermaidLoading;
|
|
1288
|
+
const wanted = currentMermaidTheme();
|
|
1289
|
+
if (configuredTheme !== wanted) configure(loaded, wanted);
|
|
1290
|
+
return loaded;
|
|
1144
1291
|
}
|
|
1145
1292
|
//#endregion
|
|
1146
1293
|
//#region src/MermaidDiagram.tsx
|
|
@@ -1193,7 +1340,7 @@ const MaximizeIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
|
1193
1340
|
strokeWidth: "2",
|
|
1194
1341
|
strokeLinecap: "round",
|
|
1195
1342
|
strokeLinejoin: "round",
|
|
1196
|
-
className: "w-4 h-4 text-gray-600",
|
|
1343
|
+
className: "w-4 h-4 text-gray-600 dark:text-slate-200",
|
|
1197
1344
|
children: [
|
|
1198
1345
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "15 3 21 3 21 9" }),
|
|
1199
1346
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "9 21 3 21 3 15" }),
|
|
@@ -1354,32 +1501,32 @@ function ZoomableSvg({ svg }) {
|
|
|
1354
1501
|
dangerouslySetInnerHTML: { __html: svg }
|
|
1355
1502
|
})
|
|
1356
1503
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1357
|
-
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",
|
|
1358
1505
|
children: [
|
|
1359
1506
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1360
1507
|
onClick: () => zoomBy(1 / 1.3),
|
|
1361
|
-
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",
|
|
1362
1509
|
"aria-label": "Zoom out",
|
|
1363
1510
|
title: "Zoom out",
|
|
1364
1511
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinusIcon, {})
|
|
1365
1512
|
}),
|
|
1366
1513
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1367
1514
|
onClick: reset,
|
|
1368
|
-
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]",
|
|
1369
1516
|
title: "Reset zoom (or double-click)",
|
|
1370
1517
|
children: [Math.round(scale * 100), "%"]
|
|
1371
1518
|
}),
|
|
1372
1519
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1373
1520
|
onClick: () => zoomBy(1.3),
|
|
1374
|
-
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",
|
|
1375
1522
|
"aria-label": "Zoom in",
|
|
1376
1523
|
title: "Zoom in",
|
|
1377
1524
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlusIcon, {})
|
|
1378
1525
|
}),
|
|
1379
|
-
/* @__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" }),
|
|
1380
1527
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1381
1528
|
onClick: reset,
|
|
1382
|
-
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",
|
|
1383
1530
|
"aria-label": "Reset view",
|
|
1384
1531
|
title: "Reset view",
|
|
1385
1532
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetIcon, {})
|
|
@@ -1417,38 +1564,74 @@ function DiagramModal({ isOpen, onClose, children }) {
|
|
|
1417
1564
|
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4",
|
|
1418
1565
|
onClick: onClose,
|
|
1419
1566
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1420
|
-
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",
|
|
1421
1568
|
role: "dialog",
|
|
1422
1569
|
"aria-modal": "true",
|
|
1423
1570
|
onClick: (e) => e.stopPropagation(),
|
|
1424
1571
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1425
|
-
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",
|
|
1426
1573
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1427
1574
|
className: "text-lg font-semibold",
|
|
1428
1575
|
children: "Mermaid Diagram"
|
|
1429
1576
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1430
1577
|
onClick: onClose,
|
|
1431
|
-
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",
|
|
1432
1579
|
"aria-label": "Close modal",
|
|
1433
1580
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, {})
|
|
1434
1581
|
})]
|
|
1435
1582
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1436
|
-
className: "flex-1 overflow-hidden bg-gray-50",
|
|
1583
|
+
className: "flex-1 overflow-hidden bg-gray-50 dark:bg-slate-800",
|
|
1437
1584
|
children
|
|
1438
1585
|
})]
|
|
1439
1586
|
})
|
|
1440
1587
|
});
|
|
1441
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
|
+
}
|
|
1442
1617
|
const MermaidDiagramInner = ({ code }) => {
|
|
1443
|
-
const
|
|
1444
|
-
const [svg, setSvg] = (0, react.useState)(() => svgCache.get(code) || "");
|
|
1445
|
-
const [errorMessage, setErrorMessage] = (0, react.useState)(null);
|
|
1618
|
+
const theme = useMermaidTheme();
|
|
1446
1619
|
const [showSource, setShowSource] = (0, react.useState)(false);
|
|
1447
1620
|
const [isModalOpen, setIsModalOpen] = (0, react.useState)(false);
|
|
1448
|
-
const [isLoading, setIsLoading] = (0, react.useState)(!hasCached);
|
|
1449
1621
|
const [minHeight, setMinHeight] = (0, react.useState)("auto");
|
|
1450
1622
|
const containerRef = (0, react.useRef)(null);
|
|
1451
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);
|
|
1452
1635
|
const stableId = (0, react.useMemo)(() => {
|
|
1453
1636
|
let hash = 0;
|
|
1454
1637
|
for (let i = 0; i < code.length; i++) {
|
|
@@ -1458,9 +1641,14 @@ const MermaidDiagramInner = ({ code }) => {
|
|
|
1458
1641
|
}
|
|
1459
1642
|
return `mermaid-${Math.abs(hash).toString(36)}`;
|
|
1460
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;
|
|
1461
1648
|
(0, react.useEffect)(() => {
|
|
1462
|
-
if (
|
|
1649
|
+
if (getCachedSvg(code, theme) !== void 0) return;
|
|
1463
1650
|
let mounted = true;
|
|
1651
|
+
const key = `${theme} ${stableId}`;
|
|
1464
1652
|
const renderDiagram = async () => {
|
|
1465
1653
|
try {
|
|
1466
1654
|
if (containerRef.current) {
|
|
@@ -1472,17 +1660,18 @@ const MermaidDiagramInner = ({ code }) => {
|
|
|
1472
1660
|
const id = `${stableId}-${Date.now()}`;
|
|
1473
1661
|
const { svg: renderedSvg } = await m.render(id, code);
|
|
1474
1662
|
if (mounted) {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1663
|
+
setCachedSvg(code, renderedSvg, theme);
|
|
1664
|
+
setRendered({
|
|
1665
|
+
key,
|
|
1666
|
+
svg: renderedSvg
|
|
1667
|
+
});
|
|
1479
1668
|
}
|
|
1480
1669
|
} catch (err) {
|
|
1481
1670
|
console.error("Mermaid render error:", err);
|
|
1482
|
-
if (mounted) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
}
|
|
1671
|
+
if (mounted) setFailure({
|
|
1672
|
+
key,
|
|
1673
|
+
message: extractErrorMessage(err)
|
|
1674
|
+
});
|
|
1486
1675
|
}
|
|
1487
1676
|
};
|
|
1488
1677
|
renderDiagram();
|
|
@@ -1492,7 +1681,7 @@ const MermaidDiagramInner = ({ code }) => {
|
|
|
1492
1681
|
}, [
|
|
1493
1682
|
code,
|
|
1494
1683
|
stableId,
|
|
1495
|
-
|
|
1684
|
+
theme
|
|
1496
1685
|
]);
|
|
1497
1686
|
if (errorMessage) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1498
1687
|
"data-testid": "mermaid-container",
|
|
@@ -1532,7 +1721,7 @@ const MermaidDiagramInner = ({ code }) => {
|
|
|
1532
1721
|
dangerouslySetInnerHTML: { __html: svg }
|
|
1533
1722
|
}), svg && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1534
1723
|
onClick: () => setIsModalOpen(true),
|
|
1535
|
-
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",
|
|
1536
1725
|
"aria-label": "Maximize diagram",
|
|
1537
1726
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MaximizeIcon, {})
|
|
1538
1727
|
})]
|
|
@@ -2192,18 +2381,20 @@ async function renderMermaidBlocks(container, options = {}) {
|
|
|
2192
2381
|
const { className = "mermaid", onError } = options;
|
|
2193
2382
|
const codeBlocks = container.querySelectorAll("pre > code.language-mermaid, pre > code[class*=\"language-mermaid\"]");
|
|
2194
2383
|
if (codeBlocks.length === 0) return;
|
|
2195
|
-
|
|
2384
|
+
let loading;
|
|
2385
|
+
const mermaidOnce = () => loading ??= getMermaid();
|
|
2196
2386
|
const renderPromises = Array.from(codeBlocks).map(async (codeEl) => {
|
|
2197
2387
|
const preEl = codeEl.parentElement;
|
|
2198
2388
|
if (!preEl) return;
|
|
2199
2389
|
const code = codeEl.textContent || "";
|
|
2200
2390
|
if (!code.trim()) return;
|
|
2201
|
-
const cached =
|
|
2391
|
+
const cached = getCachedSvg(code);
|
|
2202
2392
|
if (cached) {
|
|
2203
2393
|
replaceWithSvg(preEl, cached, className);
|
|
2204
2394
|
return;
|
|
2205
2395
|
}
|
|
2206
2396
|
try {
|
|
2397
|
+
const mermaid = await mermaidOnce();
|
|
2207
2398
|
let hash = 0;
|
|
2208
2399
|
for (let i = 0; i < code.length; i++) {
|
|
2209
2400
|
hash = (hash << 5) - hash + code.charCodeAt(i);
|
|
@@ -2211,7 +2402,7 @@ async function renderMermaidBlocks(container, options = {}) {
|
|
|
2211
2402
|
}
|
|
2212
2403
|
const id = `mermaid-${Math.abs(hash).toString(36)}-${Date.now()}`;
|
|
2213
2404
|
const { svg } = await mermaid.render(id, code);
|
|
2214
|
-
|
|
2405
|
+
setCachedSvg(code, svg);
|
|
2215
2406
|
replaceWithSvg(preEl, svg, className);
|
|
2216
2407
|
} catch (err) {
|
|
2217
2408
|
if (onError) onError(code, err instanceof Error ? err : new Error(String(err)));
|
|
@@ -2219,9 +2410,34 @@ async function renderMermaidBlocks(container, options = {}) {
|
|
|
2219
2410
|
});
|
|
2220
2411
|
await Promise.all(renderPromises);
|
|
2221
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
|
+
];
|
|
2222
2434
|
function replaceWithSvg(preEl, svg, className) {
|
|
2223
2435
|
const wrapper = document.createElement("div");
|
|
2224
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
|
+
}
|
|
2225
2441
|
wrapper.innerHTML = svg;
|
|
2226
2442
|
preEl.replaceWith(wrapper);
|
|
2227
2443
|
}
|