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