vantage-md 0.7.1 → 0.8.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.
- package/CHANGELOG.md +173 -157
- package/README.md +26 -0
- package/dist/directives.css +671 -0
- package/dist/index.cjs +968 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +130 -34
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +130 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +957 -130
- package/dist/index.js.map +1 -1
- package/dist/inline-svg.css +55 -0
- package/dist/line-anchor.css +22 -0
- package/dist/react.cjs +915 -168
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +87 -42
- package/dist/react.d.cts.map +1 -1
- package/dist/react.d.ts +87 -42
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +909 -164
- package/dist/react.js.map +1 -1
- package/dist/styles.css +3 -1
- package/dist/task-list.css +157 -0
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import ReactMarkdown from "react-markdown";
|
|
3
|
-
import remarkGfm from "remark-gfm";
|
|
4
|
-
import remarkMath from "remark-math";
|
|
5
3
|
import rehypeRaw from "rehype-raw";
|
|
6
4
|
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
7
5
|
import rehypeHighlight from "rehype-highlight";
|
|
8
6
|
import rehypeKatex from "rehype-katex";
|
|
9
7
|
import rehypeSlug from "rehype-slug";
|
|
10
8
|
import { visit } from "unist-util-visit";
|
|
9
|
+
import remarkGfm from "remark-gfm";
|
|
10
|
+
import remarkMath from "remark-math";
|
|
11
11
|
import YAML from "yaml";
|
|
12
12
|
import { parse } from "smol-toml";
|
|
13
13
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
-
import { unified } from "unified";
|
|
15
14
|
import remarkParse from "remark-parse";
|
|
15
|
+
import { unified } from "unified";
|
|
16
16
|
import remarkRehype from "remark-rehype";
|
|
17
17
|
import rehypeStringify from "rehype-stringify";
|
|
18
18
|
//#region src/rehypeVantageAnchors.ts
|
|
@@ -127,6 +127,12 @@ const VANTAGE_ALERTS = [
|
|
|
127
127
|
"warning",
|
|
128
128
|
"caution"
|
|
129
129
|
];
|
|
130
|
+
/**
|
|
131
|
+
* The class on the title element this plugin injects, which
|
|
132
|
+
* `styles/directives.css` styles. Exported because the sanitizer runs after
|
|
133
|
+
* this plugin and admits a `div`'s class only when it is this one.
|
|
134
|
+
*/
|
|
135
|
+
const ALERT_TITLE_CLASS = "vantage-alert-title";
|
|
130
136
|
/** The visible label per kind. Title case, as GitHub renders it. */
|
|
131
137
|
const ALERT_TITLES = {
|
|
132
138
|
note: "Note",
|
|
@@ -150,7 +156,7 @@ const ALERT_TITLES = {
|
|
|
150
156
|
* anchored test on the first text node is enough, and the plugin does not have
|
|
151
157
|
* to reassemble the marker across siblings.
|
|
152
158
|
*/
|
|
153
|
-
const
|
|
159
|
+
const ALERT_MARKER = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*(?:\r?\n|$)/;
|
|
154
160
|
/** The first child, if it is an element. */
|
|
155
161
|
function firstElement(node) {
|
|
156
162
|
const child = node.children.find((c) => c.type === "element" || c.type === "text" && c.value.trim() !== "");
|
|
@@ -177,7 +183,7 @@ function rehypeVantageAlerts() {
|
|
|
177
183
|
if (paragraph === void 0 || paragraph.tagName !== "p") return;
|
|
178
184
|
const lead = paragraph.children[0];
|
|
179
185
|
if (lead === void 0 || lead.type !== "text") return;
|
|
180
|
-
const match =
|
|
186
|
+
const match = ALERT_MARKER.exec(lead.value);
|
|
181
187
|
if (match === null) return;
|
|
182
188
|
const kind = match[1].toLowerCase();
|
|
183
189
|
lead.value = lead.value.slice(match[0].length);
|
|
@@ -189,7 +195,7 @@ function rehypeVantageAlerts() {
|
|
|
189
195
|
node.children.unshift({
|
|
190
196
|
type: "element",
|
|
191
197
|
tagName: "div",
|
|
192
|
-
properties: { className: [
|
|
198
|
+
properties: { className: [ALERT_TITLE_CLASS] },
|
|
193
199
|
children: [{
|
|
194
200
|
type: "text",
|
|
195
201
|
value: ALERT_TITLES[kind]
|
|
@@ -200,6 +206,10 @@ function rehypeVantageAlerts() {
|
|
|
200
206
|
}
|
|
201
207
|
//#endregion
|
|
202
208
|
//#region src/vantageDirectives.ts
|
|
209
|
+
/** Whether `name` declares a question, `question` or the deprecated `oq`. */
|
|
210
|
+
function isQuestionDirective(name) {
|
|
211
|
+
return name === "oq" || name === "question";
|
|
212
|
+
}
|
|
203
213
|
/**
|
|
204
214
|
* The `tone` vocabulary: GitHub's alert words plus `muted`.
|
|
205
215
|
*
|
|
@@ -292,13 +302,32 @@ const VANTAGE_STYLE_TARGETS = [
|
|
|
292
302
|
"div"
|
|
293
303
|
];
|
|
294
304
|
/**
|
|
295
|
-
* The tags
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* neither, so a button on one of them would build an anchor no review pass can
|
|
299
|
-
* find — the "mis-wired button" D6 forbids.
|
|
305
|
+
* The tags a `fallback` directive may **withhold**: `VANTAGE_STYLE_TARGETS`
|
|
306
|
+
* minus the six headings, written as a subtraction so the narrowing stays
|
|
307
|
+
* visible.
|
|
300
308
|
*
|
|
301
|
-
*
|
|
309
|
+
* A heading is refused because it is the document's structure rather than a
|
|
310
|
+
* stand-in for anything: the contents column lists it, other documents link to
|
|
311
|
+
* its slug, and the planning index reads sections under it. A fallback heading
|
|
312
|
+
* would be an anchor that exists on GitHub and nowhere in Vantage. What a
|
|
313
|
+
* fallback holds — a sentence naming the release a drawing needs, a link to
|
|
314
|
+
* the drawing as a file, an ASCII sketch in a fence — is a paragraph, a list,
|
|
315
|
+
* a quote, a code block, a table or a `<div>`, and those are all here.
|
|
316
|
+
*
|
|
317
|
+
* Three callers read it, and they must agree: `rehypeVantageDirectives`, which
|
|
318
|
+
* withholds the block; the planning index's scan, which must not read a
|
|
319
|
+
* question or a link from a block the page never shows; and the checker's
|
|
320
|
+
* `vantage/orphan`, which reports a fallback that withholds nothing.
|
|
321
|
+
*/
|
|
322
|
+
const VANTAGE_FALLBACK_TARGETS = VANTAGE_STYLE_TARGETS.filter((tag) => !/^h[1-6]$/.test(tag));
|
|
323
|
+
/**
|
|
324
|
+
* The tags a question directive, `oq` or `question`, may stamp — strictly the
|
|
325
|
+
* tags the review system can resolve an anchor on (`ANCHOR_TAGS` in the app's
|
|
326
|
+
* `MarkdownViewer`, and the block map in `useReviewHighlights`). `ul`, `ol`,
|
|
327
|
+
* `tr`, `hr` and `div` are in neither, so a button on one of them would build
|
|
328
|
+
* an anchor no review pass can find — the "mis-wired button" D6 forbids.
|
|
329
|
+
*
|
|
330
|
+
* The gap between this list and `VANTAGE_STYLE_TARGETS` is why a question
|
|
302
331
|
* directive at column 0 above a list silently does nothing: the target is the
|
|
303
332
|
* `<ul>`, not the `<li>`. The checker says so.
|
|
304
333
|
*/
|
|
@@ -317,9 +346,9 @@ const VANTAGE_ANCHOR_TARGETS = [
|
|
|
317
346
|
];
|
|
318
347
|
VANTAGE_ANCHOR_TARGETS.filter((tag) => tag !== "pre" && tag !== "table");
|
|
319
348
|
/**
|
|
320
|
-
* The shape of
|
|
321
|
-
*
|
|
322
|
-
* and a bare `OQ6` are not.
|
|
349
|
+
* The shape of a question directive's `id`, on `oq` and `question` alike:
|
|
350
|
+
* `OQ-` then an optional short uppercase prefix then digits. `OQ-9`, `OQ-TP6`
|
|
351
|
+
* and `OQ-A03` are ids; `OQ-foo`, `OQ-tp6` and a bare `OQ6` are not.
|
|
323
352
|
*
|
|
324
353
|
* The prefix is what keeps ids distinct once one document references another's
|
|
325
354
|
* questions — `trust-paths.md`'s `OQ-4` and a design sketch's `OQ-4` are
|
|
@@ -327,10 +356,15 @@ VANTAGE_ANCHOR_TARGETS.filter((tag) => tag !== "pre" && tag !== "table");
|
|
|
327
356
|
* reference means. It is optional because most documents never leave their own
|
|
328
357
|
* file, and requiring it everywhere would fire on every single-doc sketch.
|
|
329
358
|
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* the checker
|
|
333
|
-
*
|
|
359
|
+
* Read from here, never re-spelled, by the sanitizer that allowlists the value,
|
|
360
|
+
* by the checker's `collectOqIds` (which feeds `vantage/oq-id-format` and the
|
|
361
|
+
* anchors the checker accepts) and by the planning index. The directive plugin
|
|
362
|
+
* that stamps the anchor deliberately does not test it: it stamps any non-empty
|
|
363
|
+
* id and leaves refusing a malformed one to the sanitizer. A re-spelled copy is
|
|
364
|
+
* how the checker starts calling a working anchor malformed — and the patterns
|
|
365
|
+
* that look for an id in text (`OQ_REFERENCE` and `OQ_EXACT` in the checker's
|
|
366
|
+
* `rules/references.ts`, `OQ_TOKEN` in `planning/scan.ts` and in
|
|
367
|
+
* `FrontmatterDisplay.tsx`) restate this shape, so they change with it.
|
|
334
368
|
*/
|
|
335
369
|
const VANTAGE_OQ_ID = /^OQ-(?:[A-Z][A-Z0-9]{0,5})?[0-9]+$/;
|
|
336
370
|
const STYLE_KEYS = {
|
|
@@ -343,10 +377,21 @@ const STYLE_KEYS = {
|
|
|
343
377
|
* Name → key → the closed value set for that key.
|
|
344
378
|
*
|
|
345
379
|
* `section` and `block` share their keys: they differ in *extent*, not in what
|
|
346
|
-
* they can say.
|
|
347
|
-
*
|
|
348
|
-
* neither can be value-allowlisted,
|
|
349
|
-
* than left to a caller to guess.
|
|
380
|
+
* they can say. The two question names share theirs too, and they are the
|
|
381
|
+
* design's only values with no closed set — `id` is a token an author chose
|
|
382
|
+
* and `leaning` is a sentence (§8.3) — so neither can be value-allowlisted,
|
|
383
|
+
* which is recorded here as `null` rather than left to a caller to guess.
|
|
384
|
+
*
|
|
385
|
+
* `question` takes exactly `oq`'s keys and nothing more. Starting narrow is
|
|
386
|
+
* the forward-compatible choice: a key added later is one older viewers drop
|
|
387
|
+
* pair by pair, while a key accepted now could never be given a meaning (P0).
|
|
388
|
+
* A `leaning` on a 🔒 or ✅ question is allowed and simply not offered
|
|
389
|
+
* (`questionOffersTake`), so changing a question's state changes only its
|
|
390
|
+
* marker.
|
|
391
|
+
*
|
|
392
|
+
* `fallback` takes no keys: whether a block is withheld is the whole of what
|
|
393
|
+
* it says, and a key added later is dropped pair by pair by this release, as
|
|
394
|
+
* D2 drops any other.
|
|
350
395
|
*/
|
|
351
396
|
const DIRECTIVE_VOCABULARY = {
|
|
352
397
|
section: STYLE_KEYS,
|
|
@@ -354,9 +399,59 @@ const DIRECTIVE_VOCABULARY = {
|
|
|
354
399
|
oq: {
|
|
355
400
|
id: null,
|
|
356
401
|
leaning: null
|
|
357
|
-
}
|
|
402
|
+
},
|
|
403
|
+
question: {
|
|
404
|
+
id: null,
|
|
405
|
+
leaning: null
|
|
406
|
+
},
|
|
407
|
+
fallback: {}
|
|
358
408
|
};
|
|
359
409
|
/**
|
|
410
|
+
* Merge the question directives of one run — the comments up to the block they
|
|
411
|
+
* land on — into the one question they declare, or `undefined` when the run
|
|
412
|
+
* holds none. Directives of any other name are skipped.
|
|
413
|
+
*
|
|
414
|
+
* A run holding an `oq` is read exactly as a viewer that predates `question`
|
|
415
|
+
* reads it: that viewer drops every `question` and reads the `oq` alone, so
|
|
416
|
+
* the `oq` wins wherever it appears in the run, and only its keys apply — a
|
|
417
|
+
* key only a `question` in the same run sets applies nowhere, in no release.
|
|
418
|
+
* That is the one reading every viewer agrees on (`VANTAGE_QUESTION_NAMES`),
|
|
419
|
+
* so the same bytes file the same Take in 0.7 and in 0.8. Within one name, the
|
|
420
|
+
* last value of a key wins, as `section` and `block` resolve theirs. A pair
|
|
421
|
+
* its name does not accept, or a value outside a closed set, is dropped (D2).
|
|
422
|
+
*
|
|
423
|
+
* One function for every reader of a run: the plugin that stamps the question
|
|
424
|
+
* and the planning index that reads it, which the checker reads a question
|
|
425
|
+
* through, so none can disagree about which leaning or which id a run carries
|
|
426
|
+
* (D5).
|
|
427
|
+
*/
|
|
428
|
+
function mergeQuestionRun(run) {
|
|
429
|
+
let name;
|
|
430
|
+
for (const directive of run) {
|
|
431
|
+
if (!isQuestionDirective(directive.name)) continue;
|
|
432
|
+
if (name !== "oq") name = directive.name;
|
|
433
|
+
}
|
|
434
|
+
if (name === void 0) return void 0;
|
|
435
|
+
const accepted = DIRECTIVE_VOCABULARY[name] ?? {};
|
|
436
|
+
const keys = /* @__PURE__ */ new Map();
|
|
437
|
+
const sources = /* @__PURE__ */ new Map();
|
|
438
|
+
run.forEach((directive, index) => {
|
|
439
|
+
if (directive.name !== name) return;
|
|
440
|
+
for (const pair of directive.pairs) {
|
|
441
|
+
const values = accepted[pair.key];
|
|
442
|
+
if (values === void 0) continue;
|
|
443
|
+
if (values !== null && !values.includes(pair.value)) continue;
|
|
444
|
+
keys.set(pair.key, pair.value);
|
|
445
|
+
sources.set(pair.key, index);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
return {
|
|
449
|
+
name,
|
|
450
|
+
keys,
|
|
451
|
+
sources
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
360
455
|
* `ws` is `[ \t\r\n]` — the design's grammar leaves it undefined, and `\n` has
|
|
361
456
|
* to be in the set because a directive may legally wrap: a multi-line comment
|
|
362
457
|
* is one node whose value contains the newlines.
|
|
@@ -462,10 +557,24 @@ function parseVantageDirective(comment) {
|
|
|
462
557
|
pairs
|
|
463
558
|
};
|
|
464
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* A question directive's `leaning=` value as the page carries it: whitespace
|
|
562
|
+
* runs collapsed to one space, trimmed, and capped at `MAX_LEANING`.
|
|
563
|
+
*
|
|
564
|
+
* A wrapped directive puts newlines and indentation into the value. Two readers
|
|
565
|
+
* need the result and must agree on it: `rehypeVantageDirectives`, which stamps
|
|
566
|
+
* it as `data-vantage-leaning` for the one-click answer, and the planning index,
|
|
567
|
+
* which reports the leaning a question carries. An empty result means the
|
|
568
|
+
* question states no leaning.
|
|
569
|
+
*/
|
|
570
|
+
function normalizeLeaning(raw) {
|
|
571
|
+
return raw.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
572
|
+
}
|
|
465
573
|
//#endregion
|
|
466
574
|
//#region src/rehypeVantageDirectives.ts
|
|
467
575
|
/**
|
|
468
|
-
* What a `section`/`block` and
|
|
576
|
+
* What a `section`/`block` and a question directive (`oq`, `question`) may
|
|
577
|
+
* **target**.
|
|
469
578
|
*
|
|
470
579
|
* Both lists live in `vantageDirectives.ts`, with the reasoning for each tag,
|
|
471
580
|
* because the CLI checker resolves the same question over mdast and must reach
|
|
@@ -475,6 +584,8 @@ function parseVantageDirective(comment) {
|
|
|
475
584
|
*/
|
|
476
585
|
const STYLE_TARGET_TAGS = new Set(VANTAGE_STYLE_TARGETS);
|
|
477
586
|
const ANCHOR_TARGET_TAGS = new Set(VANTAGE_ANCHOR_TARGETS);
|
|
587
|
+
/** What a `fallback` may withhold: every style target but a heading. */
|
|
588
|
+
const FALLBACK_TARGET_TAGS = new Set(VANTAGE_FALLBACK_TARGETS);
|
|
478
589
|
const HEADING_DEPTHS = /* @__PURE__ */ new Map([
|
|
479
590
|
["h1", 1],
|
|
480
591
|
["h2", 2],
|
|
@@ -515,6 +626,19 @@ const RANGE_PROPERTIES = /* @__PURE__ */ new Map([["tone", "dataVantageTone"], [
|
|
|
515
626
|
*/
|
|
516
627
|
const POINT_PROPERTIES = /* @__PURE__ */ new Map([["badge", "dataVantageBadge"]]);
|
|
517
628
|
const RUN_PROPERTY = "dataVantageRun";
|
|
629
|
+
/**
|
|
630
|
+
* What a question stamps, whichever name declared it: `data-vantage-question`
|
|
631
|
+
* on every question, and `data-vantage-oq` besides on one an `oq` declared
|
|
632
|
+
* (`VANTAGE_QUESTION_ATTRIBUTE`, `VANTAGE_OQ_ATTRIBUTE`).
|
|
633
|
+
*
|
|
634
|
+
* Two attributes, and neither one ever means more than it says. Every release
|
|
635
|
+
* since 0.7 stamps `data-vantage-oq` on an `oq` and on nothing else, so a
|
|
636
|
+
* reader of this package's markup that has ever read it keeps reading exactly
|
|
637
|
+
* what it read; `data-vantage-question` is the one to read for "a question",
|
|
638
|
+
* and the question's state, from its marker, is what decides whether anything
|
|
639
|
+
* offers to answer it (`questionOffersTake`).
|
|
640
|
+
*/
|
|
641
|
+
const QUESTION_PROPERTY = "dataVantageQuestion";
|
|
518
642
|
const OQ_PROPERTY = "dataVantageOq";
|
|
519
643
|
const LEANING_PROPERTY = "dataVantageLeaning";
|
|
520
644
|
/**
|
|
@@ -549,8 +673,6 @@ const OQ_ID_PROPERTY = "dataVantageOqId";
|
|
|
549
673
|
const COLLAPSED_PROPERTY = "dataVantageCollapsed";
|
|
550
674
|
const COLLAPSE_GROUP_PROPERTY = "dataVantageCollapseGroup";
|
|
551
675
|
const COLLAPSE_TOGGLE_PROPERTY = "dataVantageCollapseToggle";
|
|
552
|
-
/** A review-comment body, not prose. Bounds the attribute; 500 is generous. */
|
|
553
|
-
const MAX_LEANING = 500;
|
|
554
676
|
/**
|
|
555
677
|
* Nodes that may sit between a directive and its target.
|
|
556
678
|
*
|
|
@@ -684,13 +806,14 @@ function stampStyle(children, targetIndex, name, pairs, state) {
|
|
|
684
806
|
}
|
|
685
807
|
}
|
|
686
808
|
}
|
|
687
|
-
function
|
|
688
|
-
setProperty(target,
|
|
809
|
+
function stampQuestion(target, name, pairs) {
|
|
810
|
+
setProperty(target, QUESTION_PROPERTY, "true");
|
|
811
|
+
if (name === "oq") setProperty(target, OQ_PROPERTY, "true");
|
|
689
812
|
const id = pairs.get("id");
|
|
690
813
|
if (id !== void 0 && id !== "") setProperty(target, OQ_ID_PROPERTY, id);
|
|
691
814
|
const leaning = pairs.get("leaning");
|
|
692
815
|
if (leaning === void 0) return;
|
|
693
|
-
const text = leaning
|
|
816
|
+
const text = normalizeLeaning(leaning);
|
|
694
817
|
if (text !== "") setProperty(target, LEANING_PROPERTY, text);
|
|
695
818
|
}
|
|
696
819
|
/**
|
|
@@ -705,18 +828,14 @@ function stampOq(target, pairs) {
|
|
|
705
828
|
function stampRun(children, targetIndex, run, state) {
|
|
706
829
|
const target = children[targetIndex];
|
|
707
830
|
const style = /* @__PURE__ */ new Map();
|
|
708
|
-
const oq = /* @__PURE__ */ new Map();
|
|
709
831
|
let styleName;
|
|
710
|
-
let hasOq = false;
|
|
711
832
|
for (const directive of run) if (directive.name === "section" || directive.name === "block") {
|
|
712
833
|
styleName = directive.name;
|
|
713
834
|
for (const pair of directive.pairs) style.set(pair.key, pair.value);
|
|
714
|
-
} else if (directive.name === "oq") {
|
|
715
|
-
hasOq = true;
|
|
716
|
-
for (const pair of directive.pairs) oq.set(pair.key, pair.value);
|
|
717
835
|
}
|
|
718
836
|
if (styleName !== void 0) stampStyle(children, targetIndex, styleName, style, state);
|
|
719
|
-
|
|
837
|
+
const question = mergeQuestionRun(run);
|
|
838
|
+
if (question !== void 0 && ANCHOR_TARGET_TAGS.has(target.tagName)) stampQuestion(target, question.name, question.keys);
|
|
720
839
|
}
|
|
721
840
|
/** A directive comment, or `undefined` for anything else — malformed included. */
|
|
722
841
|
function directiveOf(node) {
|
|
@@ -729,7 +848,7 @@ function directiveOf(node) {
|
|
|
729
848
|
*
|
|
730
849
|
* The whole tree, not just the root: `rehype-raw` leaves comment nodes inside
|
|
731
850
|
* `blockquote`, inside `li`, inside `td` and inline inside `p`, and the real
|
|
732
|
-
* Open Questions layout puts the
|
|
851
|
+
* Open Questions layout puts the question directive inside a list item — so a
|
|
733
852
|
* root-only walk finds none of them.
|
|
734
853
|
*
|
|
735
854
|
* Pass order is also what resolves a nested section: an inner heading's
|
|
@@ -768,8 +887,66 @@ function processChildren(parent, state) {
|
|
|
768
887
|
i = j;
|
|
769
888
|
}
|
|
770
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Remove every block a `fallback` run lands on, with the run itself.
|
|
892
|
+
*
|
|
893
|
+
* A fallback block is written for the renderers that cannot show something
|
|
894
|
+
* Vantage shows — "This drawing needs Vantage 0.8 or later", beside an inline
|
|
895
|
+
* `<svg>`. Every renderer that does not know the name drops the comment and
|
|
896
|
+
* shows the block; this one never renders it, in any mode, in print, or
|
|
897
|
+
* through any of the renderers that share this plugin (D5).
|
|
898
|
+
*
|
|
899
|
+
* The run is found exactly as `processChildren` finds one — the same skip
|
|
900
|
+
* rules, the same first-element target, the same whole-tree walk — and it
|
|
901
|
+
* withholds when any directive in it is a `fallback` and the target is on
|
|
902
|
+
* `VANTAGE_FALLBACK_TARGETS`. Anything else is left for the stamping pass,
|
|
903
|
+
* where a `fallback` is inert.
|
|
904
|
+
*
|
|
905
|
+
* A pass of its own, and first, because a `section` reaches *forward* over its
|
|
906
|
+
* siblings: stamped in one pass, a toned section would count a fallback block
|
|
907
|
+
* after it among its members, give it the run's last `end` marker, and leave
|
|
908
|
+
* the last block anyone sees marked `middle`. Withheld first, the block was
|
|
909
|
+
* never there for any later decision — the section's extent, its run markers,
|
|
910
|
+
* a collapse group's members, a review anchor or an outline entry.
|
|
911
|
+
*/
|
|
912
|
+
function withholdFallbacks(parent) {
|
|
913
|
+
const children = parent.children;
|
|
914
|
+
let i = 0;
|
|
915
|
+
while (i < children.length) {
|
|
916
|
+
const node = children[i];
|
|
917
|
+
if (node.type === "element") {
|
|
918
|
+
withholdFallbacks(node);
|
|
919
|
+
i++;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
const first = directiveOf(node);
|
|
923
|
+
if (first === void 0) {
|
|
924
|
+
i++;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
let withholds = first.name === "fallback";
|
|
928
|
+
let j = i + 1;
|
|
929
|
+
let targetIndex = -1;
|
|
930
|
+
for (; j < children.length; j++) {
|
|
931
|
+
const next = children[j];
|
|
932
|
+
if (next.type === "element") {
|
|
933
|
+
targetIndex = j;
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
if (!isSkippable(next)) break;
|
|
937
|
+
if (directiveOf(next)?.name === "fallback") withholds = true;
|
|
938
|
+
}
|
|
939
|
+
const target = targetIndex >= 0 ? children[targetIndex] : void 0;
|
|
940
|
+
if (withholds && target?.type === "element" && FALLBACK_TARGET_TAGS.has(target.tagName)) {
|
|
941
|
+
children.splice(i, targetIndex - i + 1);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
i = j;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
771
947
|
const rehypeVantageDirectives = () => {
|
|
772
948
|
return (tree) => {
|
|
949
|
+
withholdFallbacks(tree);
|
|
773
950
|
processChildren(tree, { nextGroup: 1 });
|
|
774
951
|
};
|
|
775
952
|
};
|
|
@@ -862,12 +1039,144 @@ const rehypeRestoreMathStamps = () => {
|
|
|
862
1039
|
};
|
|
863
1040
|
//#endregion
|
|
864
1041
|
//#region src/sanitize.ts
|
|
1042
|
+
/** Whether an attribute definition is for `className`. */
|
|
1043
|
+
const isClassName = (definition) => (typeof definition === "string" ? definition : definition[0]) === "className";
|
|
1044
|
+
/**
|
|
1045
|
+
* The value list the default schema gives `className` on `tag`, or an empty
|
|
1046
|
+
* list if it gives none.
|
|
1047
|
+
*/
|
|
1048
|
+
function defaultClasses(tag) {
|
|
1049
|
+
const entry = defaultSchema.attributes?.[tag]?.find(isClassName);
|
|
1050
|
+
return Array.isArray(entry) ? entry.slice(1) : [];
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* The only classes an element keeps: the ones the pipeline itself emits on it
|
|
1054
|
+
* before `rehypeSanitize` runs. Every other element keeps none.
|
|
1055
|
+
*
|
|
1056
|
+
* **A class is a style by another name.** The app ships Tailwind's utility CSS
|
|
1057
|
+
* for its own markup, so `<div class="fixed inset-0 z-50 bg-white">` in a
|
|
1058
|
+
* document laid a white sheet over the whole Vantage window, header and
|
|
1059
|
+
* sidebar included. That is the overlay `SAFE_STYLE` bans `position` to
|
|
1060
|
+
* prevent, spelled as a class instead of a declaration, and `className` used
|
|
1061
|
+
* to be admitted with any value on every element. A document could also
|
|
1062
|
+
* borrow a class the app's own code looks for: `.overflow-y-auto` is how an
|
|
1063
|
+
* anchor link finds the element to scroll when no `data-content-scroll` is in
|
|
1064
|
+
* reach, and `.heading-anchor` is what the outline strips from a heading's
|
|
1065
|
+
* text. The comment-body sanitizer (`frontend/src/lib/commentMarkdown.ts`)
|
|
1066
|
+
* already refused `class` for the same reason.
|
|
1067
|
+
*
|
|
1068
|
+
* What each entry is for, and who emits it:
|
|
1069
|
+
*
|
|
1070
|
+
* - `code`: `language-*`, from a fence's info string (`mdast-util-to-hast`).
|
|
1071
|
+
* `rehype-highlight` reads it, `rehype-katex` keys on `language-math`, and
|
|
1072
|
+
* the viewers render `language-mermaid` as a diagram.
|
|
1073
|
+
* - `ul`, `ol` and `li`: `contains-task-list` and `task-list-item`, from GFM
|
|
1074
|
+
* task lists. `styles/task-list.css` draws the checkbox from them.
|
|
1075
|
+
* - `section`, `h2` and `a`: GFM footnotes (`mdast-util-to-hast`), on the
|
|
1076
|
+
* section, its label and each back-reference.
|
|
1077
|
+
* - `div`: `ALERT_TITLE_CLASS`, on the title `rehypeVantageAlerts` injects.
|
|
1078
|
+
*
|
|
1079
|
+
* `remark-math` also emits `math-display` and `math-inline` on its `code`.
|
|
1080
|
+
* They were never kept, because the default schema's `code` entry takes only
|
|
1081
|
+
* `language-*`, and nothing needs them: `rehype-katex` keys on `language-math`
|
|
1082
|
+
* and tells display from inline by whether the `code` sits in a `pre`.
|
|
1083
|
+
*
|
|
1084
|
+
* **A class added after the sanitizer needs no entry**, and that is most of
|
|
1085
|
+
* the classes the page styles: `rehype-highlight`'s `hljs-*`, KaTeX's output,
|
|
1086
|
+
* Mermaid's diagrams, and everything the app's components and hooks add. A
|
|
1087
|
+
* document that writes one of those on an element keeps nothing.
|
|
1088
|
+
*
|
|
1089
|
+
* **Mermaid source is the route this list cannot close.** Mermaid renders
|
|
1090
|
+
* after the sanitizer and copies the names a diagram's source gives a node
|
|
1091
|
+
* (`:::name`, `class A name`, `classDef`) onto that node, so a diagram can
|
|
1092
|
+
* carry any of the app's utilities on its own nodes. The diagram bounds it:
|
|
1093
|
+
* `position` does nothing on an SVG group, and the diagram's `svg` clips what
|
|
1094
|
+
* is inside it. `frontend/e2e/mermaid.spec.ts` measures that bound. The
|
|
1095
|
+
* stylesheet a diagram's source could write did reach past the diagram, and
|
|
1096
|
+
* `MERMAID_SECURE_KEYS` in `mermaidLoader.ts` refuses the settings that write
|
|
1097
|
+
* it.
|
|
1098
|
+
*
|
|
1099
|
+
* A document may still write these names on these elements. By the time the
|
|
1100
|
+
* sanitizer runs, a hand-written `<li class="task-list-item">` and the one GFM
|
|
1101
|
+
* emitted are the same node, and none of the names can lay an element over
|
|
1102
|
+
* anything outside the document's scroll container. `styles/task-list.css`
|
|
1103
|
+
* floats the checkbox of a `task-list-item` instead of positioning it
|
|
1104
|
+
* absolutely, so it stays in the document's flow whatever the item's own
|
|
1105
|
+
* `style` does, and the checkbox takes no `style` itself (see
|
|
1106
|
+
* `UNSTYLED_TAGS`). The footnote label's class, where a stylesheet defines it,
|
|
1107
|
+
* clips its element to a single pixel.
|
|
1108
|
+
*
|
|
1109
|
+
* **The footnote label's class is read from the default schema, never
|
|
1110
|
+
* spelled here, and the tests spell it in halves.** It is a Tailwind utility,
|
|
1111
|
+
* and Tailwind generates a utility for every class name it finds in the files
|
|
1112
|
+
* it scans, comments included — this directory and all of `frontend/`. The app
|
|
1113
|
+
* renders the label as a visible heading only because nothing it scans spells
|
|
1114
|
+
* that name. Spelled whole in a test, it generated the rule, and the label
|
|
1115
|
+
* would have been hidden from sight as it is on GitHub: a change of rendering,
|
|
1116
|
+
* not a sanitizer fix.
|
|
1117
|
+
*
|
|
1118
|
+
* The list is measured, not remembered. The test "takes no class from
|
|
1119
|
+
* anything the pipeline emits" in `frontend/src/lib/sanitize.test.ts` records
|
|
1120
|
+
* every class on the tree on both sides of `rehypeSanitize`, over a document
|
|
1121
|
+
* that uses every feature that emits one, so a plugin that starts emitting a
|
|
1122
|
+
* new class fails there instead of losing it in every renderer.
|
|
1123
|
+
*/
|
|
1124
|
+
const PIPELINE_CLASSES = {
|
|
1125
|
+
code: [/^language-./],
|
|
1126
|
+
ul: ["contains-task-list"],
|
|
1127
|
+
ol: ["contains-task-list"],
|
|
1128
|
+
li: ["task-list-item"],
|
|
1129
|
+
section: ["footnotes"],
|
|
1130
|
+
h2: defaultClasses("h2"),
|
|
1131
|
+
a: ["data-footnote-backref"],
|
|
1132
|
+
div: [ALERT_TITLE_CLASS]
|
|
1133
|
+
};
|
|
865
1134
|
/**
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
|
|
870
|
-
|
|
1135
|
+
* `tag`'s attributes: the default schema's, less any `className`, then its
|
|
1136
|
+
* `PIPELINE_CLASSES` entry, then `extra`.
|
|
1137
|
+
*
|
|
1138
|
+
* **Always a value list, never a bare `"className"`.** `hast-util-sanitize`
|
|
1139
|
+
* reads a bare name, or a tuple holding no values, as "any value", and an
|
|
1140
|
+
* element's own entry is consulted before `*`'s — the first match wins, and
|
|
1141
|
+
* `*` is not consulted when that element's own entry filters a class list down
|
|
1142
|
+
* to nothing. An element with no entry has none added.
|
|
1143
|
+
*/
|
|
1144
|
+
function withClasses(tag, ...extra) {
|
|
1145
|
+
const classes = PIPELINE_CLASSES[tag] ?? [];
|
|
1146
|
+
const own = (defaultSchema.attributes?.[tag] ?? []).filter((definition) => !isClassName(definition));
|
|
1147
|
+
if (classes.length > 0) own.push(["className", ...classes]);
|
|
1148
|
+
return [...own, ...extra];
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* CSS properties an inline `style` may set.
|
|
1152
|
+
*
|
|
1153
|
+
* **The only `style` this list ever filters is one a document wrote by hand.**
|
|
1154
|
+
* Nothing the pipeline generates reaches it: `rehypeKatex` and `rehypeHighlight`
|
|
1155
|
+
* both run *after* `rehypeSanitize` (`pipeline.ts`), so their output is trusted
|
|
1156
|
+
* rather than filtered, and `remark-gfm` emits table alignment as an `align`
|
|
1157
|
+
* attribute rather than as CSS. So this is a filter on untrusted author HTML and
|
|
1158
|
+
* nothing else — which is exactly the hole that made it necessary: `<div
|
|
1159
|
+
* style="position:fixed;inset:0">` covered the viewport and
|
|
1160
|
+
* `style="background:url(https://…)"` called home on render, both verbatim,
|
|
1161
|
+
* because `rehype-sanitize` does not parse CSS. Scripts were never the risk
|
|
1162
|
+
* here; layout and network were.
|
|
1163
|
+
*
|
|
1164
|
+
* The list is therefore deliberately typographic: the styling a prose document
|
|
1165
|
+
* has any business asking for. It is *not* sized to KaTeX, and a KaTeX release
|
|
1166
|
+
* that starts using a new property is a non-event here — `\pmb` already emits
|
|
1167
|
+
* `text-shadow`, which is not on this list and renders anyway.
|
|
1168
|
+
*
|
|
1169
|
+
* **The design doc used to argue the opposite — that `style` had to be allowed
|
|
1170
|
+
* and `position` enumerated because KaTeX needs them — and it was wrong.** The
|
|
1171
|
+
* measurement behind it was real (KaTeX does emit `position:relative` on every
|
|
1172
|
+
* integral) but the inference was not, because the sanitizer has finished before
|
|
1173
|
+
* the first KaTeX span exists. Rebuilding the shipped rehype order with a filter
|
|
1174
|
+
* that rejects *every* value leaves all ten of the integral's style attributes
|
|
1175
|
+
* untouched. The "Security" section of `docs/reference/inline-markup.md`
|
|
1176
|
+
* records the correction; the test that would catch a reordering is in
|
|
1177
|
+
* `frontend/src/lib/sanitize.test.ts`.
|
|
1178
|
+
*/
|
|
1179
|
+
const SAFE_STYLE_PROPERTIES = [
|
|
871
1180
|
"height",
|
|
872
1181
|
"min-height",
|
|
873
1182
|
"max-height",
|
|
@@ -922,12 +1231,89 @@ const DECLARATION = `(?:(?:${[
|
|
|
922
1231
|
"white-space",
|
|
923
1232
|
"vertical-align",
|
|
924
1233
|
"list-style-type",
|
|
925
|
-
"display",
|
|
926
1234
|
"float",
|
|
927
1235
|
"clear",
|
|
928
1236
|
"opacity",
|
|
929
1237
|
"overflow"
|
|
930
|
-
]
|
|
1238
|
+
];
|
|
1239
|
+
/**
|
|
1240
|
+
* A `style` value we will keep, as a whole.
|
|
1241
|
+
*
|
|
1242
|
+
* One rule beyond the property list does the work: **no parentheses anywhere**,
|
|
1243
|
+
* which closes `url(…)` and `expression(…)` in one stroke — the network and
|
|
1244
|
+
* legacy-script vectors. Its cost is borne entirely by authors, who lose
|
|
1245
|
+
* `calc()`, `rgb()` and `var()` along with them; that is the trade, and it is
|
|
1246
|
+
* worth it for a filter this small.
|
|
1247
|
+
*
|
|
1248
|
+
* **`position` is not on the property list at all**, so every value of it is
|
|
1249
|
+
* refused — `static` and `relative` along with `fixed` and `sticky`. It used to
|
|
1250
|
+
* be enumerated, on the belief that KaTeX needed `relative`; KaTeX renders after
|
|
1251
|
+
* the sanitizer and never meets this regex, so the enumeration was buying
|
|
1252
|
+
* nothing but the residual it conceded. Banning the property closes that
|
|
1253
|
+
* residual, and it is bigger than "overlaps its neighbors" made it sound:
|
|
1254
|
+
* measured in Chrome against the viewer's real ancestor chain, an author's
|
|
1255
|
+
* `position:absolute;top:0;left:0;width:100%;height:100%` is sized to the whole
|
|
1256
|
+
* content pane (the nearest positioned ancestor is outside the scroll
|
|
1257
|
+
* container), is not clipped by the scroller, and survives scrolling to the end
|
|
1258
|
+
* of the document. It was `position:fixed` in all but the keyword.
|
|
1259
|
+
*
|
|
1260
|
+
* Matching is all-or-nothing: one unrecognized declaration drops the whole
|
|
1261
|
+
* attribute, and the element renders unstyled rather than partly styled. That
|
|
1262
|
+
* is the safe direction to fail, and it degrades to plain text rather than to a
|
|
1263
|
+
* broken page.
|
|
1264
|
+
*
|
|
1265
|
+
* **The grammar must stay unambiguous, and `;` is what keeps it so.** The value
|
|
1266
|
+
* class is "anything but the delimiters", which includes whitespace — a value
|
|
1267
|
+
* legitimately contains it (`margin: 0 auto`). So if whitespace could *also*
|
|
1268
|
+
* end a declaration, both constructs would compete for the same characters and
|
|
1269
|
+
* the match would fork at every declaration; on a value that ultimately fails,
|
|
1270
|
+
* the engine explores every fork. An earlier form of this regex separated
|
|
1271
|
+
* declarations with `\s*;?\s*`, and 200 document-controlled characters took the
|
|
1272
|
+
* renderer — and the CLI checker, and therefore CI — 94 seconds. Requiring `;`
|
|
1273
|
+
* pins each declaration's extent to the delimiter positions, so there is exactly
|
|
1274
|
+
* one way to parse any input and rejection is linear. `VALUE` absorbs the
|
|
1275
|
+
* padding on both sides for the same reason: a separate `\\s*` next to it would
|
|
1276
|
+
* put the ambiguity straight back. Pinned by the flat-time test in
|
|
1277
|
+
* `frontend/src/lib/sanitize.test.ts` — do not loosen the separator.
|
|
1278
|
+
*
|
|
1279
|
+
* Residual, stated plainly and now genuinely small: negative `margin` still lets
|
|
1280
|
+
* an element overlap its neighbors *inside the flow*. That one scrolls with the
|
|
1281
|
+
* content and is clipped by the scroll container, and closing it means giving up
|
|
1282
|
+
* margins, which prose actually uses. Containment in the stylesheet, not another
|
|
1283
|
+
* rule here, is what would close it. `overflow: visible` on an inline `<svg>` is
|
|
1284
|
+
* the same residual by another route — see `SVG_ROOT_ATTRIBUTES`.
|
|
1285
|
+
*
|
|
1286
|
+
* **The app's table of contents is inside that scroll container too**, beside
|
|
1287
|
+
* the document column, so a block carried over it took its clicks whenever the
|
|
1288
|
+
* block painted in the positioned layer: one with `opacity`, a toned block, a
|
|
1289
|
+
* heading, or a link to anywhere. The contents sit at `z-10` for that
|
|
1290
|
+
* (`frontend/src/components/TableOfContents.tsx`), and a document cannot climb
|
|
1291
|
+
* back over them only because `z-index` is not on the property list.
|
|
1292
|
+
*/
|
|
1293
|
+
const VALUE = `[^;:()"'\\\\]*`;
|
|
1294
|
+
const DISPLAY_DECLARATION = `display\\s*:\\s*(?:${[
|
|
1295
|
+
"none",
|
|
1296
|
+
"block",
|
|
1297
|
+
"inline-block",
|
|
1298
|
+
"inline-flex",
|
|
1299
|
+
"inline-grid",
|
|
1300
|
+
"inline-table",
|
|
1301
|
+
"inline",
|
|
1302
|
+
"flow-root",
|
|
1303
|
+
"flex",
|
|
1304
|
+
"grid",
|
|
1305
|
+
"table-row-group",
|
|
1306
|
+
"table-row",
|
|
1307
|
+
"table-header-group",
|
|
1308
|
+
"table-footer-group",
|
|
1309
|
+
"table-cell",
|
|
1310
|
+
"table-column-group",
|
|
1311
|
+
"table-column",
|
|
1312
|
+
"table-caption",
|
|
1313
|
+
"table",
|
|
1314
|
+
"list-item"
|
|
1315
|
+
].join("|")})\\s*(?:!\\s*important\\s*)?`;
|
|
1316
|
+
const DECLARATION = `(?:(?:${SAFE_STYLE_PROPERTIES.join("|")})\\s*:${VALUE}|${DISPLAY_DECLARATION})`;
|
|
931
1317
|
const SAFE_STYLE = new RegExp(`^\\s*(?:${DECLARATION};\\s*)*${DECLARATION}?$`, "i");
|
|
932
1318
|
/**
|
|
933
1319
|
* A collapse group id: one or more digits, anchored.
|
|
@@ -939,6 +1325,281 @@ const SAFE_STYLE = new RegExp(`^\\s*(?:${DECLARATION};\\s*)*${DECLARATION}?$`, "
|
|
|
939
1325
|
*/
|
|
940
1326
|
const COLLAPSE_GROUP_ID = /^[0-9]+$/;
|
|
941
1327
|
/**
|
|
1328
|
+
* Inline SVG, admitted as static drawing and nothing else.
|
|
1329
|
+
*
|
|
1330
|
+
* The allowlist is shapes, text and grouping. Everything in SVG that can run
|
|
1331
|
+
* code, fetch, or reach outside the element is refused: `script`,
|
|
1332
|
+
* `foreignObject`, `use` and `image` (both take an `href`), `xlink:href` on
|
|
1333
|
+
* `a`, the `animate`/`set` family (which can rewrite an `href` after the
|
|
1334
|
+
* sanitizer has looked), `style` elements, and every attribute that takes a
|
|
1335
|
+
* `url(…)` reference — `filter`, `mask`, `clip-path`, `marker-*`, `cursor`.
|
|
1336
|
+
*
|
|
1337
|
+
* Gradients, patterns and `<defs>` are out for a second reason: they are only
|
|
1338
|
+
* reachable through `url(#id)`, and the sanitizer prefixes every `id` with
|
|
1339
|
+
* `user-content-`, so the reference would dangle even if it were allowed.
|
|
1340
|
+
*
|
|
1341
|
+
* **Refusing an element is not the same as removing it.** `hast-util-sanitize`
|
|
1342
|
+
* *unwraps* an element it does not allow — drops the tag and keeps its
|
|
1343
|
+
* children — unless the tag is on `strip`. See `SVG_STRIPPED` for why that
|
|
1344
|
+
* matters here, and for the pass that removes those containers instead.
|
|
1345
|
+
*
|
|
1346
|
+
* `switch` is admitted, as a plain container, because draw.io wraps every label
|
|
1347
|
+
* in one: `<switch><foreignObject>…</foreignObject><text>…</text></switch>`. A
|
|
1348
|
+
* `switch` renders only its first child whose conditions hold, and with the
|
|
1349
|
+
* `foreignObject` removed and the conditional attributes (`requiredFeatures`,
|
|
1350
|
+
* `systemLanguage`) refused, that is the `<text>` fallback. Unwrapped instead,
|
|
1351
|
+
* it would render *every* branch — including the "Text is not SVG - cannot
|
|
1352
|
+
* display" notice draw.io appends for viewers without `foreignObject`.
|
|
1353
|
+
*
|
|
1354
|
+
* Every child requires an `svg` ancestor, so no shape or text element is
|
|
1355
|
+
* admitted into HTML flow on its own.
|
|
1356
|
+
*
|
|
1357
|
+
* `title` and `desc` are not children at all. They are admitted nowhere, inside
|
|
1358
|
+
* a drawing they are removed with their contents (see `SVG_STRIPPED`), and
|
|
1359
|
+
* `aria-label` on the root is the drawing's accessible name. Both are HTML
|
|
1360
|
+
* integration points — the parser reads what is inside them as HTML — and an
|
|
1361
|
+
* `svg` ancestor turned out not to be enough to keep a `<title>` in SVG
|
|
1362
|
+
* context. `<math><svg><title>` keeps MathML context, and React hoists any
|
|
1363
|
+
* `title` outside SVG context into the page's `<head>`, so the document set the
|
|
1364
|
+
* tab title. In `renderMarkdown`'s string output, `<svg><desc><title>`,
|
|
1365
|
+
* `<svg><title><title>` and a `title` inside a `foreignObject` all re-parse into
|
|
1366
|
+
* an HTML `title`, which is what `document.title` reads on a page that has none
|
|
1367
|
+
* of its own.
|
|
1368
|
+
*/
|
|
1369
|
+
const SVG_CHILD_TAGS = [
|
|
1370
|
+
"g",
|
|
1371
|
+
"path",
|
|
1372
|
+
"rect",
|
|
1373
|
+
"circle",
|
|
1374
|
+
"ellipse",
|
|
1375
|
+
"line",
|
|
1376
|
+
"polyline",
|
|
1377
|
+
"polygon",
|
|
1378
|
+
"text",
|
|
1379
|
+
"tspan",
|
|
1380
|
+
"switch"
|
|
1381
|
+
];
|
|
1382
|
+
/**
|
|
1383
|
+
* Refused SVG elements removed *with their contents* when they sit inside an
|
|
1384
|
+
* `<svg>`, rather than unwrapped.
|
|
1385
|
+
*
|
|
1386
|
+
* Each of these holds children that are never meant to be painted where they
|
|
1387
|
+
* stand: a clip region, a mask, a gradient's stops, a marker's arrowhead, a
|
|
1388
|
+
* symbol's body, a filter's primitives, an exporter's RDF, the text of a
|
|
1389
|
+
* `title` or `desc` (see `SVG_CHILD_TAGS`), or — for `foreignObject` — HTML.
|
|
1390
|
+
* Unwrapped, those children land in the drawing as ordinary shapes. A default
|
|
1391
|
+
* Figma export ends in `<defs><clipPath><rect fill="white"/>`, and unwrapping
|
|
1392
|
+
* it painted that white rect over the whole drawing; a `<marker>` arrowhead
|
|
1393
|
+
* became a stray triangle at the origin. The HTML inside a `foreignObject`
|
|
1394
|
+
* survived as descendants of `svg`, which React then created as invisible
|
|
1395
|
+
* SVG-namespace `div`s and `p`s — the anchor an Open Question button looks for
|
|
1396
|
+
* — and which a browser re-parsing `renderMarkdown`'s string output breaks out
|
|
1397
|
+
* of the `svg` at, spilling the rest of the drawing into the page.
|
|
1398
|
+
*
|
|
1399
|
+
* **Only inside an `<svg>`, which is why this is a pass and not the schema's
|
|
1400
|
+
* `strip`.** `strip` removes a tag wherever it stands, and most of these names
|
|
1401
|
+
* are lowercase in prose too. There a bare `<pattern>` on a line of its own is
|
|
1402
|
+
* an HTML element nothing closes, so every Markdown block after it was parsed
|
|
1403
|
+
* into it, and stripping it removed the rest of the document — in every
|
|
1404
|
+
* renderer, with `vantage-check` silent. Inside a drawing the same container is
|
|
1405
|
+
* bounded by the `svg`, because a Markdown paragraph or heading breaks out of
|
|
1406
|
+
* SVG. Outside one the sanitizer unwraps these like any tag it does not know:
|
|
1407
|
+
* `title` and `desc` are not admitted anywhere, so no `title` element survives
|
|
1408
|
+
* to be hoisted, and what they held is text or ordinary sanitized HTML.
|
|
1409
|
+
*
|
|
1410
|
+
* Names compare case-insensitively. The camel-cased ones are how the HTML
|
|
1411
|
+
* parser spells these elements in SVG, but under `<math><svg>` the parser stays
|
|
1412
|
+
* in MathML and leaves them lowercase.
|
|
1413
|
+
*/
|
|
1414
|
+
const SVG_STRIPPED = [
|
|
1415
|
+
"foreignObject",
|
|
1416
|
+
"defs",
|
|
1417
|
+
"clipPath",
|
|
1418
|
+
"mask",
|
|
1419
|
+
"pattern",
|
|
1420
|
+
"marker",
|
|
1421
|
+
"symbol",
|
|
1422
|
+
"linearGradient",
|
|
1423
|
+
"radialGradient",
|
|
1424
|
+
"filter",
|
|
1425
|
+
"metadata",
|
|
1426
|
+
"title",
|
|
1427
|
+
"desc"
|
|
1428
|
+
];
|
|
1429
|
+
const SVG_STRIPPED_NAMES = new Set(SVG_STRIPPED.map((name) => name.toLowerCase()));
|
|
1430
|
+
/**
|
|
1431
|
+
* The camel-cased `SVG_STRIPPED` names, which the schema strips on its own.
|
|
1432
|
+
*
|
|
1433
|
+
* Only an SVG-namespace element carries one of these spellings, so stripping
|
|
1434
|
+
* them everywhere costs prose nothing. It keeps the worst of the list — the
|
|
1435
|
+
* HTML inside a `foreignObject` — out of a drawing for a consumer who hands
|
|
1436
|
+
* `sanitizeSchema` to `rehype-sanitize` without the rest of `buildPipeline`.
|
|
1437
|
+
*/
|
|
1438
|
+
const SVG_STRIPPED_EVERYWHERE = SVG_STRIPPED.filter((name) => name !== name.toLowerCase());
|
|
1439
|
+
function stripSvgContainers(parent, inSvg) {
|
|
1440
|
+
if (inSvg) parent.children = parent.children.filter((child) => child.type !== "element" || !SVG_STRIPPED_NAMES.has(child.tagName.toLowerCase()));
|
|
1441
|
+
for (const child of parent.children) if (child.type === "element") stripSvgContainers(child, inSvg || child.tagName.toLowerCase() === "svg");
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Remove every `SVG_STRIPPED` element below an `svg`, with its contents.
|
|
1445
|
+
*
|
|
1446
|
+
* Runs immediately before `rehypeSanitize` (`pipeline.ts`), and only when the
|
|
1447
|
+
* pipeline sanitizes: with the sanitizer off these elements are left to work
|
|
1448
|
+
* as SVG defines them.
|
|
1449
|
+
*/
|
|
1450
|
+
function rehypeStripSvgContainers() {
|
|
1451
|
+
return (tree) => {
|
|
1452
|
+
stripSvgContainers(tree, false);
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* A paint value: a keyword, a named color, or a hex color. No parentheses, for
|
|
1457
|
+
* the same reason `SAFE_STYLE` refuses them — `fill="url(https://…)"` is a
|
|
1458
|
+
* render-time fetch, and `rgb()` is the price of closing it.
|
|
1459
|
+
*/
|
|
1460
|
+
const SVG_PAINT = /^(?:#[0-9a-f]{3,8}|[a-z]+)$/i;
|
|
1461
|
+
/**
|
|
1462
|
+
* Attributes any SVG element may carry, drawing and typography alike.
|
|
1463
|
+
*
|
|
1464
|
+
* **Each entry is a hast property name, which is not always the camel case of
|
|
1465
|
+
* the attribute.** `property-information` decides the spelling, and it has
|
|
1466
|
+
* `strokeLineCap` for `stroke-linecap` — a naive `strokeLinecap` matches
|
|
1467
|
+
* nothing, and the attribute is dropped in every renderer without a word. The
|
|
1468
|
+
* table-driven test in `frontend/src/lib/sanitize.test.ts` renders every entry
|
|
1469
|
+
* in its written spelling, so a misspelled one fails there.
|
|
1470
|
+
*
|
|
1471
|
+
* `stroke-dasharray` (and with it `stroke-dashoffset`, which does nothing
|
|
1472
|
+
* alone) is refused, because it lets a few bytes buy an unbounded paint. The
|
|
1473
|
+
* number of dashes is the path's length over the dash period, both in user
|
|
1474
|
+
* units the document chooses. Measured in headless Chromium, fifty 90-byte
|
|
1475
|
+
* paths with a 0.0011 dash took 21 s to paint and a hundred took 37 s. A value
|
|
1476
|
+
* grammar cannot bound it: refusing sub-unit dashes is undone by scaling the
|
|
1477
|
+
* user units, and the same fifty paths with `stroke-dasharray="1.1"` under a
|
|
1478
|
+
* `viewBox` a thousand times larger still took 15 s.
|
|
1479
|
+
*/
|
|
1480
|
+
const SVG_ATTRIBUTES = [
|
|
1481
|
+
["fill", SVG_PAINT],
|
|
1482
|
+
["stroke", SVG_PAINT],
|
|
1483
|
+
"fillOpacity",
|
|
1484
|
+
"fillRule",
|
|
1485
|
+
"strokeOpacity",
|
|
1486
|
+
"strokeWidth",
|
|
1487
|
+
"strokeLineCap",
|
|
1488
|
+
"strokeLineJoin",
|
|
1489
|
+
"opacity",
|
|
1490
|
+
"transform",
|
|
1491
|
+
"fontFamily",
|
|
1492
|
+
"fontSize",
|
|
1493
|
+
"fontWeight",
|
|
1494
|
+
"fontStyle",
|
|
1495
|
+
"letterSpacing",
|
|
1496
|
+
"textAnchor",
|
|
1497
|
+
"dominantBaseline",
|
|
1498
|
+
"x",
|
|
1499
|
+
"y",
|
|
1500
|
+
"x1",
|
|
1501
|
+
"y1",
|
|
1502
|
+
"x2",
|
|
1503
|
+
"y2",
|
|
1504
|
+
"cx",
|
|
1505
|
+
"cy",
|
|
1506
|
+
"r",
|
|
1507
|
+
"rx",
|
|
1508
|
+
"ry",
|
|
1509
|
+
"dx",
|
|
1510
|
+
"dy",
|
|
1511
|
+
"d",
|
|
1512
|
+
"points"
|
|
1513
|
+
];
|
|
1514
|
+
/**
|
|
1515
|
+
* The root `<svg>`'s attributes: `SVG_ATTRIBUTES` less `transform`, plus the
|
|
1516
|
+
* viewport, a `role` from a closed list, and the accessible name.
|
|
1517
|
+
*
|
|
1518
|
+
* `transform` on the root is not an SVG transform at all. The root is an
|
|
1519
|
+
* in-flow CSS box, and the attribute becomes a CSS transform of that box, so
|
|
1520
|
+
* `<svg width="10" height="10" transform="translate(-300 -300) scale(80)">`
|
|
1521
|
+
* laid out as a 10px box and painted as an 800px one over the paragraphs
|
|
1522
|
+
* around it. On a child it moves shapes inside the viewport, which clips them.
|
|
1523
|
+
*
|
|
1524
|
+
* Residual: that clip is the root's `overflow`, which `SAFE_STYLE` lets a
|
|
1525
|
+
* document set. `style="overflow:visible"` on the root lets a child with a
|
|
1526
|
+
* large `transform` or large coordinates paint over its neighbors — measured in
|
|
1527
|
+
* Chromium, the same overlap. It is the negative-`margin` residual described at
|
|
1528
|
+
* `SAFE_STYLE` by another route: it stays in the flow, scrolls with the content,
|
|
1529
|
+
* and the scroll container clips it.
|
|
1530
|
+
*/
|
|
1531
|
+
const SVG_ROOT_ATTRIBUTES = [
|
|
1532
|
+
...SVG_ATTRIBUTES.filter((attribute) => attribute !== "transform"),
|
|
1533
|
+
"xmlns",
|
|
1534
|
+
"viewBox",
|
|
1535
|
+
"preserveAspectRatio",
|
|
1536
|
+
[
|
|
1537
|
+
"role",
|
|
1538
|
+
"img",
|
|
1539
|
+
"presentation",
|
|
1540
|
+
"none"
|
|
1541
|
+
],
|
|
1542
|
+
"ariaLabel"
|
|
1543
|
+
];
|
|
1544
|
+
/** Every element the sanitizer admits. */
|
|
1545
|
+
const TAG_NAMES = [
|
|
1546
|
+
...defaultSchema.tagNames || [],
|
|
1547
|
+
"math",
|
|
1548
|
+
"semantics",
|
|
1549
|
+
"mrow",
|
|
1550
|
+
"mi",
|
|
1551
|
+
"mo",
|
|
1552
|
+
"mn",
|
|
1553
|
+
"msup",
|
|
1554
|
+
"msub",
|
|
1555
|
+
"mfrac",
|
|
1556
|
+
"mover",
|
|
1557
|
+
"munder",
|
|
1558
|
+
"msqrt",
|
|
1559
|
+
"mroot",
|
|
1560
|
+
"mtable",
|
|
1561
|
+
"mtr",
|
|
1562
|
+
"mtd",
|
|
1563
|
+
"mtext",
|
|
1564
|
+
"mspace",
|
|
1565
|
+
"annotation",
|
|
1566
|
+
"figure",
|
|
1567
|
+
"figcaption",
|
|
1568
|
+
"summary",
|
|
1569
|
+
"details",
|
|
1570
|
+
"svg",
|
|
1571
|
+
...SVG_CHILD_TAGS
|
|
1572
|
+
];
|
|
1573
|
+
/**
|
|
1574
|
+
* The admitted elements that take no `style`: only `input`, which GFM emits as
|
|
1575
|
+
* a task list's checkbox, and never with a style.
|
|
1576
|
+
*
|
|
1577
|
+
* `styles/task-list.css` draws that checkbox and positions it, so on it `top`
|
|
1578
|
+
* and `left` are not the no-ops they are on an element nothing positions, and
|
|
1579
|
+
* with `width`, `height` and `background-color` beside them a document laid a
|
|
1580
|
+
* box of its own over the page. Measured in Chromium at 1280x800 while the
|
|
1581
|
+
* checkbox was positioned absolutely,
|
|
1582
|
+
* `top:-3000px;left:-3000px;width:9000px;height:9000px;background-color:white`
|
|
1583
|
+
* on it covered the whole content pane. It needed no class written:
|
|
1584
|
+
* GFM puts `task-list-item` on the item, and a checkbox typed into the item's
|
|
1585
|
+
* text is a child of that item too.
|
|
1586
|
+
*
|
|
1587
|
+
* **This is why `style` is on each element's own entry, never on `*`.**
|
|
1588
|
+
* `hast-util-sanitize` consults `*` whenever an element's own entry yields
|
|
1589
|
+
* nothing, so no entry on `input` could refuse a value that `*` admits. An
|
|
1590
|
+
* element takes `style` only by being in `TAG_NAMES` and not in this set.
|
|
1591
|
+
*/
|
|
1592
|
+
const UNSTYLED_TAGS = /* @__PURE__ */ new Set(["input"]);
|
|
1593
|
+
/**
|
|
1594
|
+
* `attributes`, with `["style", SAFE_STYLE]` added to the entry of every
|
|
1595
|
+
* element in `TAG_NAMES` but the `UNSTYLED_TAGS`.
|
|
1596
|
+
*/
|
|
1597
|
+
function withStyle(attributes) {
|
|
1598
|
+
const styled = { ...attributes };
|
|
1599
|
+
for (const tag of new Set(TAG_NAMES)) if (!UNSTYLED_TAGS.has(tag)) styled[tag] = [...styled[tag] ?? [], ["style", SAFE_STYLE]];
|
|
1600
|
+
return styled;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
942
1603
|
* Never set `allowComments` here.
|
|
943
1604
|
*
|
|
944
1605
|
* `hast-util-sanitize` drops comment nodes because that boolean defaults to
|
|
@@ -952,38 +1613,16 @@ const COLLAPSE_GROUP_ID = /^[0-9]+$/;
|
|
|
952
1613
|
*/
|
|
953
1614
|
const sanitizeSchema = {
|
|
954
1615
|
...defaultSchema,
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
"
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
"mn",
|
|
963
|
-
"msup",
|
|
964
|
-
"msub",
|
|
965
|
-
"mfrac",
|
|
966
|
-
"mover",
|
|
967
|
-
"munder",
|
|
968
|
-
"msqrt",
|
|
969
|
-
"mroot",
|
|
970
|
-
"mtable",
|
|
971
|
-
"mtr",
|
|
972
|
-
"mtd",
|
|
973
|
-
"mtext",
|
|
974
|
-
"mspace",
|
|
975
|
-
"annotation",
|
|
976
|
-
"figure",
|
|
977
|
-
"figcaption",
|
|
978
|
-
"summary",
|
|
979
|
-
"details"
|
|
980
|
-
],
|
|
981
|
-
attributes: {
|
|
1616
|
+
strip: [...defaultSchema.strip || [], ...SVG_STRIPPED_EVERYWHERE],
|
|
1617
|
+
tagNames: TAG_NAMES,
|
|
1618
|
+
ancestors: {
|
|
1619
|
+
...defaultSchema.ancestors,
|
|
1620
|
+
...Object.fromEntries(SVG_CHILD_TAGS.map((tag) => [tag, ["svg"]]))
|
|
1621
|
+
},
|
|
1622
|
+
attributes: withStyle({
|
|
982
1623
|
...defaultSchema.attributes,
|
|
983
1624
|
"*": [
|
|
984
1625
|
...defaultSchema.attributes?.["*"] || [],
|
|
985
|
-
"className",
|
|
986
|
-
["style", SAFE_STYLE],
|
|
987
1626
|
"dataSourceLine",
|
|
988
1627
|
["dataVantageTone", ...VANTAGE_TONES],
|
|
989
1628
|
["dataVantageEmphasis", ...VANTAGE_EMPHASIS],
|
|
@@ -993,35 +1632,22 @@ const sanitizeSchema = {
|
|
|
993
1632
|
["dataVantageCollapseToggle", COLLAPSE_GROUP_ID],
|
|
994
1633
|
["dataVantageRun", ...VANTAGE_RUNS],
|
|
995
1634
|
["dataVantageOq", "true"],
|
|
1635
|
+
["dataVantageQuestion", "true"],
|
|
996
1636
|
["dataVantageOqId", VANTAGE_OQ_ID],
|
|
997
1637
|
["dataVantageAlert", ...VANTAGE_ALERTS],
|
|
998
1638
|
"dataVantageLeaning"
|
|
999
1639
|
],
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
...defaultSchema.attributes?.span || [],
|
|
1003
|
-
"className",
|
|
1004
|
-
["style", SAFE_STYLE]
|
|
1005
|
-
],
|
|
1006
|
-
div: [
|
|
1007
|
-
...defaultSchema.attributes?.div || [],
|
|
1008
|
-
"className",
|
|
1009
|
-
["style", SAFE_STYLE]
|
|
1010
|
-
],
|
|
1011
|
-
a: [
|
|
1012
|
-
...defaultSchema.attributes?.a || [],
|
|
1013
|
-
"id",
|
|
1014
|
-
"className"
|
|
1015
|
-
],
|
|
1640
|
+
...Object.fromEntries(Object.keys(PIPELINE_CLASSES).map((tag) => [tag, withClasses(tag)])),
|
|
1641
|
+
a: withClasses("a", "id"),
|
|
1016
1642
|
math: ["xmlns"],
|
|
1017
1643
|
annotation: ["encoding"],
|
|
1018
1644
|
img: [...defaultSchema.attributes?.img || [], "loading"],
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1645
|
+
svg: SVG_ROOT_ATTRIBUTES,
|
|
1646
|
+
...Object.fromEntries(SVG_CHILD_TAGS.map((tag) => [tag, SVG_ATTRIBUTES]))
|
|
1647
|
+
})
|
|
1022
1648
|
};
|
|
1023
1649
|
//#endregion
|
|
1024
|
-
//#region src/
|
|
1650
|
+
//#region src/remarkPlugins.ts
|
|
1025
1651
|
/**
|
|
1026
1652
|
* The mdast half of the chain. Exported on its own because there is a real
|
|
1027
1653
|
* mdast-only consumer: the CLI checker parses documents without ever running
|
|
@@ -1035,6 +1661,8 @@ function buildRemarkPlugins(options = {}) {
|
|
|
1035
1661
|
if (math) plugins.push([remarkMath, { singleDollarTextMath: false }]);
|
|
1036
1662
|
return plugins;
|
|
1037
1663
|
}
|
|
1664
|
+
//#endregion
|
|
1665
|
+
//#region src/pipeline.ts
|
|
1038
1666
|
/** The hast half. Deliberately not exported: see `buildPipeline`. */
|
|
1039
1667
|
function buildRehypePlugins(options = {}) {
|
|
1040
1668
|
const { math = true, highlight = true, sourceLines = true, sanitize = true, bodyLineOffset = 0 } = options;
|
|
@@ -1042,7 +1670,7 @@ function buildRehypePlugins(options = {}) {
|
|
|
1042
1670
|
if (sourceLines) plugins.push([rehypeSourceLines, { offset: bodyLineOffset }]);
|
|
1043
1671
|
plugins.push(rehypeVantageAlerts);
|
|
1044
1672
|
plugins.push(rehypeVantageDirectives);
|
|
1045
|
-
if (sanitize) plugins.push([rehypeSanitize, sanitizeSchema]);
|
|
1673
|
+
if (sanitize) plugins.push(rehypeStripSvgContainers, [rehypeSanitize, sanitizeSchema]);
|
|
1046
1674
|
plugins.push(rehypeVantageAnchors);
|
|
1047
1675
|
plugins.push(rehypeSlug);
|
|
1048
1676
|
if (highlight) plugins.push(rehypeHighlight);
|
|
@@ -1326,6 +1954,9 @@ const cacheKey = (code, theme) => `${theme}\u0000${code}`;
|
|
|
1326
1954
|
function getCachedSvg(code, theme = currentMermaidPalette()) {
|
|
1327
1955
|
return svgCache.get(cacheKey(code, theme));
|
|
1328
1956
|
}
|
|
1957
|
+
function hasCachedSvg(code, theme = currentMermaidPalette()) {
|
|
1958
|
+
return svgCache.has(cacheKey(code, theme));
|
|
1959
|
+
}
|
|
1329
1960
|
function setCachedSvg(code, svg, theme = currentMermaidPalette()) {
|
|
1330
1961
|
svgCache.set(cacheKey(code, theme), svg);
|
|
1331
1962
|
}
|
|
@@ -1338,6 +1969,43 @@ let mermaidLoading = null;
|
|
|
1338
1969
|
* configured for, `null` until loaded.
|
|
1339
1970
|
*/
|
|
1340
1971
|
let configuredTheme = null;
|
|
1972
|
+
/**
|
|
1973
|
+
* The configuration keys a diagram's own source may not set, from an `init`
|
|
1974
|
+
* directive or from frontmatter: Mermaid's default list, then the three
|
|
1975
|
+
* that write a diagram's stylesheet.
|
|
1976
|
+
*
|
|
1977
|
+
* **A diagram's stylesheet reaches past the diagram.** Mermaid puts every
|
|
1978
|
+
* selector of it under the diagram's id, but leaves the names of its
|
|
1979
|
+
* `@keyframes` global, so a diagram that defined `flash-update` redefined the
|
|
1980
|
+
* animation the viewer plays on a block that just changed. Given
|
|
1981
|
+
* `position: fixed` at the size of the window, the next flash laid that block,
|
|
1982
|
+
* and the link it held, over the header and the sidebar. The same stylesheet
|
|
1983
|
+
* fetched images from any host, which is why `SAFE_STYLE` refuses parentheses.
|
|
1984
|
+
*
|
|
1985
|
+
* - `themeCSS` is a stylesheet, and Mermaid checks only that its braces pair.
|
|
1986
|
+
* - `fontFamily` is copied into `themeVariables` *after* Mermaid's check on
|
|
1987
|
+
* theme variables has run, and lands in the stylesheet as a declaration's
|
|
1988
|
+
* value, where a nested block becomes a rule of its own.
|
|
1989
|
+
* - `altFontFamily` lands in a rule the browser parses first, which drops
|
|
1990
|
+
* what does not belong. It is refused anyway: nothing needs it, and that
|
|
1991
|
+
* parse is all that stands in the way.
|
|
1992
|
+
*
|
|
1993
|
+
* Mermaid applies the list at every depth, so `themeVariables.fontFamily` in
|
|
1994
|
+
* a diagram is refused too. The list is spelled whole rather than relying on
|
|
1995
|
+
* Mermaid merging it with its default. `frontend/e2e/mermaid.spec.ts` measures
|
|
1996
|
+
* all four routes; see "Mermaid" in `docs/reference/inline-markup.md`.
|
|
1997
|
+
*/
|
|
1998
|
+
const MERMAID_SECURE_KEYS = [
|
|
1999
|
+
"secure",
|
|
2000
|
+
"securityLevel",
|
|
2001
|
+
"startOnLoad",
|
|
2002
|
+
"maxTextSize",
|
|
2003
|
+
"suppressErrorRendering",
|
|
2004
|
+
"maxEdges",
|
|
2005
|
+
"themeCSS",
|
|
2006
|
+
"fontFamily",
|
|
2007
|
+
"altFontFamily"
|
|
2008
|
+
];
|
|
1341
2009
|
function configure(m, palette) {
|
|
1342
2010
|
const theme = mermaidThemeOf(palette);
|
|
1343
2011
|
m.initialize({
|
|
@@ -1345,7 +2013,8 @@ function configure(m, palette) {
|
|
|
1345
2013
|
theme,
|
|
1346
2014
|
themeVariables: mermaidThemeVariables(theme),
|
|
1347
2015
|
securityLevel: "strict",
|
|
1348
|
-
suppressErrorRendering: true
|
|
2016
|
+
suppressErrorRendering: true,
|
|
2017
|
+
secure: MERMAID_SECURE_KEYS
|
|
1349
2018
|
});
|
|
1350
2019
|
configuredTheme = palette;
|
|
1351
2020
|
}
|
|
@@ -1388,7 +2057,7 @@ const AlertTriangleIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
|
1388
2057
|
strokeWidth: "2",
|
|
1389
2058
|
strokeLinecap: "round",
|
|
1390
2059
|
strokeLinejoin: "round",
|
|
1391
|
-
className: "w-4 h-4 shrink-0",
|
|
2060
|
+
className: "block w-4 h-4 shrink-0",
|
|
1392
2061
|
children: [
|
|
1393
2062
|
/* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }),
|
|
1394
2063
|
/* @__PURE__ */ jsx("path", { d: "M12 9v4" }),
|
|
@@ -1404,7 +2073,7 @@ const ChevronDownIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
|
1404
2073
|
strokeWidth: "2",
|
|
1405
2074
|
strokeLinecap: "round",
|
|
1406
2075
|
strokeLinejoin: "round",
|
|
1407
|
-
className: "w-3 h-3",
|
|
2076
|
+
className: "block w-3 h-3",
|
|
1408
2077
|
children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
|
|
1409
2078
|
});
|
|
1410
2079
|
const ChevronUpIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
@@ -1416,7 +2085,7 @@ const ChevronUpIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
|
1416
2085
|
strokeWidth: "2",
|
|
1417
2086
|
strokeLinecap: "round",
|
|
1418
2087
|
strokeLinejoin: "round",
|
|
1419
|
-
className: "w-3 h-3",
|
|
2088
|
+
className: "block w-3 h-3",
|
|
1420
2089
|
children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" })
|
|
1421
2090
|
});
|
|
1422
2091
|
const MaximizeIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
@@ -1428,7 +2097,7 @@ const MaximizeIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
|
1428
2097
|
strokeWidth: "2",
|
|
1429
2098
|
strokeLinecap: "round",
|
|
1430
2099
|
strokeLinejoin: "round",
|
|
1431
|
-
className: "w-4 h-4 text-gray-600 dark:text-slate-200",
|
|
2100
|
+
className: "block w-4 h-4 text-gray-600 dark:text-slate-200",
|
|
1432
2101
|
children: [
|
|
1433
2102
|
/* @__PURE__ */ jsx("polyline", { points: "15 3 21 3 21 9" }),
|
|
1434
2103
|
/* @__PURE__ */ jsx("polyline", { points: "9 21 3 21 3 15" }),
|
|
@@ -1455,7 +2124,7 @@ const CloseIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
|
1455
2124
|
strokeWidth: "2",
|
|
1456
2125
|
strokeLinecap: "round",
|
|
1457
2126
|
strokeLinejoin: "round",
|
|
1458
|
-
className: "w-5 h-5",
|
|
2127
|
+
className: "block w-5 h-5",
|
|
1459
2128
|
children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
|
|
1460
2129
|
});
|
|
1461
2130
|
const PlusIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
@@ -1467,7 +2136,7 @@ const PlusIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
|
1467
2136
|
strokeWidth: "2",
|
|
1468
2137
|
strokeLinecap: "round",
|
|
1469
2138
|
strokeLinejoin: "round",
|
|
1470
|
-
className: "w-4 h-4",
|
|
2139
|
+
className: "block w-4 h-4",
|
|
1471
2140
|
children: [/* @__PURE__ */ jsx("path", { d: "M5 12h14" }), /* @__PURE__ */ jsx("path", { d: "M12 5v14" })]
|
|
1472
2141
|
});
|
|
1473
2142
|
const MinusIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
@@ -1479,7 +2148,7 @@ const MinusIcon = () => /* @__PURE__ */ jsx("svg", {
|
|
|
1479
2148
|
strokeWidth: "2",
|
|
1480
2149
|
strokeLinecap: "round",
|
|
1481
2150
|
strokeLinejoin: "round",
|
|
1482
|
-
className: "w-4 h-4",
|
|
2151
|
+
className: "block w-4 h-4",
|
|
1483
2152
|
children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" })
|
|
1484
2153
|
});
|
|
1485
2154
|
const ResetIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
@@ -1491,7 +2160,7 @@ const ResetIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
|
1491
2160
|
strokeWidth: "2",
|
|
1492
2161
|
strokeLinecap: "round",
|
|
1493
2162
|
strokeLinejoin: "round",
|
|
1494
|
-
className: "w-4 h-4",
|
|
2163
|
+
className: "block w-4 h-4",
|
|
1495
2164
|
children: [/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }), /* @__PURE__ */ jsx("path", { d: "M3 3v5h5" })]
|
|
1496
2165
|
});
|
|
1497
2166
|
const MIN_SCALE = .2;
|
|
@@ -1760,8 +2429,9 @@ const MermaidDiagramInner = ({ code }) => {
|
|
|
1760
2429
|
});
|
|
1761
2430
|
}
|
|
1762
2431
|
} catch (err) {
|
|
2432
|
+
if (!mounted) return;
|
|
1763
2433
|
console.error("Mermaid render error:", err);
|
|
1764
|
-
|
|
2434
|
+
setFailure({
|
|
1765
2435
|
key,
|
|
1766
2436
|
message: extractErrorMessage(err)
|
|
1767
2437
|
});
|
|
@@ -2019,6 +2689,34 @@ function TagList({ items }) {
|
|
|
2019
2689
|
}, item))
|
|
2020
2690
|
});
|
|
2021
2691
|
}
|
|
2692
|
+
/**
|
|
2693
|
+
* An `OQ-…` token, shaped as `VANTAGE_OQ_ID` shapes an id and bounded as the
|
|
2694
|
+
* planning scan bounds one: by what may not touch it, so an id inside a longer
|
|
2695
|
+
* token (`OQ-B12x`, `xOQ-B1`) is not a token at all. The leading bound is a
|
|
2696
|
+
* group rather than a lookbehind, which Safari parses only since 16.4.
|
|
2697
|
+
*/
|
|
2698
|
+
const OQ_TOKEN = /(^|[^A-Za-z0-9])(OQ-(?:[A-Z][A-Z0-9]{0,5})?[0-9]+)(?![A-Za-z0-9])/g;
|
|
2699
|
+
/** `text`, with each token in `ids` as a link to its question. */
|
|
2700
|
+
function LinkedIds({ text, ids }) {
|
|
2701
|
+
const out = [];
|
|
2702
|
+
let last = 0;
|
|
2703
|
+
for (const match of text.matchAll(OQ_TOKEN)) {
|
|
2704
|
+
const id = match[2];
|
|
2705
|
+
if (id === void 0 || !ids.includes(id)) continue;
|
|
2706
|
+
const start = match.index + match[1].length;
|
|
2707
|
+
if (start > last) out.push(text.slice(last, start));
|
|
2708
|
+
out.push(/* @__PURE__ */ jsx("a", {
|
|
2709
|
+
href: `#${id}`,
|
|
2710
|
+
children: id
|
|
2711
|
+
}, start));
|
|
2712
|
+
last = start + id.length;
|
|
2713
|
+
}
|
|
2714
|
+
if (last < text.length) out.push(text.slice(last));
|
|
2715
|
+
return /* @__PURE__ */ jsx("span", {
|
|
2716
|
+
className: "font-medium",
|
|
2717
|
+
children: out
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2022
2720
|
function ValueCell({ value }) {
|
|
2023
2721
|
if (isStringArray(value) && value.length > 0) return /* @__PURE__ */ jsx(TagList, { items: value });
|
|
2024
2722
|
if (isPlainObject(value)) return /* @__PURE__ */ jsx("pre", {
|
|
@@ -2040,7 +2738,7 @@ function flattenEntries(entries) {
|
|
|
2040
2738
|
}
|
|
2041
2739
|
return result;
|
|
2042
2740
|
}
|
|
2043
|
-
const FrontmatterDisplayInner = ({ frontmatter }) => {
|
|
2741
|
+
const FrontmatterDisplayInner = ({ frontmatter, linkIds }) => {
|
|
2044
2742
|
const entries = flattenEntries(Object.entries(frontmatter));
|
|
2045
2743
|
const { statusChip } = readVantageFrontmatter(frontmatter);
|
|
2046
2744
|
if (entries.length === 0 && statusChip === void 0) return null;
|
|
@@ -2066,7 +2764,10 @@ const FrontmatterDisplayInner = ({ frontmatter }) => {
|
|
|
2066
2764
|
children: key
|
|
2067
2765
|
}), /* @__PURE__ */ jsx("td", {
|
|
2068
2766
|
className: "py-2 text-slate-800 dark:text-slate-200 align-top",
|
|
2069
|
-
children: /* @__PURE__ */ jsx(
|
|
2767
|
+
children: key === "next" && linkIds !== void 0 && typeof value === "string" && !/[\r\n]/.test(value.trim()) && value === frontmatter["next"] ? /* @__PURE__ */ jsx(LinkedIds, {
|
|
2768
|
+
text: value,
|
|
2769
|
+
ids: linkIds
|
|
2770
|
+
}) : /* @__PURE__ */ jsx(ValueCell, { value })
|
|
2070
2771
|
})]
|
|
2071
2772
|
}, key)) })
|
|
2072
2773
|
})
|
|
@@ -2342,17 +3043,16 @@ const MarkdownViewerInner = ({ content, currentPath = "", hash = "", baseUrl = "
|
|
|
2342
3043
|
imageApiBase
|
|
2343
3044
|
]);
|
|
2344
3045
|
const markdownComponents = useMemo(() => ({
|
|
2345
|
-
a({ href, children, ...props }) {
|
|
2346
|
-
const resolvedHref = resolveHref(href);
|
|
3046
|
+
a({ node, href, children, ...props }) {
|
|
2347
3047
|
return /* @__PURE__ */ jsx("a", {
|
|
2348
|
-
href:
|
|
3048
|
+
href: href === void 0 ? void 0 : resolveHref(href),
|
|
2349
3049
|
onClick: (e) => href && handleLinkClick(e, href),
|
|
2350
3050
|
...props,
|
|
2351
3051
|
children
|
|
2352
3052
|
});
|
|
2353
3053
|
},
|
|
2354
3054
|
code(props) {
|
|
2355
|
-
const { children, className: codeClassName, ...rest } = props;
|
|
3055
|
+
const { node, children, className: codeClassName, ...rest } = props;
|
|
2356
3056
|
const match = /language-(\w+)/.exec(codeClassName || "");
|
|
2357
3057
|
if (match && match[1] === "mermaid") return /* @__PURE__ */ jsx(MermaidDiagram, { code: String(children).replace(/\n$/, "") });
|
|
2358
3058
|
return /* @__PURE__ */ jsx("code", {
|
|
@@ -2396,61 +3096,8 @@ const MarkdownViewerInner = ({ content, currentPath = "", hash = "", baseUrl = "
|
|
|
2396
3096
|
};
|
|
2397
3097
|
const MarkdownViewer = memo(MarkdownViewerInner, (prevProps, nextProps) => prevProps.content === nextProps.content && prevProps.currentPath === nextProps.currentPath && prevProps.hash === nextProps.hash && prevProps.className === nextProps.className);
|
|
2398
3098
|
//#endregion
|
|
2399
|
-
//#region src/renderMarkdown.ts
|
|
2400
|
-
/**
|
|
2401
|
-
* Render a markdown string to HTML using the full Vantage pipeline.
|
|
2402
|
-
*
|
|
2403
|
-
* Features (all enabled by default):
|
|
2404
|
-
* - GitHub Flavored Markdown (tables, strikethrough, task lists)
|
|
2405
|
-
* - KaTeX math rendering, inline and block ($$...$$ only; single $ is not a delimiter)
|
|
2406
|
-
* - Syntax highlighting via highlight.js
|
|
2407
|
-
* - `data-source-line` attributes for line anchors
|
|
2408
|
-
* - XSS sanitization
|
|
2409
|
-
* - Heading slugs/anchors
|
|
2410
|
-
* - YAML/TOML frontmatter parsing
|
|
2411
|
-
*
|
|
2412
|
-
* Mermaid diagrams are NOT rendered server-side (they require a browser).
|
|
2413
|
-
* Mermaid code blocks are preserved as `<pre><code class="language-mermaid">`.
|
|
2414
|
-
* Use the React `<MarkdownViewer>` component for client-side mermaid rendering.
|
|
2415
|
-
*/
|
|
2416
|
-
async function renderMarkdown(content, options = {}) {
|
|
2417
|
-
const { gfm = true, math = true, highlight = true, sourceLines = true, sanitize = true, frontmatter: parseFm = true, tree } = options;
|
|
2418
|
-
let parsed;
|
|
2419
|
-
if (parseFm) parsed = parseFrontmatter(content);
|
|
2420
|
-
else parsed = {
|
|
2421
|
-
frontmatter: {},
|
|
2422
|
-
body: content,
|
|
2423
|
-
format: "none",
|
|
2424
|
-
bodyLineOffset: 0
|
|
2425
|
-
};
|
|
2426
|
-
const { remarkPlugins, rehypePlugins } = buildPipeline({
|
|
2427
|
-
gfm,
|
|
2428
|
-
math,
|
|
2429
|
-
highlight,
|
|
2430
|
-
sourceLines,
|
|
2431
|
-
sanitize,
|
|
2432
|
-
bodyLineOffset: parsed.bodyLineOffset
|
|
2433
|
-
});
|
|
2434
|
-
const processor = unified().use(remarkParse).use(remarkPlugins).use(remarkRehype, { allowDangerousHtml: true }).use(rehypePlugins).use(rehypeStringify);
|
|
2435
|
-
const result = tree === void 0 ? String(await processor.process(parsed.body)) : processor.stringify(await processor.run(tree, parsed.body), parsed.body);
|
|
2436
|
-
return {
|
|
2437
|
-
html: String(result),
|
|
2438
|
-
frontmatter: parsed.frontmatter,
|
|
2439
|
-
body: parsed.body
|
|
2440
|
-
};
|
|
2441
|
-
}
|
|
2442
|
-
//#endregion
|
|
2443
3099
|
//#region src/renderMermaidBlocks.ts
|
|
2444
3100
|
/**
|
|
2445
|
-
* Client-side utility to find and render mermaid code blocks in a container.
|
|
2446
|
-
*
|
|
2447
|
-
* After calling `renderMarkdown()`, mermaid blocks come through as
|
|
2448
|
-
* `<pre><code class="language-mermaid">...</code></pre>`. This function
|
|
2449
|
-
* finds those blocks and replaces them with rendered SVG diagrams.
|
|
2450
|
-
*
|
|
2451
|
-
* Framework-agnostic — works in any browser environment.
|
|
2452
|
-
*/
|
|
2453
|
-
/**
|
|
2454
3101
|
* Find all `<pre><code class="language-mermaid">` blocks in a container
|
|
2455
3102
|
* and replace them with rendered SVG diagrams.
|
|
2456
3103
|
*
|
|
@@ -2531,6 +3178,104 @@ function replaceWithSvg(preEl, svg, className) {
|
|
|
2531
3178
|
wrapper.innerHTML = svg;
|
|
2532
3179
|
preEl.replaceWith(wrapper);
|
|
2533
3180
|
}
|
|
3181
|
+
/** The viewer's parser: the remark half of `buildPipeline`. */
|
|
3182
|
+
const fenceParser = unified().use(remarkParse).use(buildRemarkPlugins());
|
|
3183
|
+
/**
|
|
3184
|
+
* The code of every Mermaid fence in `markdown`, in source order, exactly as
|
|
3185
|
+
* a viewer's `MermaidDiagram` receives it: the fence's content, without its
|
|
3186
|
+
* container's indentation or its closing newline.
|
|
3187
|
+
*
|
|
3188
|
+
* A fence is a diagram when its info string's first word begins with the word
|
|
3189
|
+
* `mermaid`, which is the test the viewer applies to the `language-…` class
|
|
3190
|
+
* the fence renders with (`/language-(\w+)/`). Text that never says
|
|
3191
|
+
* `mermaid` is not parsed at all.
|
|
3192
|
+
*/
|
|
3193
|
+
function mermaidFences(markdown) {
|
|
3194
|
+
if (!markdown.includes("mermaid")) return [];
|
|
3195
|
+
const codes = [];
|
|
3196
|
+
visit(fenceParser.parse(markdown), "code", (node) => {
|
|
3197
|
+
if (/^\w+/.exec(node.lang ?? "")?.[0] === "mermaid") codes.push(node.value);
|
|
3198
|
+
});
|
|
3199
|
+
return codes;
|
|
3200
|
+
}
|
|
3201
|
+
/** Draws under way, by palette and code, so asking twice draws once. */
|
|
3202
|
+
const drawing = /* @__PURE__ */ new Map();
|
|
3203
|
+
let drawn = 0;
|
|
3204
|
+
/**
|
|
3205
|
+
* Draw one diagram into the SVG cache that `MermaidDiagram` reads when it
|
|
3206
|
+
* mounts, so a diagram rendered after this has settled is drawn at its full
|
|
3207
|
+
* size on its first paint instead of growing into place once Mermaid answers.
|
|
3208
|
+
* The planning page draws every diagram of the cards it is about to show this
|
|
3209
|
+
* way, before it commits them.
|
|
3210
|
+
*
|
|
3211
|
+
* Mermaid is loaded through `getMermaid`, so the drawing is configured, and
|
|
3212
|
+
* made safe, exactly as every other diagram of the page is. `theme` is the
|
|
3213
|
+
* palette key (`currentMermaidPalette`) to draw for; when the page has moved
|
|
3214
|
+
* to another palette by the time Mermaid is ready, nothing is cached, since
|
|
3215
|
+
* Mermaid now draws for the page's palette and not for `theme`.
|
|
3216
|
+
*
|
|
3217
|
+
* Resolves once the diagram is in the cache, at once when it already was, and
|
|
3218
|
+
* rejects when Mermaid cannot draw it.
|
|
3219
|
+
*/
|
|
3220
|
+
function prerenderMermaid(code, theme = currentMermaidPalette()) {
|
|
3221
|
+
if (!code.trim() || hasCachedSvg(code, theme)) return Promise.resolve();
|
|
3222
|
+
const key = `${theme}\u0000${code}`;
|
|
3223
|
+
let pending = drawing.get(key);
|
|
3224
|
+
if (pending === void 0) {
|
|
3225
|
+
pending = (async () => {
|
|
3226
|
+
const mermaid = await getMermaid();
|
|
3227
|
+
if (currentMermaidPalette() !== theme) return;
|
|
3228
|
+
const { svg } = await mermaid.render(`mermaid-ahead-${(++drawn).toString(36)}`, code);
|
|
3229
|
+
setCachedSvg(code, svg, theme);
|
|
3230
|
+
})().finally(() => drawing.delete(key));
|
|
3231
|
+
drawing.set(key, pending);
|
|
3232
|
+
}
|
|
3233
|
+
return pending;
|
|
3234
|
+
}
|
|
3235
|
+
//#endregion
|
|
3236
|
+
//#region src/renderMarkdown.ts
|
|
3237
|
+
/**
|
|
3238
|
+
* Render a markdown string to HTML using the full Vantage pipeline.
|
|
3239
|
+
*
|
|
3240
|
+
* Features (all enabled by default):
|
|
3241
|
+
* - GitHub Flavored Markdown (tables, strikethrough, task lists)
|
|
3242
|
+
* - KaTeX math rendering, inline and block ($$...$$ only; single $ is not a delimiter)
|
|
3243
|
+
* - Syntax highlighting via highlight.js
|
|
3244
|
+
* - `data-source-line` attributes for line anchors
|
|
3245
|
+
* - XSS sanitization
|
|
3246
|
+
* - Heading slugs/anchors
|
|
3247
|
+
* - YAML/TOML frontmatter parsing
|
|
3248
|
+
*
|
|
3249
|
+
* Mermaid diagrams are NOT rendered server-side (they require a browser).
|
|
3250
|
+
* Mermaid code blocks are preserved as `<pre><code class="language-mermaid">`.
|
|
3251
|
+
* Use the React `<MarkdownViewer>` component for client-side mermaid rendering.
|
|
3252
|
+
*/
|
|
3253
|
+
async function renderMarkdown(content, options = {}) {
|
|
3254
|
+
const { gfm = true, math = true, highlight = true, sourceLines = true, sanitize = true, frontmatter: parseFm = true, tree } = options;
|
|
3255
|
+
let parsed;
|
|
3256
|
+
if (parseFm) parsed = parseFrontmatter(content);
|
|
3257
|
+
else parsed = {
|
|
3258
|
+
frontmatter: {},
|
|
3259
|
+
body: content,
|
|
3260
|
+
format: "none",
|
|
3261
|
+
bodyLineOffset: 0
|
|
3262
|
+
};
|
|
3263
|
+
const { remarkPlugins, rehypePlugins } = buildPipeline({
|
|
3264
|
+
gfm,
|
|
3265
|
+
math,
|
|
3266
|
+
highlight,
|
|
3267
|
+
sourceLines,
|
|
3268
|
+
sanitize,
|
|
3269
|
+
bodyLineOffset: parsed.bodyLineOffset
|
|
3270
|
+
});
|
|
3271
|
+
const processor = unified().use(remarkParse).use(remarkPlugins).use(remarkRehype, { allowDangerousHtml: true }).use(rehypePlugins).use(rehypeStringify);
|
|
3272
|
+
const result = tree === void 0 ? String(await processor.process(parsed.body)) : processor.stringify(await processor.run(tree, parsed.body), parsed.body);
|
|
3273
|
+
return {
|
|
3274
|
+
html: String(result),
|
|
3275
|
+
frontmatter: parsed.frontmatter,
|
|
3276
|
+
body: parsed.body
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
2534
3279
|
//#endregion
|
|
2535
3280
|
//#region src/resolveLinks.ts
|
|
2536
3281
|
/**
|
|
@@ -2576,6 +3321,6 @@ function resolveLinks(html, options = {}) {
|
|
|
2576
3321
|
});
|
|
2577
3322
|
}
|
|
2578
3323
|
//#endregion
|
|
2579
|
-
export { DOC_STATUSES, DOC_STATUS_TONES, DocumentStatusChip, FrontmatterDisplay, MarkdownViewer, MermaidDiagram, VANTAGE_FRONTMATTER_KEYS, buildPipeline, buildRemarkPlugins, clearLineAnchorHighlights, isDocStatus, parseFrontmatter, parseLineAnchor, readVantageFrontmatter, rehypeSourceLines, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor, useLineAnchor };
|
|
3324
|
+
export { DOC_STATUSES, DOC_STATUS_TONES, DocumentStatusChip, FrontmatterDisplay, MarkdownViewer, MermaidDiagram, VANTAGE_FRONTMATTER_KEYS, buildPipeline, buildRemarkPlugins, clearLineAnchorHighlights, isDocStatus, mermaidFences, parseFrontmatter, parseLineAnchor, prerenderMermaid, readVantageFrontmatter, rehypeSourceLines, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor, useLineAnchor };
|
|
2580
3325
|
|
|
2581
3326
|
//# sourceMappingURL=react.js.map
|