@orkestrel/markdown 0.0.13 → 0.0.15

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