@orkestrel/markdown 0.0.13 → 0.0.14

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.
@@ -5,7 +5,7 @@ import { HTML, SAFE_ATTRIBUTES, SAFE_URL_SCHEMES, TABLE_ALIGNMENTS, UNSAFE_ELEME
5
5
  * Caps the recursion depth the parse pipeline (`parseDocument` and its
6
6
  * `parsers.ts` helpers), the `helpers.ts` traversal / projection functions
7
7
  * (`markdownToHTML`, `renderMarkdown`, `walkNodes`, `foldNode`, `rewriteDocument`),
8
- * and the `compilers.ts` renderer (`renderHTML`) honor before degrading. It bounds blockquote nesting, inline
8
+ * and the `compilers.ts` renderer (`renderHTML`) honor before degrading, at 64. It bounds blockquote nesting, inline
9
9
  * nesting (emphasis / links), and traversal / projection recursion so pathological
10
10
  * or hostile input cannot exhaust the call stack. {@link htmlToMarkdown} is the
11
11
  * inherited exception: its fold and depth cap belong to `@orkestrel/html`.
@@ -160,7 +160,7 @@ function isEmphasisNode(node) {
160
160
  * Determines whether a node is an inline code span.
161
161
  *
162
162
  * @remarks
163
- * Narrows to {@link CodeSpanNode} - the node whose `element` discriminant is
163
+ * Narrows to {@link CodeSpanNode} the node whose `element` discriminant is
164
164
  * `'codeSpan'`.
165
165
  *
166
166
  * @param node - The AST node to test
@@ -217,11 +217,11 @@ function isImageNode(node) {
217
217
  return node.element === "image";
218
218
  }
219
219
  /**
220
- * Determines whether an arbitrary value is a valid {@link InlineNode} - a text
220
+ * Determines whether an arbitrary value is a valid {@link InlineNode} a text
221
221
  * run, emphasis, code span, hard break, link, or image, recursively validated.
222
222
  *
223
223
  * @remarks
224
- * Total: never throws, even on cyclic or pathologically deep input - every
224
+ * Total: never throws, even on cyclic or pathologically deep input every
225
225
  * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
226
226
  * throw-contained per the `@orkestrel/contract` guard contract.
227
227
  *
@@ -256,16 +256,16 @@ var isInlineNode = unionOf(recordOf({
256
256
  children: arrayOf(lazyOf(() => isInlineNode))
257
257
  }));
258
258
  /**
259
- * Determines whether an arbitrary value is a valid {@link BlockNode} - a
259
+ * Determines whether an arbitrary value is a valid {@link BlockNode} a
260
260
  * heading, paragraph, list, table, code block, blockquote, or thematic break,
261
261
  * recursively validated.
262
262
  *
263
263
  * @remarks
264
- * Total: never throws, even on cyclic or pathologically deep input - every
264
+ * Total: never throws, even on cyclic or pathologically deep input every
265
265
  * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
266
266
  * throw-contained per the `@orkestrel/contract` guard contract.
267
267
  * A list item's shape is inlined here (and in {@link isMarkdownNode}) rather
268
- * than named separately - it is used at exactly these two sites.
268
+ * than named separately it is used at exactly these two sites.
269
269
  *
270
270
  * @param value - The value to test
271
271
  * @returns True if `value` is a well-formed {@link BlockNode}; false otherwise
@@ -307,16 +307,16 @@ var isBlockNode = unionOf(recordOf({
307
307
  children: arrayOf(lazyOf(() => isBlockNode))
308
308
  }), recordOf({ element: literalOf("thematicBreak") }));
309
309
  /**
310
- * Determines whether an arbitrary value is a valid {@link MarkdownNode} - the
310
+ * Determines whether an arbitrary value is a valid {@link MarkdownNode} the
311
311
  * {@link MarkdownDocument} root, a {@link BlockNode}, a {@link ListItemNode}, or
312
312
  * an {@link InlineNode}, recursively validated.
313
313
  *
314
314
  * @remarks
315
- * Total: never throws, even on cyclic or pathologically deep input - every
315
+ * Total: never throws, even on cyclic or pathologically deep input every
316
316
  * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
317
317
  * throw-contained per the `@orkestrel/contract` guard contract.
318
318
  * A list item's shape is inlined here (and in {@link isBlockNode}) rather than
319
- * named separately - it is used at exactly these two sites.
319
+ * named separately it is used at exactly these two sites.
320
320
  *
321
321
  * @param value - The value to test
322
322
  * @returns True if `value` is a well-formed {@link MarkdownNode}; false otherwise
@@ -334,12 +334,12 @@ var isMarkdownNode = unionOf(lazyOf(() => isMarkdownDocument), lazyOf(() => isBl
334
334
  children: arrayOf(lazyOf(() => isBlockNode))
335
335
  }), lazyOf(() => isInlineNode));
336
336
  /**
337
- * Determines whether an arbitrary value is a valid {@link MarkdownDocument} -
337
+ * Determines whether an arbitrary value is a valid {@link MarkdownDocument}
338
338
  * the parsed-AST root {@link parseDocument} returns, recursively
339
339
  * validated.
340
340
  *
341
341
  * @remarks
342
- * Total: never throws, even on cyclic or pathologically deep input - every
342
+ * Total: never throws, even on cyclic or pathologically deep input every
343
343
  * combinator involved (`recordOf`, `arrayOf`) is throw-contained per the
344
344
  * `@orkestrel/contract` guard contract.
345
345
  *
@@ -511,7 +511,8 @@ function parseBlocks(lines, depth, spans = /* @__PURE__ */ new Map(), end) {
511
511
  }
512
512
  /**
513
513
  * Parses a markdown string into a typed {@link MarkdownDocument} AST through the
514
- * block phase.
514
+ * block phase — the document half of what {@link parseProvenance} returns. Malformed
515
+ * markdown degrades to literal text, so the parse never throws.
515
516
  *
516
517
  * @param markdown - The markdown source to parse.
517
518
  * @returns The parsed document.
@@ -526,7 +527,8 @@ function parseDocument(markdown) {
526
527
  return document;
527
528
  }
528
529
  /**
529
- * Parses a markdown string into a document and its original-source spans.
530
+ * Parses a markdown string into a document and its original-source spans. Malformed
531
+ * markdown degrades to literal text, so the parse never throws.
530
532
  *
531
533
  * @param markdown - The markdown source to parse.
532
534
  * @returns The parsed document and its node-identity span map.
@@ -551,7 +553,8 @@ function parseProvenance(markdown) {
551
553
  }
552
554
  /**
553
555
  * Parses inline markdown text (emphasis, code spans, links, images, and hard
554
- * breaks) into inline AST nodes, coalescing adjacent text runs.
556
+ * breaks) into inline AST nodes, coalescing adjacent text runs and reading no block
557
+ * structure. Malformed markdown degrades to literal text, so the parse never throws.
555
558
  *
556
559
  * @param text - The inline markdown text to parse.
557
560
  * @returns The parsed inline nodes.
@@ -781,7 +784,7 @@ function normalizeParagraphLine(source, breaks) {
781
784
  }], "");
782
785
  }
783
786
  /**
784
- * Counts the leading space / tab characters on `line` (a tab counts as one) - the
787
+ * Counts the leading space / tab characters on `line` (a tab counts as one) the
785
788
  * indent that decides whether a list item's continuation belongs to the item.
786
789
  *
787
790
  * @param line - The line to measure
@@ -799,7 +802,7 @@ function countIndent(line) {
799
802
  return count;
800
803
  }
801
804
  /**
802
- * Checks whether `character` is whitespace under the emphasis flanking rule - a space, a
805
+ * Checks whether `character` is whitespace under the emphasis flanking rule a space, a
803
806
  * tab, or a newline.
804
807
  *
805
808
  * @param character - The character to test
@@ -815,7 +818,7 @@ function isFlankingWhitespace(character) {
815
818
  return character === " " || character === " " || character === "\n";
816
819
  }
817
820
  /**
818
- * Checks whether `character` is escapable by a leading backslash - the ASCII punctuation
821
+ * Checks whether `character` is escapable by a leading backslash the ASCII punctuation
819
822
  * markdown gives meaning to (so `\*` becomes `*` but `\.` stays `\.`).
820
823
  *
821
824
  * @param character - The single character after a backslash
@@ -831,7 +834,7 @@ function isEscapable(character) {
831
834
  return /[\\`*_{}[\]()#+\-.!>~|]/.test(character);
832
835
  }
833
836
  /**
834
- * Checks whether `line` is blank - empty, or containing only whitespace - the markdown
837
+ * Checks whether `line` is blank empty, or containing only whitespace the markdown
835
838
  * definition of a blank line that block parsing uses to separate paragraphs, skip
836
839
  * gaps, and end list continuations.
837
840
  *
@@ -847,7 +850,7 @@ function isBlankLine(line) {
847
850
  return isEmptyString(line.trim());
848
851
  }
849
852
  /**
850
- * Checks whether `line` is a blockquote line (`>` optionally indented up to three spaces) -
853
+ * Checks whether `line` is a blockquote line (`>` optionally indented up to three spaces)
851
854
  * its content is de-quoted by {@link stripQuote}.
852
855
  *
853
856
  * @param line - The candidate line
@@ -862,7 +865,7 @@ function isQuote(line) {
862
865
  return /^\s{0,3}>/.test(line);
863
866
  }
864
867
  /**
865
- * Checks whether `line` closes a fence opened by `marker` - the same fence character, a run
868
+ * Checks whether `line` closes a fence opened by `marker` the same fence character, a run
866
869
  * at least as long, and nothing else but surrounding whitespace.
867
870
  *
868
871
  * @param line - The candidate closing line
@@ -888,7 +891,7 @@ function isFenceClose(line, marker) {
888
891
  return index === line.length;
889
892
  }
890
893
  /**
891
- * Checks whether `character` is a regex-`\s`-equivalent whitespace character - the
894
+ * Checks whether `character` is a regex-`\s`-equivalent whitespace character the
892
895
  * character class {@link isFenceClose}'s scan treats as surrounding padding.
893
896
  *
894
897
  * @param character - The single character to test, or `undefined` past the end of a line
@@ -904,7 +907,7 @@ function isFenceWhitespace(character) {
904
907
  return character === " " || character === " " || character === "\n" || character === "\r" || character === "\f" || character === "\v";
905
908
  }
906
909
  /**
907
- * Checks whether `line` is a thematic break (horizontal rule) - three or more of the SAME
910
+ * Checks whether `line` is a thematic break (horizontal rule) three or more of the same
908
911
  * marker `-`, `*`, or `_` (optionally space-separated) and nothing else (`---`,
909
912
  * `***`, `___`, `- - -`).
910
913
  *
@@ -924,9 +927,9 @@ function isThematicBreak(line) {
924
927
  return [...stripped].every((character) => character === marker);
925
928
  }
926
929
  /**
927
- * Checks whether the pair (`header`, `delimiter`) opens a GFM table - `delimiter` is a row of
930
+ * Checks whether the pair (`header`, `delimiter`) opens a GFM table `delimiter` is a row of
928
931
  * `|`-separated cells each matching `:?-+:?`, the GFM rule that a table requires a
929
- * header row IMMEDIATELY followed by a delimiter row.
932
+ * header row immediately followed by a delimiter row.
930
933
  *
931
934
  * @param header - The candidate header line
932
935
  * @param delimiter - The line after it (the candidate delimiter)
@@ -1056,10 +1059,11 @@ function stripQuote(source) {
1056
1059
  return sliceSource(source, (/^\s{0,3}>\s?/.exec(source.text)?.[0] ?? "").length, source.text.length);
1057
1060
  }
1058
1061
  /**
1059
- * Splits one GFM table row into its cell strings - outer pipes are optional, an escaped
1060
- * pipe (`\|`) inside a cell is NOT a separator (it becomes a literal `|`), and the
1061
- * empty leading / trailing cell produced by an outer `|` is dropped. Derives the string
1062
- * form from {@link splitTableSources}, which owns the escaped-pipe splitting rule.
1062
+ * Splits one GFM table row into its cell strings outer pipes are optional, a pipe
1063
+ * escaped by a leading backslash inside a cell is not a separator (it becomes a literal
1064
+ * pipe character), and the empty leading / trailing cell an outer pipe produces is
1065
+ * dropped. Derives the string form from {@link splitTableSources}, which owns the
1066
+ * escaped-pipe splitting rule.
1063
1067
  *
1064
1068
  * @param row - The raw table row line
1065
1069
  * @returns The row's cells, in column order
@@ -1122,7 +1126,7 @@ function splitTableSources(row) {
1122
1126
  return cells;
1123
1127
  }
1124
1128
  /**
1125
- * Derives the per-column {@link TableAlign} list from a GFM delimiter row - `:---`
1129
+ * Derives the per-column {@link TableAlign} list from a GFM delimiter row `:---`
1126
1130
  * left, `---:` right, `:---:` center, and `---` as the explicit no-alignment
1127
1131
  * marker represented by `null`.
1128
1132
  *
@@ -1146,8 +1150,8 @@ function delimiterToAlignments(delimiter) {
1146
1150
  });
1147
1151
  }
1148
1152
  /**
1149
- * Checks whether the line at `index` starts a NEW block kind (heading / fence / thematic
1150
- * break / blockquote / list / table) - the paragraph collector stops at such a line
1153
+ * Checks whether the line at `index` starts a new block kind (heading / fence / thematic
1154
+ * break / blockquote / list / table) the paragraph collector stops at such a line
1151
1155
  * so a block following a paragraph without a blank line still parses (a trusted-input
1152
1156
  * caller writing a `##` heading directly under a paragraph, with no intervening blank
1153
1157
  * line).
@@ -1166,7 +1170,7 @@ function startsBlock(lines, index) {
1166
1170
  return extractHeading(line) !== void 0 || extractFence(line) !== void 0 || isThematicBreak(line) || isQuote(line) || extractListItem(line) !== void 0 || isTableStart(line, lines[index + 1]);
1167
1171
  }
1168
1172
  /**
1169
- * Resolves backslash escapes in a raw string to their literal characters - used for a
1173
+ * Resolves backslash escapes in a raw string to their literal characters used for a
1170
1174
  * link `href` (which is not otherwise inline-parsed) and any plain text run.
1171
1175
  *
1172
1176
  * @param text - The raw text possibly carrying `\x` escapes
@@ -1189,7 +1193,7 @@ function unescapeText(text) {
1189
1193
  return out;
1190
1194
  }
1191
1195
  /**
1192
- * Merges adjacent text nodes into one - the inline scanner emits a text node per
1196
+ * Merges adjacent text nodes into one the inline scanner emits a text node per
1193
1197
  * unrecognized character, so coalescing keeps the AST clean and assertion-friendly.
1194
1198
  *
1195
1199
  * @param nodes - The inline nodes (possibly with adjacent text runs)
@@ -1228,7 +1232,7 @@ function coalesceText(nodes, spans) {
1228
1232
  }
1229
1233
  /**
1230
1234
  * Scans an inline code span at `start` (a `` ` ``-run … a matching `` ` ``-run of the
1231
- * SAME length, the CommonMark rule that lets a span contain backticks). Returns the
1235
+ * same length, the CommonMark rule that lets a span contain backticks). Returns the
1232
1236
  * span's literal text + end index, or `undefined` when no matching closer exists (it
1233
1237
  * then degrades to literal backticks).
1234
1238
  *
@@ -1262,7 +1266,7 @@ function scanCode(source, start, to) {
1262
1266
  }
1263
1267
  }
1264
1268
  /**
1265
- * Locates a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1269
+ * Locates a link `[text](href)` at `start` the text runs to a balanced `]`, then `(`
1266
1270
  * must immediately follow and the destination runs to the matching `)` (both respect
1267
1271
  * nested delimiters + escapes). Returns the label close and syntax end, or `undefined` when the shape
1268
1272
  * does not hold (it then degrades to a literal `[`).
@@ -1320,7 +1324,7 @@ function locateLink(source, start, to) {
1320
1324
  };
1321
1325
  }
1322
1326
  /**
1323
- * Scans a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1327
+ * Scans a link `[text](href)` at `start` the text runs to a balanced `]`, then `(`
1324
1328
  * must immediately follow and the destination runs to the matching `)` (both respect
1325
1329
  * nested delimiters + escapes) through {@link locateLink}, and returns the parsed node
1326
1330
  * and end index. Returns `undefined` when the shape does not hold (it then degrades to
@@ -1353,7 +1357,7 @@ function scanLink(source, start, to, depth = 0) {
1353
1357
  };
1354
1358
  }
1355
1359
  /**
1356
- * Locates an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1360
+ * Locates an emphasis run at `start` (`*` / `_`, doubled for strong) finds the nearest
1357
1361
  * matching closing run of the same marker + width while skipping complete nested
1358
1362
  * runs from the other marker family, and requires non-space immediately inside both
1359
1363
  * delimiters (the CommonMark flanking simplification that blocks `* x *`). Returns
@@ -1412,7 +1416,7 @@ function locateEmphasis(source, start, to) {
1412
1416
  }
1413
1417
  }
1414
1418
  /**
1415
- * Scans an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1419
+ * Scans an emphasis run at `start` (`*` / `_`, doubled for strong) finds the nearest
1416
1420
  * matching closing run of the same marker + width while skipping complete nested runs
1417
1421
  * from the other marker family, and requires non-space immediately inside both
1418
1422
  * delimiters (the CommonMark flanking simplification that blocks `* x *`) through
@@ -1446,7 +1450,7 @@ function scanEmphasis(source, start, to, depth = 0) {
1446
1450
  };
1447
1451
  }
1448
1452
  /**
1449
- * Scans the window `[from, to)` of `source` into inline nodes - the single recursive
1453
+ * Scans the window `[from, to)` of `source` into inline nodes the single recursive
1450
1454
  * engine the inline phase runs on (emphasis, link text, and image alternative
1451
1455
  * content recurse through it). Linear:
1452
1456
  * each character is consumed once; a failed construct emits its opening character as
@@ -1458,10 +1462,10 @@ function scanEmphasis(source, start, to, depth = 0) {
1458
1462
  * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1459
1463
  * incremented by one on every recursive descent {@link scanInlineSource} makes into
1460
1464
  * itself for a link's text, an image's alternative content, or an emphasis run's
1461
- * children. At {@link MAX_DEPTH} the window is never scanned for markup - it emits as
1462
- * a single literal text node - so pathological nesting (`[[[[…`, `****…`) cannot
1465
+ * children. At {@link MAX_DEPTH} the window is never scanned for markup it emits as
1466
+ * a single literal text node so pathological nesting (`[[[[…`, `****…`) cannot
1463
1467
  * exhaust the call stack.
1464
- * @returns The parsed inline nodes (NOT yet coalesced)
1468
+ * @returns The parsed inline nodes (not yet coalesced)
1465
1469
  *
1466
1470
  * @example
1467
1471
  * ```ts
@@ -2159,15 +2163,15 @@ function markdownToHTML(node) {
2159
2163
  };
2160
2164
  }
2161
2165
  /**
2162
- * Renders a {@link MarkdownNode} to its CANONICAL markdown source - the inverse
2163
- * projection of `renderHTML`, and the serializer a `parse(renderMarkdown(doc))`
2166
+ * Renders a {@link MarkdownNode} to its canonical markdown source the inverse
2167
+ * projection of `renderHTML`. It is the serializer a `parse(renderMarkdown(doc))`
2164
2168
  * round-trip is built on. Canonical forms: `*` / `**` emphasis at even emphasis
2165
2169
  * nesting depths and `_` / `__` at odd depths, `- ` bullets, `N. ` sequential
2166
2170
  * ordinals (from the list's `start`), `---` thematic breaks, fenced code blocks
2167
2171
  * (backtick run widened past any 3+ backtick run inside the body), ATX headings,
2168
- * `> `-prefixed blockquote lines, GFM tables (1-space-padded cells, `\|`-escaped
2169
- * pipes, an alignment delimiter row), `[text](href)` links, `![alt](src)` images,
2170
- * and two-space hard breaks. A `text` node's literal content is backslash-escaped
2172
+ * `> `-prefixed blockquote lines, GFM tables (1-space-padded cells, a backslash
2173
+ * before each literal pipe, an alignment delimiter row), `[text](href)` links,
2174
+ * `![alt](src)` images, and two-space hard breaks. A `text` node's literal content is backslash-escaped
2171
2175
  * wherever it would otherwise re-parse as markup, so parsing the rendered source
2172
2176
  * returns the node it was rendered from.
2173
2177
  *
@@ -2484,8 +2488,8 @@ function createProjection(parts = {}) {
2484
2488
  };
2485
2489
  }
2486
2490
  /**
2487
- * Trims the whitespace at the two ends of an inline run - the leading whitespace of a
2488
- * leading text node and the trailing whitespace of a trailing one - dropping either
2491
+ * Trims the whitespace at the two ends of an inline run the leading whitespace of a
2492
+ * leading text node and the trailing whitespace of a trailing one dropping either
2489
2493
  * node when nothing survives.
2490
2494
  *
2491
2495
  * @remarks
@@ -2530,11 +2534,11 @@ function trimInlines(nodes) {
2530
2534
  * ending or spent as a space.
2531
2535
  *
2532
2536
  * @remarks
2533
- * A hard break is ` \n` in markdown source, so it survives a re-parse only BETWEEN
2537
+ * A hard break is ` \n` in markdown source, so it survives a re-parse only between
2534
2538
  * two lines of content and only with no whitespace touching it: a leading or trailing
2535
2539
  * break has no line to end, a run of breaks reads as one blank line (which would end
2536
2540
  * the paragraph), and a space beside one is eaten by the parser's line trimming. Where
2537
- * a break cannot be written at all - a heading and a table cell are one line each - it
2541
+ * a break cannot be written at all a heading and a table cell are one line each it
2538
2542
  * becomes the space it stood for.
2539
2543
  *
2540
2544
  * @param nodes - The inline run to normalize
@@ -2590,14 +2594,14 @@ function normalizeInlines(nodes, breaks) {
2590
2594
  return coalesceText(out);
2591
2595
  }
2592
2596
  /**
2593
- * Combines the projections of one node's children into the projection of that node -
2597
+ * Combines the projections of one node's children into the projection of that node
2594
2598
  * the single place inline runs become paragraphs, so no ancestor has to decide it
2595
2599
  * twice.
2596
2600
  *
2597
2601
  * @remarks
2598
2602
  * A child is either inline or block, never both, so merging preserves source order
2599
2603
  * exactly: an inline run is held pending until a block arrives, then written out as a
2600
- * paragraph BEFORE it. That is what keeps `<div>lead<p>a</p></div>` two paragraphs in
2604
+ * paragraph before it. That is what keeps `<div>lead<p>a</p></div>` two paragraphs in
2601
2605
  * the order they were written rather than two lists that lost their interleaving. A
2602
2606
  * pending run carrying no text is dropped rather than becoming a blank paragraph.
2603
2607
  * Direct cells become one row before a later row, while cells/rows before a block
@@ -2677,7 +2681,7 @@ function mergeProjections(children) {
2677
2681
  });
2678
2682
  }
2679
2683
  /**
2680
- * Reads a projection as BLOCK content - the view a document, a blockquote, and a list
2684
+ * Reads a projection as block content the view a document, a blockquote, and a list
2681
2685
  * item each need.
2682
2686
  *
2683
2687
  * @remarks
@@ -2723,7 +2727,7 @@ function projectionToBlocks(projection) {
2723
2727
  return blocks;
2724
2728
  }
2725
2729
  /**
2726
- * Reads a projection as INLINE content - the view a link, an emphasis, and a table cell
2730
+ * Reads a projection as inline content the view a link, an emphasis, and a table cell
2727
2731
  * each need.
2728
2732
  *
2729
2733
  * @remarks
@@ -2750,7 +2754,7 @@ function projectionToInlines(projection) {
2750
2754
  }];
2751
2755
  }
2752
2756
  /**
2753
- * Projects one HTML leaf - a text node, a comment, or a doctype - to its
2757
+ * Projects one HTML leaf a text node, a comment, or a doctype to its
2754
2758
  * {@link MarkdownProjection}.
2755
2759
  *
2756
2760
  * @remarks
@@ -2780,8 +2784,8 @@ function projectHTMLLeaf(leaf) {
2780
2784
  });
2781
2785
  }
2782
2786
  /**
2783
- * Projects one HTML container - the document root or an element - from its children's
2784
- * already-computed projections. THE element mapping, and the only place that decides
2787
+ * Projects one HTML container the document root or an element from its children's
2788
+ * already-computed projections. The element mapping, and the only place that decides
2785
2789
  * what an HTML tag becomes in markdown.
2786
2790
  *
2787
2791
  * @remarks
@@ -2793,13 +2797,13 @@ function projectHTMLLeaf(leaf) {
2793
2797
  * inline runs wrapped in paragraphs; `ul` / `ol` a list, ordered from the tag and
2794
2798
  * numbered from `start`; `th` / `td`, `tr`, and `table` a GFM table whose column
2795
2799
  * alignment comes from each header-position cell's `align` attribute. Every
2796
- * `UNSAFE_ELEMENTS` subtree contributes nothing at all, text included. Every OTHER
2800
+ * `UNSAFE_ELEMENTS` subtree contributes nothing at all, text included. Every other
2797
2801
  * element unwraps to its children, so wrapper soup melts while its content keeps its
2798
- * shape - `<div><p>a</p><p>b</p></div>` stays two paragraphs.
2802
+ * shape `<div><p>a</p><p>b</p></div>` stays two paragraphs.
2799
2803
  *
2800
2804
  * Three mappings read their own node rather than only their children's projections,
2801
2805
  * because HTML puts the fact in a position rather than in a value: a `pre` takes its
2802
- * body from its `code` child's raw text, and a list takes one item per `li` child - so
2806
+ * body from its `code` child's raw text, and a list takes one item per `li` child so
2803
2807
  * an empty `<li>` is still an item, while the whitespace between two of them is not.
2804
2808
  * A `tr` accepts only its own direct cells, and a table derives the first `th`-bearing
2805
2809
  * row from its own source structure.
@@ -3087,36 +3091,36 @@ function projectHTMLNode(node, children) {
3087
3091
  return merged;
3088
3092
  }
3089
3093
  /**
3090
- * Projects an `@orkestrel/html` {@link HTMLNode} into a {@link MarkdownDocument} - the
3094
+ * Projects an `@orkestrel/html` {@link HTMLNode} into a {@link MarkdownDocument} the
3091
3095
  * HTML→markdown direction, and the inverse of {@link markdownToHTML}.
3092
3096
  *
3093
3097
  * @remarks
3094
- * **Engine.** One total handler table - {@link projectHTMLNode} for the containers,
3095
- * {@link projectHTMLLeaf} for the leaves - folded by `@orkestrel/html`'s own `foldNode`, so
3098
+ * **Engine.** One total handler table {@link projectHTMLNode} for the containers,
3099
+ * {@link projectHTMLLeaf} for the leaves folded by `@orkestrel/html`'s own `foldNode`, so
3096
3100
  * depth capping, cycle safety, and bottom-up ordering are inherited rather than
3097
3101
  * rebuilt. Total: hostile, cyclic, and pathologically deep input degrades instead of
3098
3102
  * throwing.
3099
3103
  *
3100
3104
  * **Composed depth.** Both packages cap recursion at 64, and html's cap is reached
3101
- * first: a document nested past it projects to a chain bounded by THAT cap, with the
3105
+ * first: a document nested past it projects to a chain bounded by that cap, with the
3102
3106
  * content below it truncated before markdown ever sees it. Since the projected chain
3103
3107
  * can be a level or two deeper than {@link MAX_DEPTH}, the serializer's own cap can
3104
- * then truncate again - so the anchor law below is a law within the depth budget, and
3108
+ * then truncate again so the anchor law that follows is a law within the depth budget, and
3105
3109
  * beyond it only totality is promised.
3106
3110
  *
3107
3111
  * **Safety.** Every `href` and `src` is re-sanitized through
3108
3112
  * `sanitizeURL(value, SAFE_URL_SCHEMES)` whether or not the AST was ever sanitized,
3109
3113
  * because a hand-built one never was. A refused destination empties to `''` and the
3110
- * link or image is KEPT - `[text]()` - since a bad URL is no reason to lose the words
3114
+ * link or image is kept `[text]()` because a bad URL is no reason to lose the words
3111
3115
  * around it. An `UNSAFE_ELEMENTS` subtree contributes nothing at all, text included, so
3112
3116
  * a `script` body can never resurface as prose.
3113
3117
  *
3114
3118
  * **The anchor law.** HTML→markdown is lossy, so the fixpoint that matters is the
3115
- * PROJECTED AST, not the input bytes:
3119
+ * projected AST, not the input bytes:
3116
3120
  * `parseDocument(renderMarkdown(htmlToMarkdown(x)))` deep-equals `htmlToMarkdown(x)`.
3117
3121
  * The projection therefore emits canonical markdown shapes rather than literal
3118
- * translations - whitespace collapsed, edges trimmed, a blank paragraph dropped, a hard
3119
- * break only where a line can end - because a shape markdown cannot write back is a
3122
+ * translations whitespace collapsed, edges trimmed, a blank paragraph dropped, a hard
3123
+ * break only where a line can end because a shape markdown cannot write back is a
3120
3124
  * shape this projection has no business producing.
3121
3125
  *
3122
3126
  * @param node - The HTML document or bare node to project
@@ -3143,7 +3147,7 @@ function htmlToMarkdown(node) {
3143
3147
  };
3144
3148
  }
3145
3149
  /**
3146
- * Walks a {@link MarkdownNode} depth-first, pre-order, root-inclusive - yields
3150
+ * Walks a {@link MarkdownNode} depth-first, pre-order, root-inclusive yields
3147
3151
  * the node itself, then recurses into its children (block children, list items,
3148
3152
  * image/link inline children, table header/row cells' inline nodes) in walk order.
3149
3153
  *
@@ -3206,16 +3210,16 @@ function* walkNodes(node) {
3206
3210
  }
3207
3211
  }
3208
3212
  /**
3209
- * Folds a {@link MarkdownNode} into a `T` through a total catamorphism - children are
3213
+ * Folds a {@link MarkdownNode} into a `T` through a total catamorphism children are
3210
3214
  * folded first (post-order), then the node's own {@link MarkdownHandler} is invoked
3211
3215
  * with the already-folded children.
3212
3216
  *
3213
3217
  * @remarks
3214
- * **Table contract.** A {@link TableNode} has no single `children` array - its cells
3218
+ * **Table contract.** A {@link TableNode} has no single `children` array its cells
3215
3219
  * live in `header` (one inline-node list per column) and `rows` (a list of such
3216
- * rows). The `table` handler receives ONE folded `T` per inline node, flattened in
3217
- * walk order across ALL cells - every header cell's inline nodes (column order), then
3218
- * every body row's cells' inline nodes (row order, then column order) - and reads
3220
+ * rows). The `table` handler receives one folded `T` per inline node, flattened in
3221
+ * walk order across all cells every header cell's inline nodes (column order), then
3222
+ * every body row's cells' inline nodes (row order, then column order) and reads
3219
3223
  * `node.header[c].length` / `node.rows[r][c].length` off the table node itself to
3220
3224
  * recover cell boundaries within the flat list.
3221
3225
  *
@@ -3358,10 +3362,10 @@ function foldNode(node, handlers, depth) {
3358
3362
  }
3359
3363
  }
3360
3364
  /**
3361
- * Rewrites a {@link MarkdownDocument} bottom-up (copy-on-write) - each node's children
3365
+ * Rewrites a {@link MarkdownDocument} bottom-up (copy-on-write) each node's children
3362
3366
  * are rewritten first (post-order), then `rewrite` is applied to the node itself; the
3363
- * document ROOT is never passed to `rewrite` (the `element: 'document'` invariant
3364
- * always holds). A table's inline cells and a list's items ARE rewritten.
3367
+ * document root is never passed to `rewrite` (the `element: 'document'` invariant
3368
+ * always holds). A table's inline cells and a list's items are rewritten too.
3365
3369
  *
3366
3370
  * @remarks
3367
3371
  * Never mutates `document`. An unchanged subtree keeps its input identity. A parent
@@ -3370,12 +3374,12 @@ function foldNode(node, handlers, depth) {
3370
3374
  * whose `element` does not fit the slot it was called for (a block slot handed a
3371
3375
  * non-{@link BlockNode}, an inline slot handed a non-{@link InlineNode}, a list-item
3372
3376
  * slot handed a non-`listItem`), the ill-fitting result is discarded and the accepted
3373
- * input child is reused - `rewriteDocument` stays total and never produces a
3377
+ * input child is reused `rewriteDocument` stays total and never produces a
3374
3378
  * structurally invalid document.
3375
3379
  *
3376
3380
  * Descent is capped at {@link MAX_DEPTH}, the same cap {@link walkNodes} and
3377
3381
  * {@link foldNode} observe: at `depth >= MAX_DEPTH` the subtree is passed through
3378
- * UNCHANGED (by reference, not rebuilt, and `rewrite` is not invoked on it) instead of
3382
+ * unchanged (by reference, not rebuilt, and `rewrite` is not invoked on it) instead of
3379
3383
  * recursing further, so a pathologically deep adopted document cannot exhaust the
3380
3384
  * call stack. {@link MarkdownInterface.map} inherits this cap since it delegates here.
3381
3385
  *
@@ -3642,7 +3646,7 @@ function rewriteDocument(document, rewrite) {
3642
3646
  }
3643
3647
  /**
3644
3648
  * Concatenates the `value` / `code` content of every descendant text / code-span /
3645
- * code-block node under `node`, including image alternative content, in walk order -
3649
+ * code-block node under `node`, including image alternative content, in walk order
3646
3650
  * the plain-text projection of an AST (search indexing, word counts, a text-only
3647
3651
  * preview).
3648
3652
  *
@@ -3719,6 +3723,9 @@ function flattenText(node) {
3719
3723
  * Renders a {@link MarkdownNode} to sanitized canonical HTML.
3720
3724
  *
3721
3725
  * @remarks
3726
+ * Sanitization is unconditional: the function takes one argument and declares no
3727
+ * options, so no call shape opts out of it.
3728
+ *
3722
3729
  * Markdown widens `@orkestrel/html`'s attribute floor by exactly `src`, because image
3723
3730
  * syntax is meaningless without its source. `src` is still a URL attribute, so the
3724
3731
  * floor refuses `javascript:`, `data:`, `vbscript:`, and `file:` values. A stricter
@@ -3740,7 +3747,7 @@ function renderHTML(node) {
3740
3747
  //#endregion
3741
3748
  //#region src/core/shapers.ts
3742
3749
  /**
3743
- * Describes the shape of a {@link TextNode} - a plain-text leaf inline run.
3750
+ * Describes the shape of a {@link TextNode} a plain-text leaf inline run.
3744
3751
  *
3745
3752
  * @example
3746
3753
  * ```ts
@@ -3756,7 +3763,7 @@ var textShape = objectShape({
3756
3763
  value: stringShape()
3757
3764
  });
3758
3765
  /**
3759
- * Describes the shape of a {@link CodeSpanNode} - an inline code span (`` `code` ``).
3766
+ * Describes the shape of a {@link CodeSpanNode} an inline code span (`` `code` ``).
3760
3767
  *
3761
3768
  * @example
3762
3769
  * ```ts
@@ -3772,7 +3779,7 @@ var codeSpanShape = objectShape({
3772
3779
  value: stringShape()
3773
3780
  });
3774
3781
  /**
3775
- * Describes the shape of a {@link LineBreakNode} - a GFM hard line-break leaf.
3782
+ * Describes the shape of a {@link LineBreakNode} a GFM hard line-break leaf.
3776
3783
  *
3777
3784
  * @example
3778
3785
  * ```ts
@@ -3785,7 +3792,7 @@ var codeSpanShape = objectShape({
3785
3792
  */
3786
3793
  var lineBreakShape = objectShape({ element: literalShape(["break"]) });
3787
3794
  /**
3788
- * Describes the shape of a {@link CodeBlockNode} - a fenced code block. `lang` is
3795
+ * Describes the shape of a {@link CodeBlockNode} a fenced code block. `lang` is
3789
3796
  * optional (absent when the opening fence carries no info-string).
3790
3797
  *
3791
3798
  * @example
@@ -3804,7 +3811,7 @@ var codeBlockShape = objectShape({
3804
3811
  code: stringShape()
3805
3812
  });
3806
3813
  /**
3807
- * Describes the shape of a {@link ThematicBreakNode} - a horizontal rule. Carries no
3814
+ * Describes the shape of a {@link ThematicBreakNode} a horizontal rule. Carries no
3808
3815
  * fields beyond its `element` discriminant.
3809
3816
  *
3810
3817
  * @example
@@ -3818,8 +3825,9 @@ var codeBlockShape = objectShape({
3818
3825
  */
3819
3826
  var thematicBreakShape = objectShape({ element: literalShape(["thematicBreak"]) });
3820
3827
  /**
3821
- * Describes the shape of a {@link TableAlign} - the per-column GFM table alignment
3822
- * literal.
3828
+ * Describes the shape of a {@link TableAlign} the per-column GFM table alignment
3829
+ * literal. Absence is no member of it, so the shape refuses the `null` a bare `---`
3830
+ * delimiter takes in a `TableNode`'s `align` list.
3823
3831
  *
3824
3832
  * @example
3825
3833
  * ```ts
@@ -3838,7 +3846,7 @@ var tableAlignShape = literalShape([
3838
3846
  "center"
3839
3847
  ]);
3840
3848
  /**
3841
- * Describes the shape of {@link ListItemMatch} - the parsed parts of a single list-item
3849
+ * Describes the shape of {@link ListItemMatch} the parsed parts of a single list-item
3842
3850
  * line the block phase's list detector returns. Fully non-recursive (no
3843
3851
  * nested node fields), so every field shapes directly.
3844
3852
  *
@@ -3867,14 +3875,14 @@ var listItemMatchShape = objectShape({
3867
3875
  *
3868
3876
  * @remarks
3869
3877
  * - **Construction.** Given a `string`, the constructor runs {@link parseProvenance} (the
3870
- * block phase then the inline phase) once, keeping the AST and a COPY of the span map
3871
- * that parse recorded. Given a {@link MarkdownDocument}, the document is adopted AS-IS
3872
- * and is NOT re-validated - gate an untrusted value with `isMarkdownDocument` first.
3873
- * - **Provenance.** {@link span} reads the region of the ORIGINAL constructor string a
3878
+ * block phase then the inline phase) once, keeping the AST and a copy of the span map
3879
+ * that parse recorded. Given a {@link MarkdownDocument}, the document is adopted as-is
3880
+ * and is not re-validated gate an untrusted value with `isMarkdownDocument` first.
3881
+ * - **Provenance.** {@link span} reads the region of the original constructor string a
3874
3882
  * node was produced from, and it is handle-relative: a string-constructed handle exposes
3875
3883
  * the regions of the nodes it parsed, an adopted document exposes none, and a node from
3876
3884
  * another handle reports `undefined` here whatever that handle reports. Each call
3877
- * returns a fresh value. A node reports the region THIS handle holds for its identity,
3885
+ * returns a fresh value. A node reports the region this handle holds for its identity,
3878
3886
  * else the region of the direct input a rewrite named for it, else `undefined`: a text
3879
3887
  * run the parse joined from adjacent scanner output reports the region enclosing its
3880
3888
  * parts, and only a rewrite output that holds no region of its own and was assembled
@@ -3882,23 +3890,22 @@ var listItemMatchShape = objectShape({
3882
3890
  * {@link map} carries provenance across the rewrite: an unchanged node keeps its
3883
3891
  * region, a one-source replacement takes the region of the node it replaced, and a
3884
3892
  * rebuilt parent takes its original's.
3885
- * - **Immutable.** {@link map} never mutates the stored AST - it returns a NEW `Markdown`
3893
+ * - **Immutable.** {@link map} never mutates the stored AST it returns a new `Markdown`
3886
3894
  * instance; the document root invariant (`element: 'document'`) always holds. An
3887
3895
  * identity rewrite still returns a new handle, over the same document tree.
3888
3896
  * - **Traversal order.** {@link walk} and the `find` / `filter` / `reduce` queries built
3889
3897
  * on it walk the AST depth-first, pre-order, root-inclusive (through {@link walkNodes});
3890
- * `stream` is shallow - only the document's direct block children.
3898
+ * `stream` is shallow only the document's direct block children.
3891
3899
  *
3892
- * @example
3900
+ * @example Construct from a string and narrow with a guard
3893
3901
  * ```ts
3894
- * import { Markdown, isHeadingNode, renderMarkdown } from '@src/core'
3902
+ * import { Markdown, isHeadingNode } from '@orkestrel/markdown'
3895
3903
  *
3896
3904
  * const markdown = new Markdown('# Title\n\nA **bold** [link](https://x.dev).')
3897
- * const heading = markdown.find(isHeadingNode) // the HeadingNode, or undefined
3898
- * const shouted = markdown.map((node) =>
3899
- * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node,
3900
- * )
3901
- * renderMarkdown(shouted.document) // '# TITLE\n\nA **BOLD** [LINK](https://x.dev).'
3905
+ * markdown.document.children[0] // { element: 'heading', level: 1, children: [...] }
3906
+ *
3907
+ * const heading = markdown.find(isHeadingNode) // HeadingNode | undefined, narrowed
3908
+ * if (heading !== undefined) heading.level // number — narrowed to HeadingNode
3902
3909
  * ```
3903
3910
  */
3904
3911
  var Markdown = class Markdown {
@@ -3943,7 +3950,7 @@ var Markdown = class Markdown {
3943
3950
  };
3944
3951
  }
3945
3952
  /**
3946
- * Returns THE deep traversal - a lazy, depth-first, pre-order, root-inclusive generator
3953
+ * Returns the deep traversal a lazy, depth-first, pre-order, root-inclusive generator
3947
3954
  * over every {@link MarkdownNode} in the document. `find` / `filter` / `reduce`
3948
3955
  * all iterate this single traversal.
3949
3956
  *
@@ -3995,7 +4002,7 @@ var Markdown = class Markdown {
3995
4002
  }
3996
4003
  /**
3997
4004
  * Returns a web-standard {@link ReadableStream} over the document's top-level block nodes
3998
- * (shallow, source order) - a fresh, pull-based source per call: one block is
4005
+ * (shallow, source order) a fresh, pull-based source per call: one block is
3999
4006
  * enqueued per `pull`, so a slow reader's backpressure is respected. Cancellable,
4000
4007
  * async-iterable wherever the platform supports it (Node, Deno), and pipeable
4001
4008
  * through any {@link TransformStream} / {@link WritableStream}.
@@ -4050,7 +4057,7 @@ var Markdown = class Markdown {
4050
4057
  //#region src/core/factories.ts
4051
4058
  /**
4052
4059
  * Creates a stateful markdown handle from a markdown string or an already-parsed
4053
- * {@link MarkdownDocument} - a typed AST plus the query, rewrite, and fold operations
4060
+ * {@link MarkdownDocument} a typed AST plus the query, rewrite, and fold operations
4054
4061
  * {@link MarkdownInterface} exposes.
4055
4062
  *
4056
4063
  * @remarks
@@ -4058,9 +4065,9 @@ var Markdown = class Markdown {
4058
4065
  * fenced code / blockquotes / thematic breaks) then an inline phase (emphasis /
4059
4066
  * inline code / links / images / hard breaks) to build a render-agnostic
4060
4067
  * {@link MarkdownDocument}. Given a
4061
- * {@link MarkdownDocument}, adopts it AS-IS without re-validation - gate an untrusted
4068
+ * {@link MarkdownDocument}, adopts it as-is without re-validation gate an untrusted
4062
4069
  * value with `isMarkdownDocument` first. Pure + total parse (malformed markdown
4063
- * degrades to text, never throws) and zero-dependency - a hand-written scanner, no
4070
+ * degrades to text, never throws) and zero-dependency a hand-written scanner, no
4064
4071
  * regex-only structural parse, linear-time (no ReDoS).
4065
4072
  *
4066
4073
  * @param input - A markdown string to parse, or an already-parsed {@link MarkdownDocument}
@@ -4079,7 +4086,7 @@ function createMarkdown(input) {
4079
4086
  }
4080
4087
  /**
4081
4088
  * Compiles the {@link textShape} into a {@link ContractInterface} for
4082
- * {@link TextNode} - a guard, coercing parser, JSON Schema, and seeded
4089
+ * {@link TextNode} a guard, coercing parser, JSON Schema, and seeded
4083
4090
  * generator from one shape declaration.
4084
4091
  *
4085
4092
  * @returns A `TextNode` contract bundling `schema` / `is` / `parse` / `generate`
@@ -4097,7 +4104,7 @@ function createTextContract() {
4097
4104
  }
4098
4105
  /**
4099
4106
  * Compiles the {@link codeSpanShape} into a {@link ContractInterface} for
4100
- * {@link CodeSpanNode} - a guard, coercing parser, JSON Schema, and seeded
4107
+ * {@link CodeSpanNode} a guard, coercing parser, JSON Schema, and seeded
4101
4108
  * generator from one shape declaration.
4102
4109
  *
4103
4110
  * @returns A `CodeSpanNode` contract bundling `schema` / `is` / `parse` / `generate`
@@ -4131,7 +4138,7 @@ function createLineBreakContract() {
4131
4138
  }
4132
4139
  /**
4133
4140
  * Compiles the {@link codeBlockShape} into a {@link ContractInterface} for
4134
- * {@link CodeBlockNode} - a guard, coercing parser, JSON Schema, and seeded
4141
+ * {@link CodeBlockNode} a guard, coercing parser, JSON Schema, and seeded
4135
4142
  * generator from one shape declaration.
4136
4143
  *
4137
4144
  * @returns A `CodeBlockNode` contract bundling `schema` / `is` / `parse` / `generate`
@@ -4149,7 +4156,7 @@ function createCodeBlockContract() {
4149
4156
  }
4150
4157
  /**
4151
4158
  * Compiles the {@link thematicBreakShape} into a {@link ContractInterface} for
4152
- * {@link ThematicBreakNode} - a guard, coercing parser, JSON Schema, and
4159
+ * {@link ThematicBreakNode} a guard, coercing parser, JSON Schema, and
4153
4160
  * seeded generator from one shape declaration.
4154
4161
  *
4155
4162
  * @returns A `ThematicBreakNode` contract bundling `schema` / `is` / `parse` / `generate`