@orkestrel/markdown 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,1459 @@
1
- export type * from './types.js';
2
- export * from './constants.js';
3
- export * from './helpers.js';
4
- export * from './parsers.js';
5
- export * from './shapers.js';
6
- export * from './validators.js';
7
- export * from './Markdown.js';
8
- export * from './factories.js';
1
+ import { BooleanShape } from '@orkestrel/contract';
2
+ import { ContractInterface } from '@orkestrel/contract';
3
+ import { Guard } from '@orkestrel/contract';
4
+ import { LiteralShape } from '@orkestrel/contract';
5
+ import { NumberShape } from '@orkestrel/contract';
6
+ import { ObjectShape } from '@orkestrel/contract';
7
+ import { OptionalShape } from '@orkestrel/contract';
8
+ import { StringShape } from '@orkestrel/contract';
9
+
10
+ /** A node that can appear at the block level of a document (or inside a list item / blockquote). */
11
+ export declare type BlockNode = HeadingNode | ParagraphNode | ListNode | TableNode | CodeBlockNode | BlockquoteNode | ThematicBreakNode;
12
+
13
+ /** A blockquote - `>`-prefixed lines; `children` the block content parsed from the de-quoted lines (so quotes nest). */
14
+ export declare interface BlockquoteNode {
15
+ readonly element: 'blockquote';
16
+ /** The block content of the quote (the `>`-stripped lines, re-parsed as blocks). */
17
+ readonly children: readonly BlockNode[];
18
+ }
19
+
20
+ /**
21
+ * Merge adjacent text nodes into one - the inline scanner emits a text node per
22
+ * unrecognized character, so coalescing keeps the AST clean and assertion-friendly.
23
+ *
24
+ * @param nodes - The inline nodes (possibly with adjacent text runs)
25
+ * @returns The nodes with consecutive text nodes concatenated
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * coalesceText([{ element: 'text', value: 'a' }, { element: 'text', value: 'b' }])
30
+ * // [{ element: 'text', value: 'ab' }]
31
+ * ```
32
+ */
33
+ export declare function coalesceText(nodes: readonly InlineNode[]): readonly InlineNode[];
34
+
35
+ /**
36
+ * A fenced code block - ```` ```lang ````. `code` is the verbatim block content (no
37
+ * inner markdown; the closing fence and the trailing newline are stripped), `lang`
38
+ * the info-string language tag (the first word after the opening fence), absent when
39
+ * none was given.
40
+ */
41
+ export declare interface CodeBlockNode {
42
+ readonly element: 'codeBlock';
43
+ /** The info-string language tag (first word after the opening fence), if any. */
44
+ readonly lang?: string;
45
+ /** The verbatim code content (no inner markdown; HTML-escaped at render). */
46
+ readonly code: string;
47
+ }
48
+
49
+ /**
50
+ * The shape of a {@link CodeBlockNode} - a fenced code block. `lang` is
51
+ * optional (absent when the opening fence carries no info-string).
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { createContract } from '@orkestrel/contract'
56
+ * import { codeBlockShape } from '@src/core'
57
+ *
58
+ * const codeBlock = createContract(codeBlockShape)
59
+ * codeBlock.is({ element: 'codeBlock', code: 'x' }) // true
60
+ * codeBlock.is({ element: 'codeBlock', code: 'x', lang: 'ts' }) // true
61
+ * ```
62
+ */
63
+ export declare const codeBlockShape: ObjectShape<{
64
+ element: LiteralShape<readonly ["codeBlock"]>;
65
+ lang: OptionalShape<StringShape>;
66
+ code: StringShape;
67
+ }, false>;
68
+
69
+ /**
70
+ * An inline code span - `` `code` ``. `value` is the verbatim span text; no inner
71
+ * markdown is parsed (code is literal), and the renderer HTML-escapes it inside a
72
+ * `<code>` element.
73
+ */
74
+ export declare interface CodeSpanNode {
75
+ readonly element: 'codeSpan';
76
+ /** The verbatim code text (no inner markdown; HTML-escaped at render). */
77
+ readonly value: string;
78
+ }
79
+
80
+ /**
81
+ * The shape of a {@link CodeSpanNode} - an inline code span (`` `code` ``).
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * import { createContract } from '@orkestrel/contract'
86
+ * import { codeSpanShape } from '@src/core'
87
+ *
88
+ * const codeSpan = createContract(codeSpanShape)
89
+ * codeSpan.is({ element: 'codeSpan', value: 'const x = 1' }) // true
90
+ * ```
91
+ */
92
+ export declare const codeSpanShape: ObjectShape<{
93
+ element: LiteralShape<readonly ["codeSpan"]>;
94
+ value: StringShape;
95
+ }, false>;
96
+
97
+ /**
98
+ * Collects a list starting at the first item, gathering sibling items at the
99
+ * same indent/ordering and recursing into each item's own block content.
100
+ *
101
+ * @param lines - The markdown lines to scan.
102
+ * @param start - The index of the first list item.
103
+ * @param depth - The current recursion depth (each item recurses at `depth + 1`).
104
+ * @returns The parsed list node and the index of the first line after it.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * collectList(['- item'], 0, 0) // { node: { element: 'list', ... }, next: 1 }
109
+ * ```
110
+ */
111
+ export declare function collectList(lines: readonly string[], start: number, depth: number): {
112
+ readonly node: ListNode;
113
+ readonly next: number;
114
+ };
115
+
116
+ /**
117
+ * Collects a GFM table starting at a header row, parsing the header, the
118
+ * alignment row, and every contiguous body row that follows.
119
+ *
120
+ * @param lines - The markdown lines to scan.
121
+ * @param start - The index of the header row.
122
+ * @returns The parsed table node and the index of the first line after it.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * collectTable(['| a |', '| - |'], 0) // { node: { element: 'table', ... }, next: 2 }
127
+ * ```
128
+ */
129
+ export declare function collectTable(lines: readonly string[], start: number): {
130
+ readonly node: TableNode;
131
+ readonly next: number;
132
+ };
133
+
134
+ /**
135
+ * Compile the {@link codeBlockShape} into a {@link ContractInterface} for
136
+ * {@link CodeBlockNode} - a guard, coercing parser, JSON Schema, and seeded
137
+ * generator from one shape declaration (AGENTS §14).
138
+ *
139
+ * @returns A `CodeBlockNode` contract bundling `schema` / `is` / `parse` / `generate`
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * import { createCodeBlockContract } from '@src/core'
144
+ *
145
+ * const codeBlock = createCodeBlockContract()
146
+ * codeBlock.is({ element: 'codeBlock', code: 'x' }) // true
147
+ * ```
148
+ */
149
+ export declare function createCodeBlockContract(): ContractInterface<CodeBlockNode>;
150
+
151
+ /**
152
+ * Compile the {@link codeSpanShape} into a {@link ContractInterface} for
153
+ * {@link CodeSpanNode} - a guard, coercing parser, JSON Schema, and seeded
154
+ * generator from one shape declaration (AGENTS §14).
155
+ *
156
+ * @returns A `CodeSpanNode` contract bundling `schema` / `is` / `parse` / `generate`
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * import { createCodeSpanContract } from '@src/core'
161
+ *
162
+ * const codeSpan = createCodeSpanContract()
163
+ * codeSpan.is({ element: 'codeSpan', value: 'const x = 1' }) // true
164
+ * ```
165
+ */
166
+ export declare function createCodeSpanContract(): ContractInterface<CodeSpanNode>;
167
+
168
+ /**
169
+ * Create a stateful markdown handle from a markdown string or an already-parsed
170
+ * {@link MarkdownDocument} - a typed AST plus the query, rewrite, and fold operations
171
+ * {@link MarkdownInterface} exposes.
172
+ *
173
+ * @remarks
174
+ * Given a `string`, runs a block phase (headings / paragraphs / lists / GFM tables /
175
+ * fenced code / blockquotes / thematic breaks) then an inline phase (emphasis /
176
+ * inline code / links) to build a render-agnostic {@link MarkdownDocument}. Given a
177
+ * {@link MarkdownDocument}, adopts it AS-IS without re-validation - gate an untrusted
178
+ * value with `isMarkdownDocument` first. Pure + total parse (malformed markdown
179
+ * degrades to text, never throws) and zero-dependency - a hand-written scanner, no
180
+ * regex-only structural parse, linear-time (no ReDoS).
181
+ *
182
+ * @param input - A markdown string to parse, or an already-parsed {@link MarkdownDocument}
183
+ * @returns A working {@link MarkdownInterface}
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * import { createMarkdown } from '@src/core'
188
+ *
189
+ * const markdown = createMarkdown('# Hi\n\nRead the [guide](./guide.md).')
190
+ * markdown.document.children[0] // { element: 'heading', ... }
191
+ * ```
192
+ */
193
+ export declare function createMarkdown(input: string | MarkdownDocument): MarkdownInterface;
194
+
195
+ /**
196
+ * Compile the {@link textShape} into a {@link ContractInterface} for
197
+ * {@link TextNode} - a guard, coercing parser, JSON Schema, and seeded
198
+ * generator from one shape declaration (AGENTS §14).
199
+ *
200
+ * @returns A `TextNode` contract bundling `schema` / `is` / `parse` / `generate`
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * import { createTextContract } from '@src/core'
205
+ *
206
+ * const text = createTextContract()
207
+ * text.is({ element: 'text', value: 'hi' }) // true
208
+ * ```
209
+ */
210
+ export declare function createTextContract(): ContractInterface<TextNode>;
211
+
212
+ /**
213
+ * Compile the {@link thematicBreakShape} into a {@link ContractInterface} for
214
+ * {@link ThematicBreakNode} - a guard, coercing parser, JSON Schema, and
215
+ * seeded generator from one shape declaration (AGENTS §14).
216
+ *
217
+ * @returns A `ThematicBreakNode` contract bundling `schema` / `is` / `parse` / `generate`
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * import { createThematicBreakContract } from '@src/core'
222
+ *
223
+ * const thematicBreak = createThematicBreakContract()
224
+ * thematicBreak.is({ element: 'thematicBreak' }) // true
225
+ * ```
226
+ */
227
+ export declare function createThematicBreakContract(): ContractInterface<ThematicBreakNode>;
228
+
229
+ /**
230
+ * Emphasized inline content - `*italic*` / `_italic_` (`strong: false`) or
231
+ * `**bold**` / `__bold__` (`strong: true`). `children` are the nested inline nodes,
232
+ * so emphasis composes (a `**bold _and italic_**` is a strong node wrapping a text
233
+ * node and an emphasis node).
234
+ */
235
+ export declare interface EmphasisNode {
236
+ readonly element: 'emphasis';
237
+ /** `true` for strong (`**` / `__`, → `<strong>`); `false` for ordinary emphasis (`*` / `_`, → `<em>`). */
238
+ readonly strong: boolean;
239
+ /** The emphasized inline content. */
240
+ readonly children: readonly InlineNode[];
241
+ }
242
+
243
+ /**
244
+ * HTML-escape text content - `&` / `<` / `>` / `"` / `'` to their entities - so text
245
+ * from a markdown document can never inject markup. The renderer applies this to every
246
+ * text run, code body, and (escaped further) attribute value.
247
+ *
248
+ * @param text - The raw text
249
+ * @returns The HTML-escaped text
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * escapeHtml('<a>&"\'') // '&lt;a&gt;&amp;&quot;&#39;'
254
+ * ```
255
+ */
256
+ export declare function escapeHtml(text: string): string;
257
+
258
+ /**
259
+ * Extract a fenced-code opening line (```` ``` ```` or `~~~`, optionally with an info
260
+ * string) into its `{ marker, lang }`, or `undefined` when `line` is not a fence
261
+ * opener. `marker` is the exact fence run (the closer must match the same character +
262
+ * at least the same length); `lang` is the first word of the info string.
263
+ *
264
+ * @param line - The candidate line
265
+ * @returns The fence marker run and its language tag, or `undefined`
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * extractFence('```ts') // { marker: '```', lang: 'ts' }
270
+ * ```
271
+ */
272
+ export declare function extractFence(line: string): {
273
+ readonly marker: string;
274
+ readonly lang: string | undefined;
275
+ } | undefined;
276
+
277
+ /**
278
+ * Extract an ATX heading line (`#` … `######` followed by text) into its
279
+ * `{ level, text }`, or `undefined` when `line` is not a heading. A run of more than 6
280
+ * `#`s, or `#`s not followed by whitespace + text, is not a
281
+ * heading; an optional closing `###` run is stripped.
282
+ *
283
+ * @param line - The candidate line
284
+ * @returns The heading level (1–6) and its raw inline text, or `undefined`
285
+ *
286
+ * @example
287
+ * ```ts
288
+ * extractHeading('## Title') // { level: 2, text: 'Title' }
289
+ * ```
290
+ */
291
+ export declare function extractHeading(line: string): {
292
+ readonly level: number;
293
+ readonly text: string;
294
+ } | undefined;
295
+
296
+ /**
297
+ * Extract a list-item line (`-` / `*` / `+` bullet, or `1.` / `1)` ordinal, followed by
298
+ * a space) into its {@link ListItemParts}, or `undefined` when `line` is not a list
299
+ * item. `content` is the text after the marker; `marker` is the full marker-plus-space
300
+ * width (for measuring a continuation's indent).
301
+ *
302
+ * @param line - The candidate line
303
+ * @returns The list-item parts, or `undefined` when not a list item
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * extractListItem('- item') // { ordered: false, start: 1, content: 'item', indent: 0, marker: 2 }
308
+ * ```
309
+ */
310
+ export declare function extractListItem(line: string): ListItemParts | undefined;
311
+
312
+ /**
313
+ * Concatenate the `value` / `code` content of every descendant text / code-span /
314
+ * code-block node under `node`, in walk order - the plain-text projection of an AST
315
+ * (search indexing, word counts, a text-only preview).
316
+ *
317
+ * @remarks
318
+ * Total: never throws. Descent stops at {@link MAX_DEPTH} (contributes `''` past the
319
+ * cap instead of recursing further).
320
+ *
321
+ * @param node - The AST node to flatten (a full document, or any sub-node)
322
+ * @returns The concatenated text content
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * flattenText({ element: 'paragraph', children: [
327
+ * { element: 'text', value: 'a ' },
328
+ * { element: 'codeSpan', value: 'b' },
329
+ * ] })
330
+ * // 'a b'
331
+ * ```
332
+ */
333
+ export declare function flattenText(node: MarkdownNode): string;
334
+
335
+ /**
336
+ * Fold a {@link MarkdownNode} into a `T` via a total catamorphism - children are
337
+ * folded first (post-order), then the node's own {@link MarkdownHandler} is invoked
338
+ * with the already-folded children.
339
+ *
340
+ * @remarks
341
+ * **Table contract.** A {@link TableNode} has no single `children` array - its cells
342
+ * live in `header` (one inline-node list per column) and `rows` (a list of such
343
+ * rows). The `table` handler receives ONE folded `T` per inline node, flattened in
344
+ * walk order across ALL cells - every header cell's inline nodes (column order), then
345
+ * every body row's cells' inline nodes (row order, then column order) - and reads
346
+ * `node.header[c].length` / `node.rows[r][c].length` off the table node itself to
347
+ * recover cell boundaries within the flat list.
348
+ *
349
+ * Total: never throws. At `depth >= {@link MAX_DEPTH}` the node's handler is invoked
350
+ * with an empty children list instead of recursing further.
351
+ *
352
+ * @param node - The AST node to fold
353
+ * @param handlers - The total {@link MarkdownHandlers} table, one handler per element
354
+ * @param depth - The starting recursion depth (pass `0` at the entry point)
355
+ * @returns The folded `T`
356
+ *
357
+ * @example
358
+ * ```ts
359
+ * const countHandlers: MarkdownHandlers<number> = {
360
+ * document: (_, children) => children.reduce((a, b) => a + b, 1),
361
+ * // ...one handler per element, each summing its folded children
362
+ * }
363
+ * foldNode(document, countHandlers, 0) // total node count
364
+ * ```
365
+ */
366
+ export declare function foldNode<T>(node: MarkdownNode, handlers: MarkdownHandlers<T>, depth: number): T;
367
+
368
+ /**
369
+ * An ATX heading - `#` … `######`. `level` is 1–6 (the number of leading `#`),
370
+ * `children` the inline content of the heading text.
371
+ */
372
+ export declare interface HeadingNode {
373
+ readonly element: 'heading';
374
+ /** The heading level, 1 (`#`) through 6 (`######`). */
375
+ readonly level: number;
376
+ /** The inline content of the heading text. */
377
+ readonly children: readonly InlineNode[];
378
+ }
379
+
380
+ /** A node that can appear inside inline content (a heading / paragraph / cell / list item / link text). */
381
+ export declare type InlineNode = TextNode | EmphasisNode | CodeSpanNode | LinkNode;
382
+
383
+ /**
384
+ * Whether `line` is blank - empty, or containing only whitespace - the markdown
385
+ * definition of a blank line that block parsing uses to separate paragraphs, skip
386
+ * gaps, and end list continuations.
387
+ *
388
+ * @param line - The candidate line
389
+ * @returns `true` when the line is blank
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * isBlankLine(' ') // true
394
+ * ```
395
+ */
396
+ export declare function isBlankLine(line: string): boolean;
397
+
398
+ /**
399
+ * Determine whether an arbitrary value is a valid {@link BlockNode} - a
400
+ * heading, paragraph, list, table, code block, blockquote, or thematic break,
401
+ * recursively validated.
402
+ *
403
+ * @remarks
404
+ * Total: never throws, even on cyclic or pathologically deep input - every
405
+ * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
406
+ * throw-contained per the `@orkestrel/contract` guard contract (AGENTS §14).
407
+ * A list item's shape is inlined here (and in {@link isMarkdownNode}) rather
408
+ * than named separately - it is used at exactly these two sites.
409
+ *
410
+ * @param value - The value to test
411
+ * @returns `true` when `value` is a well-formed {@link BlockNode}
412
+ *
413
+ * @example
414
+ * ```ts
415
+ * import { isBlockNode } from '@orkestrel/markdown'
416
+ *
417
+ * isBlockNode({ element: 'thematicBreak' }) // true
418
+ * isBlockNode({ element: 'heading' }) // false - missing `level` / `children`
419
+ * ```
420
+ */
421
+ export declare const isBlockNode: Guard<BlockNode>;
422
+
423
+ /**
424
+ * Determine whether a node is a blockquote block.
425
+ *
426
+ * @example
427
+ * ```ts
428
+ * isBlockquoteNode({ element: 'blockquote', children: [] }) // true
429
+ * ```
430
+ */
431
+ export declare function isBlockquoteNode(node: MarkdownNode): node is BlockquoteNode;
432
+
433
+ /**
434
+ * Determine whether a node is a fenced code block.
435
+ *
436
+ * @example
437
+ * ```ts
438
+ * isCodeBlockNode({ element: 'codeBlock', code: 'x' }) // true
439
+ * ```
440
+ */
441
+ export declare function isCodeBlockNode(node: MarkdownNode): node is CodeBlockNode;
442
+
443
+ /**
444
+ * Determine whether a node is an inline code span.
445
+ *
446
+ * @remarks
447
+ * Narrows to {@link CodeSpanNode} - the node whose `element` discriminant is
448
+ * `'codeSpan'`.
449
+ *
450
+ * @example
451
+ * ```ts
452
+ * isCodeSpanNode({ element: 'codeSpan', value: 'x' }) // true
453
+ * ```
454
+ */
455
+ export declare function isCodeSpanNode(node: MarkdownNode): node is CodeSpanNode;
456
+
457
+ /**
458
+ * Determine whether a node is an emphasis run (`*em*` / `**strong**`).
459
+ *
460
+ * @example
461
+ * ```ts
462
+ * isEmphasisNode({ element: 'emphasis', strong: false, children: [] }) // true
463
+ * ```
464
+ */
465
+ export declare function isEmphasisNode(node: MarkdownNode): node is EmphasisNode;
466
+
467
+ /**
468
+ * Whether `character` is escapable by a leading backslash - the ASCII punctuation
469
+ * markdown gives meaning to (so `\*` becomes `*` but `\.` stays `\.`).
470
+ *
471
+ * @param character - The single character after a backslash
472
+ * @returns `true` when a backslash before it is an escape
473
+ *
474
+ * @example
475
+ * ```ts
476
+ * isEscapable('*') // true
477
+ * isEscapable('a') // false
478
+ * ```
479
+ */
480
+ export declare function isEscapable(character: string): boolean;
481
+
482
+ /**
483
+ * Whether `line` closes a fence opened by `marker` - the same fence character, a run
484
+ * at least as long, and nothing else but surrounding whitespace.
485
+ *
486
+ * @param line - The candidate closing line
487
+ * @param marker - The opening fence's marker run (from {@link extractFence})
488
+ * @returns `true` when `line` closes the fence
489
+ *
490
+ * @example
491
+ * ```ts
492
+ * isFenceClose('```', '```') // true
493
+ * ```
494
+ */
495
+ export declare function isFenceClose(line: string, marker: string): boolean;
496
+
497
+ /**
498
+ * Whether `character` is a regex-`\s`-equivalent whitespace character - the
499
+ * character class {@link isFenceClose}'s scan treats as surrounding padding.
500
+ *
501
+ * @param character - The single character to test, or `undefined` past the end of a line
502
+ * @returns `true` when it is whitespace
503
+ *
504
+ * @example
505
+ * ```ts
506
+ * isFenceWhitespace(' ') // true
507
+ * isFenceWhitespace(undefined) // false
508
+ * ```
509
+ */
510
+ export declare function isFenceWhitespace(character: string | undefined): boolean;
511
+
512
+ /** Determine whether a node is a heading block. */
513
+ export declare function isHeadingNode(node: MarkdownNode): node is HeadingNode;
514
+
515
+ /**
516
+ * Determine whether an arbitrary value is a valid {@link InlineNode} - a text
517
+ * run, emphasis, code span, or link, recursively validated.
518
+ *
519
+ * @remarks
520
+ * Total: never throws, even on cyclic or pathologically deep input - every
521
+ * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
522
+ * throw-contained per the `@orkestrel/contract` guard contract (AGENTS §14).
523
+ *
524
+ * @param value - The value to test
525
+ * @returns `true` when `value` is a well-formed {@link InlineNode}
526
+ *
527
+ * @example
528
+ * ```ts
529
+ * import { isInlineNode } from '@orkestrel/markdown'
530
+ *
531
+ * isInlineNode({ element: 'text', value: 'hi' }) // true
532
+ * isInlineNode({ element: 'text' }) // false - missing `value`
533
+ * ```
534
+ */
535
+ export declare const isInlineNode: Guard<InlineNode>;
536
+
537
+ /** Determine whether a node is a link. */
538
+ export declare function isLinkNode(node: MarkdownNode): node is LinkNode;
539
+
540
+ /**
541
+ * Determine whether a node is a list block.
542
+ *
543
+ * @example
544
+ * ```ts
545
+ * isListNode({ element: 'list', ordered: false, start: 1, items: [] }) // true
546
+ * ```
547
+ */
548
+ export declare function isListNode(node: MarkdownNode): node is ListNode;
549
+
550
+ /**
551
+ * Determine whether an arbitrary value is a valid {@link MarkdownDocument} -
552
+ * the parsed-AST root {@link parseDocument} returns, recursively
553
+ * validated.
554
+ *
555
+ * @remarks
556
+ * Total: never throws, even on cyclic or pathologically deep input - every
557
+ * combinator involved (`recordOf`, `arrayOf`) is throw-contained per the
558
+ * `@orkestrel/contract` guard contract (AGENTS §14).
559
+ *
560
+ * @param value - The value to test
561
+ * @returns `true` when `value` is a well-formed {@link MarkdownDocument}
562
+ *
563
+ * @example
564
+ * ```ts
565
+ * import { isMarkdownDocument } from '@orkestrel/markdown'
566
+ *
567
+ * isMarkdownDocument({ element: 'document', children: [] }) // true
568
+ * isMarkdownDocument({ element: 'document' }) // false - missing `children`
569
+ * ```
570
+ */
571
+ export declare const isMarkdownDocument: Guard<MarkdownDocument>;
572
+
573
+ /**
574
+ * Determine whether an arbitrary value is a valid {@link MarkdownNode} - the
575
+ * {@link MarkdownDocument} root, a {@link BlockNode}, a {@link ListItemNode}, or
576
+ * an {@link InlineNode}, recursively validated.
577
+ *
578
+ * @remarks
579
+ * Total: never throws, even on cyclic or pathologically deep input - every
580
+ * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
581
+ * throw-contained per the `@orkestrel/contract` guard contract (AGENTS §14).
582
+ * A list item's shape is inlined here (and in {@link isBlockNode}) rather than
583
+ * named separately - it is used at exactly these two sites.
584
+ *
585
+ * @param value - The value to test
586
+ * @returns `true` when `value` is a well-formed {@link MarkdownNode}
587
+ *
588
+ * @example
589
+ * ```ts
590
+ * import { isMarkdownNode } from '@orkestrel/markdown'
591
+ *
592
+ * isMarkdownNode({ element: 'text', value: 'hi' }) // true
593
+ * isMarkdownNode({ element: 'bogus' }) // false
594
+ * ```
595
+ */
596
+ export declare const isMarkdownNode: Guard<MarkdownNode>;
597
+
598
+ /**
599
+ * Determine whether a node is a paragraph block.
600
+ *
601
+ * @example
602
+ * ```ts
603
+ * isParagraphNode({ element: 'paragraph', children: [] }) // true
604
+ * ```
605
+ */
606
+ export declare function isParagraphNode(node: MarkdownNode): node is ParagraphNode;
607
+
608
+ /**
609
+ * Whether `line` is a blockquote line (`>` optionally indented up to three spaces) -
610
+ * its content is de-quoted by {@link stripQuote}.
611
+ *
612
+ * @param line - The candidate line
613
+ * @returns `true` when the line begins a blockquote
614
+ *
615
+ * @example
616
+ * ```ts
617
+ * isQuote('> quoted') // true
618
+ * ```
619
+ */
620
+ export declare function isQuote(line: string): boolean;
621
+
622
+ /** Determine whether a node is a GFM table block. */
623
+ export declare function isTableNode(node: MarkdownNode): node is TableNode;
624
+
625
+ /**
626
+ * Whether the pair (`header`, `delimiter`) opens a GFM table - `delimiter` is a row of
627
+ * `|`-separated cells each matching `:?-+:?`, the GFM rule that a table requires a
628
+ * header row IMMEDIATELY followed by a delimiter row.
629
+ *
630
+ * @param header - The candidate header line
631
+ * @param delimiter - The line after it (the candidate delimiter)
632
+ * @returns `true` when the two lines open a table
633
+ *
634
+ * @example
635
+ * ```ts
636
+ * isTableStart('| a |', '| - |') // true
637
+ * ```
638
+ */
639
+ export declare function isTableStart(header: string, delimiter: string | undefined): boolean;
640
+
641
+ /**
642
+ * Determine whether a node is a plain text run.
643
+ *
644
+ * @example
645
+ * ```ts
646
+ * isTextNode({ element: 'text', value: 'hi' }) // true
647
+ * ```
648
+ */
649
+ export declare function isTextNode(node: MarkdownNode): node is TextNode;
650
+
651
+ /**
652
+ * Whether `line` is a thematic break (horizontal rule) - three or more of the SAME
653
+ * marker `-`, `*`, or `_` (optionally space-separated) and nothing else (`---`,
654
+ * `***`, `___`, `- - -`).
655
+ *
656
+ * @param line - The candidate line
657
+ * @returns `true` when the line is a thematic break
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * isThematicBreak('---') // true
662
+ * ```
663
+ */
664
+ export declare function isThematicBreak(line: string): boolean;
665
+
666
+ /**
667
+ * Determine whether a node is a thematic break (horizontal rule) block.
668
+ *
669
+ * @example
670
+ * ```ts
671
+ * isThematicBreakNode({ element: 'thematicBreak' }) // true
672
+ * ```
673
+ */
674
+ export declare function isThematicBreakNode(node: MarkdownNode): node is ThematicBreakNode;
675
+
676
+ /**
677
+ * Whether `character` is an inline whitespace character (space / tab / newline) - the
678
+ * emphasis flanking rule's space test.
679
+ *
680
+ * @param character - The character to test
681
+ * @returns `true` when it is inline whitespace
682
+ *
683
+ * @example
684
+ * ```ts
685
+ * isWhitespace(' ') // true
686
+ * isWhitespace('a') // false
687
+ * ```
688
+ */
689
+ export declare function isWhitespace(character: string): boolean;
690
+
691
+ /**
692
+ * The count of leading space / tab characters on `line` (a tab counts as one) - the
693
+ * indent that decides whether a list item's continuation belongs to the item.
694
+ *
695
+ * @param line - The line to measure
696
+ * @returns The number of leading space / tab characters
697
+ *
698
+ * @example
699
+ * ```ts
700
+ * leadingIndent(' text') // 2
701
+ * ```
702
+ */
703
+ export declare function leadingIndent(line: string): number;
704
+
705
+ /**
706
+ * An inline link - `[text](href)`. `children` are the inline nodes of the link text;
707
+ * `href` is the destination, sanitized at render (a `javascript:` / other unsafe
708
+ * scheme is dropped to an empty `href`, and the value is HTML-attribute-escaped).
709
+ */
710
+ export declare interface LinkNode {
711
+ readonly element: 'link';
712
+ /** The link destination (sanitized + attribute-escaped at render). */
713
+ readonly href: string;
714
+ /** The inline content of the link text. */
715
+ readonly children: readonly InlineNode[];
716
+ }
717
+
718
+ /** One item of a {@link ListNode} - `children` the block content of the item (typically one paragraph, plus any nested list). */
719
+ export declare interface ListItemNode {
720
+ readonly element: 'listItem';
721
+ /** The block content of the list item (its text as a paragraph, plus any nested list). */
722
+ readonly children: readonly BlockNode[];
723
+ }
724
+
725
+ /**
726
+ * The parsed parts of a single list-item line - the value the block phase's
727
+ * list detector returns for a `-` / `*` / `+` bullet or a `1.` / `1)` ordinal line.
728
+ */
729
+ export declare interface ListItemParts {
730
+ /** `true` for an ordered (`1.` / `1)`) item, `false` for a bullet (`-` / `*` / `+`). */
731
+ readonly ordered: boolean;
732
+ /** The ordinal of an ordered item (its number); `1` for a bullet. */
733
+ readonly start: number;
734
+ /** The item's text after the marker. */
735
+ readonly content: string;
736
+ /** The leading-space indent of the marker. */
737
+ readonly indent: number;
738
+ /** The full marker width (indent + bullet/ordinal + the following space) - the continuation indent. */
739
+ readonly marker: number;
740
+ }
741
+
742
+ /**
743
+ * The shape of {@link ListItemParts} - the parsed parts of a single list-item
744
+ * line the block phase's list detector returns. Fully non-recursive (no
745
+ * nested node fields), so every field shapes directly.
746
+ *
747
+ * @example
748
+ * ```ts
749
+ * import { createContract } from '@orkestrel/contract'
750
+ * import { listItemPartsShape } from '@src/core'
751
+ *
752
+ * const listItemParts = createContract(listItemPartsShape)
753
+ * listItemParts.is({ ordered: false, start: 1, content: 'hi', indent: 0, marker: 2 }) // true
754
+ * ```
755
+ */
756
+ export declare const listItemPartsShape: ObjectShape<{
757
+ ordered: BooleanShape;
758
+ start: NumberShape;
759
+ content: StringShape;
760
+ indent: NumberShape;
761
+ marker: NumberShape;
762
+ }, false>;
763
+
764
+ /**
765
+ * A list - bulleted (`-` / `*` / `+`, `ordered: false`) or numbered (`1.` / `1)`,
766
+ * `ordered: true`). `start` is the first ordinal of an ordered list (usually `1`).
767
+ * Nesting is expressed by a {@link ListNode} appearing in a {@link ListItemNode}'s
768
+ * `children`.
769
+ */
770
+ export declare interface ListNode {
771
+ readonly element: 'list';
772
+ /** `true` for an ordered (numbered) list (→ `<ol>`); `false` for a bulleted list (→ `<ul>`). */
773
+ readonly ordered: boolean;
774
+ /** The starting ordinal of an ordered list (the first item's number); `1` for a bulleted list. */
775
+ readonly start: number;
776
+ /** The list's items, in order. */
777
+ readonly items: readonly ListItemNode[];
778
+ }
779
+
780
+ /**
781
+ * A stateful, parsed markdown document - wraps a typed {@link MarkdownDocument} AST
782
+ * with the query (`find` / `filter` / `reduce` / iteration), rewrite (`map`), fold, and
783
+ * streaming operations {@link MarkdownInterface} declares.
784
+ *
785
+ * @remarks
786
+ * - **Construction.** Given a `string`, the constructor runs {@link parseDocument} (the
787
+ * block phase then the inline phase) to build the AST. Given a {@link MarkdownDocument},
788
+ * the document is adopted AS-IS and is NOT re-validated - a caller adopting an
789
+ * untrusted value should gate it with `isMarkdownDocument` first.
790
+ * - **Immutable.** {@link map} never mutates the stored AST - it returns a NEW `Markdown`
791
+ * instance; the document root invariant (`element: 'document'`) always holds.
792
+ * - **Traversal order.** {@link walk} and the `find` / `filter` / `reduce` queries built
793
+ * on it walk the AST depth-first, pre-order, root-inclusive (via {@link walkNodes});
794
+ * `stream` is shallow - only the document's direct block children.
795
+ *
796
+ * @example
797
+ * ```ts
798
+ * import { Markdown, isHeadingNode, renderMarkdown } from '@src/core'
799
+ *
800
+ * const markdown = new Markdown('# Title\n\nA **bold** [link](https://x.dev).')
801
+ * const heading = markdown.find(isHeadingNode) // the HeadingNode, or undefined
802
+ * const shouted = markdown.map((node) =>
803
+ * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node,
804
+ * )
805
+ * renderMarkdown(shouted.document) // '# TITLE\n\nA **BOLD** [LINK](https://x.dev).'
806
+ * ```
807
+ */
808
+ export declare class Markdown implements MarkdownInterface {
809
+ #private;
810
+ constructor(input: string | MarkdownDocument);
811
+ /** The stored {@link MarkdownDocument} AST root. */
812
+ get document(): MarkdownDocument;
813
+ /**
814
+ * THE deep traversal - a lazy, depth-first, pre-order, root-inclusive generator
815
+ * over every {@link MarkdownNode} in the document. `find` / `filter` / `reduce`
816
+ * all iterate this single traversal.
817
+ *
818
+ * @example
819
+ * ```ts
820
+ * for (const node of markdown.walk()) {
821
+ * // every node, depth-first, pre-order, root-inclusive
822
+ * }
823
+ *
824
+ * // also consumable by for-await - JS accepts a sync iterable in for-await
825
+ * for await (const node of markdown.walk()) {
826
+ * // same sequence, no separate async iterator needed
827
+ * }
828
+ * ```
829
+ */
830
+ walk(): Generator<MarkdownNode>;
831
+ find<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): T | undefined;
832
+ find(predicate: (node: MarkdownNode) => boolean): MarkdownNode | undefined;
833
+ filter<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): readonly T[];
834
+ filter(predicate: (node: MarkdownNode) => boolean): readonly MarkdownNode[];
835
+ /** Rewrites the AST bottom-up (copy-on-write) and returns a new {@link Markdown}. */
836
+ map(rewrite: MarkdownRewriteHandler): MarkdownInterface;
837
+ /** Folds the AST depth-first, pre-order into an accumulator. */
838
+ reduce<T>(callback: (accumulator: T, node: MarkdownNode) => T, initial: T): T;
839
+ /** Runs a total catamorphism over the document using a {@link MarkdownHandlers} table. */
840
+ fold<T>(handlers: MarkdownHandlers<T>): T;
841
+ /**
842
+ * A web-standard {@link ReadableStream} over the document's top-level block nodes
843
+ * (shallow, source order) - a fresh, pull-based source per call: one block is
844
+ * enqueued per `pull`, so a slow reader's backpressure is respected. Cancellable,
845
+ * async-iterable wherever the platform supports it (Node, Deno), and pipeable
846
+ * through any {@link TransformStream} / {@link WritableStream}.
847
+ *
848
+ * @example
849
+ * ```ts
850
+ * // universal - works in every ReadableStream-supporting environment
851
+ * const reader = markdown.stream().getReader()
852
+ * for (let result = await reader.read(); !result.done; result = await reader.read()) {
853
+ * console.log(result.value) // one BlockNode
854
+ * }
855
+ *
856
+ * // Node / Deno / Firefox support async iteration of ReadableStream natively;
857
+ * // other environments should use the reader loop above instead.
858
+ * for await (const block of markdown.stream()) {
859
+ * console.log(block)
860
+ * }
861
+ * ```
862
+ */
863
+ stream(): ReadableStream<BlockNode>;
864
+ }
865
+
866
+ /**
867
+ * The root of a parsed markdown AST - the ordered block children of the whole
868
+ * document. The value {@link MarkdownInterface.document} holds.
869
+ */
870
+ export declare interface MarkdownDocument {
871
+ readonly element: 'document';
872
+ /** The document's top-level block nodes, in source order. */
873
+ readonly children: readonly BlockNode[];
874
+ }
875
+
876
+ /**
877
+ * A fold handler for one AST element - receives the node and its children
878
+ * ALREADY folded to `T`, and produces the node's own `T`. The building block of a
879
+ * {@link MarkdownHandlers} catamorphism table.
880
+ */
881
+ export declare type MarkdownHandler<TNode, T> = (node: TNode, children: readonly T[]) => T;
882
+
883
+ /**
884
+ * The total catamorphism table for {@link MarkdownInterface.fold} - one
885
+ * {@link MarkdownHandler} per AST element, keyed by its `element` discriminant. Every
886
+ * key is required: a fold is total over the AST, so there is no element it can skip.
887
+ */
888
+ export declare interface MarkdownHandlers<T> {
889
+ /** Folds a {@link MarkdownDocument} root from its already-folded block children. */
890
+ readonly document: MarkdownHandler<MarkdownDocument, T>;
891
+ /** Folds a {@link HeadingNode} from its already-folded inline children. */
892
+ readonly heading: MarkdownHandler<HeadingNode, T>;
893
+ /** Folds a {@link ParagraphNode} from its already-folded inline children. */
894
+ readonly paragraph: MarkdownHandler<ParagraphNode, T>;
895
+ /** Folds a {@link ThematicBreakNode} (leaf - always called with an empty children list). */
896
+ readonly thematicBreak: MarkdownHandler<ThematicBreakNode, T>;
897
+ /** Folds a {@link BlockquoteNode} from its already-folded block children. */
898
+ readonly blockquote: MarkdownHandler<BlockquoteNode, T>;
899
+ /** Folds a {@link CodeBlockNode} (leaf - always called with an empty children list). */
900
+ readonly codeBlock: MarkdownHandler<CodeBlockNode, T>;
901
+ /** Folds a {@link ListNode} from its already-folded item children. */
902
+ readonly list: MarkdownHandler<ListNode, T>;
903
+ /** Folds a {@link ListItemNode} from its already-folded block children. */
904
+ readonly listItem: MarkdownHandler<ListItemNode, T>;
905
+ /**
906
+ * Folds a {@link TableNode} from its cells' already-folded inline nodes, flattened
907
+ * to ONE folded `T` per inline node - header cells first (column order), then body
908
+ * rows' cells (row order, then column order). It is NOT a leaf: recover cell
909
+ * boundaries from `node.header[c].length` / `node.rows[r][c].length` against the
910
+ * flat `children` list.
911
+ */
912
+ readonly table: MarkdownHandler<TableNode, T>;
913
+ /** Folds a {@link TextNode} (leaf - always called with an empty children list). */
914
+ readonly text: MarkdownHandler<TextNode, T>;
915
+ /** Folds an {@link EmphasisNode} from its already-folded inline children. */
916
+ readonly emphasis: MarkdownHandler<EmphasisNode, T>;
917
+ /** Folds a {@link CodeSpanNode} (leaf - always called with an empty children list). */
918
+ readonly codeSpan: MarkdownHandler<CodeSpanNode, T>;
919
+ /** Folds a {@link LinkNode} from its already-folded inline children. */
920
+ readonly link: MarkdownHandler<LinkNode, T>;
921
+ }
922
+
923
+ /**
924
+ * A stateful, parsed markdown document: the typed {@link MarkdownDocument} AST plus
925
+ * the query, rewrite, and fold operations over it.
926
+ *
927
+ * @remarks
928
+ * - **Immutable.** {@link MarkdownInterface.map} never mutates the stored AST - it
929
+ * returns a NEW {@link MarkdownInterface} instance; the document root invariant
930
+ * (`element: 'document'`) always holds.
931
+ * - **Traversal order.** `walk` / `find` / `filter` / `reduce` walk the AST
932
+ * depth-first, pre-order, root-inclusive; `stream` is shallow - only the
933
+ * document's direct block children.
934
+ * - **`stream`.** Returns a web-standard {@link ReadableStream} over the top-level
935
+ * blocks - a fresh, pull-based source per call: exactly one block is enqueued per
936
+ * `pull`, so a slow consumer's backpressure is respected and no work happens ahead
937
+ * of demand. Cancellable via the returned stream's own `cancel()`, async-iterable
938
+ * wherever the platform supports it (Node, Deno, and browsers that ship the
939
+ * proposal), and pipeable through any {@link TransformStream} / {@link WritableStream}.
940
+ * - **The seven-method surface.** `document` (the AST root), `walk` (the deep
941
+ * traversal), `find` / `filter` / `reduce` (queries built on `walk`), `map` (the
942
+ * bottom-up rewrite), `fold` (the total catamorphism), and `stream` (the shallow,
943
+ * backpressured top-level source).
944
+ */
945
+ export declare interface MarkdownInterface {
946
+ /** The stored {@link MarkdownDocument} AST root. */
947
+ readonly document: MarkdownDocument;
948
+ /**
949
+ * THE deep traversal - a lazy, depth-first, pre-order, root-inclusive
950
+ * {@link Generator} over every {@link MarkdownNode} in the document. The sync
951
+ * `for (const node of markdown.walk())` surface is also consumable by
952
+ * `for await (const node of markdown.walk())` (JavaScript accepts a sync
953
+ * iterable in a `for await`), so async pipelines need no separate iterator.
954
+ * Contrast with {@link stream}: `walk` is deep, every-node, and sync; `stream`
955
+ * is shallow (top-level blocks only) and backpressure-respecting.
956
+ */
957
+ walk(): Generator<MarkdownNode>;
958
+ /** Finds the first node (depth-first, pre-order) narrowed by a type guard. */
959
+ find<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): T | undefined;
960
+ /** Finds the first node (depth-first, pre-order) matching a predicate. */
961
+ find(predicate: (node: MarkdownNode) => boolean): MarkdownNode | undefined;
962
+ /** Collects every node (depth-first, pre-order) narrowed by a type guard. */
963
+ filter<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): readonly T[];
964
+ /** Collects every node (depth-first, pre-order) matching a predicate. */
965
+ filter(predicate: (node: MarkdownNode) => boolean): readonly MarkdownNode[];
966
+ /** Rewrites the AST bottom-up (copy-on-write) and returns a new {@link MarkdownInterface}. */
967
+ map(rewrite: MarkdownRewriteHandler): MarkdownInterface;
968
+ /** Folds the AST depth-first, pre-order into an accumulator. */
969
+ reduce<T>(callback: (accumulator: T, node: MarkdownNode) => T, initial: T): T;
970
+ /** Runs a total catamorphism over the document using a {@link MarkdownHandlers} table. */
971
+ fold<T>(handlers: MarkdownHandlers<T>): T;
972
+ /**
973
+ * A web-standard {@link ReadableStream} over the document's top-level block nodes
974
+ * (shallow, source order) - a lazy, pull-based, backpressure-respecting source. A
975
+ * fresh, independently-replayable stream every call; never mutates the document.
976
+ */
977
+ stream(): ReadableStream<BlockNode>;
978
+ }
979
+
980
+ /**
981
+ * Any node in a markdown AST - the {@link MarkdownDocument} root, a {@link BlockNode},
982
+ * a {@link ListItemNode}, or an {@link InlineNode}. The exhaustive set the renderer's
983
+ * `switch` covers.
984
+ */
985
+ export declare type MarkdownNode = MarkdownDocument | BlockNode | ListItemNode | InlineNode;
986
+
987
+ /**
988
+ * A copy-on-write node rewrite applied bottom-up by {@link MarkdownInterface.map} -
989
+ * receives one node (its own children already rewritten) and returns its
990
+ * replacement (the same node, unchanged, or a new node).
991
+ */
992
+ export declare type MarkdownRewriteHandler = (node: MarkdownNode) => MarkdownNode;
993
+
994
+ /**
995
+ * The maximum recursion depth the parse pipeline (`parseDocument` and its
996
+ * `parsers.ts` helpers) and the `helpers.ts` traversal / render functions
997
+ * (`renderHTML`, `renderMarkdown`, `walkNodes`, `foldNode`) honor before degrading to
998
+ * literal text - bounds blockquote nesting, inline nesting (emphasis / links), and
999
+ * traversal/render recursion so pathological or hostile input (deeply nested
1000
+ * blockquotes, runaway emphasis) cannot exhaust the call stack. Past this depth the
1001
+ * parser treats the remaining content as literal text instead of recursing further.
1002
+ */
1003
+ export declare const MAX_DEPTH = 64;
1004
+
1005
+ /** A paragraph - a run of non-blank lines that is not another block; `children` its inline content. */
1006
+ export declare interface ParagraphNode {
1007
+ readonly element: 'paragraph';
1008
+ /** The inline content of the paragraph. */
1009
+ readonly children: readonly InlineNode[];
1010
+ }
1011
+
1012
+ /**
1013
+ * Parses a run of markdown lines into a block AST, recursing into nested
1014
+ * blockquotes, list items, and depth-capped degrade paragraphs.
1015
+ *
1016
+ * @param lines - The markdown lines to parse.
1017
+ * @param depth - The current recursion depth (blockquotes/lists increment it).
1018
+ * @returns The parsed block nodes.
1019
+ *
1020
+ * @example
1021
+ * ```ts
1022
+ * parseBlocks(['# Hi'], 0) // [{ element: 'heading', level: 1, children: [...] }]
1023
+ * ```
1024
+ */
1025
+ export declare function parseBlocks(lines: readonly string[], depth: number): readonly BlockNode[];
1026
+
1027
+ /**
1028
+ * Parses a markdown string into a typed {@link MarkdownDocument} AST via the
1029
+ * block phase.
1030
+ *
1031
+ * @param markdown - The markdown source to parse.
1032
+ * @returns The parsed document.
1033
+ */
1034
+ export declare function parseDocument(markdown: string): MarkdownDocument;
1035
+
1036
+ /**
1037
+ * Parses inline markdown text (emphasis, code spans, links) into inline AST
1038
+ * nodes, coalescing adjacent text runs.
1039
+ *
1040
+ * @param text - The inline markdown text to parse.
1041
+ * @returns The parsed inline nodes.
1042
+ */
1043
+ export declare function parseInline(text: string): readonly InlineNode[];
1044
+
1045
+ /**
1046
+ * Render a {@link MarkdownNode} (typically a {@link MarkdownDocument}) to a safe HTML
1047
+ * string - the recursive AST → HTML engine (headings, paragraphs, lists, GFM tables,
1048
+ * fenced code, blockquotes, links, emphasis, inline code), escaping every text run and
1049
+ * sanitizing every link `href`.
1050
+ *
1051
+ * @remarks
1052
+ * Total: never throws. At {@link MAX_DEPTH} a value-bearing node (`text` / `codeSpan`)
1053
+ * degrades to its escaped `value`; any other node degrades to `''` instead of
1054
+ * recursing further, so pathologically deep input cannot exhaust the call stack.
1055
+ *
1056
+ * @param node - The AST node to render (a full document, or any sub-node)
1057
+ * @returns The rendered, XSS-safe HTML string
1058
+ *
1059
+ * @example
1060
+ * ```ts
1061
+ * renderHTML({ element: 'document', children: [
1062
+ * { element: 'heading', level: 1, children: [{ element: 'text', value: 'Hi' }] },
1063
+ * ] })
1064
+ * // '<h1>Hi</h1>'
1065
+ * ```
1066
+ */
1067
+ export declare function renderHTML(node: MarkdownNode): string;
1068
+
1069
+ /**
1070
+ * Render a {@link MarkdownNode} to its CANONICAL markdown source - the inverse
1071
+ * projection of `renderHTML`, and the serializer a `parse(renderMarkdown(doc))`
1072
+ * round-trip is built on. Canonical forms: `*em*` / `**strong**` (underscore emphasis
1073
+ * normalizes to asterisks), `- ` bullets, `N. ` sequential ordinals (from the list's
1074
+ * `start`), `---` thematic breaks, fenced code blocks (backtick run widened past any
1075
+ * 3+ backtick run inside the body), ATX headings, `> `-prefixed blockquote lines, GFM
1076
+ * tables (1-space-padded cells, `\|`-escaped pipes, an alignment delimiter row), and
1077
+ * `[text](href)` links. A `text` node's literal content is backslash-escaped wherever
1078
+ * it would otherwise re-parse as markup (AGENTS §14 parse↔render soundness).
1079
+ *
1080
+ * @remarks
1081
+ * Total: never throws. At {@link MAX_DEPTH} a value-bearing node degrades to its
1082
+ * escaped `value`; any other node degrades to `''`. Blocks are joined by exactly one
1083
+ * blank line; a document with zero blocks renders `''`.
1084
+ *
1085
+ * @param node - The AST node to render (a full document, or any sub-node)
1086
+ * @returns The canonical markdown source
1087
+ *
1088
+ * @example
1089
+ * ```ts
1090
+ * renderMarkdown({ element: 'document', children: [
1091
+ * { element: 'heading', level: 2, children: [{ element: 'text', value: 'Hi' }] },
1092
+ * ] })
1093
+ * // '## Hi'
1094
+ * ```
1095
+ */
1096
+ export declare function renderMarkdown(node: MarkdownNode): string;
1097
+
1098
+ /**
1099
+ * Rewrite a {@link MarkdownDocument} bottom-up (copy-on-write) - each node's children
1100
+ * are rewritten first (post-order), then `rewrite` is applied to the node itself; the
1101
+ * document ROOT is never passed to `rewrite` (the `element: 'document'` invariant
1102
+ * always holds). A table's inline cells and a list's items ARE rewritten.
1103
+ *
1104
+ * @remarks
1105
+ * Never mutates `document` - every level is rebuilt into a fresh object/array, even
1106
+ * when `rewrite` returns its input unchanged. When `rewrite` returns a node whose
1107
+ * `element` does not fit the slot it was called for (a block slot handed a
1108
+ * non-{@link BlockNode}, an inline slot handed a non-{@link InlineNode}, a list-item
1109
+ * slot handed a non-`listItem`), the ill-fitting result is discarded and the
1110
+ * freshly-rebuilt (unrewritten-at-this-level) node is kept instead - `rewriteDocument`
1111
+ * stays total and never produces a structurally invalid document.
1112
+ *
1113
+ * Descent is capped at {@link MAX_DEPTH}, the same cap {@link walkNodes} and
1114
+ * {@link foldNode} observe: at `depth >= MAX_DEPTH` the subtree is passed through
1115
+ * UNCHANGED (by reference, not rebuilt, and `rewrite` is not invoked on it) instead of
1116
+ * recursing further, so a pathologically deep adopted document cannot exhaust the
1117
+ * call stack. {@link MarkdownInterface.map} inherits this cap since it delegates here.
1118
+ *
1119
+ * @param document - The document AST to rewrite
1120
+ * @param rewrite - The bottom-up {@link MarkdownRewriteHandler}
1121
+ * @returns A new, rewritten {@link MarkdownDocument}
1122
+ *
1123
+ * @example
1124
+ * ```ts
1125
+ * rewriteDocument(document, (node) =>
1126
+ * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node,
1127
+ * )
1128
+ * ```
1129
+ */
1130
+ export declare function rewriteDocument(document: MarkdownDocument, rewrite: MarkdownRewriteHandler): MarkdownDocument;
1131
+
1132
+ /**
1133
+ * The URL schemes `renderHTML` permits on a link `href` - anything else (notably
1134
+ * `javascript:`, `data:`, `vbscript:`, `file:`) is dropped to an empty `href` so a
1135
+ * hostile link can never execute. Frozen, lower-case; a relative / anchor /
1136
+ * scheme-less `href` (no `scheme:` prefix) is always allowed.
1137
+ */
1138
+ export declare const SAFE_URL_SCHEMES: ReadonlySet<string>;
1139
+
1140
+ /**
1141
+ * Sanitize + HTML-attribute-escape a link `href` - a destination whose scheme is not
1142
+ * in {@link SAFE_URL_SCHEMES} (notably `javascript:` / `data:` / `vbscript:`), or that
1143
+ * is protocol-relative (`//host/path`, or a backslash variant a browser normalizes to
1144
+ * the same effect - `\\host`, `/\host`, `\/host` - inherits whatever scheme the
1145
+ * embedding page is served over, including an unsafe one), is dropped to an empty
1146
+ * string; a relative / anchor / scheme-less (and non-protocol-relative) destination
1147
+ * (including a SINGLE leading `/` or `\`) is kept;
1148
+ * the surviving value is then HTML-escaped. Defence-in-depth against an XSS `href`,
1149
+ * even though the input is trusted.
1150
+ *
1151
+ * @param href - The raw link destination
1152
+ * @returns A safe, escaped `href` (empty when the scheme is unsafe or protocol-relative)
1153
+ *
1154
+ * @example
1155
+ * ```ts
1156
+ * sanitizeUrl('javascript:alert(1)') // ''
1157
+ * sanitizeUrl('/path') // '/path'
1158
+ * ```
1159
+ */
1160
+ export declare function sanitizeUrl(href: string): string;
1161
+
1162
+ /**
1163
+ * Scan an inline code span at `start` (a `` ` ``-run … a matching `` ` ``-run of the
1164
+ * SAME length, the CommonMark rule that lets a span contain backticks). Returns the
1165
+ * span's literal text + end index, or `undefined` when no matching closer exists (it
1166
+ * then degrades to literal backticks).
1167
+ *
1168
+ * @param source - The inline source text
1169
+ * @param start - The index of the opening backtick
1170
+ * @param to - The exclusive end of the scan window
1171
+ * @returns The span text + end index, or `undefined`
1172
+ *
1173
+ * @example
1174
+ * ```ts
1175
+ * scanCode('`code`', 0, 6) // { value: 'code', end: 6 }
1176
+ * ```
1177
+ */
1178
+ export declare function scanCode(source: string, start: number, to: number): {
1179
+ readonly value: string;
1180
+ readonly end: number;
1181
+ } | undefined;
1182
+
1183
+ /**
1184
+ * Scan an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1185
+ * matching closing run of the same marker + width, requiring non-space immediately
1186
+ * inside both delimiters (the CommonMark flanking simplification that blocks `* x *`).
1187
+ * Returns the emphasis node, or `undefined` when no valid closer exists (it then
1188
+ * degrades to a literal marker).
1189
+ *
1190
+ * @param source - The inline source text
1191
+ * @param start - The index of the opening marker
1192
+ * @param to - The exclusive end of the scan window
1193
+ * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1194
+ * at {@link MAX_DEPTH} the emphasis's children degrade to literal text instead of
1195
+ * recursing further
1196
+ * @returns The parsed {@link EmphasisNode} + end index, or `undefined`
1197
+ *
1198
+ * @example
1199
+ * ```ts
1200
+ * scanEmphasis('*em*', 0, 4)
1201
+ * // { node: { element: 'emphasis', strong: false, children: [...] }, end: 4 }
1202
+ * ```
1203
+ */
1204
+ export declare function scanEmphasis(source: string, start: number, to: number, depth?: number): {
1205
+ readonly node: EmphasisNode;
1206
+ readonly end: number;
1207
+ } | undefined;
1208
+
1209
+ /**
1210
+ * Scan the window `[from, to)` of `source` into inline nodes - the single recursive
1211
+ * engine the inline phase runs on (emphasis / link text recurse through it). Linear:
1212
+ * each character is consumed once; a failed construct emits its opening character as
1213
+ * text and advances by one, so there is no re-scan (no ReDoS).
1214
+ *
1215
+ * @param source - The inline source text
1216
+ * @param from - The inclusive start of the scan window
1217
+ * @param to - The exclusive end of the scan window
1218
+ * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1219
+ * incremented by one on every recursive descent through {@link scanLink} /
1220
+ * {@link scanEmphasis}. At {@link MAX_DEPTH} the window is never scanned for markup -
1221
+ * it emits as a single literal text node - so pathological nesting (`[[[[…`,
1222
+ * `****…`) cannot exhaust the call stack.
1223
+ * @returns The parsed inline nodes (NOT yet coalesced)
1224
+ *
1225
+ * @example
1226
+ * ```ts
1227
+ * scanInline('hi *there*', 0, 10) // [{ element: 'text', value: 'hi ' }, { element: 'emphasis', ... }]
1228
+ * ```
1229
+ */
1230
+ export declare function scanInline(source: string, from: number, to: number, depth?: number): readonly InlineNode[];
1231
+
1232
+ /**
1233
+ * Scan a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1234
+ * must immediately follow and the destination runs to the matching `)` (both respect
1235
+ * nested delimiters + escapes). Returns the link node, or `undefined` when the shape
1236
+ * does not hold (it then degrades to a literal `[`).
1237
+ *
1238
+ * @param source - The inline source text
1239
+ * @param start - The index of the opening `[`
1240
+ * @param to - The exclusive end of the scan window
1241
+ * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1242
+ * at {@link MAX_DEPTH} the link's text children degrade to literal text instead of
1243
+ * recursing further
1244
+ * @returns The parsed {@link LinkNode} + end index, or `undefined`
1245
+ *
1246
+ * @example
1247
+ * ```ts
1248
+ * scanLink('[text](url)', 0, 11)
1249
+ * // { node: { element: 'link', href: 'url', children: [...] }, end: 11 }
1250
+ * ```
1251
+ */
1252
+ export declare function scanLink(source: string, start: number, to: number, depth?: number): {
1253
+ readonly node: LinkNode;
1254
+ readonly end: number;
1255
+ } | undefined;
1256
+
1257
+ /**
1258
+ * Normalize line endings to `\n` and split a markdown document into its lines - CRLF
1259
+ * (`\r\n`) and bare CR (`\r`) both collapse to `\n` first, so a Windows-origin
1260
+ * document parses identically. A single trailing newline does not yield a final
1261
+ * empty line.
1262
+ *
1263
+ * @param markdown - The raw markdown source
1264
+ * @returns The document's lines, line-terminators stripped
1265
+ *
1266
+ * @example
1267
+ * ```ts
1268
+ * splitLines('a\r\nb\nc') // ['a', 'b', 'c']
1269
+ * ```
1270
+ */
1271
+ export declare function splitLines(markdown: string): readonly string[];
1272
+
1273
+ /**
1274
+ * Split one GFM table row into its cell strings - outer pipes are optional, an escaped
1275
+ * pipe (`\|`) inside a cell is NOT a separator (it becomes a literal `|`), and the
1276
+ * empty leading / trailing cell produced by an outer `|` is dropped.
1277
+ *
1278
+ * @param row - The raw table row line
1279
+ * @returns The row's cells, in column order
1280
+ *
1281
+ * @example
1282
+ * ```ts
1283
+ * splitTableRow('|a|b|') // ['a', 'b']
1284
+ * ```
1285
+ */
1286
+ export declare function splitTableRow(row: string): readonly string[];
1287
+
1288
+ /**
1289
+ * Whether the line at `index` starts a NEW block kind (heading / fence / thematic
1290
+ * break / blockquote / list / table) - the paragraph collector stops at such a line
1291
+ * so a block following a paragraph without a blank line still parses (a trusted-input
1292
+ * caller writing a `##` heading directly under a paragraph, with no intervening blank
1293
+ * line).
1294
+ *
1295
+ * @param lines - The document's lines
1296
+ * @param index - The line index to test
1297
+ * @returns `true` when the line begins a different block
1298
+ *
1299
+ * @example
1300
+ * ```ts
1301
+ * startsBlock(['text', '## Heading'], 1) // true
1302
+ * ```
1303
+ */
1304
+ export declare function startsBlock(lines: readonly string[], index: number): boolean;
1305
+
1306
+ /**
1307
+ * Strip one level of blockquote marker (`>` plus one optional following space) from a
1308
+ * blockquote line, so the de-quoted lines re-parse as nested blocks.
1309
+ *
1310
+ * @param line - A blockquote line (per {@link isQuote})
1311
+ * @returns The line with its leading `>` (and one space) removed
1312
+ *
1313
+ * @example
1314
+ * ```ts
1315
+ * stripQuote('> text') // 'text'
1316
+ * ```
1317
+ */
1318
+ export declare function stripQuote(line: string): string;
1319
+
1320
+ /**
1321
+ * The horizontal alignment of a GFM table column, as declared by its delimiter row
1322
+ * (`:---` left, `---:` right, `:---:` center) - `'none'` when the delimiter carries
1323
+ * no alignment colon. One entry per column, in column order.
1324
+ */
1325
+ export declare type TableAlign = 'none' | 'left' | 'right' | 'center';
1326
+
1327
+ /**
1328
+ * Derive the per-column {@link TableAlign} list from a GFM delimiter row - `:---`
1329
+ * left, `---:` right, `:---:` center, `---` none.
1330
+ *
1331
+ * @param delimiter - The table's delimiter row
1332
+ * @returns One alignment per column, in column order
1333
+ *
1334
+ * @example
1335
+ * ```ts
1336
+ * tableAlignments('| :--- | ---: |') // ['left', 'right']
1337
+ * ```
1338
+ */
1339
+ export declare function tableAlignments(delimiter: string): readonly TableAlign[];
1340
+
1341
+ /**
1342
+ * The shape of a {@link TableAlign} - the per-column GFM table alignment
1343
+ * literal.
1344
+ *
1345
+ * @example
1346
+ * ```ts
1347
+ * import { createContract } from '@orkestrel/contract'
1348
+ * import { tableAlignShape } from '@src/core'
1349
+ *
1350
+ * const tableAlign = createContract(tableAlignShape)
1351
+ * tableAlign.is('left') // true
1352
+ * tableAlign.is('center') // true
1353
+ * tableAlign.is('top') // false
1354
+ * ```
1355
+ */
1356
+ export declare const tableAlignShape: LiteralShape<readonly ["none", "left", "right", "center"]>;
1357
+
1358
+ /**
1359
+ * A GFM table - `header` the inline content of each header cell, `rows` the body
1360
+ * rows (each a list of cells, each cell inline content), `align` the per-column
1361
+ * alignment from the delimiter row. A short body row is padded with empty cells; an
1362
+ * over-long one is truncated to the header's column count.
1363
+ */
1364
+ export declare interface TableNode {
1365
+ readonly element: 'table';
1366
+ /** The header row - one cell of inline content per column. */
1367
+ readonly header: readonly (readonly InlineNode[])[];
1368
+ /** The body rows - each a list of cells, each cell inline content. */
1369
+ readonly rows: readonly (readonly (readonly InlineNode[])[])[];
1370
+ /** The per-column alignment from the delimiter row, in column order. */
1371
+ readonly align: readonly TableAlign[];
1372
+ }
1373
+
1374
+ /**
1375
+ * A run of plain text - the leaf inline node. `value` is the decoded text with
1376
+ * markdown escapes (`\*`, `\_`, …) already resolved to their literal characters; the
1377
+ * renderer HTML-escapes it (`<` / `>` / `&` / `"`) on the way out.
1378
+ */
1379
+ export declare interface TextNode {
1380
+ readonly element: 'text';
1381
+ /** The literal text content (escapes resolved, NOT yet HTML-escaped). */
1382
+ readonly value: string;
1383
+ }
1384
+
1385
+ /**
1386
+ * The shape of a {@link TextNode} - a plain-text leaf inline run.
1387
+ *
1388
+ * @example
1389
+ * ```ts
1390
+ * import { createContract } from '@orkestrel/contract'
1391
+ * import { textShape } from '@src/core'
1392
+ *
1393
+ * const text = createContract(textShape)
1394
+ * text.is({ element: 'text', value: 'hi' }) // true
1395
+ * ```
1396
+ */
1397
+ export declare const textShape: ObjectShape<{
1398
+ element: LiteralShape<readonly ["text"]>;
1399
+ value: StringShape;
1400
+ }, false>;
1401
+
1402
+ /** A thematic break - a horizontal rule (`---` / `***` / `___` on its own line). */
1403
+ export declare interface ThematicBreakNode {
1404
+ readonly element: 'thematicBreak';
1405
+ }
1406
+
1407
+ /**
1408
+ * The shape of a {@link ThematicBreakNode} - a horizontal rule. Carries no
1409
+ * fields beyond its `element` discriminant.
1410
+ *
1411
+ * @example
1412
+ * ```ts
1413
+ * import { createContract } from '@orkestrel/contract'
1414
+ * import { thematicBreakShape } from '@src/core'
1415
+ *
1416
+ * const thematicBreak = createContract(thematicBreakShape)
1417
+ * thematicBreak.is({ element: 'thematicBreak' }) // true
1418
+ * ```
1419
+ */
1420
+ export declare const thematicBreakShape: ObjectShape<{
1421
+ element: LiteralShape<readonly ["thematicBreak"]>;
1422
+ }, false>;
1423
+
1424
+ /**
1425
+ * Resolve backslash escapes in a raw string to their literal characters - used for a
1426
+ * link `href` (which is not otherwise inline-parsed) and any plain text run.
1427
+ *
1428
+ * @param text - The raw text possibly carrying `\x` escapes
1429
+ * @returns The text with escapable `\x` reduced to `x`
1430
+ *
1431
+ * @example
1432
+ * ```ts
1433
+ * unescapeText('\\*hi\\*') // '*hi*'
1434
+ * ```
1435
+ */
1436
+ export declare function unescapeText(text: string): string;
1437
+
1438
+ /**
1439
+ * Depth-first, pre-order, root-inclusive traversal of a {@link MarkdownNode} - yields
1440
+ * the node itself, then recurses into its children (block children, list items, table
1441
+ * header/row cells' inline nodes) in walk order.
1442
+ *
1443
+ * @remarks
1444
+ * Total: never throws. Descent stops at {@link MAX_DEPTH} (the node at the cap is
1445
+ * still yielded; its children are not) so pathologically deep input cannot exhaust
1446
+ * the call stack.
1447
+ *
1448
+ * @param node - The AST node to walk (a full document, or any sub-node)
1449
+ * @returns A generator yielding every visited node, pre-order
1450
+ *
1451
+ * @example
1452
+ * ```ts
1453
+ * const doc = { element: 'document', children: [{ element: 'thematicBreak' }] } as const
1454
+ * [...walkNodes(doc)].map((node) => node.element) // ['document', 'thematicBreak']
1455
+ * ```
1456
+ */
1457
+ export declare function walkNodes(node: MarkdownNode): Generator<MarkdownNode>;
1458
+
1459
+ export { }