vantage-md 0.6.1 → 0.7.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/dist/prose.css CHANGED
@@ -141,7 +141,7 @@
141
141
  `directives.css` draws a toned section's vertical rule, and clipping it breaks
142
142
  the rule for the height of every code fence. See the note there beside
143
143
  `[data-vantage-tone]:is(pre, hr)`. Keeping this unconditional rather than
144
- scoping it to toned fences means one behaviour for every fence, and no
144
+ scoping it to toned fences means one behavior for every fence, and no
145
145
  specificity tie between the two stylesheets. */
146
146
  .vantage-prose pre code {
147
147
  background: none;
package/dist/react.cjs CHANGED
@@ -121,14 +121,14 @@ const rehypeSourceLines = (options) => {
121
121
  * `remark-gfm` does not implement alerts, so until this plugin existed a
122
122
  * `> [!WARNING]` rendered as an ordinary blockquote with the literal marker
123
123
  * visible as its first words. Worse than merely unstyled: `@tailwindcss/typography`
124
- * italicises blockquotes and draws `open-quote`/`close-quote` around the first
124
+ * italicizes blockquotes and draws `open-quote`/`close-quote` around the first
125
125
  * paragraph, so a callout came out as an italic *quotation* whose opening words
126
126
  * were `"[!WARNING]`. That was the "Known gaps" entry in
127
127
  * `docs/reference/inline-markup.md` and OQ-10, filed rather than fixed, while
128
128
  * `styleGuide.ts` went on telling every agent to write them.
129
129
  *
130
130
  * The tokens are deliberately the ones the `tone` vocabulary already resolves —
131
- * an alert *is* the six-colour light/dark treatment `tone` shipped, which is
131
+ * an alert *is* the six-color light/dark treatment `tone` shipped, which is
132
132
  * exactly what the gap entry said whoever fixed this should do rather than
133
133
  * building a second palette. `[!WARNING]` and `<!-- vantage: block tone=warning -->`
134
134
  * therefore agree by construction, and adding a theme still touches one
@@ -142,7 +142,7 @@ const rehypeSourceLines = (options) => {
142
142
  *
143
143
  * ## What it does not do
144
144
  *
145
- * It does not touch a blockquote that carries no marker, and an unrecognised
145
+ * It does not touch a blockquote that carries no marker, and an unrecognized
146
146
  * marker (`[!HINT]`) is left exactly as it was — visible literal text, which is
147
147
  * the honest rendering of something GitHub also would not style. Silently
148
148
  * swallowing it would hide a typo that reads as a callout on neither renderer.
@@ -364,7 +364,7 @@ VANTAGE_ANCHOR_TARGETS.filter((tag) => tag !== "pre" && tag !== "table");
364
364
  * file, and requiring it everywhere would fire on every single-doc sketch.
365
365
  *
366
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
367
+ * plugin that stamps the anchor, the sanitizer that allowlists the value, and
368
368
  * the checker's `vantage/oq-id-format`. A fourth copy is how the checker starts
369
369
  * calling a working anchor malformed.
370
370
  */
@@ -522,7 +522,7 @@ const HEADING_DEPTHS = /* @__PURE__ */ new Map([
522
522
  /**
523
523
  * Key → hast property, for the keys that treat a whole run.
524
524
  *
525
- * A camelCase hast property serialises to the kebab-case attribute, so
525
+ * A camelCase hast property serializes to the kebab-case attribute, so
526
526
  * `dataVantageTone` is `data-vantage-tone` in every renderer.
527
527
  *
528
528
  * `tone` and `emphasis` describe what a section *is* and how loud it is, so
@@ -557,11 +557,11 @@ const LEANING_PROPERTY = "dataVantageLeaning";
557
557
  * The id, carried as a `data-` attribute rather than written straight to `id`.
558
558
  *
559
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`
560
+ * the sanitizer deletes them — and the sanitizer's default schema clobbers `id`
561
561
  * with the prefix `user-content-`. A bare `id` set here would reach the page as
562
562
  * `user-content-OQ-4`, every `#OQ-4` link in every document would land nowhere,
563
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
564
+ * on the other side of the sanitizer, which is the same reason `rehypeSlug` is
565
565
  * registered there (`pipeline.ts`).
566
566
  */
567
567
  const OQ_ID_PROPERTY = "dataVantageOqId";
@@ -593,7 +593,7 @@ const MAX_LEANING = 500;
593
593
  * A whitespace-only `text` node always does — measured, with or without a blank
594
594
  * line in the source. Comments do too, and an unrelated `<!-- TODO -->` must not
595
595
  * break the chain: it is invisible in every renderer and deleted by the
596
- * sanitiser, so letting it change a directive's meaning would make behaviour
596
+ * sanitizer, so letting it change a directive's meaning would make behavior
597
597
  * depend on something no reader can see.
598
598
  */
599
599
  function isSkippable(node) {
@@ -645,7 +645,7 @@ function accepts(name, key, value) {
645
645
  * two stamped paragraphs — and the section's one continuous vertical rule is
646
646
  * drawn per member, so an unstamped member is a hole the height of the block
647
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
648
+ * `<figure>`, against the 40px a neighbor can bleed upward, and arbitrarily
649
649
  * large for anything taller. `collapsed=true` had the same shape of bug the
650
650
  * other way round — it hid the paragraphs and left the figure on the page.
651
651
  *
@@ -827,7 +827,7 @@ function classNames(node) {
827
827
  * A `<pre>` `rehype-katex` will replace — its own condition, restated.
828
828
  *
829
829
  * `language-math` is the only class to test: `rehype-katex` keys the
830
- * pre-as-scope branch on it, and the sanitiser strips the `math-display` that
830
+ * pre-as-scope branch on it, and the sanitizer strips the `math-display` that
831
831
  * `remark-math` also emits (measured — a stamped fence arrives here with
832
832
  * `className: ["language-math"]` alone).
833
833
  */
@@ -981,7 +981,7 @@ const COLLAPSE_GROUP_ID = /^[0-9]+$/;
981
981
  * `false` — comments are not elements, so `tagNames` has nothing to do with it.
982
982
  * `rehypeVantageDirectives` relies on that deletion: it consumes a
983
983
  * `<!-- vantage: … -->` comment into attributes and deliberately leaves the node
984
- * for the sanitiser. Turning the switch on readmits every directive comment —
984
+ * for the sanitizer. Turning the switch on readmits every directive comment —
985
985
  * valid and malformed alike — into the rendered HTML, which breaks the carrier's
986
986
  * whole premise. `vantageDirectives.test.ts` ("leaves no comment in the rendered
987
987
  * markup") is the guard.
@@ -1208,6 +1208,49 @@ function currentMermaidTheme() {
1208
1208
  return typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "default";
1209
1209
  }
1210
1210
  /**
1211
+ * The attribute on `<html>` naming the active color theme. Absent means the
1212
+ * built-in look. The app sets it only once the theme's stylesheet has loaded,
1213
+ * so a reader of this attribute can trust the theme's variables are in effect.
1214
+ */
1215
+ const COLOR_THEME_ATTRIBUTE = "data-vantage-theme";
1216
+ /**
1217
+ * The attribute on `<html>` saying where the active theme came from: `"user"`
1218
+ * for a stylesheet in the reader's themes directory, `"built-in"` for one the
1219
+ * app ships. Set with {@link COLOR_THEME_ATTRIBUTE}, and absent with it.
1220
+ *
1221
+ * It exists because an id alone is not a palette. A user theme may share a
1222
+ * built-in's id — that is how a reader tweaks one — and the app applies the
1223
+ * stored built-in synchronously, then swaps in the same-id user file once
1224
+ * /api/themes answers. Keyed on the id, the diagrams drawn in between kept the
1225
+ * built-in's colors: the key did not change, so neither the cache nor
1226
+ * `useSyncExternalStore` saw a reason to redraw.
1227
+ */
1228
+ const COLOR_THEME_SOURCE_ATTRIBUTE = "data-vantage-theme-source";
1229
+ /** The active color theme's id, or `""` for the built-in look. */
1230
+ function currentColorTheme() {
1231
+ if (typeof document === "undefined") return "";
1232
+ return document.documentElement.getAttribute("data-vantage-theme") ?? "";
1233
+ }
1234
+ /**
1235
+ * Everything a rendered diagram's colors depend on, as one string: the
1236
+ * light/dark mode, plus the color theme when one is active.
1237
+ *
1238
+ * A diagram is baked at render time, so anything that changes its colors has
1239
+ * to change this key — it is what the SVG cache and the loader's "configured
1240
+ * for" check compare. Under the built-in look it is exactly the mode name,
1241
+ * which is what both keyed on before color themes existed.
1242
+ */
1243
+ function currentMermaidPalette() {
1244
+ const theme = currentColorTheme();
1245
+ if (!theme) return currentMermaidTheme();
1246
+ const user = document.documentElement.getAttribute(COLOR_THEME_SOURCE_ATTRIBUTE) === "user";
1247
+ return `${currentMermaidTheme()} ${theme}${user ? " user" : ""}`;
1248
+ }
1249
+ /** The mermaid theme name a palette key was built from. */
1250
+ function mermaidThemeOf(palette) {
1251
+ return palette.startsWith("dark") ? "dark" : "default";
1252
+ }
1253
+ /**
1211
1254
  * Theme variables per theme. Mermaid derives most of its palette from these, so
1212
1255
  * the set is deliberately small: the surfaces, the ink, and the lines.
1213
1256
  */
@@ -1231,27 +1274,108 @@ const THEME_VARIABLES = {
1231
1274
  edgeLabelBackground: "#f8fafc"
1232
1275
  }
1233
1276
  };
1277
+ /**
1278
+ * Where each variable comes from under a color theme: the palette step the
1279
+ * built-in value was chosen from. The hex above IS that step in Tailwind's own
1280
+ * palette, so reading the step back out of the page gives the same diagram
1281
+ * under the built-in look and the theme's colors under any other.
1282
+ */
1283
+ const THEME_SOURCES = {
1284
+ dark: {
1285
+ background: "--color-slate-800",
1286
+ mainBkg: "--color-slate-700",
1287
+ nodeBorder: "--color-slate-400",
1288
+ nodeTextColor: "--color-slate-100",
1289
+ lineColor: "--color-slate-400",
1290
+ textColor: "--color-slate-200",
1291
+ edgeLabelBackground: "--color-slate-800"
1292
+ },
1293
+ default: {
1294
+ background: "--color-slate-50",
1295
+ mainBkg: "--color-slate-100",
1296
+ nodeBorder: "--color-slate-500",
1297
+ nodeTextColor: "--color-slate-900",
1298
+ lineColor: "--color-slate-500",
1299
+ textColor: "--color-slate-800",
1300
+ edgeLabelBackground: "--color-slate-50"
1301
+ }
1302
+ };
1234
1303
  function mermaidThemeVariables(theme) {
1235
- return THEME_VARIABLES[theme];
1304
+ const fixed = THEME_VARIABLES[theme];
1305
+ if (!currentColorTheme()) return fixed;
1306
+ const out = {};
1307
+ for (const [key, hex] of Object.entries(fixed)) out[key] = resolveCssColor(THEME_SOURCES[theme][key]) ?? hex;
1308
+ return out;
1309
+ }
1310
+ /**
1311
+ * A custom property's color as `#rrggbb`, or `null` when it cannot be read.
1312
+ *
1313
+ * Mermaid wants hex at `initialize()` time, and a theme's value can be any CSS
1314
+ * color — `oklch()`, `color-mix()`, a `var()` of another variable. So the
1315
+ * browser does the work: a probe element resolves the property to a computed
1316
+ * color, and a 1×1 canvas turns that into sRGB bytes whatever syntax it came
1317
+ * back in. No canvas (jsdom, a locked-down embed) is `null`, and the caller
1318
+ * falls back to the built-in value.
1319
+ *
1320
+ * A property nobody declared is `null` too, and has to be checked for up
1321
+ * front: `color: var(--unset)` is invalid at computed-value time, so the probe
1322
+ * *inherits* its color instead — the page's text color, which the canvas
1323
+ * would dutifully turn into a plausible hex and mermaid would paint every node
1324
+ * box with.
1325
+ */
1326
+ function resolveCssColor(property) {
1327
+ if (typeof document === "undefined") return null;
1328
+ if (!getComputedStyle(document.documentElement).getPropertyValue(property).trim()) return null;
1329
+ const probe = document.createElement("span");
1330
+ probe.style.display = "none";
1331
+ probe.style.color = `var(${property})`;
1332
+ document.documentElement.appendChild(probe);
1333
+ const computed = getComputedStyle(probe).color;
1334
+ probe.remove();
1335
+ if (!computed) return null;
1336
+ const ctx = pixelContext();
1337
+ if (!ctx) return null;
1338
+ ctx.fillStyle = computed;
1339
+ ctx.fillRect(0, 0, 1, 1);
1340
+ const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
1341
+ return `#${[
1342
+ r,
1343
+ g,
1344
+ b
1345
+ ].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
1346
+ }
1347
+ /** A 1×1 2D context to read a color back from, or `null` where there is none. */
1348
+ function pixelContext() {
1349
+ try {
1350
+ const canvas = document.createElement("canvas");
1351
+ canvas.width = canvas.height = 1;
1352
+ return canvas.getContext("2d", { willReadFrequently: true });
1353
+ } catch {
1354
+ return null;
1355
+ }
1236
1356
  }
1237
1357
  //#endregion
1238
1358
  //#region src/mermaidCache.ts
1239
1359
  const svgCache = /* @__PURE__ */ new Map();
1240
- const cacheKey = (code, theme) => `${theme}${code}`;
1360
+ const cacheKey = (code, theme) => `${theme}\u0000${code}`;
1241
1361
  /** The SVG for this fence in the theme the page is currently asking for. */
1242
- function getCachedSvg(code, theme = currentMermaidTheme()) {
1362
+ function getCachedSvg(code, theme = currentMermaidPalette()) {
1243
1363
  return svgCache.get(cacheKey(code, theme));
1244
1364
  }
1245
- function setCachedSvg(code, svg, theme = currentMermaidTheme()) {
1365
+ function setCachedSvg(code, svg, theme = currentMermaidPalette()) {
1246
1366
  svgCache.set(cacheKey(code, theme), svg);
1247
1367
  }
1248
1368
  //#endregion
1249
1369
  //#region src/mermaidLoader.ts
1250
1370
  let mermaidInstance = null;
1251
1371
  let mermaidLoading = null;
1252
- /** The theme the loaded instance was last configured for, `null` until loaded. */
1372
+ /**
1373
+ * The palette key (`currentMermaidPalette`) the loaded instance was last
1374
+ * configured for, `null` until loaded.
1375
+ */
1253
1376
  let configuredTheme = null;
1254
- function configure(m, theme) {
1377
+ function configure(m, palette) {
1378
+ const theme = mermaidThemeOf(palette);
1255
1379
  m.initialize({
1256
1380
  startOnLoad: false,
1257
1381
  theme,
@@ -1259,7 +1383,7 @@ function configure(m, theme) {
1259
1383
  securityLevel: "strict",
1260
1384
  suppressErrorRendering: true
1261
1385
  });
1262
- configuredTheme = theme;
1386
+ configuredTheme = palette;
1263
1387
  }
1264
1388
  /**
1265
1389
  * The mermaid module, configured for the theme the page is asking for *now*.
@@ -1273,19 +1397,19 @@ function configure(m, theme) {
1273
1397
  * served instead (`mermaidCache.ts`).
1274
1398
  */
1275
1399
  async function getMermaid() {
1276
- const theme = currentMermaidTheme();
1400
+ const theme = currentMermaidPalette();
1277
1401
  if (mermaidInstance) {
1278
1402
  if (configuredTheme !== theme) configure(mermaidInstance, theme);
1279
1403
  return mermaidInstance;
1280
1404
  }
1281
1405
  if (!mermaidLoading) mermaidLoading = import("mermaid").then((mod) => {
1282
1406
  const m = mod.default;
1283
- configure(m, currentMermaidTheme());
1407
+ configure(m, currentMermaidPalette());
1284
1408
  mermaidInstance = m;
1285
1409
  return m;
1286
1410
  });
1287
1411
  const loaded = await mermaidLoading;
1288
- const wanted = currentMermaidTheme();
1412
+ const wanted = currentMermaidPalette();
1289
1413
  if (configuredTheme !== wanted) configure(loaded, wanted);
1290
1414
  return loaded;
1291
1415
  }
@@ -1588,7 +1712,7 @@ function DiagramModal({ isOpen, onClose, children }) {
1588
1712
  }
1589
1713
  /**
1590
1714
  * The palette the page is asking for, re-read whenever `<html>`'s class list
1591
- * changes.
1715
+ * or its color-theme attribute changes.
1592
1716
  *
1593
1717
  * A diagram is an SVG baked at render time, so unlike everything else on the
1594
1718
  * page it does not restyle when the theme flips — it has to be drawn again. The
@@ -1605,14 +1729,19 @@ function subscribeToTheme(onChange) {
1605
1729
  const observer = new MutationObserver(onChange);
1606
1730
  observer.observe(document.documentElement, {
1607
1731
  attributes: true,
1608
- attributeFilter: ["class"]
1732
+ attributeFilter: [
1733
+ "class",
1734
+ COLOR_THEME_ATTRIBUTE,
1735
+ COLOR_THEME_SOURCE_ATTRIBUTE
1736
+ ]
1609
1737
  });
1610
1738
  return () => observer.disconnect();
1611
1739
  }
1612
1740
  /** Server render has no `<html>` to read, and no diagram to draw either. */
1613
1741
  const serverTheme = () => "default";
1742
+ /** The palette key the page is asking for (see `currentMermaidPalette`). */
1614
1743
  function useMermaidTheme() {
1615
- return (0, react.useSyncExternalStore)(subscribeToTheme, currentMermaidTheme, serverTheme);
1744
+ return (0, react.useSyncExternalStore)(subscribeToTheme, currentMermaidPalette, serverTheme);
1616
1745
  }
1617
1746
  const MermaidDiagramInner = ({ code }) => {
1618
1747
  const theme = useMermaidTheme();
@@ -1754,7 +1883,7 @@ const DOC_STATUSES = [
1754
1883
  /** Every key this build knows under `vantage:`. Closed. */
1755
1884
  const VANTAGE_FRONTMATTER_KEYS = ["status-chip"];
1756
1885
  /**
1757
- * Which tone each status borrows its colours from.
1886
+ * Which tone each status borrows its colors from.
1758
1887
  *
1759
1888
  * The chip has no palette of its own: it reuses the tone chips
1760
1889
  * (`.vantage-chip--<tone>` in `styles/directives.css`), which is also what makes
@@ -1873,7 +2002,7 @@ function readStatusChip(frontmatter, raw, issues) {
1873
2002
  *
1874
2003
  * Styling comes entirely from `styles/directives.css` — `.vantage-chip` for the
1875
2004
  * geometry, shared by selector list with the `badge=` pseudo-element, and
1876
- * `.vantage-chip--<tone>` for the colours. No Tailwind utility appears here, so
2005
+ * `.vantage-chip--<tone>` for the colors. No Tailwind utility appears here, so
1877
2006
  * the chip survives in a consumer that does not run Tailwind and no class name
1878
2007
  * has to be discovered by a content scan.
1879
2008
  */
@@ -1988,7 +2117,7 @@ const FrontmatterDisplay = (0, react.memo)(FrontmatterDisplayInner);
1988
2117
  *
1989
2118
  * Split out from scrollToLineAnchor.ts so that non-browser consumers — the
1990
2119
  * `vantage-check` CLI, which validates `#L42` links against the file on disk —
1991
- * can share the *same* syntax the viewer honours instead of reimplementing it
2120
+ * can share the *same* syntax the viewer honors instead of reimplementing it
1992
2121
  * and drifting.
1993
2122
  */
1994
2123
  /**
@@ -2305,10 +2434,6 @@ const MarkdownViewer = (0, react.memo)(MarkdownViewerInner, (prevProps, nextProp
2305
2434
  //#endregion
2306
2435
  //#region src/renderMarkdown.ts
2307
2436
  /**
2308
- * Framework-agnostic markdown -> HTML rendering pipeline.
2309
- * Uses the same remark/rehype chain as the Vantage viewer.
2310
- */
2311
- /**
2312
2437
  * Render a markdown string to HTML using the full Vantage pipeline.
2313
2438
  *
2314
2439
  * Features (all enabled by default):
@@ -2325,7 +2450,7 @@ const MarkdownViewer = (0, react.memo)(MarkdownViewerInner, (prevProps, nextProp
2325
2450
  * Use the React `<MarkdownViewer>` component for client-side mermaid rendering.
2326
2451
  */
2327
2452
  async function renderMarkdown(content, options = {}) {
2328
- const { gfm = true, math = true, highlight = true, sourceLines = true, sanitize = true, frontmatter: parseFm = true } = options;
2453
+ const { gfm = true, math = true, highlight = true, sourceLines = true, sanitize = true, frontmatter: parseFm = true, tree } = options;
2329
2454
  let parsed;
2330
2455
  if (parseFm) parsed = parseFrontmatter(content);
2331
2456
  else parsed = {
@@ -2342,7 +2467,8 @@ async function renderMarkdown(content, options = {}) {
2342
2467
  sanitize,
2343
2468
  bodyLineOffset: parsed.bodyLineOffset
2344
2469
  });
2345
- const result = await (0, unified.unified)().use(remark_parse.default).use(remarkPlugins).use(remark_rehype.default, { allowDangerousHtml: true }).use(rehypePlugins).use(rehype_stringify.default).process(parsed.body);
2470
+ const processor = (0, unified.unified)().use(remark_parse.default).use(remarkPlugins).use(remark_rehype.default, { allowDangerousHtml: true }).use(rehypePlugins).use(rehype_stringify.default);
2471
+ const result = tree === void 0 ? String(await processor.process(parsed.body)) : processor.stringify(await processor.run(tree, parsed.body), parsed.body);
2346
2472
  return {
2347
2473
  html: String(result),
2348
2474
  frontmatter: parsed.frontmatter,