vantage-md 0.1.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { unified } from 'unified';
2
2
  import remarkParse from 'remark-parse';
3
+ import remarkRehype from 'remark-rehype';
4
+ import rehypeStringify from 'rehype-stringify';
3
5
  import remarkGfm from 'remark-gfm';
4
6
  import remarkMath from 'remark-math';
5
- import remarkRehype from 'remark-rehype';
6
7
  import rehypeRaw from 'rehype-raw';
7
8
  import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
8
9
  import rehypeHighlight from 'rehype-highlight';
9
10
  import rehypeKatex from 'rehype-katex';
10
11
  import rehypeSlug from 'rehype-slug';
11
- import rehypeStringify from 'rehype-stringify';
12
12
  import YAML from 'yaml';
13
13
  import { parse } from 'smol-toml';
14
14
 
@@ -33,25 +33,511 @@ var BLOCK_TAGS = /* @__PURE__ */ new Set([
33
33
  "hr",
34
34
  "div"
35
35
  ]);
36
- function visit(node) {
36
+ function visit(node, offset) {
37
37
  if ("children" in node) {
38
38
  for (const child of node.children) {
39
39
  if (child.type === "element") {
40
40
  if (BLOCK_TAGS.has(child.tagName) && child.position?.start?.line) {
41
41
  child.properties = child.properties || {};
42
- child.properties["dataSourceLine"] = child.position.start.line;
42
+ child.properties["dataSourceLine"] = child.position.start.line + offset;
43
43
  }
44
- visit(child);
44
+ visit(child, offset);
45
45
  }
46
46
  }
47
47
  }
48
48
  }
49
- var rehypeSourceLines = () => {
49
+ var rehypeSourceLines = (options) => {
50
+ const offset = options?.offset ?? 0;
50
51
  return (tree) => {
51
- visit(tree);
52
+ visit(tree, offset);
52
53
  };
53
54
  };
54
55
  var rehypeSourceLines_default = rehypeSourceLines;
56
+
57
+ // src/vantageDirectives.ts
58
+ var VANTAGE_SENTINEL = "vantage:";
59
+ var DIRECTIVE_NAMES = ["section", "block", "oq"];
60
+ var VANTAGE_TONES = [
61
+ "note",
62
+ "tip",
63
+ "important",
64
+ "warning",
65
+ "caution",
66
+ "muted"
67
+ ];
68
+ var VANTAGE_EMPHASIS = ["strong", "normal", "quiet"];
69
+ var VANTAGE_BADGES = [
70
+ "draft",
71
+ "stale",
72
+ "blocked",
73
+ "done",
74
+ "wip"
75
+ ];
76
+ var VANTAGE_COLLAPSED = ["true", "false"];
77
+ var VANTAGE_RUNS = ["start", "middle", "end", "only"];
78
+ var VANTAGE_STYLE_TARGETS = [
79
+ "p",
80
+ "h1",
81
+ "h2",
82
+ "h3",
83
+ "h4",
84
+ "h5",
85
+ "h6",
86
+ "li",
87
+ "blockquote",
88
+ "pre",
89
+ "table",
90
+ "tr",
91
+ "ul",
92
+ "ol",
93
+ "hr",
94
+ "div"
95
+ ];
96
+ var VANTAGE_ANCHOR_TARGETS = [
97
+ "p",
98
+ "h1",
99
+ "h2",
100
+ "h3",
101
+ "h4",
102
+ "h5",
103
+ "h6",
104
+ "li",
105
+ "blockquote",
106
+ "pre",
107
+ "table"
108
+ ];
109
+ var VANTAGE_OQ_HOST_TARGETS = VANTAGE_ANCHOR_TARGETS.filter(
110
+ (tag) => tag !== "pre" && tag !== "table"
111
+ );
112
+ var STYLE_KEYS = {
113
+ tone: VANTAGE_TONES,
114
+ emphasis: VANTAGE_EMPHASIS,
115
+ badge: VANTAGE_BADGES,
116
+ collapsed: VANTAGE_COLLAPSED
117
+ };
118
+ var DIRECTIVE_VOCABULARY = {
119
+ section: STYLE_KEYS,
120
+ block: STYLE_KEYS,
121
+ oq: { id: null, leaning: null }
122
+ };
123
+ var WS = /[ \t\r\n]*/y;
124
+ var SENTINEL_PREFIX = /^[ \t\r\n]*vantage:/;
125
+ var NAME = /[a-z][a-z0-9-]*/y;
126
+ var UNQUOTED = /[A-Za-z0-9_.:#-]+/y;
127
+ var QUOTED = /"[^"]*"/y;
128
+ function hasVantageSentinel(comment) {
129
+ return SENTINEL_PREFIX.test(comment);
130
+ }
131
+ function token(comment, offset) {
132
+ const rest = comment.slice(offset);
133
+ const end = rest.search(/[ \t\r\n]/);
134
+ const word = end === -1 ? rest : rest.slice(0, end);
135
+ return word.length > 24 ? `${word.slice(0, 24)}\u2026` : word;
136
+ }
137
+ function matchAt(pattern, comment, offset) {
138
+ pattern.lastIndex = offset;
139
+ const match = pattern.exec(comment);
140
+ return match === null ? null : match[0];
141
+ }
142
+ function skipWhitespace(comment, offset) {
143
+ return matchAt(WS, comment, offset)?.length ?? 0;
144
+ }
145
+ function malformed(reason, offset) {
146
+ return { kind: "malformed", reason, offset };
147
+ }
148
+ function parseVantageDirective(comment) {
149
+ const sentinel = SENTINEL_PREFIX.exec(comment);
150
+ if (sentinel === null) return null;
151
+ let at = sentinel[0].length;
152
+ at += skipWhitespace(comment, at);
153
+ const nameOffset = at;
154
+ const name = matchAt(NAME, comment, at);
155
+ if (name === null) {
156
+ return malformed("no directive name after `vantage:`", at);
157
+ }
158
+ at += name.length;
159
+ const pairs = [];
160
+ while (at < comment.length) {
161
+ const gap = skipWhitespace(comment, at);
162
+ at += gap;
163
+ if (at >= comment.length) break;
164
+ if (gap === 0) {
165
+ return malformed(`\`${token(comment, at)}\` needs a space before it`, at);
166
+ }
167
+ const keyOffset = at;
168
+ const key = matchAt(NAME, comment, at);
169
+ if (key === null) {
170
+ return malformed(
171
+ `\`${token(comment, at)}\` is not a \`key=value\` pair`,
172
+ at
173
+ );
174
+ }
175
+ at += key.length;
176
+ if (comment[at] !== "=") {
177
+ return malformed(`\`${key}\` is not followed by \`=value\``, at);
178
+ }
179
+ at += 1;
180
+ const valueOffset = at;
181
+ const quoted = matchAt(QUOTED, comment, at);
182
+ if (quoted !== null) {
183
+ at += quoted.length;
184
+ pairs.push({
185
+ key,
186
+ value: quoted.slice(1, -1),
187
+ keyOffset,
188
+ valueOffset,
189
+ quoted: true
190
+ });
191
+ continue;
192
+ }
193
+ const unquoted = matchAt(UNQUOTED, comment, at);
194
+ if (unquoted === null) {
195
+ const found = token(comment, at);
196
+ return malformed(
197
+ found === "" ? `\`${key}=\` has no value` : `\`${found}\` is not a valid value for \`${key}\``,
198
+ at
199
+ );
200
+ }
201
+ at += unquoted.length;
202
+ pairs.push({ key, value: unquoted, keyOffset, valueOffset, quoted: false });
203
+ }
204
+ return { kind: "directive", name, nameOffset, pairs };
205
+ }
206
+
207
+ // src/rehypeVantageDirectives.ts
208
+ var STYLE_TARGET_TAGS = new Set(VANTAGE_STYLE_TARGETS);
209
+ var ANCHOR_TARGET_TAGS = new Set(VANTAGE_ANCHOR_TARGETS);
210
+ var HEADING_DEPTHS = /* @__PURE__ */ new Map([
211
+ ["h1", 1],
212
+ ["h2", 2],
213
+ ["h3", 3],
214
+ ["h4", 4],
215
+ ["h5", 5],
216
+ ["h6", 6]
217
+ ]);
218
+ var RANGE_PROPERTIES = /* @__PURE__ */ new Map([
219
+ ["tone", "dataVantageTone"],
220
+ ["emphasis", "dataVantageEmphasis"]
221
+ ]);
222
+ var POINT_PROPERTIES = /* @__PURE__ */ new Map([["badge", "dataVantageBadge"]]);
223
+ var RUN_PROPERTY = "dataVantageRun";
224
+ var OQ_PROPERTY = "dataVantageOq";
225
+ var LEANING_PROPERTY = "dataVantageLeaning";
226
+ var COLLAPSED_PROPERTY = "dataVantageCollapsed";
227
+ var COLLAPSE_GROUP_PROPERTY = "dataVantageCollapseGroup";
228
+ var COLLAPSE_TOGGLE_PROPERTY = "dataVantageCollapseToggle";
229
+ var MAX_LEANING = 500;
230
+ function isSkippable(node) {
231
+ if (node.type === "comment" || node.type === "doctype") return true;
232
+ if (node.type === "text") return node.value.trim() === "";
233
+ return false;
234
+ }
235
+ function headingDepth(node) {
236
+ if (node.type !== "element") return void 0;
237
+ return HEADING_DEPTHS.get(node.tagName);
238
+ }
239
+ function setProperty(element, property, value) {
240
+ element.properties = element.properties ?? {};
241
+ element.properties[property] = value;
242
+ }
243
+ function runValue(index, length) {
244
+ if (length === 1) return "only";
245
+ if (index === 0) return "start";
246
+ return index === length - 1 ? "end" : "middle";
247
+ }
248
+ function vocabularyOf(name, key) {
249
+ return DIRECTIVE_VOCABULARY[name]?.[key];
250
+ }
251
+ function accepts(name, key, value) {
252
+ const values = vocabularyOf(name, key);
253
+ if (values === void 0) return false;
254
+ return values === null || values.includes(value);
255
+ }
256
+ function styleRange(children, targetIndex, name) {
257
+ const range = [targetIndex];
258
+ const depth = name === "section" ? headingDepth(children[targetIndex]) : void 0;
259
+ if (depth === void 0) return range;
260
+ for (let i = targetIndex + 1; i < children.length; i++) {
261
+ const node = children[i];
262
+ const nodeDepth = headingDepth(node);
263
+ if (nodeDepth !== void 0 && nodeDepth <= depth) break;
264
+ if (node.type === "element" && STYLE_TARGET_TAGS.has(node.tagName)) {
265
+ range.push(i);
266
+ }
267
+ }
268
+ return range;
269
+ }
270
+ function collapsesSection(name, pairs, target) {
271
+ if (name !== "section") return false;
272
+ if (pairs.get("collapsed") !== "true") return false;
273
+ return headingDepth(target) !== void 0;
274
+ }
275
+ function stampStyle(children, targetIndex, name, pairs, state) {
276
+ const target = children[targetIndex];
277
+ if (!STYLE_TARGET_TAGS.has(target.tagName)) return;
278
+ const rangeStamps = [];
279
+ const targetStamps = [];
280
+ for (const [key, value] of pairs) {
281
+ if (!accepts(name, key, value)) continue;
282
+ const rangeProperty = RANGE_PROPERTIES.get(key);
283
+ if (rangeProperty !== void 0) {
284
+ rangeStamps.push([rangeProperty, value]);
285
+ continue;
286
+ }
287
+ const pointProperty = POINT_PROPERTIES.get(key);
288
+ if (pointProperty !== void 0) targetStamps.push([pointProperty, value]);
289
+ }
290
+ const collapses = collapsesSection(name, pairs, target);
291
+ if (rangeStamps.length === 0 && targetStamps.length === 0 && !collapses) {
292
+ return;
293
+ }
294
+ const range = styleRange(children, targetIndex, name);
295
+ const group = collapses && range.length > 1 ? String(state.nextGroup++) : void 0;
296
+ for (let i = 0; i < range.length; i++) {
297
+ const element = children[range[i]];
298
+ for (const [property, value] of rangeStamps) {
299
+ setProperty(element, property, value);
300
+ }
301
+ if (i === 0) {
302
+ for (const [property, value] of targetStamps) {
303
+ setProperty(element, property, value);
304
+ }
305
+ }
306
+ if (rangeStamps.length > 0) {
307
+ setProperty(element, RUN_PROPERTY, runValue(i, range.length));
308
+ }
309
+ if (group === void 0) continue;
310
+ if (i === 0) {
311
+ setProperty(element, COLLAPSE_TOGGLE_PROPERTY, group);
312
+ } else {
313
+ setProperty(element, COLLAPSED_PROPERTY, "true");
314
+ setProperty(element, COLLAPSE_GROUP_PROPERTY, group);
315
+ }
316
+ }
317
+ }
318
+ function stampOq(target, pairs) {
319
+ setProperty(target, OQ_PROPERTY, "true");
320
+ const leaning = pairs.get("leaning");
321
+ if (leaning === void 0) return;
322
+ const text = leaning.replace(/\s+/g, " ").trim().slice(0, MAX_LEANING);
323
+ if (text !== "") setProperty(target, LEANING_PROPERTY, text);
324
+ }
325
+ function stampRun(children, targetIndex, run, state) {
326
+ const target = children[targetIndex];
327
+ const style = /* @__PURE__ */ new Map();
328
+ const oq = /* @__PURE__ */ new Map();
329
+ let styleName;
330
+ let hasOq = false;
331
+ for (const directive of run) {
332
+ if (directive.name === "section" || directive.name === "block") {
333
+ styleName = directive.name;
334
+ for (const pair of directive.pairs) style.set(pair.key, pair.value);
335
+ } else if (directive.name === "oq") {
336
+ hasOq = true;
337
+ for (const pair of directive.pairs) oq.set(pair.key, pair.value);
338
+ }
339
+ }
340
+ if (styleName !== void 0) {
341
+ stampStyle(children, targetIndex, styleName, style, state);
342
+ }
343
+ if (hasOq && ANCHOR_TARGET_TAGS.has(target.tagName)) {
344
+ stampOq(target, oq);
345
+ }
346
+ }
347
+ function directiveOf(node) {
348
+ if (node.type !== "comment") return void 0;
349
+ const parsed = parseVantageDirective(node.value);
350
+ return parsed !== null && parsed.kind === "directive" ? parsed : void 0;
351
+ }
352
+ function processChildren(parent, state) {
353
+ const children = parent.children;
354
+ let i = 0;
355
+ while (i < children.length) {
356
+ const node = children[i];
357
+ if (node.type === "element") {
358
+ processChildren(node, state);
359
+ i++;
360
+ continue;
361
+ }
362
+ const first = directiveOf(node);
363
+ if (first === void 0) {
364
+ i++;
365
+ continue;
366
+ }
367
+ const run = [first];
368
+ let j = i + 1;
369
+ let targetIndex = -1;
370
+ for (; j < children.length; j++) {
371
+ const next = children[j];
372
+ if (next.type === "element") {
373
+ targetIndex = j;
374
+ break;
375
+ }
376
+ if (!isSkippable(next)) break;
377
+ const directive = directiveOf(next);
378
+ if (directive !== void 0) run.push(directive);
379
+ }
380
+ if (targetIndex >= 0) stampRun(children, targetIndex, run, state);
381
+ i = j;
382
+ }
383
+ }
384
+ var rehypeVantageDirectives = () => {
385
+ return (tree) => {
386
+ processChildren(tree, { nextGroup: 1 });
387
+ };
388
+ };
389
+ var rehypeVantageDirectives_default = rehypeVantageDirectives;
390
+
391
+ // src/rehypeVantageMathStamps.ts
392
+ var CARRIED_KEY = "vantageDisplayMathStamps";
393
+ function classNames(node) {
394
+ const value = node.properties?.className;
395
+ return Array.isArray(value) ? value.map(String) : [];
396
+ }
397
+ function isDisplayMath(node) {
398
+ if (node.type !== "element" || node.tagName !== "pre") return false;
399
+ return node.children.some(
400
+ (child) => child.type === "element" && child.tagName === "code" && classNames(child).includes("language-math")
401
+ );
402
+ }
403
+ function carriedProperties(properties) {
404
+ const carried = {};
405
+ for (const [key, value] of Object.entries(properties ?? {})) {
406
+ if (key === "dataSourceLine" || key.startsWith("dataVantage")) {
407
+ carried[key] = value;
408
+ }
409
+ }
410
+ return carried;
411
+ }
412
+ function collect(parent, out) {
413
+ const children = parent.children;
414
+ for (let i = 0; i < children.length; i++) {
415
+ const node = children[i];
416
+ if (node.type !== "element") continue;
417
+ if (isDisplayMath(node)) {
418
+ const properties = carriedProperties(node.properties);
419
+ if (Object.keys(properties).length > 0) {
420
+ out.push({
421
+ parent,
422
+ anchor: i === 0 ? void 0 : children[i - 1],
423
+ properties
424
+ });
425
+ }
426
+ continue;
427
+ }
428
+ collect(node, out);
429
+ }
430
+ }
431
+ function reapply(carried) {
432
+ for (const { parent, anchor, properties } of carried) {
433
+ const siblings = parent.children;
434
+ let index = 0;
435
+ if (anchor !== void 0) {
436
+ const at = siblings.indexOf(anchor);
437
+ if (at === -1) continue;
438
+ index = at + 1;
439
+ }
440
+ const replacement = siblings[index];
441
+ if (replacement === void 0 || replacement.type !== "element") continue;
442
+ if (!classNames(replacement).some((name) => name.startsWith("katex"))) {
443
+ continue;
444
+ }
445
+ replacement.properties ??= {};
446
+ for (const [key, value] of Object.entries(properties)) {
447
+ replacement.properties[key] ??= value;
448
+ }
449
+ }
450
+ }
451
+ var rehypeCaptureMathStamps = () => {
452
+ return (tree, file) => {
453
+ const carried = [];
454
+ collect(tree, carried);
455
+ file.data[CARRIED_KEY] = carried;
456
+ };
457
+ };
458
+ var rehypeRestoreMathStamps = () => {
459
+ return (_tree, file) => {
460
+ const carried = file.data[CARRIED_KEY];
461
+ delete file.data[CARRIED_KEY];
462
+ if (Array.isArray(carried)) reapply(carried);
463
+ };
464
+ };
465
+ var SAFE_STYLE_PROPERTIES = [
466
+ // Box metrics. `top`/`right`/`bottom`/`left` are inert now that `position` is
467
+ // banned, and they stay only because dropping them would fail the whole
468
+ // attribute for a document that writes one — the all-or-nothing rule below
469
+ // makes every removal a behaviour change. They buy an attacker nothing that
470
+ // negative `margin` does not already buy.
471
+ "height",
472
+ "min-height",
473
+ "max-height",
474
+ "width",
475
+ "min-width",
476
+ "max-width",
477
+ "top",
478
+ "bottom",
479
+ "left",
480
+ "right",
481
+ "margin",
482
+ "margin-top",
483
+ "margin-right",
484
+ "margin-bottom",
485
+ "margin-left",
486
+ "padding",
487
+ "padding-top",
488
+ "padding-right",
489
+ "padding-bottom",
490
+ "padding-left",
491
+ // Rules and boxes.
492
+ "border",
493
+ "border-style",
494
+ "border-color",
495
+ "border-width",
496
+ "border-top-width",
497
+ "border-right-width",
498
+ "border-bottom-width",
499
+ "border-left-width",
500
+ "border-top-style",
501
+ "border-right-style",
502
+ "border-bottom-style",
503
+ "border-left-style",
504
+ "border-top-color",
505
+ "border-right-color",
506
+ "border-bottom-color",
507
+ "border-left-color",
508
+ "border-radius",
509
+ // Typography.
510
+ "color",
511
+ "background-color",
512
+ "font",
513
+ "font-size",
514
+ "font-style",
515
+ "font-weight",
516
+ "font-family",
517
+ "font-variant",
518
+ "line-height",
519
+ "letter-spacing",
520
+ "word-spacing",
521
+ "text-align",
522
+ "text-decoration",
523
+ "text-indent",
524
+ "white-space",
525
+ "vertical-align",
526
+ "list-style-type",
527
+ // Flow.
528
+ "display",
529
+ "float",
530
+ "clear",
531
+ "opacity",
532
+ "overflow"
533
+ ];
534
+ var VALUE = `[^;:()"'\\\\]*`;
535
+ var DECLARATION = `(?:(?:${SAFE_STYLE_PROPERTIES.join("|")})\\s*:${VALUE})`;
536
+ var SAFE_STYLE = new RegExp(
537
+ `^\\s*(?:${DECLARATION};\\s*)*${DECLARATION}?$`,
538
+ "i"
539
+ );
540
+ var COLLAPSE_GROUP_ID = /^[0-9]+$/;
55
541
  var sanitizeSchema = {
56
542
  ...defaultSchema,
57
543
  tagNames: [
@@ -87,20 +573,94 @@ var sanitizeSchema = {
87
573
  "*": [
88
574
  ...defaultSchema.attributes?.["*"] || [],
89
575
  "className",
90
- "style",
91
- "dataSourceLine"
576
+ ["style", SAFE_STYLE],
577
+ "dataSourceLine",
578
+ // What `rehypeVantageDirectives` compiles a `<!-- vantage: … -->` comment
579
+ // into, named individually — never by a `data-vantage-*` wildcard, which
580
+ // would readmit whatever a future bug emits and whatever a document
581
+ // hand-writes as raw HTML.
582
+ //
583
+ // The value lists are the belt to the plugin's braces: the vocabulary is
584
+ // closed in the plugin *and* here, imported from the one module that
585
+ // defines it, so even if a refactor let an unvalidated value reach the
586
+ // tree the sanitiser still refuses it.
587
+ ["dataVantageTone", ...VANTAGE_TONES],
588
+ ["dataVantageEmphasis", ...VANTAGE_EMPHASIS],
589
+ ["dataVantageBadge", ...VANTAGE_BADGES],
590
+ ["dataVantageCollapsed", ...VANTAGE_COLLAPSED],
591
+ // The other half of `collapsed`: which group a hidden block belongs to,
592
+ // and which group a heading toggles. Both are plugin-minted counters with
593
+ // no vocabulary to allowlist, so they take a pattern instead —
594
+ // `hast-util-sanitize` accepts a `RegExp` in place of a literal value.
595
+ // A pattern rather than a bare name because the JS interpolates the value
596
+ // into a selector: anything but digits has no business reaching it.
597
+ ["dataVantageCollapseGroup", COLLAPSE_GROUP_ID],
598
+ ["dataVantageCollapseToggle", COLLAPSE_GROUP_ID],
599
+ ["dataVantageRun", ...VANTAGE_RUNS],
600
+ ["dataVantageOq", "true"],
601
+ // The design's one genuinely free-text value: the body of a review
602
+ // comment, so it cannot be value-allowlisted and this entry is name-only.
603
+ // Two defences remain rather than three — `hast` escapes the value on
604
+ // serialisation and React sets it through the DOM property path, so it
605
+ // cannot break out of the attribute — and the honest record of that is in
606
+ // the design doc rather than a third layer implied here.
607
+ "dataVantageLeaning"
92
608
  ],
93
609
  code: [...defaultSchema.attributes?.code || [], "className"],
94
- span: [...defaultSchema.attributes?.span || [], "className", "style"],
95
- div: [...defaultSchema.attributes?.div || [], "className", "style"],
610
+ span: [
611
+ ...defaultSchema.attributes?.span || [],
612
+ "className",
613
+ ["style", SAFE_STYLE]
614
+ ],
615
+ div: [
616
+ ...defaultSchema.attributes?.div || [],
617
+ "className",
618
+ ["style", SAFE_STYLE]
619
+ ],
96
620
  a: [...defaultSchema.attributes?.a || [], "id", "className"],
97
621
  math: ["xmlns"],
98
622
  annotation: ["encoding"],
99
623
  img: [...defaultSchema.attributes?.img || [], "loading"],
100
- td: [...defaultSchema.attributes?.td || [], "style"],
101
- th: [...defaultSchema.attributes?.th || [], "style"]
624
+ td: [...defaultSchema.attributes?.td || [], ["style", SAFE_STYLE]],
625
+ th: [...defaultSchema.attributes?.th || [], ["style", SAFE_STYLE]]
102
626
  }
103
627
  };
628
+
629
+ // src/pipeline.ts
630
+ function buildRemarkPlugins(options = {}) {
631
+ const { gfm = true, math = true } = options;
632
+ const plugins = [];
633
+ if (gfm) plugins.push([remarkGfm, { singleTilde: false }]);
634
+ if (math) plugins.push([remarkMath, { singleDollarTextMath: false }]);
635
+ return plugins;
636
+ }
637
+ function buildRehypePlugins(options = {}) {
638
+ const {
639
+ math = true,
640
+ highlight = true,
641
+ sourceLines = true,
642
+ sanitize = true,
643
+ bodyLineOffset = 0
644
+ } = options;
645
+ const plugins = [rehypeRaw];
646
+ if (sourceLines) {
647
+ plugins.push([rehypeSourceLines_default, { offset: bodyLineOffset }]);
648
+ }
649
+ plugins.push(rehypeVantageDirectives_default);
650
+ if (sanitize) plugins.push([rehypeSanitize, sanitizeSchema]);
651
+ plugins.push(rehypeSlug);
652
+ if (highlight) plugins.push(rehypeHighlight);
653
+ if (math) {
654
+ plugins.push(rehypeCaptureMathStamps, rehypeKatex, rehypeRestoreMathStamps);
655
+ }
656
+ return plugins;
657
+ }
658
+ function buildPipeline(options = {}) {
659
+ return {
660
+ remarkPlugins: buildRemarkPlugins(options),
661
+ rehypePlugins: buildRehypePlugins(options)
662
+ };
663
+ }
104
664
  function parseFrontmatter(content) {
105
665
  if (content.startsWith("+++")) {
106
666
  return parseFrontmatterWithDelimiter(content, "+++", "toml");
@@ -108,25 +668,75 @@ function parseFrontmatter(content) {
108
668
  if (content.startsWith("---")) {
109
669
  return parseFrontmatterWithDelimiter(content, "---", "yaml");
110
670
  }
111
- return { frontmatter: {}, body: content, format: "none" };
671
+ return withOffset(content, {
672
+ frontmatter: {},
673
+ body: content,
674
+ format: "none"
675
+ });
676
+ }
677
+ function withOffset(content, parsed) {
678
+ const stripped = content.slice(0, content.length - parsed.body.length);
679
+ let bodyLineOffset = 0;
680
+ for (const ch of stripped) {
681
+ if (ch === "\n") bodyLineOffset++;
682
+ }
683
+ return { ...parsed, bodyLineOffset };
112
684
  }
113
685
  function parseFrontmatterWithDelimiter(content, delimiter, format) {
114
686
  const searchStart = delimiter.length;
115
687
  const endIndex = content.indexOf(`
116
688
  ${delimiter}`, searchStart);
117
689
  if (endIndex === -1) {
118
- return { frontmatter: {}, body: content, format: "none" };
690
+ return withOffset(content, {
691
+ frontmatter: {},
692
+ body: content,
693
+ format: "none",
694
+ problem: { kind: "unterminated", delimiter }
695
+ });
119
696
  }
120
697
  const raw = content.slice(searchStart + 1, endIndex).trim();
121
698
  const bodyStart = endIndex + 1 + delimiter.length;
122
699
  const body = content.slice(bodyStart).replace(/^\n/, "");
123
700
  try {
124
- const frontmatter = format === "toml" ? parse(raw) : YAML.parse(raw);
125
- return { frontmatter: frontmatter || {}, body, format };
126
- } catch {
127
- return { frontmatter: {}, body: content, format: "none" };
701
+ const parsed = format === "toml" ? parse(raw) : YAML.parse(raw);
702
+ return withOffset(content, {
703
+ frontmatter: parsed || {},
704
+ body,
705
+ format,
706
+ ...isMapping(parsed) ? {} : { problem: { kind: "not-a-mapping", delimiter } }
707
+ });
708
+ } catch (error) {
709
+ return withOffset(content, {
710
+ frontmatter: {},
711
+ body: content,
712
+ format: "none",
713
+ problem: { kind: "invalid", delimiter, ...errorPosition(error) }
714
+ });
128
715
  }
129
716
  }
717
+ function isMapping(value) {
718
+ return value === null || value === void 0 || typeof value === "object" && !Array.isArray(value);
719
+ }
720
+ function errorPosition(error) {
721
+ const message = error instanceof Error ? error.message : String(error);
722
+ const source = error;
723
+ const yamlPosition = source?.linePos?.[0];
724
+ if (typeof yamlPosition?.line === "number") {
725
+ return {
726
+ message,
727
+ line: yamlPosition.line,
728
+ ...typeof yamlPosition.col === "number" ? { column: yamlPosition.col } : {}
729
+ };
730
+ }
731
+ if (typeof source?.line === "number") {
732
+ return {
733
+ message,
734
+ line: source.line,
735
+ ...typeof source.column === "number" ? { column: source.column } : {}
736
+ };
737
+ }
738
+ return { message };
739
+ }
130
740
 
131
741
  // src/renderMarkdown.ts
132
742
  async function renderMarkdown(content, options = {}) {
@@ -142,27 +752,22 @@ async function renderMarkdown(content, options = {}) {
142
752
  if (parseFm) {
143
753
  parsed = parseFrontmatter(content);
144
754
  } else {
145
- parsed = { frontmatter: {}, body: content, format: "none" };
146
- }
147
- const remarkPlugins = [];
148
- const rehypePlugins = [];
149
- if (gfm) remarkPlugins.push([remarkGfm, { singleTilde: false }]);
150
- if (math) remarkPlugins.push([remarkMath, { singleDollarTextMath: false }]);
151
- rehypePlugins.push([rehypeRaw]);
152
- if (sourceLines) rehypePlugins.push([rehypeSourceLines_default]);
153
- if (sanitize) rehypePlugins.push([rehypeSanitize, sanitizeSchema]);
154
- rehypePlugins.push([rehypeSlug]);
155
- if (highlight) rehypePlugins.push([rehypeHighlight]);
156
- if (math) rehypePlugins.push([rehypeKatex]);
157
- let processor = unified().use(remarkParse);
158
- for (const [plugin, ...args] of remarkPlugins) {
159
- processor = processor.use(plugin, ...args);
160
- }
161
- processor = processor.use(remarkRehype, { allowDangerousHtml: true });
162
- for (const [plugin, ...args] of rehypePlugins) {
163
- processor = processor.use(plugin, ...args);
164
- }
165
- processor = processor.use(rehypeStringify);
755
+ parsed = {
756
+ frontmatter: {},
757
+ body: content,
758
+ format: "none",
759
+ bodyLineOffset: 0
760
+ };
761
+ }
762
+ const { remarkPlugins, rehypePlugins } = buildPipeline({
763
+ gfm,
764
+ math,
765
+ highlight,
766
+ sourceLines,
767
+ sanitize,
768
+ bodyLineOffset: parsed.bodyLineOffset
769
+ });
770
+ const processor = unified().use(remarkParse).use(remarkPlugins).use(remarkRehype, { allowDangerousHtml: true }).use(rehypePlugins).use(rehypeStringify);
166
771
  const result = await processor.process(parsed.body);
167
772
  return {
168
773
  html: String(result),
@@ -171,8 +776,7 @@ async function renderMarkdown(content, options = {}) {
171
776
  };
172
777
  }
173
778
 
174
- // src/scrollToLineAnchor.ts
175
- var HIGHLIGHT_CLASS = "line-anchor-highlight";
779
+ // src/lineAnchor.ts
176
780
  function parseLineAnchor(hash) {
177
781
  if (!hash) return null;
178
782
  const frag = hash.startsWith("#") ? hash.slice(1) : hash;
@@ -182,6 +786,9 @@ function parseLineAnchor(hash) {
182
786
  const end = match[2] ? parseInt(match[2], 10) : start;
183
787
  return { start: Math.min(start, end), end: Math.max(start, end) };
184
788
  }
789
+
790
+ // src/scrollToLineAnchor.ts
791
+ var HIGHLIGHT_CLASS = "line-anchor-highlight";
185
792
  function clearLineAnchorHighlights(container) {
186
793
  container.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((node) => {
187
794
  node.classList.remove(HIGHLIGHT_CLASS);
@@ -194,10 +801,7 @@ function scrollToLineAnchor(container, hash) {
194
801
  const blocks = container.querySelectorAll("[data-source-line]");
195
802
  let firstMatch = null;
196
803
  for (const block of blocks) {
197
- const line = parseInt(
198
- block.dataset.sourceLine || "0",
199
- 10
200
- );
804
+ const line = parseInt(block.dataset.sourceLine || "0", 10);
201
805
  if (line >= range.start && line <= range.end) {
202
806
  block.classList.add(HIGHLIGHT_CLASS);
203
807
  if (!firstMatch) firstMatch = block;
@@ -244,6 +848,74 @@ function findScrollParent(el) {
244
848
  return null;
245
849
  }
246
850
 
851
+ // src/vantageFrontmatter.ts
852
+ var DOC_STATUSES = [
853
+ "draft",
854
+ "in-review",
855
+ "accepted",
856
+ "deprecated"
857
+ ];
858
+ var VANTAGE_FRONTMATTER_KEYS = ["status-chip"];
859
+ var DOC_STATUS_TONES = {
860
+ draft: "muted",
861
+ "in-review": "warning",
862
+ accepted: "tip",
863
+ deprecated: "caution"
864
+ };
865
+ var STATUS_CHIP_VALUES = [
866
+ ...DOC_STATUSES,
867
+ "true",
868
+ "false"
869
+ ];
870
+ function isDocStatus(value) {
871
+ return typeof value === "string" && DOC_STATUSES.includes(value);
872
+ }
873
+ function isTable(value) {
874
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
875
+ }
876
+ function readVantageFrontmatter(frontmatter) {
877
+ const issues = [];
878
+ if (!Object.hasOwn(frontmatter, "vantage")) return { issues };
879
+ const value = frontmatter["vantage"];
880
+ if (!isTable(value)) {
881
+ issues.push({ kind: "not-a-table", value });
882
+ return { issues };
883
+ }
884
+ let statusChip;
885
+ for (const key of Object.keys(value)) {
886
+ if (!VANTAGE_FRONTMATTER_KEYS.includes(key)) {
887
+ issues.push({ kind: "unknown-key", key });
888
+ continue;
889
+ }
890
+ if (key === "status-chip") {
891
+ statusChip = readStatusChip(frontmatter, value[key], issues);
892
+ }
893
+ }
894
+ return { ...statusChip === void 0 ? {} : { statusChip }, issues };
895
+ }
896
+ function readStatusChip(frontmatter, raw, issues) {
897
+ const status = frontmatter["status"];
898
+ if (raw === false) return void 0;
899
+ if (raw === true) {
900
+ if (isDocStatus(status)) return status;
901
+ issues.push({ kind: "status-chip-orphan", status });
902
+ return void 0;
903
+ }
904
+ if (isDocStatus(raw)) {
905
+ if (isDocStatus(status) && status !== raw) {
906
+ issues.push({ kind: "status-chip-disagrees", chip: raw, status });
907
+ }
908
+ return raw;
909
+ }
910
+ issues.push({
911
+ kind: "bad-value",
912
+ key: "status-chip",
913
+ value: raw,
914
+ legal: STATUS_CHIP_VALUES
915
+ });
916
+ return void 0;
917
+ }
918
+
247
919
  // src/mermaidCache.ts
248
920
  var svgCache = /* @__PURE__ */ new Map();
249
921
 
@@ -341,6 +1013,127 @@ function resolveLinks(html, options = {}) {
341
1013
  );
342
1014
  }
343
1015
 
344
- export { clearLineAnchorHighlights, parseFrontmatter, parseLineAnchor, rehypeSourceLines_default as rehypeSourceLines, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor };
1016
+ // src/styleGuide.ts
1017
+ var STYLE_GUIDE = `## Markdown style guide (for Vantage viewer)
1018
+
1019
+ When writing or updating markdown documents that will be viewed in Vantage, follow these conventions:
1020
+
1021
+ ### Structure
1022
+ - Use headings (## and ###) to organize content \u2014 they become navigable outline anchors.
1023
+ - Keep paragraphs focused and concise. Break up dense text with subheadings, lists, or tables.
1024
+
1025
+ ### Links and cross-references
1026
+ - **Relative paths only**: Always link relative to the *current file's directory*:
1027
+ - Sibling in same folder: \`[Other Doc](./other-doc.md)\` or \`[Other Doc](other-doc.md)\`
1028
+ - Subdirectory: \`[Design Doc](./design/auth.md)\`
1029
+ - Parent / sibling folder: \`[Overview](../overview.md)\` or \`[Spec](../specs/api.md)\`
1030
+ - **Never use leading slashes**:
1031
+ - \u274C \`[Doc](/docs/guide.md)\` (breaks web routing and multi-repo scoping)
1032
+ - \u2705 \`[Doc](../docs/guide.md)\` or \`[Doc](./guide.md)\`
1033
+ - **Never use absolute filesystem paths or URI schemes**:
1034
+ - \u274C \`file:///workspace/docs/guide.md\`, \`/workspace/docs/guide.md\`, \`C:\\...\`
1035
+ - \u2705 \`[Doc](./guide.md)\` or \`[Doc](../guide.md)\`
1036
+ - **Always include the file extension**: Use \`.md\`, \`.ts\`, \`.go\`, etc. (e.g. \`[Model](model.go)\`).
1037
+ - **Line anchors and ranges**:
1038
+ - Link to specific lines: \`[Handler](../server/api.go#L42)\` or \`[Range](../server/api.go#L42-L58)\`
1039
+ - Same-file line anchor: \`[See lines](#L10-L25)\`
1040
+ - Vantage scrolls to and highlights the target lines.
1041
+ - **Section anchors**:
1042
+ - Same doc: \`[Usage](#usage)\`
1043
+ - Cross-doc: \`[Architecture](../overview.md#system-architecture)\`
1044
+ - Anchor slugs are lowercase, hyphenated, and punctuation-stripped.
1045
+ - **Backticks in links**: Place backticks inside the link label, not around the markdown link syntax:
1046
+ - \u2705 \`[\`config.json\`](./config.json)\` or \`[config.json](./config.json)\`
1047
+ - \u274C \`\`[config.json](./config.json)\`\`
1048
+
1049
+ ### Frontmatter (Metadata)
1050
+ - Include structured metadata at the very top of docs delimited by \`---\` (YAML) or \`+++\` (TOML). Vantage renders this as a metadata card:
1051
+ \`\`\`yaml
1052
+ ---
1053
+ title: "Feature Specification"
1054
+ author: "Agent"
1055
+ date: 2026-08-15
1056
+ status: in-review # draft | in-review | accepted | deprecated
1057
+ tags: [architecture, backend, api]
1058
+ summary: "Brief description of the document purpose."
1059
+ vantage:
1060
+ status-chip: true # show \`status\` as a chip above the metadata card
1061
+ ---
1062
+ \`\`\`
1063
+ - **Nothing may sit above the opening delimiter** \u2014 not a blank line, not an editorial comment, not a \`<!-- vantage: \u2026 -->\` directive. Frontmatter is recognised only at the very first byte of the file (in Vantage, on GitHub, and in every other reader), so one line above it turns the whole block into body text: a horizontal rule followed by a heading made of the raw keys, with every field lost. \`vantage-check\` reports it as \`frontmatter/not-at-top\`.
1064
+ - **\`vantage:\` is Vantage's own reserved key.** It holds chrome that belongs to the file rather than to a section, it never shows up in the metadata card, and every other renderer ignores it. One key today: \`status-chip\`.
1065
+ - **Prefer \`status-chip: true\`**, which shows the document's own \`status:\` and therefore cannot disagree with it. A literal \`status-chip: accepted\` is accepted too, but it is a second value that goes stale on its own \u2014 \`vantage-check\` reports the disagreement.
1066
+ - The chip's vocabulary is \`status\`'s, exactly: \`draft | in-review | accepted | deprecated\`, lowercase. \`Draft\` renders no chip at all, silently.
1067
+
1068
+ ### Mermaid diagrams
1069
+ - Use \`\`\`mermaid code blocks for flowcharts, sequence diagrams, and architecture diagrams. Vantage provides interactive zoom, pan, dark/light theme adaptation, and SVG export.
1070
+ - **Quote labels with special characters**: Always quote node labels containing parentheses, brackets, or colons to prevent syntax errors:
1071
+ \`\`\`mermaid
1072
+ flowchart TD
1073
+ client["Client (React SPA)"] -->|WebSocket| srv["Vantage Server (Go)"]
1074
+ srv --> git["Git CLI (git diff)"]
1075
+ \`\`\`
1076
+
1077
+ ### Code blocks and diffs
1078
+ - Always tag fenced code blocks with language identifiers (\`ts\`, \`go\`, \`python\`, \`bash\`, \`json\`, \`yaml\`, \`diff\`, \`sql\`, etc.) for syntax highlighting.
1079
+ - For proposed code modifications, use \`\`\`diff blocks with \`+\` and \`-\` prefixes:
1080
+ \`\`\`diff
1081
+ -const oldUrl = "/api/v1";
1082
+ +const newUrl = "/api/v2";
1083
+ \`\`\`
1084
+
1085
+ ### Callouts and alerts
1086
+ - Use GitHub-style blockquote callouts for notes, tips, and warnings:
1087
+ > [!NOTE]
1088
+ > Background context or helpful explanation.
1089
+
1090
+ > [!TIP]
1091
+ > Best practice advice or optimization suggestions.
1092
+
1093
+ > [!IMPORTANT]
1094
+ > Key requirements or crucial information.
1095
+
1096
+ > [!WARNING]
1097
+ > Urgent caution, breaking changes, or potential pitfalls.
1098
+
1099
+ > [!CAUTION]
1100
+ > High-risk actions that could cause data loss or security issues.
1101
+
1102
+ ### Vantage directives (optional, and Vantage-only)
1103
+
1104
+ Vantage reads a few styling hints from ordinary HTML comments. Every other renderer \u2014 GitHub included \u2014 drops them, so a document has to read exactly the same without them: directives decorate, they never carry meaning. One goes on a line of its own, with a blank line after it, and applies to the block that follows:
1105
+
1106
+ \`\`\`markdown
1107
+ <!-- vantage: section tone=warning badge=stale -->
1108
+
1109
+ ## Migration path
1110
+
1111
+ The steps below predate the rewrite.
1112
+ \`\`\`
1113
+
1114
+ - **Three names**: \`section\` (the heading and everything under it), \`block\` (the one block after it), \`oq\` (one answerable Open Question).
1115
+ - **The keys and values are a closed set**: \`tone\` = \`note | tip | important | warning | caution | muted\`; \`emphasis\` = \`strong | normal | quiet\`; \`badge\` = \`draft | stale | blocked | done | wip\`; \`collapsed\` = \`true | false\`. Name a *tone*, never a colour \u2014 the theme decides what a warning looks like, in light mode, in dark mode, and in print.
1116
+ - **Use them sparingly.** One or two per document, on the sections that genuinely differ. A document where everything is toned says nothing, and a rainbow one is harder to read than a plain one.
1117
+ - **Anything outside those sets is silently ignored** \u2014 nothing breaks, and nothing styles either. Run \`vantage-check\` on the document: the \`vantage/*\` rules are the only thing that will ever tell you a directive did nothing.
1118
+ - **Always close the comment with \`-->\`.** Never \`--!>\`, and never leave it open: Markdown reads every line below an unclosed \`<!--\` as part of the comment, and the whole rest of the document vanishes from the page. For the same reason \`-->\` cannot appear *inside* a value \u2014 it ends the comment early and spills the remainder into the page as literal text.
1119
+ - **In a list, indent the directive inside the item**, with blank lines around it (below). At the start of a line between two items it ends the list and starts a second one, which changes the numbering and the spacing in every renderer \u2014 the one thing a directive must never do.
1120
+ - **A \`leaning\` restates the leaning; it is never "yes".** The one-click button in review mode files that text as a review comment, and the comment is all the agent reading it has \u2014 nobody remembers which button was clicked. \`leaning="Yes"\` beside a two-branch question is a support ticket.
1121
+
1122
+ \`\`\`markdown
1123
+ 1. **OQ-9: Queue position on re-entry.**
1124
+
1125
+ <!-- vantage: oq id=OQ-9 leaning="Back of the queue \u2014 the fix might interact with what merged while it was out." -->
1126
+
1127
+ _Leaning:_ Back of the queue.
1128
+ \`\`\`
1129
+
1130
+ ### Tables, task lists, and math
1131
+ - **Tables**: Use standard markdown tables for structured comparisons and schemas.
1132
+ - **Task lists**: Use \`- [ ]\` and \`- [x]\` for actionable checklists and status tracking.
1133
+ - **LaTeX Math**: Use \`$$...$$\` for *all* KaTeX math \u2014 display blocks (\`$$\` alone on its own lines) and inline alike (\`$$E = mc^2$$\` mid-sentence).
1134
+ - Single dollars are **not** math delimiters: \`$HOME\` and \`$100\` stay literal, so prose and shell snippets are safe to write as-is.
1135
+ `;
1136
+
1137
+ export { DIRECTIVE_NAMES, DIRECTIVE_VOCABULARY, DOC_STATUSES, DOC_STATUS_TONES, SAFE_STYLE, STYLE_GUIDE, VANTAGE_BADGES, VANTAGE_COLLAPSED, VANTAGE_EMPHASIS, VANTAGE_FRONTMATTER_KEYS, VANTAGE_OQ_HOST_TARGETS, VANTAGE_RUNS, VANTAGE_SENTINEL, VANTAGE_TONES, buildPipeline, buildRemarkPlugins, clearLineAnchorHighlights, hasVantageSentinel, isDocStatus, parseFrontmatter, parseLineAnchor, parseVantageDirective, readVantageFrontmatter, rehypeSourceLines_default as rehypeSourceLines, rehypeVantageDirectives_default as rehypeVantageDirectives, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor };
345
1138
  //# sourceMappingURL=index.js.map
346
1139
  //# sourceMappingURL=index.js.map