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