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