@orkestrel/markdown 0.0.17 → 0.0.18

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.
@@ -1,6 +1,6 @@
1
1
  import { arrayOf, booleanShape, createContract, integerShape, isBoolean, isEmptyString, isNonEmptyArray, isNonEmptyString, isNumber, isString, lazyOf, literalOf, literalShape, nullableOf, objectShape, optionalShape, parseInteger, recordOf, stringShape, unionOf } from "@orkestrel/contract";
2
2
  import { HTML, SAFE_ATTRIBUTES, SAFE_URL_SCHEMES, TABLE_ALIGNMENTS, UNSAFE_ELEMENTS, attributeOf, collapseSpace, foldNode as foldNode$1, renderHTML as renderHTML$1, renderText, sanitizeURL } from "@orkestrel/html";
3
- //#region src/core/constants.ts
3
+ // src/core/constants.ts
4
4
  /**
5
5
  * Caps the recursion depth the parse pipeline (`parseDocument` and its
6
6
  * `parsers.ts` helpers), the `helpers.ts` traversal / projection functions
@@ -28,8 +28,7 @@ var EMPTY_PROJECTION = Object.freeze({
28
28
  cells: Object.freeze([]),
29
29
  rows: Object.freeze([])
30
30
  });
31
- //#endregion
32
- //#region src/core/validators.ts
31
+ // src/core/validators.ts
33
32
  /**
34
33
  * Determines whether a node is a heading block.
35
34
  *
@@ -358,8 +357,26 @@ var isMarkdownDocument = recordOf({
358
357
  element: literalOf("document"),
359
358
  children: arrayOf(isBlockNode)
360
359
  });
361
- //#endregion
362
- //#region src/core/parsers.ts
360
+ // src/core/MarkdownLineIterator.ts
361
+ /** Keeps a block parse's line cursor and its unconsumed lookahead. */
362
+ var MarkdownLineIterator = class extends Iterator {
363
+ #indices;
364
+ #lookahead;
365
+ constructor(indices) {
366
+ super();
367
+ this.#indices = indices;
368
+ this.#lookahead = indices.next();
369
+ }
370
+ get value() {
371
+ return this.#lookahead.done ? void 0 : this.#lookahead.value;
372
+ }
373
+ next() {
374
+ const result = this.#lookahead;
375
+ if (!result.done) this.#lookahead = this.#indices.next();
376
+ return result;
377
+ }
378
+ };
379
+ // src/core/parsers.ts
363
380
  /**
364
381
  * Parses a run of markdown lines into a block AST, recursing into nested
365
382
  * blockquotes, list items, and depth-capped degrade paragraphs.
@@ -396,41 +413,39 @@ function parseBlocks(lines, depth, spans = /* @__PURE__ */ new Map(), end) {
396
413
  return [paragraph];
397
414
  }
398
415
  const blocks = [];
399
- let index = 0;
400
- while (index < lines.length) {
416
+ const indices = new MarkdownLineIterator(lines.keys());
417
+ indices.reduce((index, step) => {
418
+ if (step < index) return index;
401
419
  const line = text[index] ?? "";
402
- if (isBlankLine(line)) {
403
- index += 1;
404
- continue;
405
- }
420
+ if (isBlankLine(line)) return index + 1;
406
421
  const fence = extractFence(line);
407
422
  if (fence) {
408
423
  const start = index;
409
424
  const body = [];
410
- let closed = false;
411
- index += 1;
412
- while (index < lines.length && !isFenceClose(text[index] ?? "", fence.marker)) {
413
- const bodyLine = lines[index];
414
- if (bodyLine !== void 0) body.push(bodyLine);
415
- index += 1;
416
- }
417
- if (index < lines.length) {
418
- closed = true;
419
- index += 1;
425
+ for (const cursor of indices) {
426
+ const closed = isFenceClose(text[cursor] ?? "", fence.marker);
427
+ body.push([
428
+ cursor,
429
+ closed,
430
+ closed ? void 0 : lines[cursor]
431
+ ]);
432
+ if (closed) break;
420
433
  }
434
+ const closed = body.at(-1)?.[1] ?? false;
435
+ const next = (body.at(-1)?.[0] ?? index) + 1;
421
436
  const node = {
422
437
  element: "codeBlock",
423
438
  ...fence.lang === void 0 ? {} : { lang: fence.lang },
424
- code: joinSources(body, "\n").text
439
+ code: joinSources(body.flatMap(([, , source]) => source === void 0 ? [] : [source]), "\n").text
425
440
  };
426
- const source = joinSources(lines.slice(start, index), "\n");
441
+ const source = joinSources(lines.slice(start, next), "\n");
427
442
  const span = projectSpan(source, 0, source.text.length);
428
443
  if (span !== void 0) spans.set(node, !closed && end !== void 0 ? {
429
444
  start: span.start,
430
445
  end
431
446
  } : span);
432
447
  blocks.push(node);
433
- continue;
448
+ return next;
434
449
  }
435
450
  if (isThematicBreak(line)) {
436
451
  const node = { element: "thematicBreak" };
@@ -438,8 +453,7 @@ function parseBlocks(lines, depth, spans = /* @__PURE__ */ new Map(), end) {
438
453
  const span = source === void 0 ? void 0 : projectSpan(source, 0, source.text.length);
439
454
  if (span !== void 0) spans.set(node, span);
440
455
  blocks.push(node);
441
- index += 1;
442
- continue;
456
+ return index + 1;
443
457
  }
444
458
  const heading = extractHeading(line);
445
459
  if (heading) {
@@ -456,57 +470,60 @@ function parseBlocks(lines, depth, spans = /* @__PURE__ */ new Map(), end) {
456
470
  const span = source === void 0 ? void 0 : projectSpan(source, 0, source.text.length);
457
471
  if (span !== void 0) spans.set(node, span);
458
472
  blocks.push(node);
459
- index += 1;
460
- continue;
473
+ return index + 1;
461
474
  }
462
475
  if (isQuote(line)) {
463
476
  const start = index;
464
- const quoted = [];
465
- while (index < lines.length && isQuote(text[index] ?? "")) {
466
- const quotedLine = lines[index];
477
+ const first = lines[index];
478
+ const quoted = first === void 0 ? [] : [[index, stripQuote(first)]];
479
+ while (indices.value !== void 0 && isQuote(text[indices.value] ?? "")) {
480
+ const cursor = indices.next().value;
481
+ if (cursor === void 0) break;
482
+ const quotedLine = lines[cursor];
467
483
  if (quotedLine === void 0) break;
468
- quoted.push(stripQuote(quotedLine));
469
- index += 1;
484
+ quoted.push([cursor, stripQuote(quotedLine)]);
470
485
  }
471
- const source = joinSources(lines.slice(start, index), "\n");
486
+ const next = quoted.length === 0 ? index : (quoted.at(-1)?.[0] ?? index) + 1;
487
+ const source = joinSources(lines.slice(start, next), "\n");
472
488
  const span = projectSpan(source, 0, source.text.length);
473
489
  const node = {
474
490
  element: "blockquote",
475
- children: parseBlocks(quoted, depth + 1, spans, index === lines.length && end !== void 0 ? end : span?.end)
491
+ children: parseBlocks(quoted.map(([, quotedLine]) => quotedLine), depth + 1, spans, next === lines.length && end !== void 0 ? end : span?.end)
476
492
  };
477
493
  if (span !== void 0) spans.set(node, span);
478
494
  blocks.push(node);
479
- continue;
495
+ return next;
480
496
  }
481
497
  if (isTableStart(line, text[index + 1])) {
482
498
  const table = collectTable(lines, index, spans);
483
499
  blocks.push(table.node);
484
- index = table.next;
485
- continue;
500
+ return table.next;
486
501
  }
487
502
  if (extractListItem(line)) {
488
503
  const list = collectList(lines, index, depth, spans, end);
489
504
  blocks.push(list.node);
490
- index = list.next;
491
- continue;
505
+ return list.next;
492
506
  }
493
507
  const start = index;
494
- const paragraph = [];
495
- while (index < lines.length && !isBlankLine(text[index] ?? "") && !(isNonEmptyArray(paragraph) && startsBlock(text, index))) {
496
- const paragraphLine = lines[index];
497
- if (paragraphLine !== void 0) paragraph.push(paragraphLine);
498
- index += 1;
508
+ const paragraph = [[index, lines[index]]];
509
+ while (indices.value !== void 0 && !isBlankLine(text[indices.value] ?? "") && !startsBlock(text, indices.value)) {
510
+ const cursor = indices.next().value;
511
+ if (cursor === void 0) break;
512
+ paragraph.push([cursor, lines[cursor]]);
499
513
  }
500
- const source = joinSources(paragraph.map((paragraphLine, position) => normalizeParagraphLine(paragraphLine, position < paragraph.length - 1)), "\n");
514
+ const next = paragraph.length === 0 ? index : (paragraph.at(-1)?.[0] ?? index) + 1;
515
+ const content = paragraph.flatMap(([, source]) => source === void 0 ? [] : [source]);
516
+ const source = joinSources(content.map((paragraphLine, position) => normalizeParagraphLine(paragraphLine, position < content.length - 1)), "\n");
501
517
  const node = {
502
518
  element: "paragraph",
503
519
  children: coalesceText(scanInlineSource(source, 0, source.text.length, spans), spans)
504
520
  };
505
- const region = joinSources(lines.slice(start, index), "\n");
521
+ const region = joinSources(lines.slice(start, next), "\n");
506
522
  const span = projectSpan(region, 0, region.text.length);
507
523
  if (span !== void 0) spans.set(node, span);
508
524
  blocks.push(node);
509
- }
525
+ return next;
526
+ }, 0);
510
527
  return blocks;
511
528
  }
512
529
  /**
@@ -533,6 +550,10 @@ function parseDocument(markdown) {
533
550
  * @param markdown - The markdown source to parse.
534
551
  * @returns The parsed document and its node-identity span map.
535
552
  *
553
+ * @remarks
554
+ * Parsing inserts into a scratch memo. The returned map contains only nodes of the
555
+ * final document, enumerated without the public walk's depth cap.
556
+ *
536
557
  * @example
537
558
  * ```ts
538
559
  * const [document, spans] = parseProvenance('# Hi')
@@ -540,16 +561,19 @@ function parseDocument(markdown) {
540
561
  * ```
541
562
  */
542
563
  function parseProvenance(markdown) {
543
- const spans = /* @__PURE__ */ new Map();
564
+ const memo = /* @__PURE__ */ new Map();
544
565
  const document = {
545
566
  element: "document",
546
- children: parseBlocks(splitLines(markdown), 0, spans, markdown.length)
567
+ children: parseBlocks(splitLines(markdown), 0, memo, markdown.length)
547
568
  };
548
- spans.set(document, {
569
+ memo.set(document, {
549
570
  start: 0,
550
571
  end: markdown.length
551
572
  });
552
- return [document, spans];
573
+ const nodes = [document];
574
+ for (const node of nodes) for (const child of listChildren(node)) nodes.push(child);
575
+ const present = new Set(nodes);
576
+ return [document, new Map([...memo].filter(([node]) => present.has(node)))];
553
577
  }
554
578
  /**
555
579
  * Parses inline markdown text (emphasis, code spans, links, images, and hard
@@ -567,8 +591,7 @@ function parseProvenance(markdown) {
567
591
  function parseInline(text) {
568
592
  return coalesceText(scanInline(text, 0, text.length));
569
593
  }
570
- //#endregion
571
- //#region src/core/helpers.ts
594
+ // src/core/helpers.ts
572
595
  /**
573
596
  * Splits a markdown document into offset-bearing lines while normalizing CRLF and
574
597
  * bare CR terminators at the line boundary. A single trailing terminator does not
@@ -583,36 +606,17 @@ function parseInline(text) {
583
606
  * ```
584
607
  */
585
608
  function splitLines(markdown) {
586
- const lines = [];
587
- let start = 0;
588
- let index = 0;
589
- while (index < markdown.length) {
590
- const character = markdown[index];
591
- if (character !== "\r" && character !== "\n") {
592
- index += 1;
593
- continue;
594
- }
595
- lines.push({
596
- text: markdown.slice(start, index),
609
+ return Iterator.from(markdown.matchAll(/([^\r\n]*)(?:\r\n|\r|\n|$)/g)).filter((match) => match.index < markdown.length || markdown.length === 0).map((match) => {
610
+ const text = match[1] ?? "";
611
+ return {
612
+ text,
597
613
  segments: [{
598
614
  offset: 0,
599
- start,
600
- end: index
615
+ start: match.index,
616
+ end: match.index + text.length
601
617
  }]
602
- });
603
- index += character === "\r" && markdown[index + 1] === "\n" ? 2 : 1;
604
- start = index;
605
- }
606
- lines.push({
607
- text: markdown.slice(start),
608
- segments: [{
609
- offset: 0,
610
- start,
611
- end: markdown.length
612
- }]
613
- });
614
- if (lines.length > 1 && lines[lines.length - 1]?.text === "") lines.pop();
615
- return lines;
618
+ };
619
+ }).toArray();
616
620
  }
617
621
  /**
618
622
  * Slices derived markdown text and narrows each intersecting source segment to the
@@ -632,24 +636,23 @@ function splitLines(markdown) {
632
636
  function sliceSource(source, from, to) {
633
637
  const start = Math.max(0, Math.min(from, source.text.length));
634
638
  const end = Math.max(start, Math.min(to, source.text.length));
635
- const segments = [];
636
- for (let index = 0; index < source.segments.length; index += 1) {
639
+ const segments = source.segments.keys().flatMap((index) => {
637
640
  const segment = source.segments[index];
638
- if (segment === void 0) continue;
641
+ if (segment === void 0) return [];
639
642
  const next = source.segments[index + 1];
640
643
  const limit = Math.min(segment.offset + (segment.end - segment.start), next === void 0 ? source.text.length : next.offset);
641
644
  const overlapStart = Math.max(start, segment.offset);
642
645
  const overlapEnd = Math.min(end, limit);
643
646
  const empty = segment.offset === limit && overlapStart === segment.offset;
644
- if (overlapStart >= overlapEnd && !empty) continue;
647
+ if (overlapStart >= overlapEnd && !empty) return [];
645
648
  const originalStart = overlapStart === limit ? segment.end : Math.min(segment.end, segment.start + overlapStart - segment.offset);
646
649
  const originalEnd = overlapEnd === limit ? segment.end : Math.min(segment.end, segment.start + overlapEnd - segment.offset);
647
- segments.push({
650
+ return [{
648
651
  offset: overlapStart - start,
649
652
  start: originalStart,
650
653
  end: originalEnd
651
- });
652
- }
654
+ }];
655
+ }).toArray();
653
656
  return {
654
657
  text: source.text.slice(start, end),
655
658
  segments
@@ -670,31 +673,34 @@ function sliceSource(source, from, to) {
670
673
  * ```
671
674
  */
672
675
  function joinSources(sources, separator) {
673
- let text = "";
676
+ const parts = [];
674
677
  const segments = [];
675
- for (let index = 0; index < sources.length; index += 1) {
678
+ sources.keys().reduce((offset, index) => {
676
679
  const source = sources[index];
677
- if (source === void 0) continue;
680
+ if (source === void 0) return offset;
678
681
  if (index > 0) {
679
682
  const previous = sources[index - 1];
680
683
  const left = previous?.segments[previous.segments.length - 1];
681
684
  const right = source.segments[0];
682
685
  if (separator.length > 0 && left !== void 0 && right !== void 0 && left.end < right.start) segments.push({
683
- offset: text.length,
686
+ offset,
684
687
  start: left.end,
685
688
  end: right.start
686
689
  });
687
- text += separator;
690
+ parts.push(separator);
688
691
  }
692
+ const start = offset + (index > 0 ? separator.length : 0);
689
693
  for (const segment of source.segments) segments.push({
690
- offset: text.length + segment.offset,
694
+ offset: start + segment.offset,
691
695
  start: segment.start,
692
696
  end: segment.end
693
697
  });
694
- text += source.text;
695
- }
698
+ const text = source.text;
699
+ parts.push(text);
700
+ return start + text.length;
701
+ }, 0);
696
702
  return {
697
- text,
703
+ text: parts.join(""),
698
704
  segments
699
705
  };
700
706
  }
@@ -715,27 +721,46 @@ function joinSources(sources, separator) {
715
721
  */
716
722
  function projectSpan(source, from, to) {
717
723
  if (from < 0 || to < from || to > source.text.length) return void 0;
718
- let start;
719
- let end;
720
- for (let index = 0; index < source.segments.length; index += 1) {
724
+ const [spanStart, spanEnd] = source.segments.keys().reduce(([start, end, found], index) => {
725
+ if (found) return [
726
+ start,
727
+ end,
728
+ found
729
+ ];
721
730
  const segment = source.segments[index];
722
- if (segment === void 0) continue;
731
+ if (segment === void 0) return [
732
+ start,
733
+ end,
734
+ false
735
+ ];
723
736
  const next = source.segments[index + 1];
724
737
  const limit = Math.min(segment.offset + (segment.end - segment.start), next === void 0 ? source.text.length : next.offset);
725
738
  if (from === to && from >= segment.offset && from <= limit) {
726
- if (next !== void 0 && from === next.offset) continue;
739
+ if (next !== void 0 && from === next.offset) return [
740
+ start,
741
+ end,
742
+ false
743
+ ];
727
744
  const position = from === limit ? segment.end : Math.min(segment.end, segment.start + from - segment.offset);
728
- return {
729
- start: position,
730
- end: position
731
- };
745
+ return [
746
+ position,
747
+ position,
748
+ true
749
+ ];
732
750
  }
733
- if (start === void 0 && from >= segment.offset && from < limit) start = segment.start + from - segment.offset;
734
- if (to > segment.offset && to <= limit) end = to === limit ? segment.end : Math.min(segment.end, segment.start + to - segment.offset);
735
- }
736
- return start === void 0 || end === void 0 ? void 0 : {
737
- start,
738
- end
751
+ return [
752
+ start === void 0 && from >= segment.offset && from < limit ? segment.start + from - segment.offset : start,
753
+ to > segment.offset && to <= limit ? to === limit ? segment.end : Math.min(segment.end, segment.start + to - segment.offset) : end,
754
+ false
755
+ ];
756
+ }, [
757
+ void 0,
758
+ void 0,
759
+ false
760
+ ]);
761
+ return spanStart === void 0 || spanEnd === void 0 ? void 0 : {
762
+ start: spanStart,
763
+ end: spanEnd
739
764
  };
740
765
  }
741
766
  /**
@@ -796,10 +821,7 @@ function normalizeParagraphLine(source, breaks) {
796
821
  * ```
797
822
  */
798
823
  function countIndent(line) {
799
- let count = 0;
800
- for (const character of line) if (character === " " || character === " ") count += 1;
801
- else break;
802
- return count;
824
+ return line.match(/^[ \t]*/)?.[0].length ?? 0;
803
825
  }
804
826
  /**
805
827
  * Checks whether `character` is whitespace under the emphasis flanking rule — a space, a
@@ -879,16 +901,8 @@ function isQuote(line) {
879
901
  */
880
902
  function isFenceClose(line, marker) {
881
903
  const character = marker[0] === "~" ? "~" : "`";
882
- let index = 0;
883
- while (index < line.length && isFenceWhitespace(line[index])) index++;
884
- let run = 0;
885
- while (index < line.length && line[index] === character) {
886
- run++;
887
- index++;
888
- }
889
- if (run < marker.length) return false;
890
- while (index < line.length && isFenceWhitespace(line[index])) index++;
891
- return index === line.length;
904
+ const trimmed = line.replace(/^[ \t\n\r\f\v]+|[ \t\n\r\f\v]+$/g, "");
905
+ return trimmed.length >= marker.length && Iterator.from(trimmed).every((value) => value === character);
892
906
  }
893
907
  /**
894
908
  * Checks whether `character` is a regex-`\s`-equivalent whitespace character — the
@@ -1094,12 +1108,11 @@ function splitTableRow(row) {
1094
1108
  function splitTableSources(row) {
1095
1109
  const source = trimSource(row);
1096
1110
  const cells = [];
1097
- let pieces = [];
1098
- let start = 0;
1099
- for (let index = 0; index < source.text.length; index += 1) {
1100
- const character = source.text[index];
1101
- if (character === "\\" && source.text[index + 1] === "|") {
1102
- pieces.push(sliceSource(source, start, index));
1111
+ const pieces = [];
1112
+ const [tailStart, tailOffset] = Iterator.from(source.text.matchAll(/\\\||\|/g)).reduce(([start, offset], match) => {
1113
+ const index = match.index;
1114
+ pieces.push(sliceSource(source, start, index));
1115
+ if (match[0] === "\\|") {
1103
1116
  const span = projectSpan(source, index, index + 2);
1104
1117
  pieces.push({
1105
1118
  text: "|",
@@ -1109,21 +1122,16 @@ function splitTableSources(row) {
1109
1122
  end: span.end
1110
1123
  }]
1111
1124
  });
1112
- index += 1;
1113
- start = index + 1;
1114
- continue;
1125
+ return [index + 2, offset];
1115
1126
  }
1116
- if (character !== "|") continue;
1117
- pieces.push(sliceSource(source, start, index));
1118
- cells.push(joinSources(pieces, ""));
1119
- pieces = [];
1120
- start = index + 1;
1121
- }
1122
- pieces.push(sliceSource(source, start, source.text.length));
1123
- cells.push(joinSources(pieces, ""));
1124
- if (isNonEmptyArray(cells) && isEmptyString((cells[0]?.text ?? "").trim())) cells.shift();
1125
- if (isNonEmptyArray(cells) && isEmptyString((cells[cells.length - 1]?.text ?? "").trim())) cells.pop();
1126
- return cells;
1127
+ cells.push(joinSources(pieces.slice(offset), ""));
1128
+ return [index + 1, pieces.length];
1129
+ }, [0, 0]);
1130
+ pieces.push(sliceSource(source, tailStart, source.text.length));
1131
+ cells.push(joinSources(pieces.slice(tailOffset), ""));
1132
+ const first = isEmptyString((cells[0]?.text ?? "").trim()) ? 1 : 0;
1133
+ const end = isEmptyString((cells.at(-1)?.text ?? "").trim()) ? cells.length - 1 : cells.length;
1134
+ return cells.slice(first, end);
1127
1135
  }
1128
1136
  /**
1129
1137
  * Derives the per-column {@link TableAlign} list from a GFM delimiter row — `:---`
@@ -1182,22 +1190,15 @@ function startsBlock(lines, index) {
1182
1190
  * ```
1183
1191
  */
1184
1192
  function unescapeText(text) {
1185
- let out = "";
1186
- for (let index = 0; index < text.length; index += 1) {
1187
- const character = text[index] ?? "";
1188
- if (character === "\\" && isEscapable(text[index + 1] ?? "")) {
1189
- out += text[index + 1] ?? "";
1190
- index += 1;
1191
- } else out += character;
1192
- }
1193
- return out;
1193
+ return text.replace(/\\([\s\S])/g, (match, character) => isEscapable(character) ? character : match);
1194
1194
  }
1195
1195
  /**
1196
1196
  * Merges adjacent text nodes into one — the inline scanner emits a text node per
1197
1197
  * unrecognized character, so coalescing keeps the AST clean and assertion-friendly.
1198
1198
  *
1199
1199
  * @param nodes - The inline nodes (possibly with adjacent text runs)
1200
- * @param spans - The optional operation-owned node span recorder
1200
+ * @param spans - The optional operation-owned span memo; inserts merged-node entries
1201
+ * without deleting or replacing constituent entries
1201
1202
  * @returns The nodes with consecutive text nodes concatenated
1202
1203
  *
1203
1204
  * @example
@@ -1207,28 +1208,53 @@ function unescapeText(text) {
1207
1208
  * ```
1208
1209
  */
1209
1210
  function coalesceText(nodes, spans) {
1210
- const out = [];
1211
- for (const node of nodes) {
1212
- const last = out[out.length - 1];
1213
- if (node.element === "text" && last !== void 0 && last.element === "text") {
1214
- const merged = {
1211
+ const result = Iterator.from(nodes).reduce(({ first, last, value, complete, log }, node) => {
1212
+ if (node.element === "text" && first?.element === "text") return {
1213
+ first,
1214
+ last: node,
1215
+ value: (value ?? first.value) + node.value,
1216
+ complete: complete && (spans?.has(node) ?? false),
1217
+ log
1218
+ };
1219
+ if (first !== void 0) {
1220
+ const merged = value === void 0 ? first : {
1215
1221
  element: "text",
1216
- value: last.value + node.value
1222
+ value
1217
1223
  };
1218
- const left = spans?.get(last);
1219
- const right = spans?.get(node);
1220
- if (spans !== void 0) {
1221
- spans.delete(last);
1222
- spans.delete(node);
1223
- if (left !== void 0 && right !== void 0) spans.set(merged, {
1224
- start: left.start,
1225
- end: right.end
1226
- });
1227
- }
1228
- out[out.length - 1] = merged;
1229
- } else out.push(node);
1230
- }
1231
- return out;
1224
+ const left = spans?.get(first);
1225
+ const right = last === void 0 ? void 0 : spans?.get(last);
1226
+ if (value !== void 0 && complete && left !== void 0 && right !== void 0) spans?.set(merged, {
1227
+ start: left.start,
1228
+ end: right.end
1229
+ });
1230
+ log.push(merged);
1231
+ }
1232
+ return {
1233
+ first: node,
1234
+ last: node,
1235
+ value: void 0,
1236
+ complete: spans?.has(node) ?? false,
1237
+ log
1238
+ };
1239
+ }, {
1240
+ first: void 0,
1241
+ last: void 0,
1242
+ value: void 0,
1243
+ complete: false,
1244
+ log: []
1245
+ });
1246
+ if (result.first === void 0) return result.log;
1247
+ const merged = result.value === void 0 ? result.first : {
1248
+ element: "text",
1249
+ value: result.value
1250
+ };
1251
+ const left = spans?.get(result.first);
1252
+ const right = result.last === void 0 ? void 0 : spans?.get(result.last);
1253
+ if (result.value !== void 0 && result.complete && left !== void 0 && right !== void 0) spans?.set(merged, {
1254
+ start: left.start,
1255
+ end: right.end
1256
+ });
1257
+ return [...result.log, merged];
1232
1258
  }
1233
1259
  /**
1234
1260
  * Scans an inline code span at `start` (a `` ` ``-run … a matching `` ` ``-run of the
@@ -1247,23 +1273,26 @@ function coalesceText(nodes, spans) {
1247
1273
  * ```
1248
1274
  */
1249
1275
  function scanCode(source, start, to) {
1250
- let run = 0;
1251
- while (start + run < to && source[start + run] === "`") run += 1;
1276
+ const run = source[start] === "`" ? countMarkdownRun(source, start, to) : 0;
1252
1277
  const open = "`".repeat(run);
1253
- let search = start + run;
1254
- for (;;) {
1278
+ const attempts = walkMarkdownCharacters(source, 0, source.length + 2);
1279
+ const [, result] = attempts.reduce(([search]) => {
1255
1280
  const closeAt = source.indexOf(open, search);
1256
- if (closeAt === -1 || closeAt + run > to) return void 0;
1281
+ if (closeAt === -1 || closeAt + run > to) {
1282
+ attempts.return(void 0);
1283
+ return [search, void 0];
1284
+ }
1257
1285
  if (source[closeAt - 1] !== "`" && source[closeAt + run] !== "`") {
1258
- let value = source.slice(start + run, closeAt);
1259
- if (value.length > 2 && value.startsWith(" ") && value.endsWith(" ") && value.trim().length > 0) value = value.slice(1, -1);
1260
- return {
1261
- value,
1286
+ attempts.return(void 0);
1287
+ const raw = source.slice(start + run, closeAt);
1288
+ return [search, {
1289
+ value: raw.length > 2 && raw.startsWith(" ") && raw.endsWith(" ") && raw.trim().length > 0 ? raw.slice(1, -1) : raw,
1262
1290
  end: closeAt + run
1263
- };
1291
+ }];
1264
1292
  }
1265
- search = closeAt + 1;
1266
- }
1293
+ return [closeAt + 1, void 0];
1294
+ }, [start + run, void 0]);
1295
+ return result;
1267
1296
  }
1268
1297
  /**
1269
1298
  * Locates a link `[text](href)` at `start` — the text runs to a balanced `]`, then `(`
@@ -1282,45 +1311,12 @@ function scanCode(source, start, to) {
1282
1311
  * ```
1283
1312
  */
1284
1313
  function locateLink(source, start, to) {
1285
- let bracketDepth = 0;
1286
- let close = -1;
1287
- for (let index = start; index < to; index += 1) {
1288
- const character = source[index] ?? "";
1289
- if (character === "\\") {
1290
- index += 1;
1291
- continue;
1292
- }
1293
- if (character === "[") bracketDepth += 1;
1294
- else if (character === "]") {
1295
- bracketDepth -= 1;
1296
- if (bracketDepth === 0) {
1297
- close = index;
1298
- break;
1299
- }
1300
- }
1301
- }
1302
- if (close === -1 || source[close + 1] !== "(") return void 0;
1303
- let parenDepth = 0;
1304
- let parenClose = -1;
1305
- for (let index = close + 1; index < to; index += 1) {
1306
- const character = source[index] ?? "";
1307
- if (character === "\\") {
1308
- index += 1;
1309
- continue;
1310
- }
1311
- if (character === "(") parenDepth += 1;
1312
- else if (character === ")") {
1313
- parenDepth -= 1;
1314
- if (parenDepth === 0) {
1315
- parenClose = index;
1316
- break;
1317
- }
1318
- }
1319
- }
1320
- if (parenClose === -1) return void 0;
1321
- return {
1314
+ const close = locateMarkdownDelimiter(source, start, to, "[", "]");
1315
+ if (close === void 0 || source[close + 1] !== "(") return void 0;
1316
+ const end = locateMarkdownDelimiter(source, close + 1, to, "(", ")");
1317
+ return end === void 0 ? void 0 : {
1322
1318
  close,
1323
- end: parenClose + 1
1319
+ end: end + 1
1324
1320
  };
1325
1321
  }
1326
1322
  /**
@@ -1376,44 +1372,38 @@ function scanLink(source, start, to, depth = 0) {
1376
1372
  */
1377
1373
  function locateEmphasis(source, start, to) {
1378
1374
  const marker = source[start] ?? "";
1379
- let run = 0;
1380
- while (start + run < to && source[start + run] === marker && run < 2) run += 1;
1375
+ const run = Math.min(2, countMarkdownRun(source, start, to));
1381
1376
  const strong = run === 2;
1382
1377
  const openEnd = start + run;
1383
1378
  if (openEnd >= to || isFlankingWhitespace(source[openEnd] ?? "")) return void 0;
1384
- let index = openEnd;
1385
- while (index < to) {
1386
- const character = source[index] ?? "";
1387
- if (character === "\\") {
1388
- index += 2;
1389
- continue;
1390
- }
1379
+ const characters = walkMarkdownCharacters(source, openEnd, to);
1380
+ const [, result] = characters.reduce(([next], [index, character]) => {
1381
+ if (index < next) return [next, void 0];
1382
+ if (character === "\\") return [index + 2, void 0];
1391
1383
  if (character === "`") {
1392
1384
  const span = scanCode(source, index, to);
1393
- index = span ? span.end : index + 1;
1394
- continue;
1385
+ return [span ? span.end : index + 1, void 0];
1395
1386
  }
1396
1387
  if ((character === "*" || character === "_") && character !== marker) {
1397
1388
  const nested = locateEmphasis(source, index, to);
1398
- if (nested !== void 0) {
1399
- index = nested.end;
1400
- continue;
1401
- }
1389
+ if (nested !== void 0) return [nested.end, void 0];
1402
1390
  }
1403
1391
  if (character === marker) {
1404
- let closeRun = 0;
1405
- while (index + closeRun < to && source[index + closeRun] === marker) closeRun += 1;
1406
- if (closeRun >= run && !isFlankingWhitespace(source[index - 1] ?? "")) return {
1407
- strong,
1408
- open: openEnd,
1409
- close: index,
1410
- end: index + run
1411
- };
1412
- index += closeRun;
1413
- continue;
1392
+ const closeRun = countMarkdownRun(source, index, to);
1393
+ if (closeRun >= run && !isFlankingWhitespace(source[index - 1] ?? "")) {
1394
+ characters.return(void 0);
1395
+ return [index + run, {
1396
+ strong,
1397
+ open: openEnd,
1398
+ close: index,
1399
+ end: index + run
1400
+ }];
1401
+ }
1402
+ return [index + closeRun, void 0];
1414
1403
  }
1415
- index += 1;
1416
- }
1404
+ return [index + 1, void 0];
1405
+ }, [openEnd, void 0]);
1406
+ return result;
1417
1407
  }
1418
1408
  /**
1419
1409
  * Scans an emphasis run at `start` (`*` / `_`, doubled for strong) — finds the nearest
@@ -1517,116 +1507,45 @@ function scanInlineSource(source, from, to, spans, depth = 0) {
1517
1507
  return [node];
1518
1508
  } else return [];
1519
1509
  const nodes = [];
1520
- let index = from;
1521
- let pending = "";
1522
- let pendingStart = from;
1523
- while (index < to) {
1524
- const character = source.text[index] ?? "";
1510
+ const parts = [];
1511
+ const [end, pendingStart, partOffset] = walkMarkdownCharacters(source.text, from, to).reduce(([next, start, offset], [index, character]) => {
1512
+ if (index < next) return [
1513
+ next,
1514
+ start,
1515
+ offset
1516
+ ];
1525
1517
  if (character === "\\" && index + 1 < to && isEscapable(source.text[index + 1] ?? "")) {
1526
- if (pending.length === 0) pendingStart = index;
1527
- pending += source.text[index + 1] ?? "";
1528
- index += 2;
1529
- continue;
1530
- }
1531
- if (character === " ") {
1532
- let spaceEnd = index;
1533
- while (spaceEnd < to && source.text[spaceEnd] === " ") spaceEnd += 1;
1534
- if (spaceEnd - index >= 2 && source.text[spaceEnd] === "\n") {
1535
- if (pending.length > 0) {
1536
- const node = {
1537
- element: "text",
1538
- value: pending
1539
- };
1540
- const span = projectSpan(source, pendingStart, index);
1541
- if (span !== void 0) spans.set(node, span);
1542
- nodes.push(node);
1543
- pending = "";
1544
- }
1545
- const node = { element: "break" };
1546
- const span = projectSpan(source, index, spaceEnd + 1);
1547
- if (span !== void 0) spans.set(node, span);
1548
- nodes.push(node);
1549
- index = spaceEnd + 1;
1550
- pendingStart = index;
1551
- continue;
1552
- }
1553
- }
1554
- let scanned;
1555
- let end = index;
1556
- if (character === "`") {
1557
- const span = scanCode(source.text, index, to);
1558
- if (span) {
1559
- scanned = {
1560
- element: "codeSpan",
1561
- value: span.value
1562
- };
1563
- end = span.end;
1564
- }
1565
- }
1566
- if (character === "!" && source.text[index + 1] === "[") {
1567
- const link = locateLink(source.text, index + 1, to);
1568
- if (link !== void 0) {
1569
- scanned = {
1570
- element: "image",
1571
- src: unescapeText(source.text.slice(link.close + 2, link.end - 1).trim()),
1572
- children: coalesceText(scanInlineSource(source, index + 2, link.close, spans, depth + 1), spans)
1573
- };
1574
- end = link.end;
1575
- }
1576
- }
1577
- if (character === "[") {
1578
- const link = locateLink(source.text, index, to);
1579
- if (link !== void 0) {
1580
- scanned = {
1581
- element: "link",
1582
- href: unescapeText(source.text.slice(link.close + 2, link.end - 1).trim()),
1583
- children: coalesceText(scanInlineSource(source, index + 1, link.close, spans, depth + 1), spans)
1584
- };
1585
- end = link.end;
1586
- }
1587
- }
1588
- if (character === "*" || character === "_") {
1589
- const emphasis = locateEmphasis(source.text, index, to);
1590
- if (emphasis !== void 0) {
1591
- scanned = {
1592
- element: "emphasis",
1593
- strong: emphasis.strong,
1594
- children: coalesceText(scanInlineSource(source, emphasis.open, emphasis.close, spans, depth + 1), spans)
1595
- };
1596
- end = emphasis.end;
1597
- }
1518
+ parts.push(source.text[index + 1] ?? "");
1519
+ return [
1520
+ index + 2,
1521
+ start,
1522
+ offset
1523
+ ];
1598
1524
  }
1525
+ const scanned = scanMarkdownInline(source, index, to, spans, depth);
1599
1526
  if (scanned !== void 0) {
1600
- if (pending.length > 0) {
1601
- const node = {
1602
- element: "text",
1603
- value: pending
1604
- };
1605
- const span = projectSpan(source, pendingStart, index);
1606
- if (span !== void 0) spans.set(node, span);
1607
- nodes.push(node);
1608
- pending = "";
1609
- }
1610
- const span = projectSpan(source, index, end);
1611
- if (span !== void 0) spans.set(scanned, span);
1612
- nodes.push(scanned);
1613
- index = end;
1614
- pendingStart = index;
1615
- continue;
1527
+ recordMarkdownText(source, parts.slice(offset).join(""), start, index, spans, nodes);
1528
+ const span = projectSpan(source, index, scanned.end);
1529
+ if (span !== void 0) spans.set(scanned.node, span);
1530
+ nodes.push(scanned.node);
1531
+ return [
1532
+ scanned.end,
1533
+ scanned.end,
1534
+ parts.length
1535
+ ];
1616
1536
  }
1617
- if (pending.length === 0) pendingStart = index;
1618
- pending += character;
1619
- index += 1;
1620
- }
1621
- if (pending.length > 0) {
1622
- const node = {
1623
- element: "text",
1624
- value: pending
1625
- };
1626
- const span = projectSpan(source, pendingStart, index);
1627
- if (span !== void 0) spans.set(node, span);
1628
- nodes.push(node);
1629
- }
1537
+ parts.push(character);
1538
+ return [
1539
+ index + 1,
1540
+ start,
1541
+ offset
1542
+ ];
1543
+ }, [
1544
+ from,
1545
+ from,
1546
+ 0
1547
+ ]);
1548
+ recordMarkdownText(source, parts.slice(partOffset).join(""), pendingStart, end, spans, nodes);
1630
1549
  return nodes;
1631
1550
  }
1632
1551
  /**
@@ -1654,30 +1573,28 @@ function collectTable(lines, start, spans = /* @__PURE__ */ new Map()) {
1654
1573
  return coalesceText(scanInlineSource(source, 0, source.text.length, spans), spans);
1655
1574
  });
1656
1575
  const align = delimiterToAlignments(lines[start + 1]?.text ?? "");
1657
- const padded = [];
1658
- for (let column = 0; column < columns; column += 1) padded.push(align[column] ?? null);
1659
- const rows = [];
1660
- let index = start + 2;
1661
- while (index < lines.length && !isBlankLine(lines[index]?.text ?? "") && (lines[index]?.text ?? "").includes("|")) {
1576
+ const padded = Array.from({ length: columns }, (_, column) => align[column] ?? null);
1577
+ const entries = [];
1578
+ for (const index of walkMarkdownLines(lines, start + 2)) {
1579
+ if (isBlankLine(lines[index]?.text ?? "") || !(lines[index]?.text ?? "").includes("|")) break;
1662
1580
  const cells = splitTableSources(lines[index] ?? {
1663
1581
  text: "",
1664
1582
  segments: []
1665
1583
  });
1666
- const row = [];
1667
- for (let column = 0; column < columns; column += 1) {
1584
+ const row = Array.from({ length: columns }, (_, column) => {
1668
1585
  const source = trimSource(cells[column] ?? {
1669
1586
  text: "",
1670
1587
  segments: []
1671
1588
  });
1672
- row.push(coalesceText(scanInlineSource(source, 0, source.text.length, spans), spans));
1673
- }
1674
- rows.push(row);
1675
- index += 1;
1589
+ return coalesceText(scanInlineSource(source, 0, source.text.length, spans), spans);
1590
+ });
1591
+ entries.push([index, row]);
1676
1592
  }
1593
+ const index = entries.length === 0 ? start + 2 : (entries.at(-1)?.[0] ?? start + 1) + 1;
1677
1594
  const node = {
1678
1595
  element: "table",
1679
1596
  header,
1680
- rows,
1597
+ rows: entries.map(([, row]) => row),
1681
1598
  align: padded
1682
1599
  };
1683
1600
  const source = joinSources(lines.slice(start, index), "\n");
@@ -1712,23 +1629,20 @@ function collectList(lines, start, depth, spans = /* @__PURE__ */ new Map(), end
1712
1629
  const topIndent = first?.indent ?? 0;
1713
1630
  const items = [];
1714
1631
  const chain = [];
1715
- let nested = true;
1716
- for (let cursor = start; cursor < lines.length; cursor += 1) {
1632
+ const nested = walkMarkdownLines(lines, start).every((cursor) => {
1717
1633
  const parsed = extractListItem(text[cursor] ?? "");
1718
1634
  const previous = chain[chain.length - 1];
1719
- if (parsed === void 0 || previous !== void 0 && (previous.content.length > 0 || parsed.indent !== previous.marker)) {
1720
- nested = false;
1721
- break;
1722
- }
1635
+ if (parsed === void 0 || previous !== void 0 && (previous.content.length > 0 || parsed.indent !== previous.marker)) return false;
1723
1636
  chain.push(parsed);
1724
- }
1637
+ return true;
1638
+ });
1725
1639
  const remaining = 64 - depth;
1726
1640
  if (nested && remaining > 0 && chain.length > remaining) {
1727
1641
  const terminal = chain[remaining - 1];
1728
1642
  const terminalLine = lines[start + remaining - 1];
1729
1643
  if (terminal !== void 0 && terminalLine !== void 0) {
1730
1644
  const sources = [sliceSource(terminalLine, terminal.marker, terminalLine.text.length)];
1731
- for (let cursor = start + remaining; cursor < lines.length; cursor += 1) {
1645
+ for (const cursor of walkMarkdownLines(lines, start + remaining)) {
1732
1646
  const line = lines[cursor];
1733
1647
  if (line !== void 0) sources.push(sliceSource(line, terminal.marker, line.text.length));
1734
1648
  }
@@ -1746,16 +1660,16 @@ function collectList(lines, start, depth, spans = /* @__PURE__ */ new Map(), end
1746
1660
  spans.set(textNode, residualSpan);
1747
1661
  spans.set(paragraph, residualSpan);
1748
1662
  }
1749
- let children = [paragraph];
1750
- let node;
1751
- for (let cursor = remaining - 1; cursor >= 0; cursor -= 1) {
1663
+ const node = Array.from({ length: Math.ceil(remaining) }, (_, step) => step).reduce((child, step) => {
1664
+ const cursor = remaining - 1 - step;
1665
+ const children = child === void 0 ? [paragraph] : [child];
1752
1666
  const parsed = chain[cursor];
1753
- if (parsed === void 0) continue;
1667
+ if (parsed === void 0) return child;
1754
1668
  const item = {
1755
1669
  element: "listItem",
1756
1670
  children
1757
1671
  };
1758
- node = {
1672
+ const parent = {
1759
1673
  element: "list",
1760
1674
  ordered: parsed.ordered,
1761
1675
  start: parsed.start,
@@ -1765,72 +1679,78 @@ function collectList(lines, start, depth, spans = /* @__PURE__ */ new Map(), end
1765
1679
  const span = projectSpan(region, 0, region.text.length);
1766
1680
  if (span !== void 0) {
1767
1681
  spans.set(item, span);
1768
- spans.set(node, span);
1682
+ spans.set(parent, span);
1769
1683
  }
1770
- children = [node];
1771
- }
1684
+ return parent;
1685
+ }, void 0);
1772
1686
  if (node !== void 0) return {
1773
1687
  node,
1774
1688
  next: lines.length
1775
1689
  };
1776
1690
  }
1777
1691
  }
1778
- let index = start;
1779
- while (index < lines.length) {
1692
+ const positions = walkMarkdownLines(lines, start);
1693
+ const finish = positions.reduce((index, position) => {
1694
+ if (position < index) return index;
1780
1695
  const parsed = extractListItem(text[index] ?? "");
1781
- if (!parsed || parsed.indent > topIndent || parsed.ordered !== ordered) break;
1696
+ if (!parsed || parsed.indent > topIndent || parsed.ordered !== ordered) {
1697
+ positions.return(void 0);
1698
+ return index;
1699
+ }
1782
1700
  const itemStart = index;
1783
1701
  const itemLine = lines[index];
1784
- if (itemLine === void 0) break;
1785
- const itemLines = [sliceSource(itemLine, parsed.marker, itemLine.text.length)];
1702
+ if (itemLine === void 0) {
1703
+ positions.return(void 0);
1704
+ return index;
1705
+ }
1706
+ const entries = [[index, sliceSource(itemLine, parsed.marker, itemLine.text.length)]];
1786
1707
  const continuation = parsed.marker;
1787
- index += 1;
1788
- while (index < lines.length) {
1789
- const nextSource = lines[index];
1708
+ for (const cursor of walkMarkdownLines(lines, index + 1)) {
1709
+ const nextSource = lines[cursor];
1790
1710
  if (nextSource === void 0) break;
1791
1711
  const next = nextSource.text;
1792
1712
  if (isBlankLine(next)) {
1793
- const after = lines[index + 1]?.text ?? "";
1794
- if (index + 1 < lines.length && !isBlankLine(after) && countIndent(after) >= continuation) {
1795
- itemLines.push(sliceSource(nextSource, 0, 0));
1796
- index += 1;
1713
+ const after = lines[cursor + 1]?.text ?? "";
1714
+ if (cursor + 1 < lines.length && !isBlankLine(after) && countIndent(after) >= continuation) {
1715
+ entries.push([cursor, sliceSource(nextSource, 0, 0)]);
1797
1716
  continue;
1798
1717
  }
1799
1718
  break;
1800
1719
  }
1801
1720
  if (countIndent(next) >= continuation) {
1802
- itemLines.push(sliceSource(nextSource, continuation, next.length));
1803
- index += 1;
1721
+ entries.push([cursor, sliceSource(nextSource, continuation, next.length)]);
1804
1722
  continue;
1805
1723
  }
1806
- if (extractListItem(next) || startsBlock(text, index)) break;
1807
- itemLines.push(trimSource(nextSource));
1808
- index += 1;
1724
+ if (extractListItem(next) || startsBlock(text, cursor)) break;
1725
+ entries.push([cursor, trimSource(nextSource)]);
1809
1726
  }
1727
+ const next = (entries.at(-1)?.[0] ?? index) + 1;
1728
+ const itemLines = entries.map(([, line]) => line);
1810
1729
  const tail = itemLines[itemLines.length - 1];
1811
1730
  const segment = tail?.segments[tail.segments.length - 1];
1812
- const itemEnd = index === lines.length && end !== void 0 ? end : segment?.end;
1731
+ const itemEnd = next === lines.length && end !== void 0 ? end : segment?.end;
1813
1732
  const item = {
1814
1733
  element: "listItem",
1815
1734
  children: parseBlocks(itemLines, depth + 1, spans, itemEnd)
1816
1735
  };
1817
- const source = joinSources(lines.slice(itemStart, index), "\n");
1736
+ const source = joinSources(lines.slice(itemStart, next), "\n");
1818
1737
  const span = projectSpan(source, 0, source.text.length);
1819
1738
  if (span !== void 0) spans.set(item, span);
1820
1739
  items.push(item);
1821
- }
1740
+ return next;
1741
+ }, start);
1822
1742
  const node = {
1823
1743
  element: "list",
1824
1744
  ordered,
1825
1745
  start: startOrdinal,
1826
1746
  items
1827
1747
  };
1828
- const source = joinSources(lines.slice(start, index), "\n");
1748
+ const source = joinSources(lines.slice(start, finish), "\n");
1829
1749
  const span = projectSpan(source, 0, source.text.length);
1830
1750
  if (span !== void 0) spans.set(node, span);
1831
1751
  return {
1832
1752
  node,
1833
- next: index
1753
+ next: finish
1834
1754
  };
1835
1755
  }
1836
1756
  /**
@@ -1873,7 +1793,7 @@ function markdownToHTML(node) {
1873
1793
  continue;
1874
1794
  }
1875
1795
  const children = [];
1876
- let depth = frame.depth;
1796
+ const depth = frame.depth + (current.element === "document" ? 0 : current.element === "table" ? 4 : 1);
1877
1797
  switch (current.element) {
1878
1798
  case "document":
1879
1799
  for (const child of current.children) if (child !== void 0) children.push(child);
@@ -1882,24 +1802,20 @@ function markdownToHTML(node) {
1882
1802
  case "paragraph":
1883
1803
  case "blockquote":
1884
1804
  for (const child of current.children) if (child !== void 0) children.push(child);
1885
- depth += 1;
1886
1805
  break;
1887
1806
  case "listItem": {
1888
1807
  const only = current.children[0];
1889
1808
  if (current.children.length === 1 && only !== void 0 && only.element === "paragraph") {
1890
1809
  for (const child of only.children) if (child !== void 0) children.push(child);
1891
1810
  } else for (const child of current.children) if (child !== void 0) children.push(child);
1892
- depth += 1;
1893
1811
  break;
1894
1812
  }
1895
1813
  case "emphasis":
1896
1814
  case "link":
1897
1815
  for (const child of current.children) if (child !== void 0) children.push(child);
1898
- depth += 1;
1899
1816
  break;
1900
1817
  case "list":
1901
1818
  for (const child of current.items) if (child !== void 0) children.push(child);
1902
- depth += 1;
1903
1819
  break;
1904
1820
  case "table":
1905
1821
  if (frame.depth + 4 > 64) {
@@ -1914,7 +1830,6 @@ function markdownToHTML(node) {
1914
1830
  for (const child of cell) if (child !== void 0) children.push(child);
1915
1831
  }
1916
1832
  }
1917
- depth += 4;
1918
1833
  }
1919
1834
  if (current.element === "codeBlock" && frame.depth + 2 > 64) {
1920
1835
  values.push(void 0);
@@ -1925,7 +1840,7 @@ function markdownToHTML(node) {
1925
1840
  expanded: true,
1926
1841
  count: children.length
1927
1842
  });
1928
- for (let index = children.length - 1; index >= 0; index -= 1) {
1843
+ for (const index of children.keys().toArray().toReversed()) {
1929
1844
  const child = children[index];
1930
1845
  if (child !== void 0) stack.push({
1931
1846
  node: child,
@@ -1936,231 +1851,207 @@ function markdownToHTML(node) {
1936
1851
  }
1937
1852
  continue;
1938
1853
  }
1939
- const children = frame.count === 0 ? [] : values.splice(values.length - frame.count, frame.count);
1940
- const projected = [];
1941
- for (const child of children) if (child !== void 0) projected.push(child);
1942
- let value;
1943
- switch (current.element) {
1944
- case "document":
1945
- value = {
1946
- category: "document",
1947
- children: projected
1948
- };
1949
- break;
1950
- case "heading":
1951
- value = {
1952
- category: "element",
1953
- name: `h${current.level}`,
1954
- attributes: [],
1955
- children: projected
1956
- };
1957
- break;
1958
- case "paragraph":
1959
- value = {
1960
- category: "element",
1961
- name: "p",
1962
- attributes: [],
1963
- children: projected
1964
- };
1965
- break;
1966
- case "thematicBreak":
1967
- value = {
1968
- category: "element",
1969
- name: "hr",
1970
- attributes: [],
1971
- children: []
1972
- };
1973
- break;
1974
- case "blockquote":
1975
- value = {
1976
- category: "element",
1977
- name: "blockquote",
1978
- attributes: [],
1979
- children: projected
1980
- };
1981
- break;
1982
- case "codeBlock":
1983
- value = {
1984
- category: "element",
1985
- name: "pre",
1986
- attributes: [],
1987
- children: [{
1988
- category: "element",
1989
- name: "code",
1990
- attributes: current.lang === void 0 ? [] : [{
1991
- name: "class",
1992
- value: `language-${current.lang}`
1993
- }],
1994
- children: [{
1995
- category: "text",
1996
- value: current.code
1997
- }]
1998
- }]
1999
- };
2000
- break;
2001
- case "list":
2002
- value = {
2003
- category: "element",
2004
- name: current.ordered ? "ol" : "ul",
2005
- attributes: current.ordered && current.start !== 1 ? [{
2006
- name: "start",
2007
- value: String(current.start)
2008
- }] : [],
2009
- children: projected
2010
- };
2011
- break;
2012
- case "listItem":
2013
- value = {
1854
+ const value = projectMarkdownNode(current, frame.count === 0 ? [] : values.splice(values.length - frame.count, frame.count));
1855
+ values.push(value);
1856
+ }
1857
+ const projected = values[0];
1858
+ if (projected?.category === "document") return projected;
1859
+ return {
1860
+ category: "document",
1861
+ children: projected === void 0 ? [] : [projected]
1862
+ };
1863
+ }
1864
+ /**
1865
+ * Projects one markdown node from its already projected children.
1866
+ *
1867
+ * @example
1868
+ * ```ts
1869
+ * projectMarkdownNode({ element: 'text', value: 'hi' }, []) // { category: 'text', value: 'hi' }
1870
+ * ```
1871
+ */
1872
+ function projectMarkdownNode(current, children) {
1873
+ const projected = [];
1874
+ for (const child of children) if (child !== void 0) projected.push(child);
1875
+ switch (current.element) {
1876
+ case "document": return {
1877
+ category: "document",
1878
+ children: projected
1879
+ };
1880
+ case "heading": return {
1881
+ category: "element",
1882
+ name: `h${current.level}`,
1883
+ attributes: [],
1884
+ children: projected
1885
+ };
1886
+ case "paragraph": return {
1887
+ category: "element",
1888
+ name: "p",
1889
+ attributes: [],
1890
+ children: projected
1891
+ };
1892
+ case "thematicBreak": return {
1893
+ category: "element",
1894
+ name: "hr",
1895
+ attributes: [],
1896
+ children: []
1897
+ };
1898
+ case "blockquote": return {
1899
+ category: "element",
1900
+ name: "blockquote",
1901
+ attributes: [],
1902
+ children: projected
1903
+ };
1904
+ case "codeBlock": return {
1905
+ category: "element",
1906
+ name: "pre",
1907
+ attributes: [],
1908
+ children: [{
1909
+ category: "element",
1910
+ name: "code",
1911
+ attributes: current.lang === void 0 ? [] : [{
1912
+ name: "class",
1913
+ value: `language-${current.lang}`
1914
+ }],
1915
+ children: [{
1916
+ category: "text",
1917
+ value: current.code
1918
+ }]
1919
+ }]
1920
+ };
1921
+ case "list": return {
1922
+ category: "element",
1923
+ name: current.ordered ? "ol" : "ul",
1924
+ attributes: current.ordered && current.start !== 1 ? [{
1925
+ name: "start",
1926
+ value: String(current.start)
1927
+ }] : [],
1928
+ children: projected
1929
+ };
1930
+ case "listItem": return {
1931
+ category: "element",
1932
+ name: "li",
1933
+ attributes: [],
1934
+ children: projected
1935
+ };
1936
+ case "table": {
1937
+ const outputs = children.values();
1938
+ const header = [];
1939
+ for (const [column, cell] of current.header.entries()) {
1940
+ if (cell === void 0) continue;
1941
+ const align = current.align[column];
1942
+ const attributes = align === "left" || align === "right" || align === "center" ? [{
1943
+ name: "align",
1944
+ value: align
1945
+ }] : [];
1946
+ const count = Iterator.from(cell).filter((child) => child !== void 0).reduce((sum) => sum + 1, 0);
1947
+ const cellChildren = [];
1948
+ for (const child of outputs.take(count).toArray()) if (child !== void 0) cellChildren.push(child);
1949
+ header.push({
2014
1950
  category: "element",
2015
- name: "li",
2016
- attributes: [],
2017
- children: projected
2018
- };
2019
- break;
2020
- case "table": {
2021
- let offset = 0;
2022
- const header = [];
2023
- for (const [column, cell] of current.header.entries()) {
1951
+ name: "th",
1952
+ attributes,
1953
+ children: cellChildren
1954
+ });
1955
+ }
1956
+ const rows = [];
1957
+ for (const row of current.rows) {
1958
+ const cells = [];
1959
+ for (const [column, cell] of row.entries()) {
2024
1960
  if (cell === void 0) continue;
2025
1961
  const align = current.align[column];
2026
1962
  const attributes = align === "left" || align === "right" || align === "center" ? [{
2027
1963
  name: "align",
2028
1964
  value: align
2029
1965
  }] : [];
2030
- let count = 0;
2031
- for (const child of cell) if (child !== void 0) count += 1;
1966
+ const count = Iterator.from(cell).filter((child) => child !== void 0).reduce((sum) => sum + 1, 0);
2032
1967
  const cellChildren = [];
2033
- for (const child of children.slice(offset, offset + count)) if (child !== void 0) cellChildren.push(child);
2034
- header.push({
1968
+ for (const child of outputs.take(count).toArray()) if (child !== void 0) cellChildren.push(child);
1969
+ cells.push({
2035
1970
  category: "element",
2036
- name: "th",
1971
+ name: "td",
2037
1972
  attributes,
2038
1973
  children: cellChildren
2039
1974
  });
2040
- offset += count;
2041
- }
2042
- const rows = [];
2043
- for (const row of current.rows) {
2044
- const cells = [];
2045
- for (const [column, cell] of row.entries()) {
2046
- if (cell === void 0) continue;
2047
- const align = current.align[column];
2048
- const attributes = align === "left" || align === "right" || align === "center" ? [{
2049
- name: "align",
2050
- value: align
2051
- }] : [];
2052
- let count = 0;
2053
- for (const child of cell) if (child !== void 0) count += 1;
2054
- const cellChildren = [];
2055
- for (const child of children.slice(offset, offset + count)) if (child !== void 0) cellChildren.push(child);
2056
- cells.push({
2057
- category: "element",
2058
- name: "td",
2059
- attributes,
2060
- children: cellChildren
2061
- });
2062
- offset += count;
2063
- }
2064
- rows.push({
2065
- category: "element",
2066
- name: "tr",
2067
- attributes: [],
2068
- children: cells
2069
- });
2070
1975
  }
2071
- const tableChildren = [{
2072
- category: "element",
2073
- name: "thead",
2074
- attributes: [],
2075
- children: [{
2076
- category: "element",
2077
- name: "tr",
2078
- attributes: [],
2079
- children: header
2080
- }]
2081
- }];
2082
- if (isNonEmptyArray(current.rows)) tableChildren.push({
1976
+ rows.push({
2083
1977
  category: "element",
2084
- name: "tbody",
1978
+ name: "tr",
2085
1979
  attributes: [],
2086
- children: rows
1980
+ children: cells
2087
1981
  });
2088
- value = {
2089
- category: "element",
2090
- name: "table",
2091
- attributes: [],
2092
- children: tableChildren
2093
- };
2094
- break;
2095
1982
  }
2096
- case "text":
2097
- value = {
2098
- category: "text",
2099
- value: current.value
2100
- };
2101
- break;
2102
- case "emphasis":
2103
- value = {
2104
- category: "element",
2105
- name: current.strong ? "strong" : "em",
2106
- attributes: [],
2107
- children: projected
2108
- };
2109
- break;
2110
- case "codeSpan":
2111
- value = {
2112
- category: "element",
2113
- name: "code",
2114
- attributes: [],
2115
- children: [{
2116
- category: "text",
2117
- value: current.value
2118
- }]
2119
- };
2120
- break;
2121
- case "link":
2122
- value = {
2123
- category: "element",
2124
- name: "a",
2125
- attributes: [{
2126
- name: "href",
2127
- value: current.href
2128
- }],
2129
- children: projected
2130
- };
2131
- break;
2132
- case "image":
2133
- value = {
2134
- category: "element",
2135
- name: "img",
2136
- attributes: [{
2137
- name: "src",
2138
- value: current.src
2139
- }, {
2140
- name: "alt",
2141
- value: flattenText(current)
2142
- }],
2143
- children: []
2144
- };
2145
- break;
2146
- case "break":
2147
- value = {
1983
+ const tableChildren = [{
1984
+ category: "element",
1985
+ name: "thead",
1986
+ attributes: [],
1987
+ children: [{
2148
1988
  category: "element",
2149
- name: "br",
1989
+ name: "tr",
2150
1990
  attributes: [],
2151
- children: []
2152
- };
2153
- break;
2154
- default: value = void 0;
1991
+ children: header
1992
+ }]
1993
+ }];
1994
+ if (isNonEmptyArray(current.rows)) tableChildren.push({
1995
+ category: "element",
1996
+ name: "tbody",
1997
+ attributes: [],
1998
+ children: rows
1999
+ });
2000
+ return {
2001
+ category: "element",
2002
+ name: "table",
2003
+ attributes: [],
2004
+ children: tableChildren
2005
+ };
2155
2006
  }
2156
- values.push(value);
2007
+ case "text": return {
2008
+ category: "text",
2009
+ value: current.value
2010
+ };
2011
+ case "emphasis": return {
2012
+ category: "element",
2013
+ name: current.strong ? "strong" : "em",
2014
+ attributes: [],
2015
+ children: projected
2016
+ };
2017
+ case "codeSpan": return {
2018
+ category: "element",
2019
+ name: "code",
2020
+ attributes: [],
2021
+ children: [{
2022
+ category: "text",
2023
+ value: current.value
2024
+ }]
2025
+ };
2026
+ case "link": return {
2027
+ category: "element",
2028
+ name: "a",
2029
+ attributes: [{
2030
+ name: "href",
2031
+ value: current.href
2032
+ }],
2033
+ children: projected
2034
+ };
2035
+ case "image": return {
2036
+ category: "element",
2037
+ name: "img",
2038
+ attributes: [{
2039
+ name: "src",
2040
+ value: current.src
2041
+ }, {
2042
+ name: "alt",
2043
+ value: flattenText(current)
2044
+ }],
2045
+ children: []
2046
+ };
2047
+ case "break": return {
2048
+ category: "element",
2049
+ name: "br",
2050
+ attributes: [],
2051
+ children: []
2052
+ };
2053
+ default: return;
2157
2054
  }
2158
- const projected = values[0];
2159
- if (projected?.category === "document") return projected;
2160
- return {
2161
- category: "document",
2162
- children: projected === void 0 ? [] : [projected]
2163
- };
2164
2055
  }
2165
2056
  /**
2166
2057
  * Renders a {@link MarkdownNode} to its canonical markdown source — the inverse
@@ -2199,7 +2090,7 @@ function renderMarkdown(node) {
2199
2090
  count: 0,
2200
2091
  escaped: "",
2201
2092
  escapeBang: false,
2202
- nesting: 0
2093
+ emphasis: 0
2203
2094
  }];
2204
2095
  const values = [];
2205
2096
  while (stack.length > 0) {
@@ -2207,51 +2098,14 @@ function renderMarkdown(node) {
2207
2098
  if (frame === void 0) continue;
2208
2099
  const current = frame.node;
2209
2100
  if (!frame.expanded) {
2210
- let escaped = "";
2211
- if ((frame.depth >= 64 || current.element === "text") && "value" in current && isString(current.value)) for (let index = 0; index < current.value.length; index += 1) {
2212
- const character = current.value[index] ?? "";
2213
- const atLineStart = index === 0 || current.value[index - 1] === "\n";
2214
- if (current.element === "text" && character === "!" && index === current.value.length - 1 && frame.escapeBang) {
2215
- escaped += "\\!";
2216
- continue;
2217
- }
2218
- if (character === "\\" || character === "*" || character === "_" || character === "`" || character === "[" || character === "]") {
2219
- escaped += `\\${character}`;
2220
- continue;
2221
- }
2222
- if (atLineStart) {
2223
- if (character === "#" || character === ">") {
2224
- escaped += `\\${character}`;
2225
- continue;
2226
- }
2227
- if ((character === "-" || character === "~") && current.value[index + 1] === character && current.value[index + 2] === character) {
2228
- escaped += `\\${character}`;
2229
- continue;
2230
- }
2231
- if ((character === "-" || character === "+") && (current.value[index + 1] ?? " ") === " ") {
2232
- escaped += `\\${character}`;
2233
- continue;
2234
- }
2235
- if (/[0-9]/.test(character)) {
2236
- let end = index;
2237
- while (end < current.value.length && /[0-9]/.test(current.value[end] ?? "")) end += 1;
2238
- const marker = current.value[end];
2239
- if ((marker === "." || marker === ")") && current.value[end + 1] === " ") {
2240
- escaped += `${current.value.slice(index, end)}\\${marker}`;
2241
- index = end;
2242
- continue;
2243
- }
2244
- }
2245
- }
2246
- escaped += character;
2247
- }
2101
+ const escaped = (frame.depth >= 64 || current.element === "text") && "value" in current && isString(current.value) ? escapeMarkdownText(current.value, current.element === "text" && frame.escapeBang) : "";
2248
2102
  if (frame.depth >= 64) {
2249
2103
  values.push(escaped);
2250
2104
  continue;
2251
2105
  }
2252
2106
  const groups = [];
2253
2107
  const adjacent = [];
2254
- let depth = frame.depth + 1;
2108
+ const depth = frame.depth + (current.element === "table" ? 2 : 1);
2255
2109
  switch (current.element) {
2256
2110
  case "document":
2257
2111
  case "blockquote":
@@ -2278,7 +2132,7 @@ function renderMarkdown(node) {
2278
2132
  }
2279
2133
  for (const row of current.rows) {
2280
2134
  if (row === void 0) continue;
2281
- for (let column = 0; column < current.header.length; column += 1) {
2135
+ for (const column of current.header.keys()) {
2282
2136
  const cell = row[column];
2283
2137
  if (cell !== void 0) {
2284
2138
  groups.push(cell);
@@ -2286,26 +2140,17 @@ function renderMarkdown(node) {
2286
2140
  }
2287
2141
  }
2288
2142
  }
2289
- depth += 1;
2290
2143
  }
2291
2144
  const children = [];
2292
2145
  const escapeBangs = [];
2293
- for (let groupIndex = 0; groupIndex < groups.length; groupIndex += 1) {
2146
+ for (const groupIndex of groups.keys()) {
2294
2147
  const group = groups[groupIndex];
2295
2148
  if (group === void 0) continue;
2296
- for (let position = 0; position < group.length; position += 1) {
2297
- const child = group[position];
2149
+ const siblings = group.filter((child) => child !== void 0);
2150
+ for (const position of siblings.keys()) {
2151
+ const child = siblings[position];
2298
2152
  if (child === void 0) continue;
2299
- let escapeBang = false;
2300
- if (adjacent[groupIndex] === true) {
2301
- let nextPosition = position + 1;
2302
- let next = group[nextPosition];
2303
- while (next === void 0 && nextPosition < group.length) {
2304
- nextPosition += 1;
2305
- next = group[nextPosition];
2306
- }
2307
- escapeBang = next?.element === "link";
2308
- }
2153
+ const escapeBang = (adjacent[groupIndex] === true ? siblings[position + 1] : void 0)?.element === "link";
2309
2154
  children.push(child);
2310
2155
  escapeBangs.push(escapeBang);
2311
2156
  }
@@ -2316,8 +2161,8 @@ function renderMarkdown(node) {
2316
2161
  count: children.length,
2317
2162
  escaped
2318
2163
  });
2319
- const nesting = current.element === "emphasis" ? frame.nesting + 1 : frame.nesting;
2320
- for (let index = children.length - 1; index >= 0; index -= 1) {
2164
+ const emphasis = current.element === "emphasis" ? frame.emphasis + 1 : frame.emphasis;
2165
+ for (const index of children.keys().toArray().toReversed()) {
2321
2166
  const child = children[index];
2322
2167
  if (child !== void 0) stack.push({
2323
2168
  node: child,
@@ -2326,136 +2171,112 @@ function renderMarkdown(node) {
2326
2171
  count: 0,
2327
2172
  escaped: "",
2328
2173
  escapeBang: escapeBangs[index] === true && depth < 64,
2329
- nesting
2174
+ emphasis
2330
2175
  });
2331
2176
  }
2332
2177
  continue;
2333
2178
  }
2334
- const children = frame.count === 0 ? [] : values.splice(values.length - frame.count, frame.count);
2335
- let value = "";
2336
- switch (current.element) {
2337
- case "codeBlock":
2338
- case "codeSpan": {
2339
- const body = current.element === "codeBlock" ? current.code : current.value;
2340
- let longest = 0;
2341
- let run = 0;
2342
- for (const character of body) if (character === "`") {
2343
- run += 1;
2344
- longest = Math.max(longest, run);
2345
- } else run = 0;
2346
- const fence = "`".repeat(Math.max(current.element === "codeBlock" ? 3 : 1, longest + 1));
2347
- if (current.element === "codeBlock") {
2348
- value = `${fence}${current.lang === void 0 ? "" : current.lang}\n${current.code}\n${fence}`;
2349
- break;
2179
+ const value = renderMarkdownNode(current, frame.count === 0 ? [] : values.splice(values.length - frame.count, frame.count), frame.escaped, frame.emphasis);
2180
+ if (stack.length === 0) return value;
2181
+ values.push(value);
2182
+ }
2183
+ return "";
2184
+ }
2185
+ /**
2186
+ * Renders one markdown node from its rendered children and escaped literal text.
2187
+ *
2188
+ * @example
2189
+ * ```ts
2190
+ * renderMarkdownNode({ element: 'thematicBreak' }, [], '', 0) // '---'
2191
+ * ```
2192
+ */
2193
+ function renderMarkdownNode(current, children, literal, emphasis) {
2194
+ switch (current.element) {
2195
+ case "codeBlock":
2196
+ case "codeSpan": {
2197
+ const body = current.element === "codeBlock" ? current.code : current.value;
2198
+ const longest = Iterator.from(body.matchAll(/`+/g)).reduce((maximum, match) => Math.max(maximum, match[0].length), 0);
2199
+ const fence = "`".repeat(Math.max(current.element === "codeBlock" ? 3 : 1, longest + 1));
2200
+ if (current.element === "codeBlock") return `${fence}${current.lang === void 0 ? "" : current.lang}\n${current.code}\n${fence}`;
2201
+ const pad = current.value.startsWith("`") || current.value.endsWith("`") ? " " : "";
2202
+ return `${fence}${pad}${current.value}${pad}${fence}`;
2203
+ }
2204
+ case "break": return " \n";
2205
+ case "document": return children.join("\n\n");
2206
+ case "heading": {
2207
+ const escaped = children.join("").replace(/(^|[^\\])(#+)$/, (_match, before, hashes) => {
2208
+ return `${before}\\${hashes[0] ?? ""}${hashes.slice(1)}`;
2209
+ });
2210
+ return `${"#".repeat(current.level)} ${escaped}`;
2211
+ }
2212
+ case "paragraph": return children.join("");
2213
+ case "thematicBreak": return "---";
2214
+ case "blockquote": return children.join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
2215
+ case "list": {
2216
+ const items = [];
2217
+ children.entries().reduce((ordinal, [position, body]) => {
2218
+ const marker = current.ordered ? `${ordinal}. ` : "- ";
2219
+ const pad = " ".repeat(marker.length);
2220
+ if (current.items[position]?.children[0]?.element === "table") {
2221
+ items.push(`${marker}\n${body.split("\n").map((line) => pad + line).join("\n")}`);
2222
+ return ordinal + 1;
2350
2223
  }
2351
- const pad = current.value.startsWith("`") || current.value.endsWith("`") ? " " : "";
2352
- value = `${fence}${pad}${current.value}${pad}${fence}`;
2353
- break;
2354
- }
2355
- case "break":
2356
- value = " \n";
2357
- break;
2358
- case "document":
2359
- value = children.join("\n\n");
2360
- break;
2361
- case "heading": {
2362
- const escaped = children.join("").replace(/(^|[^\\])(#+)$/, (_match, before, hashes) => {
2363
- return `${before}\\${hashes[0] ?? ""}${hashes.slice(1)}`;
2364
- });
2365
- value = `${"#".repeat(current.level)} ${escaped}`;
2366
- break;
2367
- }
2368
- case "paragraph":
2369
- value = children.join("");
2370
- break;
2371
- case "thematicBreak":
2372
- value = "---";
2373
- break;
2374
- case "blockquote":
2375
- value = children.join("\n\n").split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
2376
- break;
2377
- case "list": {
2378
- const items = [];
2379
- let ordinal = current.start;
2380
- for (const [position, body] of children.entries()) {
2381
- const marker = current.ordered ? `${ordinal}. ` : "- ";
2382
- ordinal += 1;
2383
- const pad = " ".repeat(marker.length);
2384
- if (current.items[position]?.children[0]?.element === "table") {
2385
- items.push(`${marker}\n${body.split("\n").map((line) => pad + line).join("\n")}`);
2386
- continue;
2387
- }
2388
- items.push(body.split("\n").map((line, index) => index === 0 ? marker + line : line === "" ? "" : pad + line).join("\n"));
2224
+ items.push(body.split("\n").map((line, index) => index === 0 ? marker + line : line === "" ? "" : pad + line).join("\n"));
2225
+ return ordinal + 1;
2226
+ }, current.start);
2227
+ return items.join("\n");
2228
+ }
2229
+ case "listItem": return children.join("\n\n");
2230
+ case "table": {
2231
+ const outputs = children.values();
2232
+ const header = [];
2233
+ for (const cell of current.header) {
2234
+ if (cell === void 0) {
2235
+ header.push("");
2236
+ continue;
2389
2237
  }
2390
- value = items.join("\n");
2391
- break;
2238
+ const count = Iterator.from(cell).filter((child) => child !== void 0).reduce((sum) => sum + 1, 0);
2239
+ header.push(outputs.take(count).toArray().join("").replace(/\|/g, "\\|"));
2392
2240
  }
2393
- case "listItem":
2394
- value = children.join("\n\n");
2395
- break;
2396
- case "table": {
2397
- let offset = 0;
2398
- const header = [];
2399
- for (const cell of current.header) {
2241
+ const delimiter = current.align.map((align) => {
2242
+ if (align === null) return "---";
2243
+ if (align === "left") return ":---";
2244
+ if (align === "right") return "---:";
2245
+ if (align === "center") return ":---:";
2246
+ return "---";
2247
+ });
2248
+ const rows = [];
2249
+ for (const row of current.rows) {
2250
+ const cells = [];
2251
+ for (const column of current.header.keys()) {
2252
+ const cell = row[column];
2400
2253
  if (cell === void 0) {
2401
- header.push("");
2254
+ cells.push("");
2402
2255
  continue;
2403
2256
  }
2404
- let count = 0;
2405
- for (const child of cell) if (child !== void 0) count += 1;
2406
- header.push(children.slice(offset, offset + count).join("").replace(/\|/g, "\\|"));
2407
- offset += count;
2408
- }
2409
- const delimiter = current.align.map((align) => {
2410
- if (align === null) return "---";
2411
- if (align === "left") return ":---";
2412
- if (align === "right") return "---:";
2413
- if (align === "center") return ":---:";
2414
- return "---";
2415
- });
2416
- const rows = [];
2417
- for (const row of current.rows) {
2418
- const cells = [];
2419
- for (let column = 0; column < current.header.length; column += 1) {
2420
- const cell = row[column];
2421
- if (cell === void 0) {
2422
- cells.push("");
2423
- continue;
2424
- }
2425
- let count = 0;
2426
- for (const child of cell) if (child !== void 0) count += 1;
2427
- cells.push(children.slice(offset, offset + count).join("").replace(/\|/g, "\\|"));
2428
- offset += count;
2429
- }
2430
- rows.push(`| ${cells.join(" | ")} |`);
2257
+ const count = Iterator.from(cell).filter((child) => child !== void 0).reduce((sum) => sum + 1, 0);
2258
+ cells.push(outputs.take(count).toArray().join("").replace(/\|/g, "\\|"));
2431
2259
  }
2432
- value = [
2433
- `| ${header.join(" | ")} |`,
2434
- `| ${delimiter.join(" | ")} |`,
2435
- ...rows
2436
- ].join("\n");
2437
- break;
2438
- }
2439
- case "text":
2440
- value = frame.escaped;
2441
- break;
2442
- case "emphasis": {
2443
- const marker = frame.nesting % 2 === 0 ? current.strong ? "**" : "*" : current.strong ? "__" : "_";
2444
- value = `${marker}${children.join("")}${marker}`;
2445
- break;
2260
+ rows.push(`| ${cells.join(" | ")} |`);
2446
2261
  }
2447
- case "link":
2448
- case "image": {
2449
- const escaped = (current.element === "link" ? current.href : current.src).replace(/[\\()]/g, (character) => `\\${character}`);
2450
- value = `${current.element === "image" ? "!" : ""}[${children.join("")}](${escaped})`;
2451
- break;
2452
- }
2453
- default: value = "";
2262
+ return [
2263
+ `| ${header.join(" | ")} |`,
2264
+ `| ${delimiter.join(" | ")} |`,
2265
+ ...rows
2266
+ ].join("\n");
2454
2267
  }
2455
- if (stack.length === 0) return value;
2456
- values.push(value);
2268
+ case "text": return literal;
2269
+ case "emphasis": {
2270
+ const marker = emphasis % 2 === 0 ? current.strong ? "**" : "*" : current.strong ? "__" : "_";
2271
+ return `${marker}${children.join("")}${marker}`;
2272
+ }
2273
+ case "link":
2274
+ case "image": {
2275
+ const escaped = (current.element === "link" ? current.href : current.src).replace(/[\\()]/g, (character) => `\\${character}`);
2276
+ return `${current.element === "image" ? "!" : ""}[${children.join("")}](${escaped})`;
2277
+ }
2278
+ default: return "";
2457
2279
  }
2458
- return "";
2459
2280
  }
2460
2281
  /**
2461
2282
  * Builds an HTML-to-markdown projection with absent fields defaulted from
@@ -2506,27 +2327,7 @@ function createProjection(parts = {}) {
2506
2327
  * ```
2507
2328
  */
2508
2329
  function trimInlines(nodes) {
2509
- const out = [];
2510
- for (const node of nodes) if (node !== void 0) out.push(node);
2511
- const first = out[0];
2512
- if (first !== void 0 && first.element === "text") {
2513
- const value = first.value.replace(/^\s+/, "");
2514
- if (isEmptyString(value)) out.shift();
2515
- else out[0] = {
2516
- element: "text",
2517
- value
2518
- };
2519
- }
2520
- const last = out[out.length - 1];
2521
- if (last !== void 0 && last.element === "text") {
2522
- const value = last.value.replace(/\s+$/, "");
2523
- if (isEmptyString(value)) out.pop();
2524
- else out[out.length - 1] = {
2525
- element: "text",
2526
- value
2527
- };
2528
- }
2529
- return out;
2330
+ return trimTrailing(trimLeading(nodes));
2530
2331
  }
2531
2332
  /**
2532
2333
  * Reduces an inline run to the shape markdown can actually write back: adjacent text
@@ -2553,45 +2354,18 @@ function trimInlines(nodes) {
2553
2354
  * ```
2554
2355
  */
2555
2356
  function normalizeInlines(nodes, breaks) {
2556
- const spent = [];
2557
- for (const node of nodes) {
2558
- if (node === void 0) continue;
2559
- if (node.element === "break" && !breaks) spent.push({
2560
- element: "text",
2561
- value: " "
2562
- });
2563
- else spent.push(node);
2564
- }
2565
- const out = [];
2566
- for (const node of coalesceText(spent)) {
2567
- if (node === void 0) continue;
2568
- const previous = out[out.length - 1];
2569
- if (node.element === "text") {
2570
- const value = previous?.element === "break" ? node.value.replace(/^\s+/, "") : node.value;
2571
- if (!isEmptyString(value)) out.push({
2572
- element: "text",
2573
- value
2574
- });
2575
- continue;
2576
- }
2577
- if (node.element === "break") {
2578
- if (previous === void 0 || previous.element === "break") continue;
2579
- if (previous.element === "text") {
2580
- const value = previous.value.replace(/\s+$/, "");
2581
- if (isEmptyString(value)) out.pop();
2582
- else out[out.length - 1] = {
2583
- element: "text",
2584
- value
2585
- };
2586
- }
2587
- if (out.length === 0) continue;
2588
- out.push(node);
2589
- continue;
2590
- }
2591
- out.push(node);
2592
- }
2593
- while (out.length > 0 && out[out.length - 1]?.element === "break") out.pop();
2594
- return coalesceText(out);
2357
+ const spent = coalesceText(Iterator.from(nodes).filter((node) => node !== void 0).map((node) => node.element === "break" && !breaks ? {
2358
+ element: "text",
2359
+ value: " "
2360
+ } : node).toArray());
2361
+ const starts = [0, ...spent.entries().flatMap(([index, node]) => node.element === "break" ? [index + 1] : [])];
2362
+ const segments = starts.map((start, index) => {
2363
+ const end = starts[index + 1];
2364
+ const segment = spent.slice(start, end === void 0 ? spent.length : end - 1).filter((node) => node.element !== "text" || !isEmptyString(node.value));
2365
+ return index < starts.length - 1 ? trimTrailing(segment) : segment;
2366
+ });
2367
+ const first = segments.findIndex((segment) => segment.length > 0);
2368
+ return coalesceText(segments.map((segment, index) => index > first ? trimLeading(segment) : segment).filter((segment) => segment.length > 0).flatMap((segment, index) => index === 0 ? segment : [{ element: "break" }, ...segment]));
2595
2369
  }
2596
2370
  /**
2597
2371
  * Combines the projections of one node's children into the projection of that node —
@@ -2621,63 +2395,72 @@ function normalizeInlines(nodes, breaks) {
2621
2395
  */
2622
2396
  function mergeProjections(children) {
2623
2397
  const blocks = [];
2624
- const cells = [];
2625
- const rows = [];
2626
- let pending = [];
2627
- let text = "";
2628
- for (const child of children) {
2629
- if (child === void 0) continue;
2630
- text += child.text;
2398
+ const text = [];
2399
+ const [tail, remainingCells, remainingRows] = Iterator.from(children).reduce(([pending, cells, rows], child) => {
2400
+ if (child === void 0) return [
2401
+ pending,
2402
+ cells,
2403
+ rows
2404
+ ];
2405
+ text.push(child.text);
2631
2406
  if (isNonEmptyArray(child.blocks)) {
2632
2407
  const flushed = trimInlines(normalizeInlines(pending, true));
2633
2408
  if (isNonEmptyArray(flushed)) blocks.push({
2634
2409
  element: "paragraph",
2635
2410
  children: flushed
2636
2411
  });
2637
- pending = [];
2638
2412
  for (const row of rows) for (const cell of row) if (cell !== void 0 && isNonEmptyArray(cell.inlines)) blocks.push({
2639
2413
  element: "paragraph",
2640
2414
  children: cell.inlines
2641
2415
  });
2642
- rows.length = 0;
2643
2416
  for (const cell of cells) if (cell !== void 0 && isNonEmptyArray(cell.inlines)) blocks.push({
2644
2417
  element: "paragraph",
2645
2418
  children: cell.inlines
2646
2419
  });
2647
- cells.length = 0;
2648
2420
  for (const block of projectionToBlocks(child)) blocks.push(block);
2649
- continue;
2421
+ return [
2422
+ [],
2423
+ [],
2424
+ []
2425
+ ];
2650
2426
  }
2651
- if (isNonEmptyArray(child.rows)) {
2652
- if (isNonEmptyArray(cells)) {
2653
- rows.push([...cells]);
2654
- cells.length = 0;
2655
- }
2427
+ const rowed = isNonEmptyArray(child.rows);
2428
+ if (rowed) {
2429
+ if (isNonEmptyArray(cells)) rows.push(cells);
2656
2430
  for (const row of child.rows) if (row !== void 0) rows.push(row);
2657
2431
  }
2658
- for (const cell of child.cells) if (cell !== void 0) cells.push(cell);
2432
+ const nextCells = rowed ? [] : cells;
2433
+ for (const cell of child.cells) if (cell !== void 0) nextCells.push(cell);
2659
2434
  for (const inline of child.inlines) if (inline !== void 0) pending.push(inline);
2660
- }
2661
- if (isNonEmptyArray(rows) && isNonEmptyArray(cells)) {
2662
- rows.push([...cells]);
2663
- cells.length = 0;
2664
- }
2435
+ return [
2436
+ pending,
2437
+ nextCells,
2438
+ rows
2439
+ ];
2440
+ }, [
2441
+ [],
2442
+ [],
2443
+ []
2444
+ ]);
2445
+ const rowed = isNonEmptyArray(remainingRows) && isNonEmptyArray(remainingCells);
2446
+ const finalRows = rowed ? [...remainingRows, [...remainingCells]] : remainingRows;
2447
+ const finalCells = rowed ? [] : remainingCells;
2665
2448
  if (!isNonEmptyArray(blocks)) return createProjection({
2666
- inlines: coalesceText(pending),
2667
- text,
2668
- cells,
2669
- rows
2449
+ inlines: coalesceText(tail),
2450
+ text: text.join(""),
2451
+ cells: finalCells,
2452
+ rows: finalRows
2670
2453
  });
2671
- const flushed = trimInlines(normalizeInlines(pending, true));
2454
+ const flushed = trimInlines(normalizeInlines(tail, true));
2672
2455
  if (isNonEmptyArray(flushed)) blocks.push({
2673
2456
  element: "paragraph",
2674
2457
  children: flushed
2675
2458
  });
2676
2459
  return createProjection({
2677
2460
  blocks,
2678
- text,
2679
- cells,
2680
- rows
2461
+ text: text.join(""),
2462
+ cells: finalCells,
2463
+ rows: finalRows
2681
2464
  });
2682
2465
  }
2683
2466
  /**
@@ -2892,21 +2675,11 @@ function projectHTMLNode(node, children) {
2892
2675
  });
2893
2676
  }
2894
2677
  case "pre": {
2895
- let position = -1;
2896
- for (const [index, child] of node.children.entries()) {
2897
- if (child?.category !== "element") continue;
2898
- position = index;
2899
- break;
2900
- }
2901
- const source = position === -1 ? void 0 : node.children[position];
2902
- const projected = position === -1 ? void 0 : children[position];
2678
+ const position = node.children.entries().find(([, child]) => child?.category === "element")?.[0];
2679
+ const source = position === void 0 ? void 0 : node.children[position];
2680
+ const projected = position === void 0 ? void 0 : children[position];
2903
2681
  if (source?.category === "element" && source.name === "code" && projected !== void 0) {
2904
- let lang;
2905
- for (const token of (attributeOf(source, "class") ?? "").split(/\s+/)) {
2906
- if (!token.startsWith("language-") || token.length <= 9 || token.includes("`")) continue;
2907
- lang = token.slice(9);
2908
- break;
2909
- }
2682
+ const lang = (attributeOf(source, "class") ?? "").split(/\s+/).find((token) => token.startsWith("language-") && token.length > 9 && !token.includes("`"))?.slice(9);
2910
2683
  return createProjection({
2911
2684
  blocks: [{
2912
2685
  element: "codeBlock",
@@ -3018,24 +2791,21 @@ function projectHTMLNode(node, children) {
3018
2791
  if (source === void 0) continue;
3019
2792
  if (source.index >= source.children.length) continue;
3020
2793
  const child = source.children[source.index];
3021
- source.index += 1;
3022
- sources.push(source);
2794
+ sources.push({
2795
+ ...source,
2796
+ index: source.index + 1,
2797
+ direct: child?.category === "element" ? child.name === "th" || child.name === "td" : source.direct
2798
+ });
3023
2799
  if (child?.category !== "element") continue;
3024
2800
  if (child.name === "th" || child.name === "td") {
3025
2801
  if (!source.direct) {
3026
2802
  headings.push(false);
3027
2803
  rowed.push(false);
3028
2804
  }
3029
- source.direct = true;
3030
2805
  continue;
3031
2806
  }
3032
- source.direct = false;
3033
2807
  if (child.name === "tr") {
3034
- let heading = false;
3035
- for (const cell of child.children) if (cell?.category === "element" && cell.name === "th") {
3036
- heading = true;
3037
- break;
3038
- }
2808
+ const heading = Iterator.from(child.children).some((cell) => cell?.category === "element" && cell.name === "th");
3039
2809
  headings.push(heading);
3040
2810
  rowed.push(true);
3041
2811
  continue;
@@ -3046,35 +2816,21 @@ function projectHTMLNode(node, children) {
3046
2816
  direct: false
3047
2817
  });
3048
2818
  }
3049
- let position;
3050
- for (const [index, heading] of headings.entries()) {
3051
- if (!heading) continue;
3052
- position = index;
3053
- break;
3054
- }
3055
- if (position === void 0) for (const [index, structural] of rowed.entries()) {
3056
- if (!structural) continue;
3057
- position = index;
3058
- break;
3059
- }
2819
+ const position = headings.entries().find(([, heading]) => heading)?.[0] ?? rowed.entries().find(([, structural]) => structural)?.[0];
3060
2820
  const headerRow = position === void 0 ? void 0 : rows[position];
3061
2821
  const columns = headerRow?.length ?? rows[0]?.length ?? 0;
3062
2822
  if (columns === 0) return createProjection({
3063
2823
  blocks: projectionToBlocks(merged),
3064
2824
  text: merged.text
3065
2825
  });
3066
- const header = [];
3067
- const align = [];
3068
- for (let column = 0; column < columns; column += 1) {
3069
- const cell = headerRow?.[column];
3070
- header.push(cell?.inlines ?? []);
3071
- align.push(cell?.align ?? null);
3072
- }
2826
+ const headerCells = Array.from({ length: columns }, (_, column) => headerRow?.[column]);
2827
+ const header = headerCells.map((cell) => cell?.inlines ?? []);
2828
+ const align = headerCells.map((cell) => cell?.align ?? null);
3073
2829
  const body = [];
3074
2830
  for (const [index, row] of rows.entries()) {
3075
2831
  if (row === void 0 || index === position) continue;
3076
2832
  const cells = [];
3077
- for (let column = 0; column < header.length; column += 1) cells.push(row[column]?.inlines ?? []);
2833
+ for (const column of header.keys()) cells.push(row[column]?.inlines ?? []);
3078
2834
  body.push(cells);
3079
2835
  }
3080
2836
  return createProjection({
@@ -3175,32 +2931,8 @@ function* walkNodes(node) {
3175
2931
  if (frame === void 0) continue;
3176
2932
  yield frame.node;
3177
2933
  if (frame.depth >= 64) continue;
3178
- const children = [];
3179
- switch (frame.node.element) {
3180
- case "document":
3181
- case "heading":
3182
- case "paragraph":
3183
- case "blockquote":
3184
- case "listItem":
3185
- case "emphasis":
3186
- case "link":
3187
- case "image":
3188
- for (const child of frame.node.children) if (child !== void 0) children.push(child);
3189
- break;
3190
- case "list":
3191
- for (const child of frame.node.items) if (child !== void 0) children.push(child);
3192
- break;
3193
- case "table":
3194
- for (const cell of frame.node.header) if (cell !== void 0) {
3195
- for (const child of cell) if (child !== void 0) children.push(child);
3196
- }
3197
- for (const row of frame.node.rows) if (row !== void 0) {
3198
- for (const cell of row) if (cell !== void 0) {
3199
- for (const child of cell) if (child !== void 0) children.push(child);
3200
- }
3201
- }
3202
- }
3203
- for (let index = children.length - 1; index >= 0; index -= 1) {
2934
+ const children = listChildren(frame.node);
2935
+ for (const index of children.keys().toArray().toReversed()) {
3204
2936
  const child = children[index];
3205
2937
  if (child !== void 0) stack.push({
3206
2938
  node: child,
@@ -3282,7 +3014,7 @@ function foldNode(node, handlers, depth) {
3282
3014
  expanded: true,
3283
3015
  count: children.length
3284
3016
  });
3285
- for (let index = children.length - 1; index >= 0; index -= 1) {
3017
+ for (const index of children.keys().toArray().toReversed()) {
3286
3018
  const child = children[index];
3287
3019
  if (child !== void 0) stack.push({
3288
3020
  node: child,
@@ -3294,71 +3026,37 @@ function foldNode(node, handlers, depth) {
3294
3026
  continue;
3295
3027
  }
3296
3028
  const children = frame.count === 0 ? [] : values.splice(values.length - frame.count, frame.count);
3297
- let value;
3298
- switch (frame.node.element) {
3299
- case "document":
3300
- value = handlers.document(frame.node, children);
3301
- break;
3302
- case "heading":
3303
- value = handlers.heading(frame.node, children);
3304
- break;
3305
- case "paragraph":
3306
- value = handlers.paragraph(frame.node, children);
3307
- break;
3308
- case "thematicBreak":
3309
- value = handlers.thematicBreak(frame.node, children);
3310
- break;
3311
- case "blockquote":
3312
- value = handlers.blockquote(frame.node, children);
3313
- break;
3314
- case "codeBlock":
3315
- value = handlers.codeBlock(frame.node, children);
3316
- break;
3317
- case "list":
3318
- value = handlers.list(frame.node, children);
3319
- break;
3320
- case "listItem":
3321
- value = handlers.listItem(frame.node, children);
3322
- break;
3323
- case "table":
3324
- value = handlers.table(frame.node, children);
3325
- break;
3326
- case "text":
3327
- value = handlers.text(frame.node, children);
3328
- break;
3329
- case "emphasis":
3330
- value = handlers.emphasis(frame.node, children);
3331
- break;
3332
- case "codeSpan":
3333
- value = handlers.codeSpan(frame.node, children);
3334
- break;
3335
- case "break":
3336
- value = handlers.break(frame.node, children);
3337
- break;
3338
- case "link":
3339
- value = handlers.link(frame.node, children);
3340
- break;
3341
- case "image": value = handlers.image(frame.node, children);
3342
- }
3029
+ const value = foldMarkdownNode(frame.node, handlers, children);
3343
3030
  if (stack.length === 0) return value;
3344
3031
  values.push(value);
3345
3032
  }
3033
+ return foldMarkdownNode(node, handlers, []);
3034
+ }
3035
+ /**
3036
+ * Dispatches one markdown node and its folded children to the matching handler.
3037
+ *
3038
+ * @example
3039
+ * ```ts
3040
+ * foldMarkdownNode({ element: 'text', value: 'hi' }, countHandlers, []) // 1
3041
+ * ```
3042
+ */
3043
+ function foldMarkdownNode(node, handlers, children) {
3346
3044
  switch (node.element) {
3347
- case "document": return handlers.document(node, []);
3348
- case "heading": return handlers.heading(node, []);
3349
- case "paragraph": return handlers.paragraph(node, []);
3350
- case "thematicBreak": return handlers.thematicBreak(node, []);
3351
- case "blockquote": return handlers.blockquote(node, []);
3352
- case "codeBlock": return handlers.codeBlock(node, []);
3353
- case "list": return handlers.list(node, []);
3354
- case "listItem": return handlers.listItem(node, []);
3355
- case "table": return handlers.table(node, []);
3356
- case "text": return handlers.text(node, []);
3357
- case "emphasis": return handlers.emphasis(node, []);
3358
- case "codeSpan": return handlers.codeSpan(node, []);
3359
- case "break": return handlers.break(node, []);
3360
- case "link": return handlers.link(node, []);
3361
- case "image": return handlers.image(node, []);
3045
+ case "document": return handlers.document(node, children);
3046
+ case "heading": return handlers.heading(node, children);
3047
+ case "paragraph": return handlers.paragraph(node, children);
3048
+ case "thematicBreak": return handlers.thematicBreak(node, children);
3049
+ case "blockquote": return handlers.blockquote(node, children);
3050
+ case "codeBlock": return handlers.codeBlock(node, children);
3051
+ case "list": return handlers.list(node, children);
3052
+ case "listItem": return handlers.listItem(node, children);
3053
+ case "table": return handlers.table(node, children);
3054
+ case "text": return handlers.text(node, children);
3055
+ case "emphasis": return handlers.emphasis(node, children);
3056
+ case "codeSpan": return handlers.codeSpan(node, children);
3057
+ case "break": return handlers.break(node, children);
3058
+ case "link": return handlers.link(node, children);
3059
+ case "image": return handlers.image(node, children);
3362
3060
  }
3363
3061
  }
3364
3062
  /**
@@ -3402,7 +3100,7 @@ function rewriteDocument(document, rewrite) {
3402
3100
  count: 0
3403
3101
  }];
3404
3102
  const values = [];
3405
- const derivations = /* @__PURE__ */ new Map();
3103
+ const derivations = [];
3406
3104
  while (stack.length > 0) {
3407
3105
  const frame = stack.pop();
3408
3106
  if (frame === void 0) continue;
@@ -3443,7 +3141,7 @@ function rewriteDocument(document, rewrite) {
3443
3141
  count: children.length
3444
3142
  });
3445
3143
  const depth = current.element === "document" ? 0 : frame.depth + 1;
3446
- for (let index = children.length - 1; index >= 0; index -= 1) {
3144
+ for (const index of children.keys().toArray().toReversed()) {
3447
3145
  const child = children[index];
3448
3146
  if (child !== void 0) stack.push({
3449
3147
  node: child,
@@ -3454,197 +3152,138 @@ function rewriteDocument(document, rewrite) {
3454
3152
  }
3455
3153
  continue;
3456
3154
  }
3457
- const children = frame.count === 0 ? [] : values.splice(values.length - frame.count, frame.count);
3458
- let rebuilt = current;
3459
- let changed = false;
3460
- switch (current.element) {
3461
- case "document": {
3462
- const blocks = [];
3463
- let offset = 0;
3464
- for (const block of current.children) {
3465
- if (block === void 0) continue;
3466
- const child = children[offset];
3467
- const accepted = child !== void 0 && isBlockNode(child) ? child : block;
3468
- blocks.push(accepted);
3469
- if (accepted !== block) changed = true;
3470
- offset += 1;
3471
- }
3472
- if (changed) rebuilt = {
3473
- element: "document",
3474
- children: blocks
3475
- };
3476
- break;
3477
- }
3478
- case "heading":
3479
- case "paragraph": {
3480
- const inlines = [];
3481
- let offset = 0;
3482
- for (const inline of current.children) {
3483
- if (inline === void 0) continue;
3484
- const child = children[offset];
3485
- const accepted = child !== void 0 && isInlineNode(child) ? child : inline;
3486
- inlines.push(accepted);
3487
- if (accepted !== inline) changed = true;
3488
- offset += 1;
3489
- }
3490
- if (changed) rebuilt = {
3491
- ...current,
3492
- children: inlines
3493
- };
3494
- break;
3495
- }
3496
- case "blockquote": {
3497
- const blocks = [];
3498
- let offset = 0;
3499
- for (const block of current.children) {
3500
- if (block === void 0) continue;
3501
- const child = children[offset];
3502
- const accepted = child !== void 0 && isBlockNode(child) ? child : block;
3503
- blocks.push(accepted);
3504
- if (accepted !== block) changed = true;
3505
- offset += 1;
3506
- }
3507
- if (changed) rebuilt = {
3508
- ...current,
3509
- children: blocks
3510
- };
3511
- break;
3512
- }
3513
- case "listItem": {
3514
- const blocks = [];
3515
- let offset = 0;
3516
- for (const block of current.children) {
3517
- if (block === void 0) continue;
3518
- const child = children[offset];
3519
- const accepted = child !== void 0 && isBlockNode(child) ? child : block;
3520
- blocks.push(accepted);
3521
- if (accepted !== block) changed = true;
3522
- offset += 1;
3523
- }
3524
- if (changed) rebuilt = {
3525
- element: "listItem",
3526
- children: blocks
3527
- };
3528
- break;
3529
- }
3530
- case "emphasis":
3531
- case "link":
3532
- case "image": {
3533
- const inlines = [];
3534
- let offset = 0;
3535
- for (const inline of current.children) {
3536
- if (inline === void 0) continue;
3537
- const child = children[offset];
3538
- const accepted = child !== void 0 && isInlineNode(child) ? child : inline;
3539
- inlines.push(accepted);
3540
- if (accepted !== inline) changed = true;
3541
- offset += 1;
3542
- }
3543
- if (changed) rebuilt = {
3544
- ...current,
3545
- children: inlines
3546
- };
3547
- break;
3548
- }
3549
- case "list": {
3550
- const items = [];
3551
- let offset = 0;
3552
- for (const item of current.items) {
3553
- if (item === void 0) continue;
3554
- const child = children[offset];
3555
- const accepted = child?.element === "listItem" ? child : item;
3556
- items.push(accepted);
3557
- if (accepted !== item) changed = true;
3558
- offset += 1;
3559
- }
3560
- if (changed) rebuilt = {
3561
- ...current,
3562
- items
3563
- };
3564
- break;
3565
- }
3566
- case "table": {
3567
- let offset = 0;
3568
- const header = [];
3569
- for (const cell of current.header) {
3570
- if (cell === void 0) continue;
3571
- const inlines = [];
3572
- for (const inline of cell) {
3573
- if (inline === void 0) continue;
3574
- const child = children[offset];
3575
- const accepted = child !== void 0 && isInlineNode(child) ? child : inline;
3576
- inlines.push(accepted);
3577
- if (accepted !== inline) changed = true;
3578
- offset += 1;
3579
- }
3580
- header.push(inlines);
3581
- }
3582
- const rows = [];
3583
- for (const row of current.rows) {
3584
- if (row === void 0) continue;
3585
- const cells = [];
3586
- for (const cell of row) {
3587
- if (cell === void 0) continue;
3588
- const inlines = [];
3589
- for (const inline of cell) {
3590
- if (inline === void 0) continue;
3591
- const child = children[offset];
3592
- const accepted = child !== void 0 && isInlineNode(child) ? child : inline;
3593
- inlines.push(accepted);
3594
- if (accepted !== inline) changed = true;
3595
- offset += 1;
3596
- }
3597
- cells.push(inlines);
3598
- }
3599
- rows.push(cells);
3600
- }
3601
- if (changed) rebuilt = {
3602
- ...current,
3603
- header,
3604
- rows
3605
- };
3606
- break;
3607
- }
3608
- }
3609
- if (rebuilt !== current) derivations.set(rebuilt, current);
3155
+ const rebuilt = rebuildMarkdownNode(current, frame.count === 0 ? [] : values.splice(values.length - frame.count, frame.count));
3156
+ if (rebuilt !== current) derivations.push([
3157
+ rebuilt,
3158
+ current,
3159
+ true
3160
+ ]);
3610
3161
  if (current.element === "document") {
3611
3162
  const result = rebuilt.element === "document" ? rebuilt : current;
3612
3163
  const output = new Set(walkNodes(result));
3613
- const retained = /* @__PURE__ */ new Map();
3614
- for (const [node, source] of derivations) if (output.has(node)) retained.set(node, source);
3615
- return [result, retained];
3616
- }
3617
- const result = rewrite(rebuilt);
3618
- let accepted = rebuilt;
3619
- switch (current.element) {
3620
- case "text":
3621
- case "emphasis":
3622
- case "codeSpan":
3623
- case "break":
3624
- case "link":
3625
- case "image":
3626
- if (isInlineNode(result)) accepted = result;
3627
- break;
3628
- case "heading":
3629
- case "paragraph":
3630
- case "list":
3631
- case "table":
3632
- case "codeBlock":
3633
- case "blockquote":
3634
- case "thematicBreak":
3635
- if (isBlockNode(result)) accepted = result;
3636
- break;
3637
- case "listItem": if (result.element === "listItem") accepted = result;
3638
- }
3639
- if (accepted !== rebuilt && accepted !== current) {
3640
- if (derivations.has(accepted) && derivations.get(accepted) !== current) derivations.set(accepted, void 0);
3641
- else derivations.set(accepted, current);
3164
+ return [result, new Map(Map.groupBy(derivations, ([node]) => node).entries().filter(([node]) => output.has(node)).map(([node, steps]) => [node, steps.reduce((source, [, input, direct], index) => index === 0 || direct || source === input ? input : void 0, void 0)]))];
3642
3165
  }
3166
+ const accepted = acceptMarkdownNode(current, rebuilt, rewrite(rebuilt));
3167
+ if (accepted !== rebuilt && accepted !== current) derivations.push([
3168
+ accepted,
3169
+ current,
3170
+ false
3171
+ ]);
3643
3172
  values.push(accepted);
3644
3173
  }
3645
3174
  return [document, /* @__PURE__ */ new Map()];
3646
3175
  }
3647
3176
  /**
3177
+ * Rebuilds one markdown node when accepted child replacements change its identity.
3178
+ *
3179
+ * @example
3180
+ * ```ts
3181
+ * const original: ParagraphNode = { element: 'paragraph', children: [] }
3182
+ * rebuildMarkdownNode(original, []) === original // true
3183
+ * ```
3184
+ */
3185
+ function rebuildMarkdownNode(current, children) {
3186
+ const outputs = children.values();
3187
+ switch (current.element) {
3188
+ case "document":
3189
+ case "blockquote":
3190
+ case "listItem": {
3191
+ const pairs = Iterator.from(current.children).filter((child) => child !== void 0).map((block) => {
3192
+ const child = outputs.next().value;
3193
+ return [block, child !== void 0 && isBlockNode(child) ? child : block];
3194
+ }).toArray();
3195
+ if (!pairs.some(([input, output]) => input !== output)) return current;
3196
+ const blocks = pairs.map(([, output]) => output);
3197
+ return current.element === "blockquote" ? {
3198
+ ...current,
3199
+ children: blocks
3200
+ } : {
3201
+ element: current.element,
3202
+ children: blocks
3203
+ };
3204
+ }
3205
+ case "heading":
3206
+ case "paragraph":
3207
+ case "emphasis":
3208
+ case "link":
3209
+ case "image": {
3210
+ const pairs = Iterator.from(current.children).filter((child) => child !== void 0).map((inline) => {
3211
+ const child = outputs.next().value;
3212
+ return [inline, child !== void 0 && isInlineNode(child) ? child : inline];
3213
+ }).toArray();
3214
+ return pairs.some(([input, output]) => input !== output) ? {
3215
+ ...current,
3216
+ children: pairs.map(([, output]) => output)
3217
+ } : current;
3218
+ }
3219
+ case "list": {
3220
+ const pairs = Iterator.from(current.items).filter((item) => item !== void 0).map((item) => {
3221
+ const child = outputs.next().value;
3222
+ return [item, child?.element === "listItem" ? child : item];
3223
+ }).toArray();
3224
+ return pairs.some(([input, output]) => input !== output) ? {
3225
+ ...current,
3226
+ items: pairs.map(([, output]) => output)
3227
+ } : current;
3228
+ }
3229
+ case "table": {
3230
+ const pairs = [];
3231
+ const header = Iterator.from(current.header).filter((cell) => cell !== void 0).map((cell) => rebuildMarkdownCell(cell, outputs, pairs)).toArray();
3232
+ const rows = Iterator.from(current.rows).filter((row) => row !== void 0).map((row) => Iterator.from(row).filter((cell) => cell !== void 0).map((cell) => rebuildMarkdownCell(cell, outputs, pairs)).toArray()).toArray();
3233
+ return pairs.some(([input, output]) => input !== output) ? {
3234
+ ...current,
3235
+ header,
3236
+ rows
3237
+ } : current;
3238
+ }
3239
+ }
3240
+ return current;
3241
+ }
3242
+ /**
3243
+ * Rebuilds a table cell and records each input and accepted output pair.
3244
+ *
3245
+ * @example
3246
+ * ```ts
3247
+ * rebuildMarkdownCell([], [].values(), []) // []
3248
+ * ```
3249
+ */
3250
+ function rebuildMarkdownCell(cell, outputs, pairs) {
3251
+ return Iterator.from(cell).filter((inline) => inline !== void 0).map((inline) => {
3252
+ const child = outputs.next().value;
3253
+ const accepted = child !== void 0 && isInlineNode(child) ? child : inline;
3254
+ pairs.push([inline, accepted]);
3255
+ return accepted;
3256
+ }).toArray();
3257
+ }
3258
+ /**
3259
+ * Accepts a rewritten node only when it fits the original node slot.
3260
+ *
3261
+ * @example
3262
+ * ```ts
3263
+ * const original: TextNode = { element: 'text', value: 'hi' }
3264
+ * acceptMarkdownNode(original, original, { element: 'thematicBreak' }) === original // true
3265
+ * ```
3266
+ */
3267
+ function acceptMarkdownNode(current, rebuilt, result) {
3268
+ switch (current.element) {
3269
+ case "text":
3270
+ case "emphasis":
3271
+ case "codeSpan":
3272
+ case "break":
3273
+ case "link":
3274
+ case "image": return isInlineNode(result) ? result : rebuilt;
3275
+ case "heading":
3276
+ case "paragraph":
3277
+ case "list":
3278
+ case "table":
3279
+ case "codeBlock":
3280
+ case "blockquote":
3281
+ case "thematicBreak": return isBlockNode(result) ? result : rebuilt;
3282
+ case "listItem": return result.element === "listItem" ? result : rebuilt;
3283
+ }
3284
+ return rebuilt;
3285
+ }
3286
+ /**
3648
3287
  * Concatenates the `value` / `code` content of every descendant text / code-span /
3649
3288
  * code-block node under `node`, including image alternative content, in walk order —
3650
3289
  * the plain-text projection of an AST (search indexing, word counts, a text-only
@@ -3671,43 +3310,19 @@ function flattenText(node) {
3671
3310
  node,
3672
3311
  depth: 0
3673
3312
  }];
3674
- let value = "";
3313
+ const parts = [];
3675
3314
  while (stack.length > 0) {
3676
3315
  const frame = stack.pop();
3677
3316
  if (frame === void 0 || frame.depth >= 64) continue;
3678
- const children = [];
3679
3317
  switch (frame.node.element) {
3680
3318
  case "text":
3681
3319
  case "codeSpan":
3682
- value += frame.node.value;
3320
+ parts.push(frame.node.value);
3683
3321
  break;
3684
- case "codeBlock":
3685
- value += frame.node.code;
3686
- break;
3687
- case "document":
3688
- case "heading":
3689
- case "paragraph":
3690
- case "blockquote":
3691
- case "listItem":
3692
- case "emphasis":
3693
- case "link":
3694
- case "image":
3695
- for (const child of frame.node.children) if (child !== void 0) children.push(child);
3696
- break;
3697
- case "list":
3698
- for (const child of frame.node.items) if (child !== void 0) children.push(child);
3699
- break;
3700
- case "table":
3701
- for (const cell of frame.node.header) if (cell !== void 0) {
3702
- for (const child of cell) if (child !== void 0) children.push(child);
3703
- }
3704
- for (const row of frame.node.rows) if (row !== void 0) {
3705
- for (const cell of row) if (cell !== void 0) {
3706
- for (const child of cell) if (child !== void 0) children.push(child);
3707
- }
3708
- }
3322
+ case "codeBlock": parts.push(frame.node.code);
3709
3323
  }
3710
- for (let index = children.length - 1; index >= 0; index -= 1) {
3324
+ const children = listChildren(frame.node);
3325
+ for (const index of children.keys().toArray().toReversed()) {
3711
3326
  const child = children[index];
3712
3327
  if (child !== void 0) stack.push({
3713
3328
  node: child,
@@ -3715,10 +3330,310 @@ function flattenText(node) {
3715
3330
  });
3716
3331
  }
3717
3332
  }
3718
- return value;
3333
+ return parts.join("");
3334
+ }
3335
+ /**
3336
+ * Yields UTF-16 offsets and characters within a markdown scan window.
3337
+ *
3338
+ * @example
3339
+ * ```ts
3340
+ * Array.from(walkMarkdownCharacters('ab', 0, 2)) // [[0, 'a'], [1, 'b']]
3341
+ * ```
3342
+ */
3343
+ function* walkMarkdownCharacters(source, from, to) {
3344
+ const positions = Array.prototype.keys.call({ length: Math.max(0, Math.ceil(to - from)) });
3345
+ for (const position of positions) {
3346
+ const index = from + position;
3347
+ yield [index, source[index] ?? ""];
3348
+ }
3349
+ }
3350
+ /**
3351
+ * Counts consecutive copies of the character at the start of a scan window.
3352
+ *
3353
+ * @example
3354
+ * ```ts
3355
+ * countMarkdownRun('***x', 0, 4) // 3
3356
+ * ```
3357
+ */
3358
+ function countMarkdownRun(source, start, to) {
3359
+ if (start >= to || source[start] === void 0) return 0;
3360
+ return /^([\s\S])\1*/.exec(source.slice(start, Math.ceil(to)))?.[0].length ?? 0;
3361
+ }
3362
+ /**
3363
+ * Finds the next defined markdown node at or after an array offset.
3364
+ *
3365
+ * @example
3366
+ * ```ts
3367
+ * findFollowingNode([], 0) // undefined
3368
+ * ```
3369
+ */
3370
+ function findFollowingNode(nodes, start) {
3371
+ return nodes.keys().drop(start).map((index) => nodes[index]).find((node) => node !== void 0) ?? nodes[Math.max(start, nodes.length)];
3372
+ }
3373
+ /**
3374
+ * Escapes literal text so markdown syntax remains literal when rendered.
3375
+ *
3376
+ * @example
3377
+ * ```ts
3378
+ * escapeMarkdownText('*hi*', false) // '\\*hi\\*'
3379
+ * ```
3380
+ */
3381
+ function escapeMarkdownText(value, bang) {
3382
+ const parts = [];
3383
+ walkMarkdownCharacters(value, 0, value.length).reduce((next, [index, character]) => {
3384
+ if (index < next) return next;
3385
+ const atLineStart = index === 0 || value[index - 1] === "\n";
3386
+ if (character === "!" && index === value.length - 1 && bang) {
3387
+ parts.push("\\!");
3388
+ return index + 1;
3389
+ }
3390
+ if ("\\*_`[]".includes(character) || atLineStart && (character === "#" || character === ">") || atLineStart && (character === "-" || character === "~") && value[index + 1] === character && value[index + 2] === character || atLineStart && (character === "-" || character === "+") && (value[index + 1] ?? " ") === " ") {
3391
+ parts.push("\\" + character);
3392
+ return index + 1;
3393
+ }
3394
+ if (atLineStart && /[0-9]/.test(character)) {
3395
+ const end = index + (/^[0-9]+/.exec(value.slice(index))?.[0].length ?? 0);
3396
+ const marker = value[end];
3397
+ if ((marker === "." || marker === ")") && value[end + 1] === " ") {
3398
+ parts.push(value.slice(index, end) + "\\" + marker);
3399
+ return end + 1;
3400
+ }
3401
+ }
3402
+ parts.push(character);
3403
+ return index + 1;
3404
+ }, 0);
3405
+ return parts.join("");
3406
+ }
3407
+ /**
3408
+ * Locates a balanced closing delimiter while skipping backslash escapes.
3409
+ *
3410
+ * @example
3411
+ * ```ts
3412
+ * locateMarkdownDelimiter('[a]', 0, 3, '[', ']') // 2
3413
+ * ```
3414
+ */
3415
+ function locateMarkdownDelimiter(source, start, to, open, close) {
3416
+ const characters = walkMarkdownCharacters(source, start, to);
3417
+ const [, , result] = characters.reduce(([depth, next], [index, character]) => {
3418
+ if (index < next) return [
3419
+ depth,
3420
+ next,
3421
+ void 0
3422
+ ];
3423
+ if (character === "\\") return [
3424
+ depth,
3425
+ index + 2,
3426
+ void 0
3427
+ ];
3428
+ if (character === open) return [
3429
+ depth + 1,
3430
+ index + 1,
3431
+ void 0
3432
+ ];
3433
+ if (character !== close) return [
3434
+ depth,
3435
+ index + 1,
3436
+ void 0
3437
+ ];
3438
+ if (depth === 1) {
3439
+ characters.return(void 0);
3440
+ return [
3441
+ 0,
3442
+ index + 1,
3443
+ index
3444
+ ];
3445
+ }
3446
+ return [
3447
+ depth - 1,
3448
+ index + 1,
3449
+ void 0
3450
+ ];
3451
+ }, [
3452
+ 0,
3453
+ start,
3454
+ void 0
3455
+ ]);
3456
+ return result;
3457
+ }
3458
+ /**
3459
+ * Appends a nonempty text node and records its original source span.
3460
+ *
3461
+ * @example
3462
+ * ```ts
3463
+ * const nodes: InlineNode[] = []
3464
+ * recordMarkdownText({ text: 'a', segments: [] }, 'a', 0, 1, new Map(), nodes)
3465
+ * nodes // [{ element: 'text', value: 'a' }]
3466
+ * ```
3467
+ */
3468
+ function recordMarkdownText(source, value, start, end, spans, nodes) {
3469
+ if (value.length === 0) return;
3470
+ const node = {
3471
+ element: "text",
3472
+ value
3473
+ };
3474
+ const span = projectSpan(source, start, end);
3475
+ if (span !== void 0) spans.set(node, span);
3476
+ nodes.push(node);
3477
+ }
3478
+ /**
3479
+ * Scans one inline construct and records the spans of its nested children.
3480
+ *
3481
+ * @example
3482
+ * ```ts
3483
+ * scanMarkdownInline({ text: '`x`', segments: [] }, 0, 3, new Map(), 0)?.end // 3
3484
+ * ```
3485
+ */
3486
+ function scanMarkdownInline(source, index, to, spans, depth) {
3487
+ const character = source.text[index] ?? "";
3488
+ if (character === " ") {
3489
+ const end = index + countMarkdownRun(source.text, index, to);
3490
+ if (end - index >= 2 && source.text[end] === "\n") return {
3491
+ node: { element: "break" },
3492
+ end: end + 1
3493
+ };
3494
+ }
3495
+ if (character === "`") {
3496
+ const span = scanCode(source.text, index, to);
3497
+ if (span) return {
3498
+ node: {
3499
+ element: "codeSpan",
3500
+ value: span.value
3501
+ },
3502
+ end: span.end
3503
+ };
3504
+ }
3505
+ if (character === "!" && source.text[index + 1] === "[") {
3506
+ const link = locateLink(source.text, index + 1, to);
3507
+ if (link !== void 0) return {
3508
+ node: {
3509
+ element: "image",
3510
+ src: unescapeText(source.text.slice(link.close + 2, link.end - 1).trim()),
3511
+ children: coalesceText(scanInlineSource(source, index + 2, link.close, spans, depth + 1), spans)
3512
+ },
3513
+ end: link.end
3514
+ };
3515
+ }
3516
+ if (character === "[") {
3517
+ const link = locateLink(source.text, index, to);
3518
+ if (link !== void 0) return {
3519
+ node: {
3520
+ element: "link",
3521
+ href: unescapeText(source.text.slice(link.close + 2, link.end - 1).trim()),
3522
+ children: coalesceText(scanInlineSource(source, index + 1, link.close, spans, depth + 1), spans)
3523
+ },
3524
+ end: link.end
3525
+ };
3526
+ }
3527
+ if (character === "*" || character === "_") {
3528
+ const emphasis = locateEmphasis(source.text, index, to);
3529
+ if (emphasis !== void 0) return {
3530
+ node: {
3531
+ element: "emphasis",
3532
+ strong: emphasis.strong,
3533
+ children: coalesceText(scanInlineSource(source, emphasis.open, emphasis.close, spans, depth + 1), spans)
3534
+ },
3535
+ end: emphasis.end
3536
+ };
3537
+ }
3538
+ }
3539
+ /**
3540
+ * Yields line offsets through a closable markdown scan iterator.
3541
+ *
3542
+ * @example
3543
+ * ```ts
3544
+ * Array.from(walkMarkdownLines(splitLines('a\nb'), 1)) // [1]
3545
+ * ```
3546
+ */
3547
+ function* walkMarkdownLines(lines, start) {
3548
+ const positions = Array.prototype.keys.call({ get length() {
3549
+ return Math.max(0, Math.ceil(lines.length - start));
3550
+ } });
3551
+ for (const position of positions) yield start + position;
3552
+ }
3553
+ /**
3554
+ * Splits inline nodes into adjacent text runs and singleton non-text runs.
3555
+ *
3556
+ * @param nodes - The inline nodes in source order
3557
+ * @returns The runs with their original node identities
3558
+ *
3559
+ * @example
3560
+ * ```ts
3561
+ * splitTextRuns([{ element: 'text', value: 'a' }]).length // 1
3562
+ * ```
3563
+ */
3564
+ function splitTextRuns(nodes) {
3565
+ const starts = nodes.entries().flatMap(([index, node]) => index === 0 || node.element !== "text" || nodes[index - 1]?.element !== "text" ? [index] : []).toArray();
3566
+ return starts.map((start, index) => nodes.slice(start, starts[index + 1]));
3567
+ }
3568
+ /**
3569
+ * Lists the direct children of a markdown node in traversal order.
3570
+ *
3571
+ * @param node - The node whose direct children to list
3572
+ * @returns The children, with table cells flattened in header then body order
3573
+ *
3574
+ * @example
3575
+ * ```ts
3576
+ * listChildren({ element: 'document', children: [] }) // []
3577
+ * ```
3578
+ */
3579
+ function listChildren(node) {
3580
+ switch (node.element) {
3581
+ case "document":
3582
+ case "heading":
3583
+ case "paragraph":
3584
+ case "blockquote":
3585
+ case "listItem":
3586
+ case "emphasis":
3587
+ case "link":
3588
+ case "image": return Iterator.from(node.children).filter((child) => child !== void 0).toArray();
3589
+ case "list": return Iterator.from(node.items).filter((child) => child !== void 0).toArray();
3590
+ case "table": return [...Iterator.from(node.header).filter((cell) => cell !== void 0).flatMap((cell) => Iterator.from(cell).filter((child) => child !== void 0)), ...Iterator.from(node.rows).filter((row) => row !== void 0).flatMap((row) => Iterator.from(row).filter((cell) => cell !== void 0)).flatMap((cell) => Iterator.from(cell).filter((child) => child !== void 0))];
3591
+ default: return [];
3592
+ }
3719
3593
  }
3720
- //#endregion
3721
- //#region src/core/compilers.ts
3594
+ /**
3595
+ * Trims leading whitespace when an inline run starts with a text node.
3596
+ *
3597
+ * @param nodes - The inline run to trim
3598
+ * @returns The run with its leading text trimmed or removed when empty
3599
+ *
3600
+ * @example
3601
+ * ```ts
3602
+ * trimLeading([{ element: 'text', value: ' a ' }]) // [{ element: 'text', value: 'a ' }]
3603
+ * ```
3604
+ */
3605
+ function trimLeading(nodes) {
3606
+ const present = Iterator.from(nodes).filter((node) => node !== void 0).toArray();
3607
+ const first = present[0];
3608
+ if (first?.element !== "text") return present;
3609
+ const value = first.value.replace(/^\s+/, "");
3610
+ return isEmptyString(value) ? present.slice(1) : [{
3611
+ element: "text",
3612
+ value
3613
+ }, ...present.slice(1)];
3614
+ }
3615
+ /**
3616
+ * Trims trailing whitespace when an inline run ends with a text node.
3617
+ *
3618
+ * @param nodes - The inline run to trim
3619
+ * @returns The run with its trailing text trimmed or removed when empty
3620
+ *
3621
+ * @example
3622
+ * ```ts
3623
+ * trimTrailing([{ element: 'text', value: ' a ' }]) // [{ element: 'text', value: ' a' }]
3624
+ * ```
3625
+ */
3626
+ function trimTrailing(nodes) {
3627
+ const present = Iterator.from(nodes).filter((node) => node !== void 0).toArray();
3628
+ const last = present.at(-1);
3629
+ if (last?.element !== "text") return present;
3630
+ const value = last.value.replace(/\s+$/, "");
3631
+ return isEmptyString(value) ? present.slice(0, -1) : [...present.slice(0, -1), {
3632
+ element: "text",
3633
+ value
3634
+ }];
3635
+ }
3636
+ // src/core/compilers.ts
3722
3637
  /**
3723
3638
  * Renders a {@link MarkdownNode} to sanitized canonical HTML.
3724
3639
  *
@@ -3744,8 +3659,7 @@ function flattenText(node) {
3744
3659
  function renderHTML(node) {
3745
3660
  return renderHTML$1(new HTML(markdownToHTML(node)).sanitize({ attributes: [...SAFE_ATTRIBUTES, "src"] }).document);
3746
3661
  }
3747
- //#endregion
3748
- //#region src/core/shapers.ts
3662
+ // src/core/shapers.ts
3749
3663
  /**
3750
3664
  * Describes the shape of a {@link TextNode} — a plain-text leaf inline run.
3751
3665
  *
@@ -3866,8 +3780,7 @@ var listItemMatchShape = objectShape({
3866
3780
  indent: integerShape(),
3867
3781
  marker: integerShape()
3868
3782
  });
3869
- //#endregion
3870
- //#region src/core/Markdown.ts
3783
+ // src/core/Markdown.ts
3871
3784
  /**
3872
3785
  * Wraps a typed {@link MarkdownDocument} AST as a stateful, parsed markdown document
3873
3786
  * with the query (`find` / `filter` / `reduce` / iteration), rewrite (`map`), fold, and
@@ -3973,9 +3886,7 @@ var Markdown = class Markdown {
3973
3886
  for (const node of this.walk()) if (predicate(node)) return node;
3974
3887
  }
3975
3888
  filter(predicate) {
3976
- const out = [];
3977
- for (const node of this.walk()) if (predicate(node)) out.push(node);
3978
- return out;
3889
+ return this.walk().filter((node) => predicate(node)).toArray();
3979
3890
  }
3980
3891
  /**
3981
3892
  * Rewrites the AST bottom-up (copy-on-write) and returns a new {@link Markdown},
@@ -3992,9 +3903,7 @@ var Markdown = class Markdown {
3992
3903
  }
3993
3904
  /** Folds the AST depth-first, pre-order into an accumulator. */
3994
3905
  reduce(callback, initial) {
3995
- let accumulator = initial;
3996
- for (const node of this.walk()) accumulator = callback(accumulator, node);
3997
- return accumulator;
3906
+ return this.walk().reduce((accumulator, node) => callback(accumulator, node), initial);
3998
3907
  }
3999
3908
  /** Runs a total catamorphism over the document using a {@link MarkdownHandlerMap} table. */
4000
3909
  fold(handlers) {
@@ -4011,7 +3920,9 @@ var Markdown = class Markdown {
4011
3920
  * ```ts
4012
3921
  * // universal - works in every ReadableStream-supporting environment
4013
3922
  * const reader = markdown.stream().getReader()
4014
- * for (let result = await reader.read(); !result.done; result = await reader.read()) {
3923
+ * for (;;) {
3924
+ * const result = await reader.read()
3925
+ * if (result.done) break
4015
3926
  * console.log(result.value) // one BlockNode
4016
3927
  * }
4017
3928
  *
@@ -4023,18 +3934,11 @@ var Markdown = class Markdown {
4023
3934
  * ```
4024
3935
  */
4025
3936
  stream() {
4026
- const blocks = this.#document.children;
4027
- let index = 0;
3937
+ const blocks = this.#document.children.values();
4028
3938
  return new ReadableStream({ pull(controller) {
4029
- if (index < blocks.length) {
4030
- const block = blocks[index];
4031
- if (block === void 0) {
4032
- controller.close();
4033
- return;
4034
- }
4035
- controller.enqueue(block);
4036
- index += 1;
4037
- } else controller.close();
3939
+ const block = blocks.next();
3940
+ if (!block.done && block.value !== void 0) controller.enqueue(block.value);
3941
+ else controller.close();
4038
3942
  } });
4039
3943
  }
4040
3944
  #derive(document, derivations) {
@@ -4053,8 +3957,7 @@ var Markdown = class Markdown {
4053
3957
  return derived;
4054
3958
  }
4055
3959
  };
4056
- //#endregion
4057
- //#region src/core/factories.ts
3960
+ // src/core/factories.ts
4058
3961
  /**
4059
3962
  * Creates a stateful markdown handle from a markdown string or an already-parsed
4060
3963
  * {@link MarkdownDocument} — a typed AST plus the query, rewrite, and fold operations
@@ -4172,7 +4075,6 @@ function createCodeBlockContract() {
4172
4075
  function createThematicBreakContract() {
4173
4076
  return createContract(thematicBreakShape);
4174
4077
  }
4175
- //#endregion
4176
- 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, isFlankingWhitespace, isHeadingNode, isImageNode, isInlineNode, isLineBreakNode, isLinkNode, isListNode, isMarkdownDocument, isMarkdownNode, isParagraphNode, isQuote, isTableNode, isTableStart, isTextNode, isThematicBreak, isThematicBreakNode, 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 };
4078
+ export { EMPTY_PROJECTION, MAX_DEPTH, Markdown, acceptMarkdownNode, coalesceText, codeBlockShape, codeSpanShape, collectList, collectTable, countIndent, countMarkdownRun, createCodeBlockContract, createCodeSpanContract, createLineBreakContract, createMarkdown, createProjection, createTextContract, createThematicBreakContract, delimiterToAlignments, escapeMarkdownText, extractFence, extractHeading, extractListItem, findFollowingNode, flattenText, foldMarkdownNode, foldNode, htmlToMarkdown, isBlankLine, isBlockNode, isBlockquoteNode, isCodeBlockNode, isCodeSpanNode, isEmphasisNode, isEscapable, isFenceClose, isFenceWhitespace, isFlankingWhitespace, isHeadingNode, isImageNode, isInlineNode, isLineBreakNode, isLinkNode, isListNode, isMarkdownDocument, isMarkdownNode, isParagraphNode, isQuote, isTableNode, isTableStart, isTextNode, isThematicBreak, isThematicBreakNode, joinSources, lineBreakShape, listChildren, listItemMatchShape, locateEmphasis, locateLink, locateMarkdownDelimiter, markdownToHTML, mergeProjections, normalizeInlines, normalizeParagraphLine, parseBlocks, parseDocument, parseInline, parseProvenance, projectHTMLLeaf, projectHTMLNode, projectMarkdownNode, projectSpan, projectionToBlocks, projectionToInlines, rebuildMarkdownCell, rebuildMarkdownNode, recordMarkdownText, renderHTML, renderMarkdown, renderMarkdownNode, rewriteDocument, scanCode, scanEmphasis, scanInline, scanInlineSource, scanLink, scanMarkdownInline, sliceSource, splitLines, splitTableRow, splitTableSources, splitTextRuns, startsBlock, stripQuote, tableAlignShape, textShape, thematicBreakShape, trimInlines, trimLeading, trimSource, trimTrailing, unescapeText, walkMarkdownCharacters, walkMarkdownLines, walkNodes };
4177
4079
 
4178
4080
  //# sourceMappingURL=index.js.map