@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.
- package/README.md +12 -6
- package/dist/src/core/index.cjs +124 -117
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +247 -218
- package/dist/src/core/index.d.ts +247 -218
- package/dist/src/core/index.js +124 -117
- package/dist/src/core/index.js.map +1 -1
- package/package.json +14 -15
package/dist/src/core/index.cjs
CHANGED
|
@@ -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}
|
|
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}
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
1061
|
-
*
|
|
1062
|
-
* empty leading / trailing cell
|
|
1063
|
-
* form from {@link splitTableSources}, which owns the
|
|
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
|
|
1151
|
-
* break / blockquote / list / table)
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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`
|
|
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`
|
|
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)
|
|
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)
|
|
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
|
|
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
|
|
1463
|
-
* a single literal text node
|
|
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 (
|
|
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 &&
|
|
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
|
|
2164
|
-
* projection of `renderHTML
|
|
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,
|
|
2170
|
-
*
|
|
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
|
+
* `` 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 &&
|
|
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
|
|
2489
|
-
* leading text node and the trailing whitespace of a trailing one
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2785
|
-
* already-computed projections.
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
3096
|
-
* {@link projectHTMLLeaf} for the leaves
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
3120
|
-
* break only where a line can end
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3218
|
-
* walk order across
|
|
3219
|
-
* every body row's cells' inline nodes (row order, then column order)
|
|
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)
|
|
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
|
|
3365
|
-
* always holds). A table's inline cells and a list's items
|
|
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
|
|
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
|
-
*
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
3872
|
-
* that parse recorded. Given a {@link MarkdownDocument}, the document is adopted
|
|
3873
|
-
* and is
|
|
3874
|
-
* - **Provenance.** {@link span} reads the region of the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3903
|
+
* import { Markdown, isHeadingNode } from '@orkestrel/markdown'
|
|
3896
3904
|
*
|
|
3897
3905
|
* const markdown = new Markdown('# Title\n\nA **bold** [link](https://x.dev).')
|
|
3898
|
-
*
|
|
3899
|
-
*
|
|
3900
|
-
*
|
|
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 (
|
|
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
|
|
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)
|
|
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}
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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`
|