@orkestrel/markdown 0.0.11 → 0.0.12

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.
@@ -451,94 +451,146 @@ var isMarkdownDocument = recordOf({
451
451
  *
452
452
  * @param lines - The markdown lines to parse.
453
453
  * @param depth - The current recursion depth (blockquotes/lists increment it).
454
+ * @param spans - The optional operation-owned node span recorder.
455
+ * @param end - The original-source end of this line run, including a removed terminator.
454
456
  * @returns The parsed block nodes.
455
457
  *
456
458
  * @example
457
459
  * ```ts
458
- * parseBlocks(['# Hi'], 0) // [{ element: 'heading', level: 1, children: [...] }]
460
+ * parseBlocks(splitLines('# Hi'), 0) // [{ element: 'heading', level: 1, children: [...] }]
459
461
  * ```
460
462
  */
461
- function parseBlocks(lines, depth) {
462
- if (depth >= 64) return lines.length > 0 ? [{
463
- element: "paragraph",
464
- children: [{
463
+ function parseBlocks(lines, depth, spans = /* @__PURE__ */ new Map(), end) {
464
+ const text = lines.map((line) => line.text);
465
+ if (depth >= 64) {
466
+ if (lines.length === 0) return [];
467
+ const source = joinSources(lines, "\n");
468
+ const inline = {
465
469
  element: "text",
466
- value: lines.join("\n")
467
- }]
468
- }] : [];
470
+ value: source.text
471
+ };
472
+ const paragraph = {
473
+ element: "paragraph",
474
+ children: [inline]
475
+ };
476
+ const span = projectSpan(source, 0, source.text.length);
477
+ if (span !== void 0) {
478
+ spans.set(inline, span);
479
+ spans.set(paragraph, span);
480
+ }
481
+ return [paragraph];
482
+ }
469
483
  const blocks = [];
470
484
  let index = 0;
471
485
  while (index < lines.length) {
472
- const line = lines[index] ?? "";
486
+ const line = text[index] ?? "";
473
487
  if (isBlankLine(line)) {
474
488
  index += 1;
475
489
  continue;
476
490
  }
477
491
  const fence = extractFence(line);
478
492
  if (fence) {
493
+ const start = index;
479
494
  const body = [];
495
+ let closed = false;
480
496
  index += 1;
481
- while (index < lines.length && !isFenceClose(lines[index] ?? "", fence.marker)) {
482
- body.push(lines[index] ?? "");
497
+ while (index < lines.length && !isFenceClose(text[index] ?? "", fence.marker)) {
498
+ const bodyLine = lines[index];
499
+ if (bodyLine !== void 0) body.push(bodyLine);
483
500
  index += 1;
484
501
  }
485
- index += 1;
486
- blocks.push({
502
+ if (index < lines.length) {
503
+ closed = true;
504
+ index += 1;
505
+ }
506
+ const node = {
487
507
  element: "codeBlock",
488
508
  ...fence.lang === void 0 ? {} : { lang: fence.lang },
489
- code: body.join("\n")
490
- });
509
+ code: joinSources(body, "\n").text
510
+ };
511
+ const source = joinSources(lines.slice(start, index), "\n");
512
+ const span = projectSpan(source, 0, source.text.length);
513
+ if (span !== void 0) spans.set(node, !closed && end !== void 0 ? {
514
+ start: span.start,
515
+ end
516
+ } : span);
517
+ blocks.push(node);
491
518
  continue;
492
519
  }
493
520
  if (isThematicBreak(line)) {
494
- blocks.push({ element: "thematicBreak" });
521
+ const node = { element: "thematicBreak" };
522
+ const source = lines[index];
523
+ const span = source === void 0 ? void 0 : projectSpan(source, 0, source.text.length);
524
+ if (span !== void 0) spans.set(node, span);
525
+ blocks.push(node);
495
526
  index += 1;
496
527
  continue;
497
528
  }
498
529
  const heading = extractHeading(line);
499
530
  if (heading) {
500
- blocks.push({
531
+ const source = lines[index];
532
+ const content = source === void 0 ? {
533
+ text: heading.text,
534
+ segments: []
535
+ } : sliceSource(source, heading.offset, heading.offset + heading.text.length);
536
+ const node = {
501
537
  element: "heading",
502
538
  level: heading.level,
503
- children: parseInline(heading.text)
504
- });
539
+ children: coalesceText(scanInlineSource(content, 0, content.text.length, spans), spans)
540
+ };
541
+ const span = source === void 0 ? void 0 : projectSpan(source, 0, source.text.length);
542
+ if (span !== void 0) spans.set(node, span);
543
+ blocks.push(node);
505
544
  index += 1;
506
545
  continue;
507
546
  }
508
547
  if (isQuote(line)) {
548
+ const start = index;
509
549
  const quoted = [];
510
- while (index < lines.length && isQuote(lines[index] ?? "")) {
511
- quoted.push(stripQuote(lines[index] ?? ""));
550
+ while (index < lines.length && isQuote(text[index] ?? "")) {
551
+ const quotedLine = lines[index];
552
+ if (quotedLine === void 0) break;
553
+ quoted.push(stripQuote(quotedLine));
512
554
  index += 1;
513
555
  }
514
- blocks.push({
556
+ const source = joinSources(lines.slice(start, index), "\n");
557
+ const span = projectSpan(source, 0, source.text.length);
558
+ const node = {
515
559
  element: "blockquote",
516
- children: parseBlocks(quoted, depth + 1)
517
- });
560
+ children: parseBlocks(quoted, depth + 1, spans, index === lines.length && end !== void 0 ? end : span?.end)
561
+ };
562
+ if (span !== void 0) spans.set(node, span);
563
+ blocks.push(node);
518
564
  continue;
519
565
  }
520
- if (isTableStart(line, lines[index + 1])) {
521
- const table = collectTable(lines, index);
566
+ if (isTableStart(line, text[index + 1])) {
567
+ const table = collectTable(lines, index, spans);
522
568
  blocks.push(table.node);
523
569
  index = table.next;
524
570
  continue;
525
571
  }
526
572
  if (extractListItem(line)) {
527
- const list = collectList(lines, index, depth);
573
+ const list = collectList(lines, index, depth, spans, end);
528
574
  blocks.push(list.node);
529
575
  index = list.next;
530
576
  continue;
531
577
  }
578
+ const start = index;
532
579
  const paragraph = [];
533
- while (index < lines.length && !isBlankLine(lines[index] ?? "") && !(isNonEmptyArray(paragraph) && startsBlock(lines, index))) {
534
- paragraph.push(lines[index] ?? "");
580
+ while (index < lines.length && !isBlankLine(text[index] ?? "") && !(isNonEmptyArray(paragraph) && startsBlock(text, index))) {
581
+ const paragraphLine = lines[index];
582
+ if (paragraphLine !== void 0) paragraph.push(paragraphLine);
535
583
  index += 1;
536
584
  }
537
- const source = paragraph.map((paragraphLine, position) => position < paragraph.length - 1 && paragraphLine.endsWith(" ") ? `${paragraphLine.trim()} ` : paragraphLine.trim()).join("\n");
538
- blocks.push({
585
+ const source = joinSources(paragraph.map((paragraphLine, position) => normalizeParagraphLine(paragraphLine, position < paragraph.length - 1)), "\n");
586
+ const node = {
539
587
  element: "paragraph",
540
- children: parseInline(source)
541
- });
588
+ children: coalesceText(scanInlineSource(source, 0, source.text.length, spans), spans)
589
+ };
590
+ const region = joinSources(lines.slice(start, index), "\n");
591
+ const span = projectSpan(region, 0, region.text.length);
592
+ if (span !== void 0) spans.set(node, span);
593
+ blocks.push(node);
542
594
  }
543
595
  return blocks;
544
596
  }
@@ -550,10 +602,26 @@ function parseBlocks(lines, depth) {
550
602
  * @returns The parsed document.
551
603
  */
552
604
  function parseDocument(markdown) {
553
- return {
605
+ const [document] = parseProvenance(markdown);
606
+ return document;
607
+ }
608
+ /**
609
+ * Parses a markdown string into a document and its original-source spans.
610
+ *
611
+ * @param markdown - The markdown source to parse.
612
+ * @returns The parsed document and its node-identity span map.
613
+ */
614
+ function parseProvenance(markdown) {
615
+ const spans = /* @__PURE__ */ new Map();
616
+ const document = {
554
617
  element: "document",
555
- children: parseBlocks(splitLines(markdown), 0)
618
+ children: parseBlocks(splitLines(markdown), 0, spans, markdown.length)
556
619
  };
620
+ spans.set(document, {
621
+ start: 0,
622
+ end: markdown.length
623
+ });
624
+ return [document, spans];
557
625
  }
558
626
  /**
559
627
  * Parses inline markdown text (emphasis, code spans, links, images, and hard
@@ -573,12 +641,25 @@ function parseInline(text) {
573
641
  * streaming operations {@link MarkdownInterface} declares.
574
642
  *
575
643
  * @remarks
576
- * - **Construction.** Given a `string`, the constructor runs {@link parseDocument} (the
577
- * block phase then the inline phase) to build the AST. Given a {@link MarkdownDocument},
578
- * the document is adopted AS-IS and is NOT re-validated - a caller adopting an
579
- * untrusted value should gate it with `isMarkdownDocument` first.
644
+ * - **Construction.** Given a `string`, the constructor runs {@link parseProvenance} (the
645
+ * block phase then the inline phase) once, keeping the AST and a COPY of the span map
646
+ * that parse recorded. Given a {@link MarkdownDocument}, the document is adopted AS-IS
647
+ * and is NOT re-validated - gate an untrusted value with `isMarkdownDocument` first.
648
+ * - **Provenance.** {@link span} reads the region of the ORIGINAL constructor string a
649
+ * node was produced from, and it is handle-relative: a string-constructed handle exposes
650
+ * the regions of the nodes it parsed, an adopted document exposes none, and a node from
651
+ * another handle reports `undefined` here whatever that handle reports. Each call
652
+ * returns a fresh value. A node reports the region THIS handle holds for its identity,
653
+ * else the region of the direct input a rewrite named for it, else `undefined`: a text
654
+ * run the parse joined from adjacent scanner output reports the region enclosing its
655
+ * parts, and only a rewrite output that holds no region of its own and was assembled
656
+ * from separate source nodes reports `undefined`.
657
+ * {@link map} carries provenance across the rewrite: an unchanged node keeps its
658
+ * region, a one-source replacement takes the region of the node it replaced, and a
659
+ * rebuilt parent takes its original's.
580
660
  * - **Immutable.** {@link map} never mutates the stored AST - it returns a NEW `Markdown`
581
- * instance; the document root invariant (`element: 'document'`) always holds.
661
+ * instance; the document root invariant (`element: 'document'`) always holds. An
662
+ * identity rewrite still returns a new handle, over the same document tree.
582
663
  * - **Traversal order.** {@link walk} and the `find` / `filter` / `reduce` queries built
583
664
  * on it walk the AST depth-first, pre-order, root-inclusive (via {@link walkNodes});
584
665
  * `stream` is shallow - only the document's direct block children.
@@ -597,14 +678,46 @@ function parseInline(text) {
597
678
  */
598
679
  var Markdown = class Markdown {
599
680
  #document;
681
+ #spans;
600
682
  constructor(input) {
601
- this.#document = typeof input === "string" ? parseDocument(input) : input;
683
+ if (typeof input === "string") {
684
+ const [document, spans] = parseProvenance(input);
685
+ this.#document = document;
686
+ this.#spans = new Map(spans);
687
+ } else {
688
+ this.#document = input;
689
+ this.#spans = /* @__PURE__ */ new Map();
690
+ }
602
691
  }
603
692
  /** The stored {@link MarkdownDocument} AST root. */
604
693
  get document() {
605
694
  return this.#document;
606
695
  }
607
696
  /**
697
+ * Reads the region of the original markdown string a node of this handle's tree was
698
+ * produced from.
699
+ *
700
+ * @param node - The node whose provenance to read
701
+ * @returns A fresh {@link MarkdownSpan}, or `undefined` when this handle holds no
702
+ * region for the node
703
+ *
704
+ * @example
705
+ * ```ts
706
+ * const source = '# Title\n\npara'
707
+ * const markdown = new Markdown(source)
708
+ * const heading = markdown.find(isHeadingNode)
709
+ * const span = heading && markdown.span(heading)
710
+ * span && source.slice(span.start, span.end) // '# Title'
711
+ * ```
712
+ */
713
+ span(node) {
714
+ const span = this.#spans.get(node);
715
+ return span === void 0 ? void 0 : {
716
+ start: span.start,
717
+ end: span.end
718
+ };
719
+ }
720
+ /**
608
721
  * THE deep traversal - a lazy, depth-first, pre-order, root-inclusive generator
609
722
  * over every {@link MarkdownNode} in the document. `find` / `filter` / `reduce`
610
723
  * all iterate this single traversal.
@@ -632,9 +745,18 @@ var Markdown = class Markdown {
632
745
  for (const node of this.walk()) if (predicate(node)) out.push(node);
633
746
  return out;
634
747
  }
635
- /** Rewrites the AST bottom-up (copy-on-write) and returns a new {@link Markdown}. */
748
+ /**
749
+ * Rewrites the AST bottom-up (copy-on-write) and returns a new {@link Markdown},
750
+ * carrying each output node's provenance across the rewrite. A rewrite that returns
751
+ * its node unchanged shares that subtree instead of copying it, so an identity
752
+ * rewrite copies no node and still returns a new handle.
753
+ *
754
+ * @param rewrite - The bottom-up node rewrite
755
+ * @returns A new handle over the rewritten document
756
+ */
636
757
  map(rewrite) {
637
- return new Markdown(rewriteDocument(this.#document, rewrite));
758
+ const [document, derivations] = rewriteDocument(this.#document, rewrite);
759
+ return this.#derive(document, derivations);
638
760
  }
639
761
  /** Folds the AST depth-first, pre-order into an accumulator. */
640
762
  reduce(callback, initial) {
@@ -683,6 +805,21 @@ var Markdown = class Markdown {
683
805
  } else controller.close();
684
806
  } });
685
807
  }
808
+ #derive(document, derivations) {
809
+ const derived = new Markdown(document);
810
+ for (const node of walkNodes(document)) {
811
+ const own = this.#spans.get(node);
812
+ if (own !== void 0) {
813
+ derived.#spans.set(node, own);
814
+ continue;
815
+ }
816
+ const source = derivations.get(node);
817
+ if (source === void 0) continue;
818
+ const span = this.#spans.get(source);
819
+ if (span !== void 0) derived.#spans.set(node, span);
820
+ }
821
+ return derived;
822
+ }
686
823
  };
687
824
  //#endregion
688
825
  //#region src/core/shapers.ts
@@ -957,25 +1094,220 @@ function createThematicBreakContract() {
957
1094
  //#endregion
958
1095
  //#region src/core/helpers.ts
959
1096
  /**
960
- * Normalize line endings to `\n` and split a markdown document into its lines - CRLF
961
- * (`\r\n`) and bare CR (`\r`) both collapse to `\n` first, so a Windows-origin
962
- * document parses identically. A single trailing newline does not yield a final
963
- * empty line.
1097
+ * Splits a markdown document into offset-bearing lines while normalizing CRLF and
1098
+ * bare CR terminators at the line boundary. A single trailing terminator does not
1099
+ * yield a final empty line.
964
1100
  *
965
1101
  * @param markdown - The raw markdown source
966
- * @returns The document's lines, line-terminators stripped
1102
+ * @returns The document's lines with their original-string coordinates
967
1103
  *
968
1104
  * @example
969
1105
  * ```ts
970
- * splitLines('a\r\nb\nc') // ['a', 'b', 'c']
1106
+ * splitLines('a\r\nb') // [{ text: 'a', segments: [{ offset: 0, start: 0, end: 1 }] }, ...]
971
1107
  * ```
972
1108
  */
973
1109
  function splitLines(markdown) {
974
- const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
975
- if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1110
+ const lines = [];
1111
+ let start = 0;
1112
+ let index = 0;
1113
+ while (index < markdown.length) {
1114
+ const character = markdown[index];
1115
+ if (character !== "\r" && character !== "\n") {
1116
+ index += 1;
1117
+ continue;
1118
+ }
1119
+ lines.push({
1120
+ text: markdown.slice(start, index),
1121
+ segments: [{
1122
+ offset: 0,
1123
+ start,
1124
+ end: index
1125
+ }]
1126
+ });
1127
+ index += character === "\r" && markdown[index + 1] === "\n" ? 2 : 1;
1128
+ start = index;
1129
+ }
1130
+ lines.push({
1131
+ text: markdown.slice(start),
1132
+ segments: [{
1133
+ offset: 0,
1134
+ start,
1135
+ end: markdown.length
1136
+ }]
1137
+ });
1138
+ if (lines.length > 1 && lines[lines.length - 1]?.text === "") lines.pop();
976
1139
  return lines;
977
1140
  }
978
1141
  /**
1142
+ * Slices derived markdown text and narrows each intersecting source segment to the
1143
+ * same text-relative range.
1144
+ *
1145
+ * @param source - The offset-bearing source to slice
1146
+ * @param from - The inclusive text offset
1147
+ * @param to - The exclusive text offset
1148
+ * @returns The sliced text and its narrowed original-string segments
1149
+ *
1150
+ * @example
1151
+ * ```ts
1152
+ * sliceSource({ text: 'abc', segments: [{ offset: 0, start: 4, end: 7 }] }, 1, 3)
1153
+ * // { text: 'bc', segments: [{ offset: 0, start: 5, end: 7 }] }
1154
+ * ```
1155
+ */
1156
+ function sliceSource(source, from, to) {
1157
+ const start = Math.max(0, Math.min(from, source.text.length));
1158
+ const end = Math.max(start, Math.min(to, source.text.length));
1159
+ const segments = [];
1160
+ for (let index = 0; index < source.segments.length; index += 1) {
1161
+ const segment = source.segments[index];
1162
+ if (segment === void 0) continue;
1163
+ const next = source.segments[index + 1];
1164
+ const limit = Math.min(segment.offset + (segment.end - segment.start), next === void 0 ? source.text.length : next.offset);
1165
+ const overlapStart = Math.max(start, segment.offset);
1166
+ const overlapEnd = Math.min(end, limit);
1167
+ const empty = segment.offset === limit && overlapStart === segment.offset;
1168
+ if (overlapStart >= overlapEnd && !empty) continue;
1169
+ const originalStart = overlapStart === limit ? segment.end : Math.min(segment.end, segment.start + overlapStart - segment.offset);
1170
+ const originalEnd = overlapEnd === limit ? segment.end : Math.min(segment.end, segment.start + overlapEnd - segment.offset);
1171
+ segments.push({
1172
+ offset: overlapStart - start,
1173
+ start: originalStart,
1174
+ end: originalEnd
1175
+ });
1176
+ }
1177
+ return {
1178
+ text: source.text.slice(start, end),
1179
+ segments
1180
+ };
1181
+ }
1182
+ /**
1183
+ * Joins offset-bearing markdown sources while mapping a separator to the original
1184
+ * region between adjacent mapped sources.
1185
+ *
1186
+ * @param sources - The sources to join
1187
+ * @param separator - The derived text inserted between sources
1188
+ * @returns The joined text and every source-backed segment
1189
+ *
1190
+ * @example
1191
+ * ```ts
1192
+ * joinSources(splitLines('a\nb'), '\n')
1193
+ * // { text: 'a\nb', segments: [...] }
1194
+ * ```
1195
+ */
1196
+ function joinSources(sources, separator) {
1197
+ let text = "";
1198
+ const segments = [];
1199
+ for (let index = 0; index < sources.length; index += 1) {
1200
+ const source = sources[index];
1201
+ if (source === void 0) continue;
1202
+ if (index > 0) {
1203
+ const previous = sources[index - 1];
1204
+ const left = previous?.segments[previous.segments.length - 1];
1205
+ const right = source.segments[0];
1206
+ if (separator.length > 0 && left !== void 0 && right !== void 0 && left.end < right.start) segments.push({
1207
+ offset: text.length,
1208
+ start: left.end,
1209
+ end: right.start
1210
+ });
1211
+ text += separator;
1212
+ }
1213
+ for (const segment of source.segments) segments.push({
1214
+ offset: text.length + segment.offset,
1215
+ start: segment.start,
1216
+ end: segment.end
1217
+ });
1218
+ text += source.text;
1219
+ }
1220
+ return {
1221
+ text,
1222
+ segments
1223
+ };
1224
+ }
1225
+ /**
1226
+ * Projects a derived text range through its segments to a half-open region of the
1227
+ * original markdown string.
1228
+ *
1229
+ * @param source - The offset-bearing source carrying the range
1230
+ * @param from - The inclusive derived-text boundary
1231
+ * @param to - The exclusive derived-text boundary
1232
+ * @returns The original-string span, or `undefined` when either boundary is unmapped
1233
+ *
1234
+ * @example
1235
+ * ```ts
1236
+ * projectSpan({ text: 'a', segments: [{ offset: 0, start: 4, end: 5 }] }, 0, 1)
1237
+ * // { start: 4, end: 5 }
1238
+ * ```
1239
+ */
1240
+ function projectSpan(source, from, to) {
1241
+ if (from < 0 || to < from || to > source.text.length) return void 0;
1242
+ let start;
1243
+ let end;
1244
+ for (let index = 0; index < source.segments.length; index += 1) {
1245
+ const segment = source.segments[index];
1246
+ if (segment === void 0) continue;
1247
+ const next = source.segments[index + 1];
1248
+ const limit = Math.min(segment.offset + (segment.end - segment.start), next === void 0 ? source.text.length : next.offset);
1249
+ if (from === to && from >= segment.offset && from <= limit) {
1250
+ if (next !== void 0 && from === next.offset) continue;
1251
+ const position = from === limit ? segment.end : Math.min(segment.end, segment.start + from - segment.offset);
1252
+ return {
1253
+ start: position,
1254
+ end: position
1255
+ };
1256
+ }
1257
+ if (start === void 0 && from >= segment.offset && from < limit) start = segment.start + from - segment.offset;
1258
+ if (to > segment.offset && to <= limit) end = to === limit ? segment.end : Math.min(segment.end, segment.start + to - segment.offset);
1259
+ }
1260
+ return start === void 0 || end === void 0 ? void 0 : {
1261
+ start,
1262
+ end
1263
+ };
1264
+ }
1265
+ /**
1266
+ * Trims an offset-bearing source without losing the coordinates of its retained text.
1267
+ *
1268
+ * @param source - The source to trim
1269
+ * @returns The trimmed text and its narrowed original-string segments
1270
+ *
1271
+ * @example
1272
+ * ```ts
1273
+ * trimSource({ text: ' a ', segments: [{ offset: 0, start: 4, end: 7 }] })
1274
+ * // { text: 'a', segments: [{ offset: 0, start: 5, end: 6 }] }
1275
+ * ```
1276
+ */
1277
+ function trimSource(source) {
1278
+ const start = source.text.length - source.text.trimStart().length;
1279
+ const end = source.text.trimEnd().length;
1280
+ return sliceSource(source, start, Math.max(start, end));
1281
+ }
1282
+ /**
1283
+ * Normalizes one paragraph line while retaining the full source run consumed by a
1284
+ * trailing-space hard break.
1285
+ *
1286
+ * @param source - The offset-bearing paragraph line
1287
+ * @param breaks - If `true`, preserves a trailing run of at least two spaces as the
1288
+ * scanner's two-space hard-break syntax; if `false`, trims the line normally
1289
+ * @returns The normalized line and its original-string segments
1290
+ *
1291
+ * @example
1292
+ * ```ts
1293
+ * normalizeParagraphLine(splitLines('text \nnext')[0], true).text // 'text '
1294
+ * ```
1295
+ */
1296
+ function normalizeParagraphLine(source, breaks) {
1297
+ if (!breaks || !source.text.endsWith(" ")) return trimSource(source);
1298
+ const contentEnd = source.text.trimEnd().length;
1299
+ const content = trimSource(sliceSource(source, 0, contentEnd));
1300
+ const span = projectSpan(source, contentEnd, source.text.length);
1301
+ return joinSources([content, {
1302
+ text: " ",
1303
+ segments: span === void 0 ? [] : [{
1304
+ offset: 0,
1305
+ start: span.start,
1306
+ end: span.end
1307
+ }]
1308
+ }], "");
1309
+ }
1310
+ /**
979
1311
  * The count of leading space / tab characters on `line` (a tab counts as one) - the
980
1312
  * indent that decides whether a list item's continuation belongs to the item.
981
1313
  *
@@ -994,25 +1326,33 @@ function countIndent(line) {
994
1326
  return count;
995
1327
  }
996
1328
  /**
997
- * Extract an ATX heading line (`#` … `######` followed by text) into its
998
- * `{ level, text }`, or `undefined` when `line` is not a heading. A run of more than 6
999
- * `#`s, or `#`s not followed by whitespace + text, is not a
1000
- * heading; an optional closing `###` run is stripped.
1329
+ * Extracts an ATX heading line (`#` … `######` followed by text) into its level,
1330
+ * trimmed text, and the text's offset inside the line. A run of more than 6 `#`s, or
1331
+ * `#`s not followed by whitespace + text, is not a heading; an optional closing
1332
+ * `###` run is stripped.
1001
1333
  *
1002
1334
  * @param line - The candidate line
1003
- * @returns The heading level (1–6) and its raw inline text, or `undefined`
1335
+ * @returns The heading level (1–6), raw inline text, and text offset, or `undefined`
1004
1336
  *
1005
1337
  * @example
1006
1338
  * ```ts
1007
- * extractHeading('## Title') // { level: 2, text: 'Title' }
1339
+ * extractHeading('## Title') // { level: 2, text: 'Title', offset: 3 }
1008
1340
  * ```
1009
1341
  */
1010
1342
  function extractHeading(line) {
1011
- const match = /^(#{1,6})(?:\s+(.*))?$/.exec(line.trimStart());
1343
+ const trimmed = line.trimStart();
1344
+ const match = /^(#{1,6})(?:\s+(.*))?$/.exec(trimmed);
1012
1345
  if (!match || match[1] === void 0) return void 0;
1346
+ const level = match[1].length;
1347
+ const raw = match[2] ?? "";
1348
+ const withoutClosing = raw.replace(/\s+#+\s*$/, "");
1349
+ const text = withoutClosing.trim();
1350
+ const found = raw.length === 0 ? trimmed.length : trimmed.indexOf(raw, level);
1351
+ const content = found < 0 ? trimmed.length : found;
1013
1352
  return {
1014
- level: match[1].length,
1015
- text: (match[2] ?? "").replace(/\s+#+\s*$/, "").trim()
1353
+ level,
1354
+ text,
1355
+ offset: line.length - trimmed.length + content + withoutClosing.length - withoutClosing.trimStart().length
1016
1356
  };
1017
1357
  }
1018
1358
  /**
@@ -1081,24 +1421,27 @@ function extractListItem(line) {
1081
1421
  }
1082
1422
  }
1083
1423
  /**
1084
- * Strip one level of blockquote marker (`>` plus one optional following space) from a
1085
- * blockquote line, so the de-quoted lines re-parse as nested blocks.
1424
+ * Strips one level of blockquote marker (`>` plus one optional following space) from
1425
+ * an offset-bearing blockquote line, so the de-quoted source re-parses as nested
1426
+ * blocks without losing its original coordinates.
1086
1427
  *
1087
- * @param line - A blockquote line (per {@link isQuote})
1088
- * @returns The line with its leading `>` (and one space) removed
1428
+ * @param source - A blockquote line (per {@link isQuote})
1429
+ * @returns The source with its leading `>` and optional space removed
1089
1430
  *
1090
1431
  * @example
1091
1432
  * ```ts
1092
- * stripQuote('> text') // 'text'
1433
+ * stripQuote({ text: '> text', segments: [{ offset: 0, start: 0, end: 6 }] })
1434
+ * // { text: 'text', segments: [{ offset: 0, start: 2, end: 6 }] }
1093
1435
  * ```
1094
1436
  */
1095
- function stripQuote(line) {
1096
- return line.replace(/^\s{0,3}>\s?/, "");
1437
+ function stripQuote(source) {
1438
+ return sliceSource(source, (/^\s{0,3}>\s?/.exec(source.text)?.[0] ?? "").length, source.text.length);
1097
1439
  }
1098
1440
  /**
1099
1441
  * Split one GFM table row into its cell strings - outer pipes are optional, an escaped
1100
1442
  * pipe (`\|`) inside a cell is NOT a separator (it becomes a literal `|`), and the
1101
- * empty leading / trailing cell produced by an outer `|` is dropped.
1443
+ * empty leading / trailing cell produced by an outer `|` is dropped. Derives the string
1444
+ * form from {@link splitTableSources}, which owns the escaped-pipe splitting rule.
1102
1445
  *
1103
1446
  * @param row - The raw table row line
1104
1447
  * @returns The row's cells, in column order
@@ -1109,22 +1452,55 @@ function stripQuote(line) {
1109
1452
  * ```
1110
1453
  */
1111
1454
  function splitTableRow(row) {
1455
+ return splitTableSources({
1456
+ text: row,
1457
+ segments: []
1458
+ }).map((cell) => cell.text);
1459
+ }
1460
+ /**
1461
+ * Splits an offset-bearing GFM table row into offset-bearing cells, retaining the
1462
+ * complete source spelling of an escaped pipe while exposing its literal value.
1463
+ *
1464
+ * @param row - The offset-bearing table row
1465
+ * @returns The row's cells with their original-string coordinates
1466
+ *
1467
+ * @example
1468
+ * ```ts
1469
+ * splitTableSources(splitLines('| a\\|b |')[0]).map((cell) => cell.text) // [' a|b ']
1470
+ * ```
1471
+ */
1472
+ function splitTableSources(row) {
1473
+ const source = trimSource(row);
1112
1474
  const cells = [];
1113
- let current = "";
1114
- const trimmed = row.trim();
1115
- for (let index = 0; index < trimmed.length; index += 1) {
1116
- const character = trimmed[index];
1117
- if (character === "\\" && trimmed[index + 1] === "|") {
1118
- current += "|";
1475
+ let pieces = [];
1476
+ let start = 0;
1477
+ for (let index = 0; index < source.text.length; index += 1) {
1478
+ const character = source.text[index];
1479
+ if (character === "\\" && source.text[index + 1] === "|") {
1480
+ pieces.push(sliceSource(source, start, index));
1481
+ const span = projectSpan(source, index, index + 2);
1482
+ pieces.push({
1483
+ text: "|",
1484
+ segments: span === void 0 ? [] : [{
1485
+ offset: 0,
1486
+ start: span.start,
1487
+ end: span.end
1488
+ }]
1489
+ });
1119
1490
  index += 1;
1120
- } else if (character === "|") {
1121
- cells.push(current);
1122
- current = "";
1123
- } else current += character;
1491
+ start = index + 1;
1492
+ continue;
1493
+ }
1494
+ if (character !== "|") continue;
1495
+ pieces.push(sliceSource(source, start, index));
1496
+ cells.push(joinSources(pieces, ""));
1497
+ pieces = [];
1498
+ start = index + 1;
1124
1499
  }
1125
- cells.push(current);
1126
- if (isNonEmptyArray(cells) && isEmptyString((cells[0] ?? "").trim())) cells.shift();
1127
- if (isNonEmptyArray(cells) && isEmptyString((cells[cells.length - 1] ?? "").trim())) cells.pop();
1500
+ pieces.push(sliceSource(source, start, source.text.length));
1501
+ cells.push(joinSources(pieces, ""));
1502
+ if (isNonEmptyArray(cells) && isEmptyString((cells[0]?.text ?? "").trim())) cells.shift();
1503
+ if (isNonEmptyArray(cells) && isEmptyString((cells[cells.length - 1]?.text ?? "").trim())) cells.pop();
1128
1504
  return cells;
1129
1505
  }
1130
1506
  /**
@@ -1199,6 +1575,7 @@ function unescapeText(text) {
1199
1575
  * unrecognized character, so coalescing keeps the AST clean and assertion-friendly.
1200
1576
  *
1201
1577
  * @param nodes - The inline nodes (possibly with adjacent text runs)
1578
+ * @param spans - The optional operation-owned node span recorder
1202
1579
  * @returns The nodes with consecutive text nodes concatenated
1203
1580
  *
1204
1581
  * @example
@@ -1207,15 +1584,27 @@ function unescapeText(text) {
1207
1584
  * // [{ element: 'text', value: 'ab' }]
1208
1585
  * ```
1209
1586
  */
1210
- function coalesceText(nodes) {
1587
+ function coalesceText(nodes, spans) {
1211
1588
  const out = [];
1212
1589
  for (const node of nodes) {
1213
1590
  const last = out[out.length - 1];
1214
- if (node.element === "text" && last !== void 0 && last.element === "text") out[out.length - 1] = {
1215
- element: "text",
1216
- value: last.value + node.value
1217
- };
1218
- else out.push(node);
1591
+ if (node.element === "text" && last !== void 0 && last.element === "text") {
1592
+ const merged = {
1593
+ element: "text",
1594
+ value: last.value + node.value
1595
+ };
1596
+ const left = spans?.get(last);
1597
+ const right = spans?.get(node);
1598
+ if (spans !== void 0) {
1599
+ spans.delete(last);
1600
+ spans.delete(node);
1601
+ if (left !== void 0 && right !== void 0) spans.set(merged, {
1602
+ start: left.start,
1603
+ end: right.end
1604
+ });
1605
+ }
1606
+ out[out.length - 1] = merged;
1607
+ } else out.push(node);
1219
1608
  }
1220
1609
  return out;
1221
1610
  }
@@ -1255,26 +1644,22 @@ function scanCode(source, start, to) {
1255
1644
  }
1256
1645
  }
1257
1646
  /**
1258
- * Scan a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1647
+ * Locates a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1259
1648
  * must immediately follow and the destination runs to the matching `)` (both respect
1260
- * nested delimiters + escapes). Returns the link node, or `undefined` when the shape
1649
+ * nested delimiters + escapes). Returns the label close and syntax end, or `undefined` when the shape
1261
1650
  * does not hold (it then degrades to a literal `[`).
1262
1651
  *
1263
1652
  * @param source - The inline source text
1264
1653
  * @param start - The index of the opening `[`
1265
1654
  * @param to - The exclusive end of the scan window
1266
- * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1267
- * at {@link MAX_DEPTH} the link's text children degrade to literal text instead of
1268
- * recursing further
1269
- * @returns The parsed {@link LinkNode} + end index, or `undefined`
1655
+ * @returns The label close and syntax end indices, or `undefined`
1270
1656
  *
1271
1657
  * @example
1272
1658
  * ```ts
1273
- * scanLink('[text](url)', 0, 11)
1274
- * // { node: { element: 'link', href: 'url', children: [...] }, end: 11 }
1659
+ * locateLink('[text](url)', 0, 11) // { close: 5, end: 11 }
1275
1660
  * ```
1276
1661
  */
1277
- function scanLink(source, start, to, depth = 0) {
1662
+ function locateLink(source, start, to) {
1278
1663
  let bracketDepth = 0;
1279
1664
  let close = -1;
1280
1665
  for (let index = start; index < to; index += 1) {
@@ -1311,38 +1696,63 @@ function scanLink(source, start, to, depth = 0) {
1311
1696
  }
1312
1697
  }
1313
1698
  if (parenClose === -1) return void 0;
1699
+ return {
1700
+ close,
1701
+ end: parenClose + 1
1702
+ };
1703
+ }
1704
+ /**
1705
+ * Scans a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1706
+ * must immediately follow and the destination runs to the matching `)` (both respect
1707
+ * nested delimiters + escapes) through {@link locateLink}, and returns the parsed node
1708
+ * and end index. Returns `undefined` when the shape does not hold (it then degrades to
1709
+ * a literal `[`).
1710
+ *
1711
+ * @param source - The inline source text
1712
+ * @param start - The index of the opening `[`
1713
+ * @param to - The exclusive end of the scan window
1714
+ * @param depth - The current inline-recursion depth, forwarded to {@link scanInline}
1715
+ * incremented by one for the link text's children. At {@link MAX_DEPTH} that
1716
+ * recursion emits the text as a single literal text node instead of scanning it.
1717
+ * @returns The parsed link and end index, or `undefined` when the shape does not hold
1718
+ *
1719
+ * @example
1720
+ * ```ts
1721
+ * scanLink('[text](url)', 0, 11)
1722
+ * // { node: { element: 'link', href: 'url', children: [{ element: 'text', value: 'text' }] }, end: 11 }
1723
+ * ```
1724
+ */
1725
+ function scanLink(source, start, to, depth = 0) {
1726
+ const located = locateLink(source, start, to);
1727
+ if (located === void 0) return void 0;
1314
1728
  return {
1315
1729
  node: {
1316
1730
  element: "link",
1317
- href: unescapeText(source.slice(close + 2, parenClose).trim()),
1318
- children: scanInline(source, start + 1, close, depth + 1)
1731
+ href: unescapeText(source.slice(located.close + 2, located.end - 1).trim()),
1732
+ children: scanInline(source, start + 1, located.close, depth + 1)
1319
1733
  },
1320
- end: parenClose + 1
1734
+ end: located.end
1321
1735
  };
1322
1736
  }
1323
1737
  /**
1324
- * Scan an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1738
+ * Locates an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1325
1739
  * matching closing run of the same marker + width while skipping complete nested
1326
1740
  * runs from the other marker family, and requires non-space immediately inside both
1327
1741
  * delimiters (the CommonMark flanking simplification that blocks `* x *`). Returns
1328
- * the emphasis node, or `undefined` when no valid closer exists (it then degrades to
1742
+ * the content and syntax bounds, or `undefined` when no valid closer exists (it then degrades to
1329
1743
  * a literal marker).
1330
1744
  *
1331
1745
  * @param source - The inline source text
1332
1746
  * @param start - The index of the opening marker
1333
1747
  * @param to - The exclusive end of the scan window
1334
- * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1335
- * at {@link MAX_DEPTH} the emphasis's children degrade to literal text instead of
1336
- * recursing further
1337
- * @returns The parsed {@link EmphasisNode} + end index, or `undefined`
1748
+ * @returns The content and syntax bounds, or `undefined`
1338
1749
  *
1339
1750
  * @example
1340
1751
  * ```ts
1341
- * scanEmphasis('*em*', 0, 4)
1342
- * // { node: { element: 'emphasis', strong: false, children: [...] }, end: 4 }
1752
+ * locateEmphasis('*em*', 0, 4) // { strong: false, open: 1, close: 3, end: 4 }
1343
1753
  * ```
1344
1754
  */
1345
- function scanEmphasis(source, start, to, depth = 0) {
1755
+ function locateEmphasis(source, start, to) {
1346
1756
  const marker = source[start] ?? "";
1347
1757
  let run = 0;
1348
1758
  while (start + run < to && source[start + run] === marker && run < 2) run += 1;
@@ -1362,7 +1772,7 @@ function scanEmphasis(source, start, to, depth = 0) {
1362
1772
  continue;
1363
1773
  }
1364
1774
  if ((character === "*" || character === "_") && character !== marker) {
1365
- const nested = scanEmphasis(source, index, to, depth + 1);
1775
+ const nested = locateEmphasis(source, index, to);
1366
1776
  if (nested !== void 0) {
1367
1777
  index = nested.end;
1368
1778
  continue;
@@ -1372,11 +1782,9 @@ function scanEmphasis(source, start, to, depth = 0) {
1372
1782
  let closeRun = 0;
1373
1783
  while (index + closeRun < to && source[index + closeRun] === marker) closeRun += 1;
1374
1784
  if (closeRun >= run && !isWhitespace(source[index - 1] ?? "")) return {
1375
- node: {
1376
- element: "emphasis",
1377
- strong,
1378
- children: scanInline(source, openEnd, index, depth + 1)
1379
- },
1785
+ strong,
1786
+ open: openEnd,
1787
+ close: index,
1380
1788
  end: index + run
1381
1789
  };
1382
1790
  index += closeRun;
@@ -1386,6 +1794,40 @@ function scanEmphasis(source, start, to, depth = 0) {
1386
1794
  }
1387
1795
  }
1388
1796
  /**
1797
+ * Scans an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1798
+ * matching closing run of the same marker + width while skipping complete nested runs
1799
+ * from the other marker family, and requires non-space immediately inside both
1800
+ * delimiters (the CommonMark flanking simplification that blocks `* x *`) through
1801
+ * {@link locateEmphasis}, and returns the parsed node and end index. Returns
1802
+ * `undefined` when no valid closer exists (it then degrades to a literal marker).
1803
+ *
1804
+ * @param source - The inline source text
1805
+ * @param start - The index of the opening marker
1806
+ * @param to - The exclusive end of the scan window
1807
+ * @param depth - The current inline-recursion depth, forwarded to {@link scanInline}
1808
+ * incremented by one for the run's children. At {@link MAX_DEPTH} that recursion
1809
+ * emits the content as a single literal text node instead of scanning it.
1810
+ * @returns The parsed emphasis and end index, or `undefined` when no closer exists
1811
+ *
1812
+ * @example
1813
+ * ```ts
1814
+ * scanEmphasis('*em*', 0, 4)
1815
+ * // { node: { element: 'emphasis', strong: false, children: [{ element: 'text', value: 'em' }] }, end: 4 }
1816
+ * ```
1817
+ */
1818
+ function scanEmphasis(source, start, to, depth = 0) {
1819
+ const located = locateEmphasis(source, start, to);
1820
+ if (located === void 0) return void 0;
1821
+ return {
1822
+ node: {
1823
+ element: "emphasis",
1824
+ strong: located.strong,
1825
+ children: scanInline(source, located.open, located.close, depth + 1)
1826
+ },
1827
+ end: located.end
1828
+ };
1829
+ }
1830
+ /**
1389
1831
  * Scan the window `[from, to)` of `source` into inline nodes - the single recursive
1390
1832
  * engine the inline phase runs on (emphasis, link text, and image alternative
1391
1833
  * content recurse through it). Linear:
@@ -1408,40 +1850,86 @@ function scanEmphasis(source, start, to, depth = 0) {
1408
1850
  * ```
1409
1851
  */
1410
1852
  function scanInline(source, from, to, depth = 0) {
1411
- if (depth >= 64) return from < to ? [{
1412
- element: "text",
1413
- value: source.slice(from, to)
1414
- }] : [];
1853
+ return scanInlineSource({
1854
+ text: source,
1855
+ segments: [{
1856
+ offset: 0,
1857
+ start: 0,
1858
+ end: source.length
1859
+ }]
1860
+ }, from, to, /* @__PURE__ */ new Map(), depth);
1861
+ }
1862
+ /**
1863
+ * Scans an offset-bearing inline window with the same engine as {@link scanInline}
1864
+ * and records each emitted node against the original markdown string.
1865
+ *
1866
+ * @param source - The offset-bearing inline source
1867
+ * @param from - The inclusive start of the scan window
1868
+ * @param to - The exclusive end of the scan window
1869
+ * @param spans - The operation-owned node span recorder
1870
+ * @param depth - The current inline-recursion depth
1871
+ * @returns The parsed inline nodes before adjacent text coalescing
1872
+ *
1873
+ * @example
1874
+ * ```ts
1875
+ * scanInlineSource(
1876
+ * { text: 'hi *there*', segments: [{ offset: 0, start: 0, end: 10 }] },
1877
+ * 0,
1878
+ * 10,
1879
+ * new Map(),
1880
+ * )
1881
+ * // [{ element: 'text', value: 'hi ' }, { element: 'emphasis', ... }]
1882
+ * ```
1883
+ */
1884
+ function scanInlineSource(source, from, to, spans, depth = 0) {
1885
+ if (depth >= 64) if (from < to) {
1886
+ const node = {
1887
+ element: "text",
1888
+ value: source.text.slice(from, to)
1889
+ };
1890
+ const span = projectSpan(source, from, to);
1891
+ if (span !== void 0) spans.set(node, span);
1892
+ return [node];
1893
+ } else return [];
1415
1894
  const nodes = [];
1416
1895
  let index = from;
1417
1896
  let pending = "";
1897
+ let pendingStart = from;
1418
1898
  while (index < to) {
1419
- const character = source[index] ?? "";
1420
- if (character === "\\" && index + 1 < to && isEscapable(source[index + 1] ?? "")) {
1421
- pending += source[index + 1] ?? "";
1899
+ const character = source.text[index] ?? "";
1900
+ if (character === "\\" && index + 1 < to && isEscapable(source.text[index + 1] ?? "")) {
1901
+ if (pending.length === 0) pendingStart = index;
1902
+ pending += source.text[index + 1] ?? "";
1422
1903
  index += 2;
1423
1904
  continue;
1424
1905
  }
1425
1906
  if (character === " ") {
1426
1907
  let spaceEnd = index;
1427
- while (spaceEnd < to && source[spaceEnd] === " ") spaceEnd += 1;
1428
- if (spaceEnd - index >= 2 && source[spaceEnd] === "\n") {
1908
+ while (spaceEnd < to && source.text[spaceEnd] === " ") spaceEnd += 1;
1909
+ if (spaceEnd - index >= 2 && source.text[spaceEnd] === "\n") {
1429
1910
  if (pending.length > 0) {
1430
- nodes.push({
1911
+ const node = {
1431
1912
  element: "text",
1432
1913
  value: pending
1433
- });
1914
+ };
1915
+ const span = projectSpan(source, pendingStart, index);
1916
+ if (span !== void 0) spans.set(node, span);
1917
+ nodes.push(node);
1434
1918
  pending = "";
1435
1919
  }
1436
- nodes.push({ element: "break" });
1920
+ const node = { element: "break" };
1921
+ const span = projectSpan(source, index, spaceEnd + 1);
1922
+ if (span !== void 0) spans.set(node, span);
1923
+ nodes.push(node);
1437
1924
  index = spaceEnd + 1;
1925
+ pendingStart = index;
1438
1926
  continue;
1439
1927
  }
1440
1928
  }
1441
1929
  let scanned;
1442
1930
  let end = index;
1443
1931
  if (character === "`") {
1444
- const span = scanCode(source, index, to);
1932
+ const span = scanCode(source.text, index, to);
1445
1933
  if (span) {
1446
1934
  scanned = {
1447
1935
  element: "codeSpan",
@@ -1450,50 +1938,70 @@ function scanInline(source, from, to, depth = 0) {
1450
1938
  end = span.end;
1451
1939
  }
1452
1940
  }
1453
- if (character === "!" && source[index + 1] === "[") {
1454
- const link = scanLink(source, index + 1, to, depth);
1455
- if (link) {
1941
+ if (character === "!" && source.text[index + 1] === "[") {
1942
+ const link = locateLink(source.text, index + 1, to);
1943
+ if (link !== void 0) {
1456
1944
  scanned = {
1457
1945
  element: "image",
1458
- src: link.node.href,
1459
- children: link.node.children
1946
+ src: unescapeText(source.text.slice(link.close + 2, link.end - 1).trim()),
1947
+ children: coalesceText(scanInlineSource(source, index + 2, link.close, spans, depth + 1), spans)
1460
1948
  };
1461
1949
  end = link.end;
1462
1950
  }
1463
1951
  }
1464
1952
  if (character === "[") {
1465
- const link = scanLink(source, index, to, depth);
1466
- if (link) {
1467
- scanned = link.node;
1953
+ const link = locateLink(source.text, index, to);
1954
+ if (link !== void 0) {
1955
+ scanned = {
1956
+ element: "link",
1957
+ href: unescapeText(source.text.slice(link.close + 2, link.end - 1).trim()),
1958
+ children: coalesceText(scanInlineSource(source, index + 1, link.close, spans, depth + 1), spans)
1959
+ };
1468
1960
  end = link.end;
1469
1961
  }
1470
1962
  }
1471
1963
  if (character === "*" || character === "_") {
1472
- const emphasis = scanEmphasis(source, index, to, depth);
1473
- if (emphasis) {
1474
- scanned = emphasis.node;
1964
+ const emphasis = locateEmphasis(source.text, index, to);
1965
+ if (emphasis !== void 0) {
1966
+ scanned = {
1967
+ element: "emphasis",
1968
+ strong: emphasis.strong,
1969
+ children: coalesceText(scanInlineSource(source, emphasis.open, emphasis.close, spans, depth + 1), spans)
1970
+ };
1475
1971
  end = emphasis.end;
1476
1972
  }
1477
1973
  }
1478
1974
  if (scanned !== void 0) {
1479
1975
  if (pending.length > 0) {
1480
- nodes.push({
1976
+ const node = {
1481
1977
  element: "text",
1482
1978
  value: pending
1483
- });
1979
+ };
1980
+ const span = projectSpan(source, pendingStart, index);
1981
+ if (span !== void 0) spans.set(node, span);
1982
+ nodes.push(node);
1484
1983
  pending = "";
1485
1984
  }
1985
+ const span = projectSpan(source, index, end);
1986
+ if (span !== void 0) spans.set(scanned, span);
1486
1987
  nodes.push(scanned);
1487
1988
  index = end;
1989
+ pendingStart = index;
1488
1990
  continue;
1489
1991
  }
1992
+ if (pending.length === 0) pendingStart = index;
1490
1993
  pending += character;
1491
1994
  index += 1;
1492
1995
  }
1493
- if (pending.length > 0) nodes.push({
1494
- element: "text",
1495
- value: pending
1496
- });
1996
+ if (pending.length > 0) {
1997
+ const node = {
1998
+ element: "text",
1999
+ value: pending
2000
+ };
2001
+ const span = projectSpan(source, pendingStart, index);
2002
+ if (span !== void 0) spans.set(node, span);
2003
+ nodes.push(node);
2004
+ }
1497
2005
  return nodes;
1498
2006
  }
1499
2007
  /**
@@ -1502,36 +2010,56 @@ function scanInline(source, from, to, depth = 0) {
1502
2010
  *
1503
2011
  * @param lines - The markdown lines to scan.
1504
2012
  * @param start - The index of the header row.
2013
+ * @param spans - The optional operation-owned node span recorder.
1505
2014
  * @returns The parsed table node and the index of the first line after it.
1506
2015
  *
1507
2016
  * @example
1508
2017
  * ```ts
1509
- * collectTable(['| a |', '| - |'], 0) // { node: { element: 'table', ... }, next: 2 }
2018
+ * collectTable(splitLines('| a |\n| - |'), 0) // { node: { element: 'table', ... }, next: 2 }
1510
2019
  * ```
1511
2020
  */
1512
- function collectTable(lines, start) {
1513
- const headerCells = splitTableRow(lines[start] ?? "");
2021
+ function collectTable(lines, start, spans = /* @__PURE__ */ new Map()) {
2022
+ const headerCells = splitTableSources(lines[start] ?? {
2023
+ text: "",
2024
+ segments: []
2025
+ });
1514
2026
  const columns = headerCells.length;
1515
- const header = headerCells.map((cell) => parseInline(cell.trim()));
1516
- const align = delimiterToAlignments(lines[start + 1] ?? "");
2027
+ const header = headerCells.map((cell) => {
2028
+ const source = trimSource(cell);
2029
+ return coalesceText(scanInlineSource(source, 0, source.text.length, spans), spans);
2030
+ });
2031
+ const align = delimiterToAlignments(lines[start + 1]?.text ?? "");
1517
2032
  const padded = [];
1518
2033
  for (let column = 0; column < columns; column += 1) padded.push(align[column] ?? null);
1519
2034
  const rows = [];
1520
2035
  let index = start + 2;
1521
- while (index < lines.length && !isBlankLine(lines[index] ?? "") && (lines[index] ?? "").includes("|")) {
1522
- const cells = splitTableRow(lines[index] ?? "");
2036
+ while (index < lines.length && !isBlankLine(lines[index]?.text ?? "") && (lines[index]?.text ?? "").includes("|")) {
2037
+ const cells = splitTableSources(lines[index] ?? {
2038
+ text: "",
2039
+ segments: []
2040
+ });
1523
2041
  const row = [];
1524
- for (let column = 0; column < columns; column += 1) row.push(parseInline((cells[column] ?? "").trim()));
2042
+ for (let column = 0; column < columns; column += 1) {
2043
+ const source = trimSource(cells[column] ?? {
2044
+ text: "",
2045
+ segments: []
2046
+ });
2047
+ row.push(coalesceText(scanInlineSource(source, 0, source.text.length, spans), spans));
2048
+ }
1525
2049
  rows.push(row);
1526
2050
  index += 1;
1527
2051
  }
2052
+ const node = {
2053
+ element: "table",
2054
+ header,
2055
+ rows,
2056
+ align: padded
2057
+ };
2058
+ const source = joinSources(lines.slice(start, index), "\n");
2059
+ const span = projectSpan(source, 0, source.text.length);
2060
+ if (span !== void 0) spans.set(node, span);
1528
2061
  return {
1529
- node: {
1530
- element: "table",
1531
- header,
1532
- rows,
1533
- align: padded
1534
- },
2062
+ node,
1535
2063
  next: index
1536
2064
  };
1537
2065
  }
@@ -1542,15 +2070,18 @@ function collectTable(lines, start) {
1542
2070
  * @param lines - The markdown lines to scan.
1543
2071
  * @param start - The index of the first list item.
1544
2072
  * @param depth - The current recursion depth (each item recurses at `depth + 1`).
2073
+ * @param spans - The optional operation-owned node span recorder.
2074
+ * @param end - The original-source end of this line run, including a removed terminator.
1545
2075
  * @returns The parsed list node and the index of the first line after it.
1546
2076
  *
1547
2077
  * @example
1548
2078
  * ```ts
1549
- * collectList(['- item'], 0, 0) // { node: { element: 'list', ... }, next: 1 }
2079
+ * collectList(splitLines('- item'), 0, 0) // { node: { element: 'list', ... }, next: 1 }
1550
2080
  * ```
1551
2081
  */
1552
- function collectList(lines, start, depth) {
1553
- const first = extractListItem(lines[start] ?? "");
2082
+ function collectList(lines, start, depth, spans = /* @__PURE__ */ new Map(), end) {
2083
+ const text = lines.map((line) => line.text);
2084
+ const first = extractListItem(text[start] ?? "");
1554
2085
  const ordered = first?.ordered ?? false;
1555
2086
  const startOrdinal = first?.start ?? 1;
1556
2087
  const topIndent = first?.indent ?? 0;
@@ -1558,7 +2089,7 @@ function collectList(lines, start, depth) {
1558
2089
  const chain = [];
1559
2090
  let nested = true;
1560
2091
  for (let cursor = start; cursor < lines.length; cursor += 1) {
1561
- const parsed = extractListItem(lines[cursor] ?? "");
2092
+ const parsed = extractListItem(text[cursor] ?? "");
1562
2093
  const previous = chain[chain.length - 1];
1563
2094
  if (parsed === void 0 || previous !== void 0 && (previous.content.length > 0 || parsed.indent !== previous.marker)) {
1564
2095
  nested = false;
@@ -1569,29 +2100,48 @@ function collectList(lines, start, depth) {
1569
2100
  const remaining = 64 - depth;
1570
2101
  if (nested && remaining > 0 && chain.length > remaining) {
1571
2102
  const terminal = chain[remaining - 1];
1572
- if (terminal !== void 0) {
1573
- const source = [terminal.content];
1574
- for (let cursor = start + remaining; cursor < lines.length; cursor += 1) source.push((lines[cursor] ?? "").slice(terminal.marker));
1575
- let children = [{
2103
+ const terminalLine = lines[start + remaining - 1];
2104
+ if (terminal !== void 0 && terminalLine !== void 0) {
2105
+ const sources = [sliceSource(terminalLine, terminal.marker, terminalLine.text.length)];
2106
+ for (let cursor = start + remaining; cursor < lines.length; cursor += 1) {
2107
+ const line = lines[cursor];
2108
+ if (line !== void 0) sources.push(sliceSource(line, terminal.marker, line.text.length));
2109
+ }
2110
+ const source = joinSources(sources, "\n");
2111
+ const textNode = {
2112
+ element: "text",
2113
+ value: source.text
2114
+ };
2115
+ const paragraph = {
1576
2116
  element: "paragraph",
1577
- children: [{
1578
- element: "text",
1579
- value: source.join("\n")
1580
- }]
1581
- }];
2117
+ children: [textNode]
2118
+ };
2119
+ const residualSpan = projectSpan(source, 0, source.text.length);
2120
+ if (residualSpan !== void 0) {
2121
+ spans.set(textNode, residualSpan);
2122
+ spans.set(paragraph, residualSpan);
2123
+ }
2124
+ let children = [paragraph];
1582
2125
  let node;
1583
2126
  for (let cursor = remaining - 1; cursor >= 0; cursor -= 1) {
1584
2127
  const parsed = chain[cursor];
1585
2128
  if (parsed === void 0) continue;
2129
+ const item = {
2130
+ element: "listItem",
2131
+ children
2132
+ };
1586
2133
  node = {
1587
2134
  element: "list",
1588
2135
  ordered: parsed.ordered,
1589
2136
  start: parsed.start,
1590
- items: [{
1591
- element: "listItem",
1592
- children
1593
- }]
2137
+ items: [item]
1594
2138
  };
2139
+ const region = joinSources(lines.slice(start + cursor).map((line) => sliceSource(line, parsed.indent, line.text.length)), "\n");
2140
+ const span = projectSpan(region, 0, region.text.length);
2141
+ if (span !== void 0) {
2142
+ spans.set(item, span);
2143
+ spans.set(node, span);
2144
+ }
1595
2145
  children = [node];
1596
2146
  }
1597
2147
  if (node !== void 0) return {
@@ -1602,43 +2152,59 @@ function collectList(lines, start, depth) {
1602
2152
  }
1603
2153
  let index = start;
1604
2154
  while (index < lines.length) {
1605
- const parsed = extractListItem(lines[index] ?? "");
2155
+ const parsed = extractListItem(text[index] ?? "");
1606
2156
  if (!parsed || parsed.indent > topIndent || parsed.ordered !== ordered) break;
1607
- const itemLines = [parsed.content];
2157
+ const itemStart = index;
2158
+ const itemLine = lines[index];
2159
+ if (itemLine === void 0) break;
2160
+ const itemLines = [sliceSource(itemLine, parsed.marker, itemLine.text.length)];
1608
2161
  const continuation = parsed.marker;
1609
2162
  index += 1;
1610
2163
  while (index < lines.length) {
1611
- const next = lines[index] ?? "";
2164
+ const nextSource = lines[index];
2165
+ if (nextSource === void 0) break;
2166
+ const next = nextSource.text;
1612
2167
  if (isBlankLine(next)) {
1613
- const after = lines[index + 1] ?? "";
2168
+ const after = lines[index + 1]?.text ?? "";
1614
2169
  if (index + 1 < lines.length && !isBlankLine(after) && countIndent(after) >= continuation) {
1615
- itemLines.push("");
2170
+ itemLines.push(sliceSource(nextSource, 0, 0));
1616
2171
  index += 1;
1617
2172
  continue;
1618
2173
  }
1619
2174
  break;
1620
2175
  }
1621
2176
  if (countIndent(next) >= continuation) {
1622
- itemLines.push(next.slice(continuation));
2177
+ itemLines.push(sliceSource(nextSource, continuation, next.length));
1623
2178
  index += 1;
1624
2179
  continue;
1625
2180
  }
1626
- if (extractListItem(next) || startsBlock(lines, index)) break;
1627
- itemLines.push(next.trim());
2181
+ if (extractListItem(next) || startsBlock(text, index)) break;
2182
+ itemLines.push(trimSource(nextSource));
1628
2183
  index += 1;
1629
2184
  }
1630
- items.push({
2185
+ const tail = itemLines[itemLines.length - 1];
2186
+ const segment = tail?.segments[tail.segments.length - 1];
2187
+ const itemEnd = index === lines.length && end !== void 0 ? end : segment?.end;
2188
+ const item = {
1631
2189
  element: "listItem",
1632
- children: parseBlocks(itemLines, depth + 1)
1633
- });
2190
+ children: parseBlocks(itemLines, depth + 1, spans, itemEnd)
2191
+ };
2192
+ const source = joinSources(lines.slice(itemStart, index), "\n");
2193
+ const span = projectSpan(source, 0, source.text.length);
2194
+ if (span !== void 0) spans.set(item, span);
2195
+ items.push(item);
1634
2196
  }
2197
+ const node = {
2198
+ element: "list",
2199
+ ordered,
2200
+ start: startOrdinal,
2201
+ items
2202
+ };
2203
+ const source = joinSources(lines.slice(start, index), "\n");
2204
+ const span = projectSpan(source, 0, source.text.length);
2205
+ if (span !== void 0) spans.set(node, span);
1635
2206
  return {
1636
- node: {
1637
- element: "list",
1638
- ordered,
1639
- start: startOrdinal,
1640
- items
1641
- },
2207
+ node,
1642
2208
  next: index
1643
2209
  };
1644
2210
  }
@@ -3169,13 +3735,14 @@ function foldNode(node, handlers, depth) {
3169
3735
  * always holds). A table's inline cells and a list's items ARE rewritten.
3170
3736
  *
3171
3737
  * @remarks
3172
- * Never mutates `document` - every level is rebuilt into a fresh object/array, even
3173
- * when `rewrite` returns its input unchanged. When `rewrite` returns a node whose
3174
- * `element` does not fit the slot it was called for (a block slot handed a
3738
+ * Never mutates `document`. An unchanged subtree keeps its input identity. A parent
3739
+ * is rebuilt only when an accepted child changes, and the returned derivation map
3740
+ * associates each rebuilt output with its input node. When `rewrite` returns a node
3741
+ * whose `element` does not fit the slot it was called for (a block slot handed a
3175
3742
  * non-{@link BlockNode}, an inline slot handed a non-{@link InlineNode}, a list-item
3176
- * slot handed a non-`listItem`), the ill-fitting result is discarded and the
3177
- * freshly-rebuilt (unrewritten-at-this-level) node is kept instead - `rewriteDocument`
3178
- * stays total and never produces a structurally invalid document.
3743
+ * slot handed a non-`listItem`), the ill-fitting result is discarded and the accepted
3744
+ * input child is reused - `rewriteDocument` stays total and never produces a
3745
+ * structurally invalid document.
3179
3746
  *
3180
3747
  * Descent is capped at {@link MAX_DEPTH}, the same cap {@link walkNodes} and
3181
3748
  * {@link foldNode} observe: at `depth >= MAX_DEPTH` the subtree is passed through
@@ -3185,11 +3752,11 @@ function foldNode(node, handlers, depth) {
3185
3752
  *
3186
3753
  * @param document - The document AST to rewrite
3187
3754
  * @param rewrite - The bottom-up {@link MarkdownRewriteHandler}
3188
- * @returns A new, rewritten {@link MarkdownDocument}
3755
+ * @returns The rewritten document and its output-to-input derivations
3189
3756
  *
3190
3757
  * @example
3191
3758
  * ```ts
3192
- * rewriteDocument(document, (node) =>
3759
+ * const [rewritten, derivations] = rewriteDocument(document, (node) =>
3193
3760
  * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node,
3194
3761
  * )
3195
3762
  * ```
@@ -3202,6 +3769,7 @@ function rewriteDocument(document, rewrite) {
3202
3769
  count: 0
3203
3770
  }];
3204
3771
  const values = [];
3772
+ const derivations = /* @__PURE__ */ new Map();
3205
3773
  while (stack.length > 0) {
3206
3774
  const frame = stack.pop();
3207
3775
  if (frame === void 0) continue;
@@ -3255,6 +3823,7 @@ function rewriteDocument(document, rewrite) {
3255
3823
  }
3256
3824
  const children = frame.count === 0 ? [] : values.splice(values.length - frame.count, frame.count);
3257
3825
  let rebuilt = current;
3826
+ let changed = false;
3258
3827
  switch (current.element) {
3259
3828
  case "document": {
3260
3829
  const blocks = [];
@@ -3262,16 +3831,16 @@ function rewriteDocument(document, rewrite) {
3262
3831
  for (const block of current.children) {
3263
3832
  if (block === void 0) continue;
3264
3833
  const child = children[offset];
3265
- blocks.push(child !== void 0 && isBlockNode(child) ? child : block);
3834
+ const accepted = child !== void 0 && isBlockNode(child) ? child : block;
3835
+ blocks.push(accepted);
3836
+ if (accepted !== block) changed = true;
3266
3837
  offset += 1;
3267
3838
  }
3268
- const result = {
3839
+ if (changed) rebuilt = {
3269
3840
  element: "document",
3270
3841
  children: blocks
3271
3842
  };
3272
- if (stack.length === 0) return result;
3273
- values.push(result);
3274
- continue;
3843
+ break;
3275
3844
  }
3276
3845
  case "heading":
3277
3846
  case "paragraph": {
@@ -3280,10 +3849,12 @@ function rewriteDocument(document, rewrite) {
3280
3849
  for (const inline of current.children) {
3281
3850
  if (inline === void 0) continue;
3282
3851
  const child = children[offset];
3283
- inlines.push(child !== void 0 && isInlineNode(child) ? child : inline);
3852
+ const accepted = child !== void 0 && isInlineNode(child) ? child : inline;
3853
+ inlines.push(accepted);
3854
+ if (accepted !== inline) changed = true;
3284
3855
  offset += 1;
3285
3856
  }
3286
- rebuilt = {
3857
+ if (changed) rebuilt = {
3287
3858
  ...current,
3288
3859
  children: inlines
3289
3860
  };
@@ -3295,10 +3866,12 @@ function rewriteDocument(document, rewrite) {
3295
3866
  for (const block of current.children) {
3296
3867
  if (block === void 0) continue;
3297
3868
  const child = children[offset];
3298
- blocks.push(child !== void 0 && isBlockNode(child) ? child : block);
3869
+ const accepted = child !== void 0 && isBlockNode(child) ? child : block;
3870
+ blocks.push(accepted);
3871
+ if (accepted !== block) changed = true;
3299
3872
  offset += 1;
3300
3873
  }
3301
- rebuilt = {
3874
+ if (changed) rebuilt = {
3302
3875
  ...current,
3303
3876
  children: blocks
3304
3877
  };
@@ -3310,10 +3883,12 @@ function rewriteDocument(document, rewrite) {
3310
3883
  for (const block of current.children) {
3311
3884
  if (block === void 0) continue;
3312
3885
  const child = children[offset];
3313
- blocks.push(child !== void 0 && isBlockNode(child) ? child : block);
3886
+ const accepted = child !== void 0 && isBlockNode(child) ? child : block;
3887
+ blocks.push(accepted);
3888
+ if (accepted !== block) changed = true;
3314
3889
  offset += 1;
3315
3890
  }
3316
- rebuilt = {
3891
+ if (changed) rebuilt = {
3317
3892
  element: "listItem",
3318
3893
  children: blocks
3319
3894
  };
@@ -3327,10 +3902,12 @@ function rewriteDocument(document, rewrite) {
3327
3902
  for (const inline of current.children) {
3328
3903
  if (inline === void 0) continue;
3329
3904
  const child = children[offset];
3330
- inlines.push(child !== void 0 && isInlineNode(child) ? child : inline);
3905
+ const accepted = child !== void 0 && isInlineNode(child) ? child : inline;
3906
+ inlines.push(accepted);
3907
+ if (accepted !== inline) changed = true;
3331
3908
  offset += 1;
3332
3909
  }
3333
- rebuilt = {
3910
+ if (changed) rebuilt = {
3334
3911
  ...current,
3335
3912
  children: inlines
3336
3913
  };
@@ -3342,10 +3919,12 @@ function rewriteDocument(document, rewrite) {
3342
3919
  for (const item of current.items) {
3343
3920
  if (item === void 0) continue;
3344
3921
  const child = children[offset];
3345
- items.push(child?.element === "listItem" ? child : item);
3922
+ const accepted = child?.element === "listItem" ? child : item;
3923
+ items.push(accepted);
3924
+ if (accepted !== item) changed = true;
3346
3925
  offset += 1;
3347
3926
  }
3348
- rebuilt = {
3927
+ if (changed) rebuilt = {
3349
3928
  ...current,
3350
3929
  items
3351
3930
  };
@@ -3360,7 +3939,9 @@ function rewriteDocument(document, rewrite) {
3360
3939
  for (const inline of cell) {
3361
3940
  if (inline === void 0) continue;
3362
3941
  const child = children[offset];
3363
- inlines.push(child !== void 0 && isInlineNode(child) ? child : inline);
3942
+ const accepted = child !== void 0 && isInlineNode(child) ? child : inline;
3943
+ inlines.push(accepted);
3944
+ if (accepted !== inline) changed = true;
3364
3945
  offset += 1;
3365
3946
  }
3366
3947
  header.push(inlines);
@@ -3375,14 +3956,16 @@ function rewriteDocument(document, rewrite) {
3375
3956
  for (const inline of cell) {
3376
3957
  if (inline === void 0) continue;
3377
3958
  const child = children[offset];
3378
- inlines.push(child !== void 0 && isInlineNode(child) ? child : inline);
3959
+ const accepted = child !== void 0 && isInlineNode(child) ? child : inline;
3960
+ inlines.push(accepted);
3961
+ if (accepted !== inline) changed = true;
3379
3962
  offset += 1;
3380
3963
  }
3381
3964
  cells.push(inlines);
3382
3965
  }
3383
3966
  rows.push(cells);
3384
3967
  }
3385
- rebuilt = {
3968
+ if (changed) rebuilt = {
3386
3969
  ...current,
3387
3970
  header,
3388
3971
  rows
@@ -3390,6 +3973,14 @@ function rewriteDocument(document, rewrite) {
3390
3973
  break;
3391
3974
  }
3392
3975
  }
3976
+ if (rebuilt !== current) derivations.set(rebuilt, current);
3977
+ if (current.element === "document") {
3978
+ const result = rebuilt.element === "document" ? rebuilt : current;
3979
+ const output = new Set(walkNodes(result));
3980
+ const retained = /* @__PURE__ */ new Map();
3981
+ for (const [node, source] of derivations) if (output.has(node)) retained.set(node, source);
3982
+ return [result, retained];
3983
+ }
3393
3984
  const result = rewrite(rebuilt);
3394
3985
  let accepted = rebuilt;
3395
3986
  switch (current.element) {
@@ -3412,12 +4003,13 @@ function rewriteDocument(document, rewrite) {
3412
4003
  break;
3413
4004
  case "listItem": if (result.element === "listItem") accepted = result;
3414
4005
  }
4006
+ if (accepted !== rebuilt && accepted !== current) {
4007
+ if (derivations.has(accepted) && derivations.get(accepted) !== current) derivations.set(accepted, void 0);
4008
+ else derivations.set(accepted, current);
4009
+ }
3415
4010
  values.push(accepted);
3416
4011
  }
3417
- return {
3418
- element: "document",
3419
- children: [...document.children]
3420
- };
4012
+ return [document, /* @__PURE__ */ new Map()];
3421
4013
  }
3422
4014
  /**
3423
4015
  * Concatenate the `value` / `code` content of every descendant text / code-span /
@@ -3493,6 +4085,6 @@ function flattenText(node) {
3493
4085
  return value;
3494
4086
  }
3495
4087
  //#endregion
3496
- export { EMPTY_PROJECTION, MAX_DEPTH, Markdown, coalesceText, codeBlockShape, codeSpanShape, collectList, collectTable, countIndent, createCodeBlockContract, createCodeSpanContract, createLineBreakContract, createMarkdown, createProjection, createTextContract, createThematicBreakContract, delimiterToAlignments, extractFence, extractHeading, extractListItem, flattenText, foldNode, htmlToMarkdown, isBlankLine, isBlockNode, isBlockquoteNode, isCodeBlockNode, isCodeSpanNode, isEmphasisNode, isEscapable, isFenceClose, isFenceWhitespace, isHeadingNode, isImageNode, isInlineNode, isLineBreakNode, isLinkNode, isListNode, isMarkdownDocument, isMarkdownNode, isParagraphNode, isQuote, isTableNode, isTableStart, isTextNode, isThematicBreak, isThematicBreakNode, isWhitespace, lineBreakShape, listItemMatchShape, markdownToHTML, mergeProjections, normalizeInlines, parseBlocks, parseDocument, parseInline, projectHTMLLeaf, projectHTMLNode, projectionToBlocks, projectionToInlines, renderHTML, renderMarkdown, rewriteDocument, scanCode, scanEmphasis, scanInline, scanLink, splitLines, splitTableRow, startsBlock, stripQuote, tableAlignShape, textShape, thematicBreakShape, trimInlines, unescapeText, walkNodes };
4088
+ export { EMPTY_PROJECTION, MAX_DEPTH, Markdown, coalesceText, codeBlockShape, codeSpanShape, collectList, collectTable, countIndent, createCodeBlockContract, createCodeSpanContract, createLineBreakContract, createMarkdown, createProjection, createTextContract, createThematicBreakContract, delimiterToAlignments, extractFence, extractHeading, extractListItem, flattenText, foldNode, htmlToMarkdown, isBlankLine, isBlockNode, isBlockquoteNode, isCodeBlockNode, isCodeSpanNode, isEmphasisNode, isEscapable, isFenceClose, isFenceWhitespace, isHeadingNode, isImageNode, isInlineNode, isLineBreakNode, isLinkNode, isListNode, isMarkdownDocument, isMarkdownNode, isParagraphNode, isQuote, isTableNode, isTableStart, isTextNode, isThematicBreak, isThematicBreakNode, isWhitespace, joinSources, lineBreakShape, listItemMatchShape, locateEmphasis, locateLink, markdownToHTML, mergeProjections, normalizeInlines, normalizeParagraphLine, parseBlocks, parseDocument, parseInline, parseProvenance, projectHTMLLeaf, projectHTMLNode, projectSpan, projectionToBlocks, projectionToInlines, renderHTML, renderMarkdown, rewriteDocument, scanCode, scanEmphasis, scanInline, scanInlineSource, scanLink, sliceSource, splitLines, splitTableRow, splitTableSources, startsBlock, stripQuote, tableAlignShape, textShape, thematicBreakShape, trimInlines, trimSource, unescapeText, walkNodes };
3497
4089
 
3498
4090
  //# sourceMappingURL=index.js.map