@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.
- package/dist/src/core/index.cjs +824 -222
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +439 -65
- package/dist/src/core/index.d.ts +439 -65
- package/dist/src/core/index.js +815 -223
- package/dist/src/core/index.js.map +1 -1
- package/package.json +16 -11
package/dist/src/core/index.cjs
CHANGED
|
@@ -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(
|
|
461
|
+
* parseBlocks(splitLines('# Hi'), 0) // [{ element: 'heading', level: 1, children: [...] }]
|
|
460
462
|
* ```
|
|
461
463
|
*/
|
|
462
|
-
function parseBlocks(lines, depth) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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:
|
|
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 =
|
|
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(
|
|
483
|
-
|
|
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
|
|
487
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
512
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
535
|
-
|
|
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
|
|
539
|
-
|
|
586
|
+
const source = joinSources(paragraph.map((paragraphLine, position) => normalizeParagraphLine(paragraphLine, position < paragraph.length - 1)), "\n");
|
|
587
|
+
const node = {
|
|
540
588
|
element: "paragraph",
|
|
541
|
-
children:
|
|
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
|
-
|
|
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
|
|
578
|
-
* block phase then the inline phase)
|
|
579
|
-
* the document is adopted AS-IS
|
|
580
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
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
|
|
1103
|
+
* @returns The document's lines with their original-string coordinates
|
|
968
1104
|
*
|
|
969
1105
|
* @example
|
|
970
1106
|
* ```ts
|
|
971
|
-
* splitLines('a\r\nb
|
|
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 =
|
|
976
|
-
|
|
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
|
-
*
|
|
999
|
-
*
|
|
1000
|
-
* `#`s
|
|
1001
|
-
*
|
|
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)
|
|
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
|
|
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
|
|
1016
|
-
text
|
|
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
|
-
*
|
|
1086
|
-
* blockquote line, so the de-quoted
|
|
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
|
|
1089
|
-
* @returns The
|
|
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'
|
|
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(
|
|
1097
|
-
return
|
|
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
|
|
1115
|
-
|
|
1116
|
-
for (let index = 0; index <
|
|
1117
|
-
const character =
|
|
1118
|
-
if (character === "\\" &&
|
|
1119
|
-
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
if ((0, _orkestrel_contract.isNonEmptyArray)(cells) && (0, _orkestrel_contract.isEmptyString)((cells[
|
|
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")
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
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:
|
|
1735
|
+
end: located.end
|
|
1322
1736
|
};
|
|
1323
1737
|
}
|
|
1324
1738
|
/**
|
|
1325
|
-
*
|
|
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
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
1460
|
-
children: link.
|
|
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 =
|
|
1467
|
-
if (link) {
|
|
1468
|
-
scanned =
|
|
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 =
|
|
1474
|
-
if (emphasis) {
|
|
1475
|
-
scanned =
|
|
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
|
-
|
|
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)
|
|
1495
|
-
|
|
1496
|
-
|
|
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(
|
|
2019
|
+
* collectTable(splitLines('| a |\n| - |'), 0) // { node: { element: 'table', ... }, next: 2 }
|
|
1511
2020
|
* ```
|
|
1512
2021
|
*/
|
|
1513
|
-
function collectTable(lines, start) {
|
|
1514
|
-
const headerCells =
|
|
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) =>
|
|
1517
|
-
|
|
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 =
|
|
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)
|
|
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(
|
|
2080
|
+
* collectList(splitLines('- item'), 0, 0) // { node: { element: 'list', ... }, next: 1 }
|
|
1551
2081
|
* ```
|
|
1552
2082
|
*/
|
|
1553
|
-
function collectList(lines, start, depth) {
|
|
1554
|
-
const
|
|
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(
|
|
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
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
let
|
|
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
|
-
|
|
1580
|
-
|
|
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(
|
|
2156
|
+
const parsed = extractListItem(text[index] ?? "");
|
|
1607
2157
|
if (!parsed || parsed.indent > topIndent || parsed.ordered !== ordered) break;
|
|
1608
|
-
const
|
|
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
|
|
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.
|
|
2178
|
+
itemLines.push(sliceSource(nextSource, continuation, next.length));
|
|
1624
2179
|
index += 1;
|
|
1625
2180
|
continue;
|
|
1626
2181
|
}
|
|
1627
|
-
if (extractListItem(next) || startsBlock(
|
|
1628
|
-
itemLines.push(
|
|
2182
|
+
if (extractListItem(next) || startsBlock(text, index)) break;
|
|
2183
|
+
itemLines.push(trimSource(nextSource));
|
|
1629
2184
|
index += 1;
|
|
1630
2185
|
}
|
|
1631
|
-
|
|
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
|
|
3174
|
-
*
|
|
3175
|
-
*
|
|
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
|
-
*
|
|
3179
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3840
|
+
if (changed) rebuilt = {
|
|
3270
3841
|
element: "document",
|
|
3271
3842
|
children: blocks
|
|
3272
3843
|
};
|
|
3273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|