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