@orkestrel/markdown 0.0.5 → 0.0.7

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,1822 @@
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 { CommentNode } from '@orkestrel/html';
3
+ import { ContractInterface } from '@orkestrel/contract';
4
+ import { DoctypeNode } from '@orkestrel/html';
5
+ import { ElementNode } from '@orkestrel/html';
6
+ import { Guard } from '@orkestrel/contract';
7
+ import { HTMLDocument } from '@orkestrel/html';
8
+ import { HTMLNode } from '@orkestrel/html';
9
+ import { LiteralShape } from '@orkestrel/contract';
10
+ import { NumberShape } from '@orkestrel/contract';
11
+ import { ObjectShape } from '@orkestrel/contract';
12
+ import { OptionalShape } from '@orkestrel/contract';
13
+ import { StringShape } from '@orkestrel/contract';
14
+ import { TextNode as TextNode_2 } from '@orkestrel/html';
15
+
16
+ /** A node that can appear at the block level of a document (or inside a list item / blockquote). */
17
+ export declare type BlockNode = HeadingNode | ParagraphNode | ListNode | TableNode | CodeBlockNode | BlockquoteNode | ThematicBreakNode;
18
+
19
+ /** A blockquote - `>`-prefixed lines; `children` the block content parsed from the de-quoted lines (so quotes nest). */
20
+ export declare interface BlockquoteNode {
21
+ readonly element: 'blockquote';
22
+ /** The block content of the quote (the `>`-stripped lines, re-parsed as blocks). */
23
+ readonly children: readonly BlockNode[];
24
+ }
25
+
26
+ /**
27
+ * Merge adjacent text nodes into one - the inline scanner emits a text node per
28
+ * unrecognized character, so coalescing keeps the AST clean and assertion-friendly.
29
+ *
30
+ * @param nodes - The inline nodes (possibly with adjacent text runs)
31
+ * @returns The nodes with consecutive text nodes concatenated
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * coalesceText([{ element: 'text', value: 'a' }, { element: 'text', value: 'b' }])
36
+ * // [{ element: 'text', value: 'ab' }]
37
+ * ```
38
+ */
39
+ export declare function coalesceText(nodes: readonly InlineNode[]): readonly InlineNode[];
40
+
41
+ /**
42
+ * A fenced code block - ```` ```lang ````. `code` is the verbatim block content (no
43
+ * inner markdown; the closing fence and the trailing newline are stripped), `lang`
44
+ * the info-string language tag (the first word after the opening fence), absent when
45
+ * none was given.
46
+ */
47
+ export declare interface CodeBlockNode {
48
+ readonly element: 'codeBlock';
49
+ /** The info-string language tag (first word after the opening fence), if any. */
50
+ readonly lang?: string;
51
+ /** The verbatim code content (no inner markdown; HTML-escaped at render). */
52
+ readonly code: string;
53
+ }
54
+
55
+ /**
56
+ * The shape of a {@link CodeBlockNode} - a fenced code block. `lang` is
57
+ * optional (absent when the opening fence carries no info-string).
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { createContract } from '@orkestrel/contract'
62
+ * import { codeBlockShape } from '@src/core'
63
+ *
64
+ * const codeBlock = createContract(codeBlockShape)
65
+ * codeBlock.is({ element: 'codeBlock', code: 'x' }) // true
66
+ * codeBlock.is({ element: 'codeBlock', code: 'x', lang: 'ts' }) // true
67
+ * ```
68
+ */
69
+ export declare const codeBlockShape: ObjectShape<{
70
+ element: LiteralShape<readonly ["codeBlock"]>;
71
+ lang: OptionalShape<StringShape>;
72
+ code: StringShape;
73
+ }, false>;
74
+
75
+ /**
76
+ * An inline code span - `` `code` ``. `value` is the verbatim span text; no inner
77
+ * markdown is parsed (code is literal), and the renderer HTML-escapes it inside a
78
+ * `<code>` element.
79
+ */
80
+ export declare interface CodeSpanNode {
81
+ readonly element: 'codeSpan';
82
+ /** The verbatim code text (no inner markdown; HTML-escaped at render). */
83
+ readonly value: string;
84
+ }
85
+
86
+ /**
87
+ * The shape of a {@link CodeSpanNode} - an inline code span (`` `code` ``).
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * import { createContract } from '@orkestrel/contract'
92
+ * import { codeSpanShape } from '@src/core'
93
+ *
94
+ * const codeSpan = createContract(codeSpanShape)
95
+ * codeSpan.is({ element: 'codeSpan', value: 'const x = 1' }) // true
96
+ * ```
97
+ */
98
+ export declare const codeSpanShape: ObjectShape<{
99
+ element: LiteralShape<readonly ["codeSpan"]>;
100
+ value: StringShape;
101
+ }, false>;
102
+
103
+ /**
104
+ * Collects a list starting at the first item, gathering sibling items at the
105
+ * same indent/ordering and recursing into each item's own block content.
106
+ *
107
+ * @param lines - The markdown lines to scan.
108
+ * @param start - The index of the first list item.
109
+ * @param depth - The current recursion depth (each item recurses at `depth + 1`).
110
+ * @returns The parsed list node and the index of the first line after it.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * collectList(['- item'], 0, 0) // { node: { element: 'list', ... }, next: 1 }
115
+ * ```
116
+ */
117
+ export declare function collectList(lines: readonly string[], start: number, depth: number): {
118
+ readonly node: ListNode;
119
+ readonly next: number;
120
+ };
121
+
122
+ /**
123
+ * Collects a GFM table starting at a header row, parsing the header, the
124
+ * alignment row, and every contiguous body row that follows.
125
+ *
126
+ * @param lines - The markdown lines to scan.
127
+ * @param start - The index of the header row.
128
+ * @returns The parsed table node and the index of the first line after it.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * collectTable(['| a |', '| - |'], 0) // { node: { element: 'table', ... }, next: 2 }
133
+ * ```
134
+ */
135
+ export declare function collectTable(lines: readonly string[], start: number): {
136
+ readonly node: TableNode;
137
+ readonly next: number;
138
+ };
139
+
140
+ /**
141
+ * The count of leading space / tab characters on `line` (a tab counts as one) - the
142
+ * indent that decides whether a list item's continuation belongs to the item.
143
+ *
144
+ * @param line - The line to measure
145
+ * @returns The number of leading space / tab characters
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * countIndent(' text') // 2
150
+ * ```
151
+ */
152
+ export declare function countIndent(line: string): number;
153
+
154
+ /**
155
+ * Compile the {@link codeBlockShape} into a {@link ContractInterface} for
156
+ * {@link CodeBlockNode} - a guard, coercing parser, JSON Schema, and seeded
157
+ * generator from one shape declaration (AGENTS §14).
158
+ *
159
+ * @returns A `CodeBlockNode` contract bundling `schema` / `is` / `parse` / `generate`
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * import { createCodeBlockContract } from '@src/core'
164
+ *
165
+ * const codeBlock = createCodeBlockContract()
166
+ * codeBlock.is({ element: 'codeBlock', code: 'x' }) // true
167
+ * ```
168
+ */
169
+ export declare function createCodeBlockContract(): ContractInterface<CodeBlockNode>;
170
+
171
+ /**
172
+ * Compile the {@link codeSpanShape} into a {@link ContractInterface} for
173
+ * {@link CodeSpanNode} - a guard, coercing parser, JSON Schema, and seeded
174
+ * generator from one shape declaration (AGENTS §14).
175
+ *
176
+ * @returns A `CodeSpanNode` contract bundling `schema` / `is` / `parse` / `generate`
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * import { createCodeSpanContract } from '@src/core'
181
+ *
182
+ * const codeSpan = createCodeSpanContract()
183
+ * codeSpan.is({ element: 'codeSpan', value: 'const x = 1' }) // true
184
+ * ```
185
+ */
186
+ export declare function createCodeSpanContract(): ContractInterface<CodeSpanNode>;
187
+
188
+ /**
189
+ * Compile the {@link lineBreakShape} into a {@link ContractInterface} for
190
+ * {@link LineBreakNode}.
191
+ *
192
+ * @returns A `LineBreakNode` contract bundling `schema` / `is` / `parse` / `generate`
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * import { createLineBreakContract } from '@src/core'
197
+ *
198
+ * createLineBreakContract().is({ element: 'break' }) // true
199
+ * ```
200
+ */
201
+ export declare function createLineBreakContract(): ContractInterface<LineBreakNode>;
202
+
203
+ /**
204
+ * Create a stateful markdown handle from a markdown string or an already-parsed
205
+ * {@link MarkdownDocument} - a typed AST plus the query, rewrite, and fold operations
206
+ * {@link MarkdownInterface} exposes.
207
+ *
208
+ * @remarks
209
+ * Given a `string`, runs a block phase (headings / paragraphs / lists / GFM tables /
210
+ * fenced code / blockquotes / thematic breaks) then an inline phase (emphasis /
211
+ * inline code / links / images / hard breaks) to build a render-agnostic
212
+ * {@link MarkdownDocument}. Given a
213
+ * {@link MarkdownDocument}, adopts it AS-IS without re-validation - gate an untrusted
214
+ * value with `isMarkdownDocument` first. Pure + total parse (malformed markdown
215
+ * degrades to text, never throws) and zero-dependency - a hand-written scanner, no
216
+ * regex-only structural parse, linear-time (no ReDoS).
217
+ *
218
+ * @param input - A markdown string to parse, or an already-parsed {@link MarkdownDocument}
219
+ * @returns A working {@link MarkdownInterface}
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * import { createMarkdown } from '@src/core'
224
+ *
225
+ * const markdown = createMarkdown('# Hi\n\nRead the [guide](./guide.md).')
226
+ * markdown.document.children[0] // { element: 'heading', ... }
227
+ * ```
228
+ */
229
+ export declare function createMarkdown(input: string | MarkdownDocument): MarkdownInterface;
230
+
231
+ /**
232
+ * Create an HTML-to-markdown projection with absent fields defaulted from
233
+ * {@link EMPTY_PROJECTION} and the block/inline exclusivity invariant enforced.
234
+ *
235
+ * @remarks
236
+ * A block-bearing projection cannot also expose inline content. Callers may provide
237
+ * both views, but `inlines` is flushed whenever `blocks` is non-empty.
238
+ *
239
+ * @param parts - The projection fields to provide
240
+ * @returns A complete invariant-preserving projection
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * createProjection({
245
+ * blocks: [{ element: 'thematicBreak' }],
246
+ * inlines: [{ element: 'text', value: 'discarded' }],
247
+ * })
248
+ * // { blocks: [{ element: 'thematicBreak' }], inlines: [], text: '', cells: [], rows: [] }
249
+ * ```
250
+ */
251
+ export declare function createProjection(parts?: Partial<MarkdownProjection>): MarkdownProjection;
252
+
253
+ /**
254
+ * Compile the {@link textShape} into a {@link ContractInterface} for
255
+ * {@link TextNode} - a guard, coercing parser, JSON Schema, and seeded
256
+ * generator from one shape declaration (AGENTS §14).
257
+ *
258
+ * @returns A `TextNode` contract bundling `schema` / `is` / `parse` / `generate`
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * import { createTextContract } from '@src/core'
263
+ *
264
+ * const text = createTextContract()
265
+ * text.is({ element: 'text', value: 'hi' }) // true
266
+ * ```
267
+ */
268
+ export declare function createTextContract(): ContractInterface<TextNode>;
269
+
270
+ /**
271
+ * Compile the {@link thematicBreakShape} into a {@link ContractInterface} for
272
+ * {@link ThematicBreakNode} - a guard, coercing parser, JSON Schema, and
273
+ * seeded generator from one shape declaration (AGENTS §14).
274
+ *
275
+ * @returns A `ThematicBreakNode` contract bundling `schema` / `is` / `parse` / `generate`
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * import { createThematicBreakContract } from '@src/core'
280
+ *
281
+ * const thematicBreak = createThematicBreakContract()
282
+ * thematicBreak.is({ element: 'thematicBreak' }) // true
283
+ * ```
284
+ */
285
+ export declare function createThematicBreakContract(): ContractInterface<ThematicBreakNode>;
286
+
287
+ /**
288
+ * Derive the per-column {@link TableAlign} list from a GFM delimiter row - `:---`
289
+ * left, `---:` right, `:---:` center, and `---` as the explicit no-alignment
290
+ * marker represented by `null`.
291
+ *
292
+ * @param delimiter - The table's delimiter row
293
+ * @returns One alignment per column, in column order
294
+ *
295
+ * @example
296
+ * ```ts
297
+ * delimiterToAlignments('| :--- | ---: |') // ['left', 'right']
298
+ * ```
299
+ */
300
+ export declare function delimiterToAlignments(delimiter: string): readonly (TableAlign | null)[];
301
+
302
+ /**
303
+ * Emphasized inline content - `*italic*` / `_italic_` (`strong: false`) or
304
+ * `**bold**` / `__bold__` (`strong: true`). `children` are the nested inline nodes,
305
+ * so emphasis composes (a `**bold _and italic_**` is a strong node wrapping a text
306
+ * node and an emphasis node).
307
+ */
308
+ export declare interface EmphasisNode {
309
+ readonly element: 'emphasis';
310
+ /** `true` for strong (`**` / `__`, → `<strong>`); `false` for ordinary emphasis (`*` / `_`, → `<em>`). */
311
+ readonly strong: boolean;
312
+ /** The emphasized inline content. */
313
+ readonly children: readonly InlineNode[];
314
+ }
315
+
316
+ /**
317
+ * The frozen empty HTML-to-markdown projection from which projection factories
318
+ * default every absent field.
319
+ *
320
+ * @example
321
+ * ```ts
322
+ * EMPTY_PROJECTION.blocks // []
323
+ * Object.isFrozen(EMPTY_PROJECTION) // true
324
+ * ```
325
+ */
326
+ export declare const EMPTY_PROJECTION: MarkdownProjection;
327
+
328
+ /**
329
+ * Extract a fenced-code opening line (```` ``` ```` or `~~~`, optionally with an info
330
+ * string) into its `{ marker, lang }`, or `undefined` when `line` is not a fence
331
+ * opener. `marker` is the exact fence run (the closer must match the same character +
332
+ * at least the same length); `lang` is the first word of the info string.
333
+ *
334
+ * @param line - The candidate line
335
+ * @returns The fence marker run and its language tag, or `undefined`
336
+ *
337
+ * @example
338
+ * ```ts
339
+ * extractFence('```ts') // { marker: '```', lang: 'ts' }
340
+ * ```
341
+ */
342
+ export declare function extractFence(line: string): {
343
+ readonly marker: string;
344
+ readonly lang: string | undefined;
345
+ } | undefined;
346
+
347
+ /**
348
+ * Extract an ATX heading line (`#` … `######` followed by text) into its
349
+ * `{ level, text }`, or `undefined` when `line` is not a heading. A run of more than 6
350
+ * `#`s, or `#`s not followed by whitespace + text, is not a
351
+ * heading; an optional closing `###` run is stripped.
352
+ *
353
+ * @param line - The candidate line
354
+ * @returns The heading level (1–6) and its raw inline text, or `undefined`
355
+ *
356
+ * @example
357
+ * ```ts
358
+ * extractHeading('## Title') // { level: 2, text: 'Title' }
359
+ * ```
360
+ */
361
+ export declare function extractHeading(line: string): {
362
+ readonly level: number;
363
+ readonly text: string;
364
+ } | undefined;
365
+
366
+ /**
367
+ * Extract a list-item line (`-` / `*` / `+` bullet, or `1.` / `1)` ordinal, followed by
368
+ * a space) into its {@link ListItemMatch}, or `undefined` when `line` is not a list
369
+ * item. `content` is the text after the marker; `marker` is the full marker-plus-space
370
+ * width (for measuring a continuation's indent).
371
+ *
372
+ * @param line - The candidate line
373
+ * @returns The list-item parts, or `undefined` when not a list item
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * extractListItem('- item') // { ordered: false, start: 1, content: 'item', indent: 0, marker: 2 }
378
+ * ```
379
+ */
380
+ export declare function extractListItem(line: string): ListItemMatch | undefined;
381
+
382
+ /**
383
+ * Concatenate the `value` / `code` content of every descendant text / code-span /
384
+ * code-block node under `node`, including image alternative content, in walk order -
385
+ * the plain-text projection of an AST (search indexing, word counts, a text-only
386
+ * preview).
387
+ *
388
+ * @remarks
389
+ * Total: never throws. Descent stops at {@link MAX_DEPTH} (contributes `''` past the
390
+ * cap instead of recursing further).
391
+ *
392
+ * @param node - The AST node to flatten (a full document, or any sub-node)
393
+ * @returns The concatenated text content
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * flattenText({ element: 'paragraph', children: [
398
+ * { element: 'text', value: 'a ' },
399
+ * { element: 'codeSpan', value: 'b' },
400
+ * ] })
401
+ * // 'a b'
402
+ * ```
403
+ */
404
+ export declare function flattenText(node: MarkdownNode): string;
405
+
406
+ /**
407
+ * Fold a {@link MarkdownNode} into a `T` via a total catamorphism - children are
408
+ * folded first (post-order), then the node's own {@link MarkdownHandler} is invoked
409
+ * with the already-folded children.
410
+ *
411
+ * @remarks
412
+ * **Table contract.** A {@link TableNode} has no single `children` array - its cells
413
+ * live in `header` (one inline-node list per column) and `rows` (a list of such
414
+ * rows). The `table` handler receives ONE folded `T` per inline node, flattened in
415
+ * walk order across ALL cells - every header cell's inline nodes (column order), then
416
+ * every body row's cells' inline nodes (row order, then column order) - and reads
417
+ * `node.header[c].length` / `node.rows[r][c].length` off the table node itself to
418
+ * recover cell boundaries within the flat list.
419
+ *
420
+ * Total: never throws. At `depth >= {@link MAX_DEPTH}` the node's handler is invoked
421
+ * with an empty children list instead of recursing further.
422
+ *
423
+ * @param node - The AST node to fold
424
+ * @param handlers - The total {@link MarkdownHandlers} table, one handler per element
425
+ * @param depth - The starting recursion depth (pass `0` at the entry point)
426
+ * @returns The folded `T`
427
+ *
428
+ * @example
429
+ * ```ts
430
+ * const countHandlers: MarkdownHandlers<number> = {
431
+ * document: (_, children) => children.reduce((a, b) => a + b, 1),
432
+ * // ...one handler per element, each summing its folded children
433
+ * }
434
+ * foldNode(document, countHandlers, 0) // total node count
435
+ * ```
436
+ */
437
+ export declare function foldNode<T>(node: MarkdownNode, handlers: MarkdownHandlers<T>, depth: number): T;
438
+
439
+ /**
440
+ * An ATX heading - `#` … `######`. `level` is 1–6 (the number of leading `#`),
441
+ * `children` the inline content of the heading text.
442
+ */
443
+ export declare interface HeadingNode {
444
+ readonly element: 'heading';
445
+ /** The heading level, 1 (`#`) through 6 (`######`). */
446
+ readonly level: number;
447
+ /** The inline content of the heading text. */
448
+ readonly children: readonly InlineNode[];
449
+ }
450
+
451
+ /**
452
+ * Project an `@orkestrel/html` {@link HTMLNode} into a {@link MarkdownDocument} - the
453
+ * HTML→markdown direction, and the inverse of {@link markdownToHTML}.
454
+ *
455
+ * @remarks
456
+ * **Engine.** One total handler table - {@link projectHTMLNode} for the containers,
457
+ * {@link projectHTMLLeaf} for the leaves - folded by `@orkestrel/html`'s own `foldNode`, so
458
+ * depth capping, cycle safety, and bottom-up ordering are inherited rather than
459
+ * rebuilt. Total: hostile, cyclic, and pathologically deep input degrades instead of
460
+ * throwing.
461
+ *
462
+ * **Composed depth.** Both packages cap recursion at 64, and html's cap is reached
463
+ * first: a document nested past it projects to a chain bounded by THAT cap, with the
464
+ * content below it truncated before markdown ever sees it. Since the projected chain
465
+ * can be a level or two deeper than {@link MAX_DEPTH}, the serializer's own cap can
466
+ * then truncate again - so the anchor law below is a law within the depth budget, and
467
+ * beyond it only totality is promised.
468
+ *
469
+ * **Safety.** Every `href` and `src` is re-sanitized through
470
+ * `sanitizeURL(value, SAFE_URL_SCHEMES)` whether or not the AST was ever sanitized,
471
+ * because a hand-built one never was. A refused destination empties to `''` and the
472
+ * link or image is KEPT - `[text]()` - since a bad URL is no reason to lose the words
473
+ * around it. An `UNSAFE_ELEMENTS` subtree contributes nothing at all, text included, so
474
+ * a `script` body can never resurface as prose.
475
+ *
476
+ * **The anchor law.** HTML→markdown is lossy, so the fixpoint that matters is the
477
+ * PROJECTED AST, not the input bytes:
478
+ * `parseDocument(renderMarkdown(htmlToMarkdown(x)))` deep-equals `htmlToMarkdown(x)`.
479
+ * The projection therefore emits canonical markdown shapes rather than literal
480
+ * translations - whitespace collapsed, edges trimmed, a blank paragraph dropped, a hard
481
+ * break only where a line can end - because a shape markdown cannot write back is a
482
+ * shape this projection has no business producing.
483
+ *
484
+ * @param node - The HTML document or bare node to project
485
+ * @returns The projected markdown document
486
+ *
487
+ * @example
488
+ * ```ts
489
+ * import { parseDocument } from '@orkestrel/html'
490
+ *
491
+ * htmlToMarkdown(parseDocument('<h1>Title</h1>'))
492
+ * // { element: 'document', children: [{ element: 'heading', level: 1, children: [...] }] }
493
+ * ```
494
+ */
495
+ export declare function htmlToMarkdown(node: HTMLNode): MarkdownDocument;
496
+
497
+ /**
498
+ * An inline image - `![alt](src)`. `children` are the inline nodes of the
499
+ * alternative content and `src` is the image destination.
500
+ */
501
+ export declare interface ImageNode {
502
+ readonly element: 'image';
503
+ /** The image destination. */
504
+ readonly src: string;
505
+ /** The inline alternative content. */
506
+ readonly children: readonly InlineNode[];
507
+ }
508
+
509
+ /** A node that can appear inside inline content (a heading / paragraph / cell / list item / link text). */
510
+ export declare type InlineNode = TextNode | EmphasisNode | CodeSpanNode | LineBreakNode | LinkNode | ImageNode;
511
+
512
+ /**
513
+ * Whether `line` is blank - empty, or containing only whitespace - the markdown
514
+ * definition of a blank line that block parsing uses to separate paragraphs, skip
515
+ * gaps, and end list continuations.
516
+ *
517
+ * @param line - The candidate line
518
+ * @returns `true` when the line is blank
519
+ *
520
+ * @example
521
+ * ```ts
522
+ * isBlankLine(' ') // true
523
+ * ```
524
+ */
525
+ export declare function isBlankLine(line: string): boolean;
526
+
527
+ /**
528
+ * Determine whether an arbitrary value is a valid {@link BlockNode} - a
529
+ * heading, paragraph, list, table, code block, blockquote, or thematic break,
530
+ * recursively validated.
531
+ *
532
+ * @remarks
533
+ * Total: never throws, even on cyclic or pathologically deep input - every
534
+ * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
535
+ * throw-contained per the `@orkestrel/contract` guard contract (AGENTS §14).
536
+ * A list item's shape is inlined here (and in {@link isMarkdownNode}) rather
537
+ * than named separately - it is used at exactly these two sites.
538
+ *
539
+ * @param value - The value to test
540
+ * @returns `true` when `value` is a well-formed {@link BlockNode}
541
+ *
542
+ * @example
543
+ * ```ts
544
+ * import { isBlockNode } from '@orkestrel/markdown'
545
+ *
546
+ * isBlockNode({ element: 'thematicBreak' }) // true
547
+ * isBlockNode({ element: 'heading' }) // false - missing `level` / `children`
548
+ * ```
549
+ */
550
+ export declare const isBlockNode: Guard<BlockNode>;
551
+
552
+ /**
553
+ * Determine whether a node is a blockquote block.
554
+ *
555
+ * @example
556
+ * ```ts
557
+ * isBlockquoteNode({ element: 'blockquote', children: [] }) // true
558
+ * ```
559
+ */
560
+ export declare function isBlockquoteNode(node: MarkdownNode): node is BlockquoteNode;
561
+
562
+ /**
563
+ * Determine whether a node is a fenced code block.
564
+ *
565
+ * @example
566
+ * ```ts
567
+ * isCodeBlockNode({ element: 'codeBlock', code: 'x' }) // true
568
+ * ```
569
+ */
570
+ export declare function isCodeBlockNode(node: MarkdownNode): node is CodeBlockNode;
571
+
572
+ /**
573
+ * Determine whether a node is an inline code span.
574
+ *
575
+ * @remarks
576
+ * Narrows to {@link CodeSpanNode} - the node whose `element` discriminant is
577
+ * `'codeSpan'`.
578
+ *
579
+ * @example
580
+ * ```ts
581
+ * isCodeSpanNode({ element: 'codeSpan', value: 'x' }) // true
582
+ * ```
583
+ */
584
+ export declare function isCodeSpanNode(node: MarkdownNode): node is CodeSpanNode;
585
+
586
+ /**
587
+ * Determine whether a node is an emphasis run (`*em*` / `**strong**`).
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * isEmphasisNode({ element: 'emphasis', strong: false, children: [] }) // true
592
+ * ```
593
+ */
594
+ export declare function isEmphasisNode(node: MarkdownNode): node is EmphasisNode;
595
+
596
+ /**
597
+ * Whether `character` is escapable by a leading backslash - the ASCII punctuation
598
+ * markdown gives meaning to (so `\*` becomes `*` but `\.` stays `\.`).
599
+ *
600
+ * @param character - The single character after a backslash
601
+ * @returns `true` when a backslash before it is an escape
602
+ *
603
+ * @example
604
+ * ```ts
605
+ * isEscapable('*') // true
606
+ * isEscapable('a') // false
607
+ * ```
608
+ */
609
+ export declare function isEscapable(character: string): boolean;
610
+
611
+ /**
612
+ * Whether `line` closes a fence opened by `marker` - the same fence character, a run
613
+ * at least as long, and nothing else but surrounding whitespace.
614
+ *
615
+ * @param line - The candidate closing line
616
+ * @param marker - The opening fence's marker run (from {@link extractFence})
617
+ * @returns `true` when `line` closes the fence
618
+ *
619
+ * @example
620
+ * ```ts
621
+ * isFenceClose('```', '```') // true
622
+ * ```
623
+ */
624
+ export declare function isFenceClose(line: string, marker: string): boolean;
625
+
626
+ /**
627
+ * Whether `character` is a regex-`\s`-equivalent whitespace character - the
628
+ * character class {@link isFenceClose}'s scan treats as surrounding padding.
629
+ *
630
+ * @param character - The single character to test, or `undefined` past the end of a line
631
+ * @returns `true` when it is whitespace
632
+ *
633
+ * @example
634
+ * ```ts
635
+ * isFenceWhitespace(' ') // true
636
+ * isFenceWhitespace(undefined) // false
637
+ * ```
638
+ */
639
+ export declare function isFenceWhitespace(character: string | undefined): boolean;
640
+
641
+ /** Determine whether a node is a heading block. */
642
+ export declare function isHeadingNode(node: MarkdownNode): node is HeadingNode;
643
+
644
+ /**
645
+ * Determine whether a node is an image.
646
+ *
647
+ * @example
648
+ * ```ts
649
+ * isImageNode({ element: 'image', src: 'x.png', children: [] }) // true
650
+ * ```
651
+ */
652
+ export declare function isImageNode(node: MarkdownNode): node is ImageNode;
653
+
654
+ /**
655
+ * Determine whether an arbitrary value is a valid {@link InlineNode} - a text
656
+ * run, emphasis, code span, hard break, link, or image, recursively validated.
657
+ *
658
+ * @remarks
659
+ * Total: never throws, even on cyclic or pathologically deep input - every
660
+ * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
661
+ * throw-contained per the `@orkestrel/contract` guard contract (AGENTS §14).
662
+ *
663
+ * @param value - The value to test
664
+ * @returns `true` when `value` is a well-formed {@link InlineNode}
665
+ *
666
+ * @example
667
+ * ```ts
668
+ * import { isInlineNode } from '@orkestrel/markdown'
669
+ *
670
+ * isInlineNode({ element: 'text', value: 'hi' }) // true
671
+ * isInlineNode({ element: 'text' }) // false - missing `value`
672
+ * ```
673
+ */
674
+ export declare const isInlineNode: Guard<InlineNode>;
675
+
676
+ /**
677
+ * Determine whether a node is a GFM hard line break.
678
+ *
679
+ * @example
680
+ * ```ts
681
+ * isLineBreakNode({ element: 'break' }) // true
682
+ * ```
683
+ */
684
+ export declare function isLineBreakNode(node: MarkdownNode): node is LineBreakNode;
685
+
686
+ /** Determine whether a node is a link. */
687
+ export declare function isLinkNode(node: MarkdownNode): node is LinkNode;
688
+
689
+ /**
690
+ * Determine whether a node is a list block.
691
+ *
692
+ * @example
693
+ * ```ts
694
+ * isListNode({ element: 'list', ordered: false, start: 1, items: [] }) // true
695
+ * ```
696
+ */
697
+ export declare function isListNode(node: MarkdownNode): node is ListNode;
698
+
699
+ /**
700
+ * Determine whether an arbitrary value is a valid {@link MarkdownDocument} -
701
+ * the parsed-AST root {@link parseDocument} returns, recursively
702
+ * validated.
703
+ *
704
+ * @remarks
705
+ * Total: never throws, even on cyclic or pathologically deep input - every
706
+ * combinator involved (`recordOf`, `arrayOf`) is throw-contained per the
707
+ * `@orkestrel/contract` guard contract (AGENTS §14).
708
+ *
709
+ * @param value - The value to test
710
+ * @returns `true` when `value` is a well-formed {@link MarkdownDocument}
711
+ *
712
+ * @example
713
+ * ```ts
714
+ * import { isMarkdownDocument } from '@orkestrel/markdown'
715
+ *
716
+ * isMarkdownDocument({ element: 'document', children: [] }) // true
717
+ * isMarkdownDocument({ element: 'document' }) // false - missing `children`
718
+ * ```
719
+ */
720
+ export declare const isMarkdownDocument: Guard<MarkdownDocument>;
721
+
722
+ /**
723
+ * Determine whether an arbitrary value is a valid {@link MarkdownNode} - the
724
+ * {@link MarkdownDocument} root, a {@link BlockNode}, a {@link ListItemNode}, or
725
+ * an {@link InlineNode}, recursively validated.
726
+ *
727
+ * @remarks
728
+ * Total: never throws, even on cyclic or pathologically deep input - every
729
+ * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is
730
+ * throw-contained per the `@orkestrel/contract` guard contract (AGENTS §14).
731
+ * A list item's shape is inlined here (and in {@link isBlockNode}) rather than
732
+ * named separately - it is used at exactly these two sites.
733
+ *
734
+ * @param value - The value to test
735
+ * @returns `true` when `value` is a well-formed {@link MarkdownNode}
736
+ *
737
+ * @example
738
+ * ```ts
739
+ * import { isMarkdownNode } from '@orkestrel/markdown'
740
+ *
741
+ * isMarkdownNode({ element: 'text', value: 'hi' }) // true
742
+ * isMarkdownNode({ element: 'bogus' }) // false
743
+ * ```
744
+ */
745
+ export declare const isMarkdownNode: Guard<MarkdownNode>;
746
+
747
+ /**
748
+ * Determine whether a node is a paragraph block.
749
+ *
750
+ * @example
751
+ * ```ts
752
+ * isParagraphNode({ element: 'paragraph', children: [] }) // true
753
+ * ```
754
+ */
755
+ export declare function isParagraphNode(node: MarkdownNode): node is ParagraphNode;
756
+
757
+ /**
758
+ * Whether `line` is a blockquote line (`>` optionally indented up to three spaces) -
759
+ * its content is de-quoted by {@link stripQuote}.
760
+ *
761
+ * @param line - The candidate line
762
+ * @returns `true` when the line begins a blockquote
763
+ *
764
+ * @example
765
+ * ```ts
766
+ * isQuote('> quoted') // true
767
+ * ```
768
+ */
769
+ export declare function isQuote(line: string): boolean;
770
+
771
+ /** Determine whether a node is a GFM table block. */
772
+ export declare function isTableNode(node: MarkdownNode): node is TableNode;
773
+
774
+ /**
775
+ * Whether the pair (`header`, `delimiter`) opens a GFM table - `delimiter` is a row of
776
+ * `|`-separated cells each matching `:?-+:?`, the GFM rule that a table requires a
777
+ * header row IMMEDIATELY followed by a delimiter row.
778
+ *
779
+ * @param header - The candidate header line
780
+ * @param delimiter - The line after it (the candidate delimiter)
781
+ * @returns `true` when the two lines open a table
782
+ *
783
+ * @example
784
+ * ```ts
785
+ * isTableStart('| a |', '| - |') // true
786
+ * ```
787
+ */
788
+ export declare function isTableStart(header: string, delimiter: string | undefined): boolean;
789
+
790
+ /**
791
+ * Determine whether a node is a plain text run.
792
+ *
793
+ * @example
794
+ * ```ts
795
+ * isTextNode({ element: 'text', value: 'hi' }) // true
796
+ * ```
797
+ */
798
+ export declare function isTextNode(node: MarkdownNode): node is TextNode;
799
+
800
+ /**
801
+ * Whether `line` is a thematic break (horizontal rule) - three or more of the SAME
802
+ * marker `-`, `*`, or `_` (optionally space-separated) and nothing else (`---`,
803
+ * `***`, `___`, `- - -`).
804
+ *
805
+ * @param line - The candidate line
806
+ * @returns `true` when the line is a thematic break
807
+ *
808
+ * @example
809
+ * ```ts
810
+ * isThematicBreak('---') // true
811
+ * ```
812
+ */
813
+ export declare function isThematicBreak(line: string): boolean;
814
+
815
+ /**
816
+ * Determine whether a node is a thematic break (horizontal rule) block.
817
+ *
818
+ * @example
819
+ * ```ts
820
+ * isThematicBreakNode({ element: 'thematicBreak' }) // true
821
+ * ```
822
+ */
823
+ export declare function isThematicBreakNode(node: MarkdownNode): node is ThematicBreakNode;
824
+
825
+ /**
826
+ * Whether `character` is an inline whitespace character (space / tab / newline) - the
827
+ * emphasis flanking rule's space test.
828
+ *
829
+ * @param character - The character to test
830
+ * @returns `true` when it is inline whitespace
831
+ *
832
+ * @example
833
+ * ```ts
834
+ * isWhitespace(' ') // true
835
+ * isWhitespace('a') // false
836
+ * ```
837
+ */
838
+ export declare function isWhitespace(character: string): boolean;
839
+
840
+ /** A GFM hard line break - two or more trailing spaces before a newline. */
841
+ export declare interface LineBreakNode {
842
+ readonly element: 'break';
843
+ }
844
+
845
+ /**
846
+ * The shape of a {@link LineBreakNode} - a GFM hard line-break leaf.
847
+ *
848
+ * @example
849
+ * ```ts
850
+ * import { createContract } from '@orkestrel/contract'
851
+ * import { lineBreakShape } from '@src/core'
852
+ *
853
+ * const lineBreak = createContract(lineBreakShape)
854
+ * lineBreak.is({ element: 'break' }) // true
855
+ * ```
856
+ */
857
+ export declare const lineBreakShape: ObjectShape<{
858
+ element: LiteralShape<readonly ["break"]>;
859
+ }, false>;
860
+
861
+ /**
862
+ * An inline link - `[text](href)`. `children` are the inline nodes of the link text.
863
+ * At render, html's floor removes a refused `href` attribute and the link keeps its
864
+ * text; {@link htmlToMarkdown} instead stores a refused destination as `''`.
865
+ */
866
+ export declare interface LinkNode {
867
+ readonly element: 'link';
868
+ /** The link destination (sanitized + attribute-escaped at render). */
869
+ readonly href: string;
870
+ /** The inline content of the link text. */
871
+ readonly children: readonly InlineNode[];
872
+ }
873
+
874
+ /**
875
+ * The parsed parts of a single list-item line - the value the block phase's
876
+ * list detector returns for a `-` / `*` / `+` bullet or a `1.` / `1)` ordinal line.
877
+ */
878
+ export declare interface ListItemMatch {
879
+ /** `true` for an ordered (`1.` / `1)`) item, `false` for a bullet (`-` / `*` / `+`). */
880
+ readonly ordered: boolean;
881
+ /** The ordinal of an ordered item (its number); `1` for a bullet. */
882
+ readonly start: number;
883
+ /** The item's text after the marker. */
884
+ readonly content: string;
885
+ /** The leading-space indent of the marker. */
886
+ readonly indent: number;
887
+ /** The full marker width (indent + bullet/ordinal + the following space) - the continuation indent. */
888
+ readonly marker: number;
889
+ }
890
+
891
+ /**
892
+ * The shape of {@link ListItemMatch} - the parsed parts of a single list-item
893
+ * line the block phase's list detector returns. Fully non-recursive (no
894
+ * nested node fields), so every field shapes directly.
895
+ *
896
+ * @example
897
+ * ```ts
898
+ * import { createContract } from '@orkestrel/contract'
899
+ * import { listItemMatchShape } from '@src/core'
900
+ *
901
+ * const listItemParts = createContract(listItemMatchShape)
902
+ * listItemParts.is({ ordered: false, start: 1, content: 'hi', indent: 0, marker: 2 }) // true
903
+ * ```
904
+ */
905
+ export declare const listItemMatchShape: ObjectShape<{
906
+ ordered: BooleanShape;
907
+ start: NumberShape;
908
+ content: StringShape;
909
+ indent: NumberShape;
910
+ marker: NumberShape;
911
+ }, false>;
912
+
913
+ /** One item of a {@link ListNode} - `children` the block content of the item (typically one paragraph, plus any nested list). */
914
+ export declare interface ListItemNode {
915
+ readonly element: 'listItem';
916
+ /** The block content of the list item (its text as a paragraph, plus any nested list). */
917
+ readonly children: readonly BlockNode[];
918
+ }
919
+
920
+ /**
921
+ * A list - bulleted (`-` / `*` / `+`, `ordered: false`) or numbered (`1.` / `1)`,
922
+ * `ordered: true`). `start` is the first ordinal of an ordered list (usually `1`).
923
+ * Nesting is expressed by a {@link ListNode} appearing in a {@link ListItemNode}'s
924
+ * `children`.
925
+ */
926
+ export declare interface ListNode {
927
+ readonly element: 'list';
928
+ /** `true` for an ordered (numbered) list (→ `<ol>`); `false` for a bulleted list (→ `<ul>`). */
929
+ readonly ordered: boolean;
930
+ /** The starting ordinal of an ordered list (the first item's number); `1` for a bulleted list. */
931
+ readonly start: number;
932
+ /** The list's items, in order. */
933
+ readonly items: readonly ListItemNode[];
934
+ }
935
+
936
+ /**
937
+ * A stateful, parsed markdown document - wraps a typed {@link MarkdownDocument} AST
938
+ * with the query (`find` / `filter` / `reduce` / iteration), rewrite (`map`), fold, and
939
+ * streaming operations {@link MarkdownInterface} declares.
940
+ *
941
+ * @remarks
942
+ * - **Construction.** Given a `string`, the constructor runs {@link parseDocument} (the
943
+ * block phase then the inline phase) to build the AST. Given a {@link MarkdownDocument},
944
+ * the document is adopted AS-IS and is NOT re-validated - a caller adopting an
945
+ * untrusted value should gate it with `isMarkdownDocument` first.
946
+ * - **Immutable.** {@link map} never mutates the stored AST - it returns a NEW `Markdown`
947
+ * instance; the document root invariant (`element: 'document'`) always holds.
948
+ * - **Traversal order.** {@link walk} and the `find` / `filter` / `reduce` queries built
949
+ * on it walk the AST depth-first, pre-order, root-inclusive (via {@link walkNodes});
950
+ * `stream` is shallow - only the document's direct block children.
951
+ *
952
+ * @example
953
+ * ```ts
954
+ * import { Markdown, isHeadingNode, renderMarkdown } from '@src/core'
955
+ *
956
+ * const markdown = new Markdown('# Title\n\nA **bold** [link](https://x.dev).')
957
+ * const heading = markdown.find(isHeadingNode) // the HeadingNode, or undefined
958
+ * const shouted = markdown.map((node) =>
959
+ * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node,
960
+ * )
961
+ * renderMarkdown(shouted.document) // '# TITLE\n\nA **BOLD** [LINK](https://x.dev).'
962
+ * ```
963
+ */
964
+ export declare class Markdown implements MarkdownInterface {
965
+ #private;
966
+ constructor(input: string | MarkdownDocument);
967
+ /** The stored {@link MarkdownDocument} AST root. */
968
+ get document(): MarkdownDocument;
969
+ /**
970
+ * THE deep traversal - a lazy, depth-first, pre-order, root-inclusive generator
971
+ * over every {@link MarkdownNode} in the document. `find` / `filter` / `reduce`
972
+ * all iterate this single traversal.
973
+ *
974
+ * @example
975
+ * ```ts
976
+ * for (const node of markdown.walk()) {
977
+ * // every node, depth-first, pre-order, root-inclusive
978
+ * }
979
+ *
980
+ * // also consumable by for-await - JS accepts a sync iterable in for-await
981
+ * for await (const node of markdown.walk()) {
982
+ * // same sequence, no separate async iterator needed
983
+ * }
984
+ * ```
985
+ */
986
+ walk(): Generator<MarkdownNode>;
987
+ find<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): T | undefined;
988
+ find(predicate: (node: MarkdownNode) => boolean): MarkdownNode | undefined;
989
+ filter<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): readonly T[];
990
+ filter(predicate: (node: MarkdownNode) => boolean): readonly MarkdownNode[];
991
+ /** Rewrites the AST bottom-up (copy-on-write) and returns a new {@link Markdown}. */
992
+ map(rewrite: MarkdownRewriteHandler): MarkdownInterface;
993
+ /** Folds the AST depth-first, pre-order into an accumulator. */
994
+ reduce<T>(callback: (accumulator: T, node: MarkdownNode) => T, initial: T): T;
995
+ /** Runs a total catamorphism over the document using a {@link MarkdownHandlers} table. */
996
+ fold<T>(handlers: MarkdownHandlers<T>): T;
997
+ /**
998
+ * A web-standard {@link ReadableStream} over the document's top-level block nodes
999
+ * (shallow, source order) - a fresh, pull-based source per call: one block is
1000
+ * enqueued per `pull`, so a slow reader's backpressure is respected. Cancellable,
1001
+ * async-iterable wherever the platform supports it (Node, Deno), and pipeable
1002
+ * through any {@link TransformStream} / {@link WritableStream}.
1003
+ *
1004
+ * @example
1005
+ * ```ts
1006
+ * // universal - works in every ReadableStream-supporting environment
1007
+ * const reader = markdown.stream().getReader()
1008
+ * for (let result = await reader.read(); !result.done; result = await reader.read()) {
1009
+ * console.log(result.value) // one BlockNode
1010
+ * }
1011
+ *
1012
+ * // Node / Deno / Firefox support async iteration of ReadableStream natively;
1013
+ * // other environments should use the reader loop above instead.
1014
+ * for await (const block of markdown.stream()) {
1015
+ * console.log(block)
1016
+ * }
1017
+ * ```
1018
+ */
1019
+ stream(): ReadableStream<BlockNode>;
1020
+ }
1021
+
1022
+ /** One projected table cell - the inline content and alignment of a `th` / `td`. */
1023
+ export declare interface MarkdownCell {
1024
+ /** The alignment the cell's `align` attribute declared; `undefined` when it declared none. */
1025
+ readonly align: TableAlign | undefined;
1026
+ /** The cell's inline content - a table cell is inline-only, so block content flattens to text. */
1027
+ readonly inlines: readonly InlineNode[];
1028
+ }
1029
+
1030
+ /**
1031
+ * The root of a parsed markdown AST - the ordered block children of the whole
1032
+ * document. The value {@link MarkdownInterface.document} holds.
1033
+ */
1034
+ export declare interface MarkdownDocument {
1035
+ readonly element: 'document';
1036
+ /** The document's top-level block nodes, in source order. */
1037
+ readonly children: readonly BlockNode[];
1038
+ }
1039
+
1040
+ /**
1041
+ * A fold handler for one AST element - receives the node and its children
1042
+ * ALREADY folded to `T`, and produces the node's own `T`. The building block of a
1043
+ * {@link MarkdownHandlers} catamorphism table.
1044
+ */
1045
+ export declare type MarkdownHandler<TNode, T> = (node: TNode, children: readonly T[]) => T;
1046
+
1047
+ /**
1048
+ * The total catamorphism table for {@link MarkdownInterface.fold} - one
1049
+ * {@link MarkdownHandler} per AST element, keyed by its `element` discriminant. Every
1050
+ * key is required: a fold is total over the AST, so there is no element it can skip.
1051
+ */
1052
+ export declare interface MarkdownHandlers<T> {
1053
+ /** Folds a {@link MarkdownDocument} root from its already-folded block children. */
1054
+ readonly document: MarkdownHandler<MarkdownDocument, T>;
1055
+ /** Folds a {@link HeadingNode} from its already-folded inline children. */
1056
+ readonly heading: MarkdownHandler<HeadingNode, T>;
1057
+ /** Folds a {@link ParagraphNode} from its already-folded inline children. */
1058
+ readonly paragraph: MarkdownHandler<ParagraphNode, T>;
1059
+ /** Folds a {@link ThematicBreakNode} (leaf - always called with an empty children list). */
1060
+ readonly thematicBreak: MarkdownHandler<ThematicBreakNode, T>;
1061
+ /** Folds a {@link BlockquoteNode} from its already-folded block children. */
1062
+ readonly blockquote: MarkdownHandler<BlockquoteNode, T>;
1063
+ /** Folds a {@link CodeBlockNode} (leaf - always called with an empty children list). */
1064
+ readonly codeBlock: MarkdownHandler<CodeBlockNode, T>;
1065
+ /** Folds a {@link ListNode} from its already-folded item children. */
1066
+ readonly list: MarkdownHandler<ListNode, T>;
1067
+ /** Folds a {@link ListItemNode} from its already-folded block children. */
1068
+ readonly listItem: MarkdownHandler<ListItemNode, T>;
1069
+ /**
1070
+ * Folds a {@link TableNode} from its cells' already-folded inline nodes, flattened
1071
+ * to ONE folded `T` per inline node - header cells first (column order), then body
1072
+ * rows' cells (row order, then column order). It is NOT a leaf: recover cell
1073
+ * boundaries from `node.header[c].length` / `node.rows[r][c].length` against the
1074
+ * flat `children` list.
1075
+ */
1076
+ readonly table: MarkdownHandler<TableNode, T>;
1077
+ /** Folds a {@link TextNode} (leaf - always called with an empty children list). */
1078
+ readonly text: MarkdownHandler<TextNode, T>;
1079
+ /** Folds an {@link EmphasisNode} from its already-folded inline children. */
1080
+ readonly emphasis: MarkdownHandler<EmphasisNode, T>;
1081
+ /** Folds a {@link CodeSpanNode} (leaf - always called with an empty children list). */
1082
+ readonly codeSpan: MarkdownHandler<CodeSpanNode, T>;
1083
+ /** Folds a {@link LineBreakNode} (leaf - always called with an empty children list). */
1084
+ readonly break: MarkdownHandler<LineBreakNode, T>;
1085
+ /** Folds a {@link LinkNode} from its already-folded inline children. */
1086
+ readonly link: MarkdownHandler<LinkNode, T>;
1087
+ /** Folds an {@link ImageNode} from its already-folded alternative content. */
1088
+ readonly image: MarkdownHandler<ImageNode, T>;
1089
+ }
1090
+
1091
+ /**
1092
+ * A stateful, parsed markdown document: the typed {@link MarkdownDocument} AST plus
1093
+ * the query, rewrite, and fold operations over it.
1094
+ *
1095
+ * @remarks
1096
+ * - **Immutable.** {@link MarkdownInterface.map} never mutates the stored AST - it
1097
+ * returns a NEW {@link MarkdownInterface} instance; the document root invariant
1098
+ * (`element: 'document'`) always holds.
1099
+ * - **Traversal order.** `walk` / `find` / `filter` / `reduce` walk the AST
1100
+ * depth-first, pre-order, root-inclusive; `stream` is shallow - only the
1101
+ * document's direct block children.
1102
+ * - **`stream`.** Returns a web-standard {@link ReadableStream} over the top-level
1103
+ * blocks - a fresh, pull-based source per call: exactly one block is enqueued per
1104
+ * `pull`, so a slow consumer's backpressure is respected and no work happens ahead
1105
+ * of demand. Cancellable via the returned stream's own `cancel()`, async-iterable
1106
+ * wherever the platform supports it (Node, Deno, and browsers that ship the
1107
+ * proposal), and pipeable through any {@link TransformStream} / {@link WritableStream}.
1108
+ * - **The seven-method surface.** `document` (the AST root), `walk` (the deep
1109
+ * traversal), `find` / `filter` / `reduce` (queries built on `walk`), `map` (the
1110
+ * bottom-up rewrite), `fold` (the total catamorphism), and `stream` (the shallow,
1111
+ * backpressured top-level source).
1112
+ */
1113
+ export declare interface MarkdownInterface {
1114
+ /** The stored {@link MarkdownDocument} AST root. */
1115
+ readonly document: MarkdownDocument;
1116
+ /**
1117
+ * THE deep traversal - a lazy, depth-first, pre-order, root-inclusive
1118
+ * {@link Generator} over every {@link MarkdownNode} in the document. The sync
1119
+ * `for (const node of markdown.walk())` surface is also consumable by
1120
+ * `for await (const node of markdown.walk())` (JavaScript accepts a sync
1121
+ * iterable in a `for await`), so async pipelines need no separate iterator.
1122
+ * Contrast with {@link stream}: `walk` is deep, every-node, and sync; `stream`
1123
+ * is shallow (top-level blocks only) and backpressure-respecting.
1124
+ */
1125
+ walk(): Generator<MarkdownNode>;
1126
+ /** Finds the first node (depth-first, pre-order) narrowed by a type guard. */
1127
+ find<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): T | undefined;
1128
+ /** Finds the first node (depth-first, pre-order) matching a predicate. */
1129
+ find(predicate: (node: MarkdownNode) => boolean): MarkdownNode | undefined;
1130
+ /** Collects every node (depth-first, pre-order) narrowed by a type guard. */
1131
+ filter<T extends MarkdownNode>(guard: (node: MarkdownNode) => node is T): readonly T[];
1132
+ /** Collects every node (depth-first, pre-order) matching a predicate. */
1133
+ filter(predicate: (node: MarkdownNode) => boolean): readonly MarkdownNode[];
1134
+ /** Rewrites the AST bottom-up (copy-on-write) and returns a new {@link MarkdownInterface}. */
1135
+ map(rewrite: MarkdownRewriteHandler): MarkdownInterface;
1136
+ /** Folds the AST depth-first, pre-order into an accumulator. */
1137
+ reduce<T>(callback: (accumulator: T, node: MarkdownNode) => T, initial: T): T;
1138
+ /** Runs a total catamorphism over the document using a {@link MarkdownHandlers} table. */
1139
+ fold<T>(handlers: MarkdownHandlers<T>): T;
1140
+ /**
1141
+ * A web-standard {@link ReadableStream} over the document's top-level block nodes
1142
+ * (shallow, source order) - a lazy, pull-based, backpressure-respecting source. A
1143
+ * fresh, independently-replayable stream every call; never mutates the document.
1144
+ */
1145
+ stream(): ReadableStream<BlockNode>;
1146
+ }
1147
+
1148
+ /**
1149
+ * Any node in a markdown AST - the {@link MarkdownDocument} root, a {@link BlockNode},
1150
+ * a {@link ListItemNode}, or an {@link InlineNode}. The exhaustive set every
1151
+ * projection's `switch` covers.
1152
+ */
1153
+ export declare type MarkdownNode = MarkdownDocument | BlockNode | ListItemNode | InlineNode;
1154
+
1155
+ /**
1156
+ * What one HTML node projects to on the way to markdown - the fold value
1157
+ * `htmlToMarkdown` carries up the AST.
1158
+ *
1159
+ * @remarks
1160
+ * A node projects to several things at once because markdown decides late what a
1161
+ * given HTML subtree becomes: a `td`'s content is inline in a table and a paragraph
1162
+ * outside one, and a `code` body is a code span in prose and a verbatim code block
1163
+ * under a `pre`. Rather than guess, each node reports every view its ancestors could
1164
+ * need, and the ancestor that knows the context takes the one it wants.
1165
+ *
1166
+ * - `blocks` / `inlines` - the block and inline views. They are exclusive by
1167
+ * construction: as soon as a node contributes a block, the inline runs around it
1168
+ * are wrapped into paragraphs, so `blocks` being non-empty means `inlines` is
1169
+ * empty and no interleaving is ever lost.
1170
+ * - `text` - the raw, uncollapsed, unescaped subtree text a code span and a
1171
+ * `pre > code` body need verbatim. An `UNSAFE_ELEMENTS` subtree contributes none
1172
+ * of it, so a script body can never resurface as prose.
1173
+ * - `cells` / `rows` - table structure in flight. A cell travels up to its `tr` and a
1174
+ * row up to its `table`, passing through the `thead` / `tbody` wrappers between
1175
+ * them untouched; whatever never reaches a table degrades to paragraphs.
1176
+ */
1177
+ export declare interface MarkdownProjection {
1178
+ /** The node's block content, with any surrounding inline runs already wrapped into paragraphs. */
1179
+ readonly blocks: readonly BlockNode[];
1180
+ /** The node's inline content; empty whenever `blocks` is not. */
1181
+ readonly inlines: readonly InlineNode[];
1182
+ /** The raw subtree text, whitespace uncollapsed and escapes unresolved. */
1183
+ readonly text: string;
1184
+ /** The cells this node contributes to an enclosing row. */
1185
+ readonly cells: readonly MarkdownCell[];
1186
+ /** The rows this node contributes to an enclosing table - each its cells, in column order. */
1187
+ readonly rows: readonly (readonly MarkdownCell[])[];
1188
+ }
1189
+
1190
+ /**
1191
+ * A copy-on-write node rewrite applied bottom-up by {@link MarkdownInterface.map} -
1192
+ * receives one node (its own children already rewritten) and returns its
1193
+ * replacement (the same node, unchanged, or a new node).
1194
+ */
1195
+ export declare type MarkdownRewriteHandler = (node: MarkdownNode) => MarkdownNode;
1196
+
1197
+ /**
1198
+ * Project a {@link MarkdownNode} into an unsanitized {@link HTMLDocument}.
1199
+ *
1200
+ * @remarks
1201
+ * The projection is pure and iterative. Text and attribute values remain literal for
1202
+ * `@orkestrel/html` to encode, and URL values remain unsanitized so callers can choose
1203
+ * their own HTML policy. Projected HTML element depth, including generated `pre > code`
1204
+ * and table scaffolding, never exceeds {@link MAX_DEPTH}. At the cap a node carrying a
1205
+ * string `value` degrades to a text node and a structural node contributes nothing.
1206
+ *
1207
+ * @param node - The markdown document or bare node to project
1208
+ * @returns An unsanitized HTML document wrapping the projected node or nodes
1209
+ *
1210
+ * @example
1211
+ * ```ts
1212
+ * markdownToHTML({ element: 'text', value: 'a & b' })
1213
+ * // { category: 'document', children: [{ category: 'text', value: 'a & b' }] }
1214
+ * ```
1215
+ */
1216
+ export declare function markdownToHTML(node: MarkdownNode): HTMLDocument;
1217
+
1218
+ /**
1219
+ * The maximum recursion depth the parse pipeline (`parseDocument` and its
1220
+ * `parsers.ts` helpers) and the `helpers.ts` traversal / projection functions
1221
+ * (`markdownToHTML`, `renderHTML`, `renderMarkdown`, `walkNodes`, `foldNode`,
1222
+ * `rewriteDocument`) honor before degrading. It bounds blockquote nesting, inline
1223
+ * nesting (emphasis / links), and traversal / projection recursion so pathological
1224
+ * or hostile input cannot exhaust the call stack. {@link htmlToMarkdown} is the
1225
+ * inherited exception: its fold and depth cap belong to `@orkestrel/html`.
1226
+ */
1227
+ export declare const MAX_DEPTH = 64;
1228
+
1229
+ /**
1230
+ * Combine the projections of one node's children into the projection of that node -
1231
+ * the single place inline runs become paragraphs, so no ancestor has to decide it
1232
+ * twice.
1233
+ *
1234
+ * @remarks
1235
+ * A child is either inline or block, never both, so merging preserves source order
1236
+ * exactly: an inline run is held pending until a block arrives, then written out as a
1237
+ * paragraph BEFORE it. That is what keeps `<div>lead<p>a</p></div>` two paragraphs in
1238
+ * the order they were written rather than two lists that lost their interleaving. A
1239
+ * pending run carrying no text is dropped rather than becoming a blank paragraph.
1240
+ * Direct cells become one row before a later row, while cells/rows before a block
1241
+ * materialize as paragraphs at that exact source position.
1242
+ *
1243
+ * @param children - The children's projections, in source order
1244
+ * @returns Their combined projection
1245
+ *
1246
+ * @example
1247
+ * ```ts
1248
+ * mergeProjections([
1249
+ * createProjection({ inlines: [{ element: 'text', value: 'a' }], text: 'a' }),
1250
+ * createProjection({ blocks: [{ element: 'thematicBreak' }] }),
1251
+ * ]).blocks
1252
+ * // [{ element: 'paragraph', children: [...] }, { element: 'thematicBreak' }]
1253
+ * ```
1254
+ */
1255
+ export declare function mergeProjections(children: readonly MarkdownProjection[]): MarkdownProjection;
1256
+
1257
+ /**
1258
+ * Reduce an inline run to the shape markdown can actually write back: adjacent text
1259
+ * coalesced, empty text dropped, and every hard break either kept as a real line
1260
+ * ending or spent as a space.
1261
+ *
1262
+ * @remarks
1263
+ * A hard break is ` \n` in markdown source, so it survives a re-parse only BETWEEN
1264
+ * two lines of content and only with no whitespace touching it: a leading or trailing
1265
+ * break has no line to end, a run of breaks reads as one blank line (which would end
1266
+ * the paragraph), and a space beside one is eaten by the parser's line trimming. Where
1267
+ * a break cannot be written at all - a heading and a table cell are one line each - it
1268
+ * becomes the space it stood for.
1269
+ *
1270
+ * @param nodes - The inline run to normalize
1271
+ * @param breaks - Whether the target context can carry a hard break at all; `false` for
1272
+ * a heading or a table cell, where every break becomes a space
1273
+ * @returns The normalized run
1274
+ *
1275
+ * @example
1276
+ * ```ts
1277
+ * normalizeInlines([{ element: 'break' }, { element: 'text', value: 'a' }], true)
1278
+ * // [{ element: 'text', value: 'a' }] - a leading break has no line to end
1279
+ * ```
1280
+ */
1281
+ export declare function normalizeInlines(nodes: readonly InlineNode[], breaks: boolean): readonly InlineNode[];
1282
+
1283
+ /** A paragraph - a run of non-blank lines that is not another block; `children` its inline content. */
1284
+ export declare interface ParagraphNode {
1285
+ readonly element: 'paragraph';
1286
+ /** The inline content of the paragraph. */
1287
+ readonly children: readonly InlineNode[];
1288
+ }
1289
+
1290
+ /**
1291
+ * Parses a run of markdown lines into a block AST, recursing into nested
1292
+ * blockquotes, list items, and depth-capped degrade paragraphs.
1293
+ *
1294
+ * @param lines - The markdown lines to parse.
1295
+ * @param depth - The current recursion depth (blockquotes/lists increment it).
1296
+ * @returns The parsed block nodes.
1297
+ *
1298
+ * @example
1299
+ * ```ts
1300
+ * parseBlocks(['# Hi'], 0) // [{ element: 'heading', level: 1, children: [...] }]
1301
+ * ```
1302
+ */
1303
+ export declare function parseBlocks(lines: readonly string[], depth: number): readonly BlockNode[];
1304
+
1305
+ /**
1306
+ * Parses a markdown string into a typed {@link MarkdownDocument} AST via the
1307
+ * block phase.
1308
+ *
1309
+ * @param markdown - The markdown source to parse.
1310
+ * @returns The parsed document.
1311
+ */
1312
+ export declare function parseDocument(markdown: string): MarkdownDocument;
1313
+
1314
+ /**
1315
+ * Parses inline markdown text (emphasis, code spans, links, images, and hard
1316
+ * breaks) into inline AST nodes, coalescing adjacent text runs.
1317
+ *
1318
+ * @param text - The inline markdown text to parse.
1319
+ * @returns The parsed inline nodes.
1320
+ */
1321
+ export declare function parseInline(text: string): readonly InlineNode[];
1322
+
1323
+ /**
1324
+ * Project one HTML leaf - a text node, a comment, or a doctype - to its
1325
+ * {@link MarkdownProjection}.
1326
+ *
1327
+ * @remarks
1328
+ * Text collapses each whitespace run to one space, which is both what HTML means by it
1329
+ * and all markdown can write back; the raw value travels on in `text` for the two
1330
+ * places that need it verbatim, a code span and a `pre > code` body. A comment and a
1331
+ * doctype carry nothing into markdown and project to nothing.
1332
+ *
1333
+ * @param leaf - The leaf node to project
1334
+ * @returns Its projection
1335
+ *
1336
+ * @example
1337
+ * ```ts
1338
+ * projectHTMLLeaf({ category: 'text', value: 'a\n b' }).inlines
1339
+ * // [{ element: 'text', value: 'a b' }]
1340
+ * ```
1341
+ */
1342
+ export declare function projectHTMLLeaf(leaf: CommentNode | DoctypeNode | TextNode_2): MarkdownProjection;
1343
+
1344
+ /**
1345
+ * Project one HTML container - the document root or an element - from its children's
1346
+ * already-computed projections. THE element mapping, and the only place that decides
1347
+ * what an HTML tag becomes in markdown.
1348
+ *
1349
+ * @remarks
1350
+ * `h1`-`h6` become headings; `p` a paragraph; `strong` / `b` and `em` / `i` emphasis;
1351
+ * `code` a code span; `pre` a code block, verbatim through a first `code` element child
1352
+ * (its `language-` class naming the language) and through `renderText` otherwise; `a`
1353
+ * and `img` a link and an image, each destination re-sanitized; `br` and `hr` a hard
1354
+ * break and a thematic break; `blockquote` and `li` their block content, with bare
1355
+ * inline runs wrapped in paragraphs; `ul` / `ol` a list, ordered from the tag and
1356
+ * numbered from `start`; `th` / `td`, `tr`, and `table` a GFM table whose column
1357
+ * alignment comes from each header-position cell's `align` attribute. Every
1358
+ * `UNSAFE_ELEMENTS` subtree contributes nothing at all, text included. Every OTHER
1359
+ * element unwraps to its children, so wrapper soup melts while its content keeps its
1360
+ * shape - `<div><p>a</p><p>b</p></div>` stays two paragraphs.
1361
+ *
1362
+ * Three mappings read their own node rather than only their children's projections,
1363
+ * because HTML puts the fact in a position rather than in a value: a `pre` takes its
1364
+ * body from its `code` child's raw text, and a list takes one item per `li` child - so
1365
+ * an empty `<li>` is still an item, while the whitespace between two of them is not.
1366
+ * A `tr` accepts only its own direct cells, and a table derives the first `th`-bearing
1367
+ * row from its own source structure.
1368
+ *
1369
+ * @param node - The document root or element to project
1370
+ * @param children - Its children's projections, in source order
1371
+ * @returns Its projection
1372
+ *
1373
+ * @example
1374
+ * ```ts
1375
+ * projectHTMLNode({ category: 'element', name: 'hr', attributes: [], children: [] }, []).blocks
1376
+ * // [{ element: 'thematicBreak' }]
1377
+ * ```
1378
+ */
1379
+ export declare function projectHTMLNode(node: ElementNode | HTMLDocument, children: readonly MarkdownProjection[]): MarkdownProjection;
1380
+
1381
+ /**
1382
+ * Read a projection as BLOCK content - the view a document, a blockquote, and a list
1383
+ * item each need.
1384
+ *
1385
+ * @remarks
1386
+ * A bare inline run becomes one paragraph, and a run carrying no text becomes nothing
1387
+ * at all, because a blank paragraph is unwritable in markdown. A cell or a row that
1388
+ * never reached a table is unwrapped here rather than dropped: a stray `<td>` is still
1389
+ * someone's content.
1390
+ *
1391
+ * @param projection - The projection to read
1392
+ * @returns Its block content
1393
+ *
1394
+ * @example
1395
+ * ```ts
1396
+ * projectionToBlocks(createProjection({ inlines: [{ element: 'text', value: 'a' }], text: 'a' }))
1397
+ * // [{ element: 'paragraph', children: [{ element: 'text', value: 'a' }] }]
1398
+ * ```
1399
+ */
1400
+ export declare function projectionToBlocks(projection: MarkdownProjection): readonly BlockNode[];
1401
+
1402
+ /**
1403
+ * Read a projection as INLINE content - the view a link, an emphasis, and a table cell
1404
+ * each need.
1405
+ *
1406
+ * @remarks
1407
+ * Inline content passes through as itself. Block content cannot: markdown has no way to
1408
+ * put a paragraph inside a table cell, so it flattens to one text node of its own words,
1409
+ * joined and whitespace-collapsed. Content that carries no text flattens to nothing
1410
+ * rather than to an empty text node, which is a shape the parser never produces.
1411
+ *
1412
+ * @param projection - The projection to read
1413
+ * @returns Its inline content
1414
+ *
1415
+ * @example
1416
+ * ```ts
1417
+ * projectionToInlines(createProjection({ inlines: [{ element: 'break' }] }))
1418
+ * // [{ element: 'break' }]
1419
+ * ```
1420
+ */
1421
+ export declare function projectionToInlines(projection: MarkdownProjection): readonly InlineNode[];
1422
+
1423
+ /**
1424
+ * Render a {@link MarkdownNode} to sanitized canonical HTML.
1425
+ *
1426
+ * @remarks
1427
+ * Markdown widens `@orkestrel/html`'s attribute floor by exactly `src`, because image
1428
+ * syntax is meaningless without its source. `src` is still a URL attribute, so the
1429
+ * floor refuses `javascript:`, `data:`, `vbscript:`, and `file:` values. A stricter
1430
+ * consumer can compose {@link markdownToHTML} with `@orkestrel/html`'s `HTML` class
1431
+ * directly.
1432
+ *
1433
+ * @param node - The markdown document or bare node to render
1434
+ * @returns Sanitized canonical HTML
1435
+ *
1436
+ * @example
1437
+ * ```ts
1438
+ * renderHTML({ element: 'paragraph', children: [{ element: 'text', value: 'a & b' }] })
1439
+ * // '<p>a &amp; b</p>'
1440
+ * ```
1441
+ */
1442
+ export declare function renderHTML(node: MarkdownNode): string;
1443
+
1444
+ /**
1445
+ * Render a {@link MarkdownNode} to its CANONICAL markdown source - the inverse
1446
+ * projection of `renderHTML`, and the serializer a `parse(renderMarkdown(doc))`
1447
+ * round-trip is built on. Canonical forms: `*` / `**` emphasis at even emphasis
1448
+ * nesting depths and `_` / `__` at odd depths, `- ` bullets, `N. ` sequential
1449
+ * ordinals (from the list's `start`), `---` thematic breaks, fenced code blocks
1450
+ * (backtick run widened past any 3+ backtick run inside the body), ATX headings,
1451
+ * `> `-prefixed blockquote lines, GFM tables (1-space-padded cells, `\|`-escaped
1452
+ * pipes, an alignment delimiter row), `[text](href)` links, `![alt](src)` images,
1453
+ * and two-space hard breaks. A `text` node's literal content is backslash-escaped
1454
+ * wherever it would otherwise re-parse as markup (AGENTS §14 parse↔render
1455
+ * soundness).
1456
+ *
1457
+ * @remarks
1458
+ * Total: never throws. At {@link MAX_DEPTH} a value-bearing node degrades to its
1459
+ * escaped `value`; any other node degrades to `''`. Blocks are joined by exactly one
1460
+ * blank line; a document with zero blocks renders `''`.
1461
+ *
1462
+ * @param node - The AST node to render (a full document, or any sub-node)
1463
+ * @returns The canonical markdown source
1464
+ *
1465
+ * @example
1466
+ * ```ts
1467
+ * renderMarkdown({ element: 'document', children: [
1468
+ * { element: 'heading', level: 2, children: [{ element: 'text', value: 'Hi' }] },
1469
+ * ] })
1470
+ * // '## Hi'
1471
+ * ```
1472
+ */
1473
+ export declare function renderMarkdown(node: MarkdownNode): string;
1474
+
1475
+ /**
1476
+ * Rewrite a {@link MarkdownDocument} bottom-up (copy-on-write) - each node's children
1477
+ * are rewritten first (post-order), then `rewrite` is applied to the node itself; the
1478
+ * document ROOT is never passed to `rewrite` (the `element: 'document'` invariant
1479
+ * always holds). A table's inline cells and a list's items ARE rewritten.
1480
+ *
1481
+ * @remarks
1482
+ * Never mutates `document` - every level is rebuilt into a fresh object/array, even
1483
+ * when `rewrite` returns its input unchanged. When `rewrite` returns a node whose
1484
+ * `element` does not fit the slot it was called for (a block slot handed a
1485
+ * non-{@link BlockNode}, an inline slot handed a non-{@link InlineNode}, a list-item
1486
+ * slot handed a non-`listItem`), the ill-fitting result is discarded and the
1487
+ * freshly-rebuilt (unrewritten-at-this-level) node is kept instead - `rewriteDocument`
1488
+ * stays total and never produces a structurally invalid document.
1489
+ *
1490
+ * Descent is capped at {@link MAX_DEPTH}, the same cap {@link walkNodes} and
1491
+ * {@link foldNode} observe: at `depth >= MAX_DEPTH` the subtree is passed through
1492
+ * UNCHANGED (by reference, not rebuilt, and `rewrite` is not invoked on it) instead of
1493
+ * recursing further, so a pathologically deep adopted document cannot exhaust the
1494
+ * call stack. {@link MarkdownInterface.map} inherits this cap since it delegates here.
1495
+ *
1496
+ * @param document - The document AST to rewrite
1497
+ * @param rewrite - The bottom-up {@link MarkdownRewriteHandler}
1498
+ * @returns A new, rewritten {@link MarkdownDocument}
1499
+ *
1500
+ * @example
1501
+ * ```ts
1502
+ * rewriteDocument(document, (node) =>
1503
+ * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node,
1504
+ * )
1505
+ * ```
1506
+ */
1507
+ export declare function rewriteDocument(document: MarkdownDocument, rewrite: MarkdownRewriteHandler): MarkdownDocument;
1508
+
1509
+ /**
1510
+ * Scan an inline code span at `start` (a `` ` ``-run … a matching `` ` ``-run of the
1511
+ * SAME length, the CommonMark rule that lets a span contain backticks). Returns the
1512
+ * span's literal text + end index, or `undefined` when no matching closer exists (it
1513
+ * then degrades to literal backticks).
1514
+ *
1515
+ * @param source - The inline source text
1516
+ * @param start - The index of the opening backtick
1517
+ * @param to - The exclusive end of the scan window
1518
+ * @returns The span text + end index, or `undefined`
1519
+ *
1520
+ * @example
1521
+ * ```ts
1522
+ * scanCode('`code`', 0, 6) // { value: 'code', end: 6 }
1523
+ * ```
1524
+ */
1525
+ export declare function scanCode(source: string, start: number, to: number): {
1526
+ readonly value: string;
1527
+ readonly end: number;
1528
+ } | undefined;
1529
+
1530
+ /**
1531
+ * Scan an emphasis run at `start` (`*` / `_`, doubled for strong) - finds the nearest
1532
+ * matching closing run of the same marker + width while skipping complete nested
1533
+ * runs from the other marker family, and requires non-space immediately inside both
1534
+ * delimiters (the CommonMark flanking simplification that blocks `* x *`). Returns
1535
+ * the emphasis node, or `undefined` when no valid closer exists (it then degrades to
1536
+ * a literal marker).
1537
+ *
1538
+ * @param source - The inline source text
1539
+ * @param start - The index of the opening marker
1540
+ * @param to - The exclusive end of the scan window
1541
+ * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1542
+ * at {@link MAX_DEPTH} the emphasis's children degrade to literal text instead of
1543
+ * recursing further
1544
+ * @returns The parsed {@link EmphasisNode} + end index, or `undefined`
1545
+ *
1546
+ * @example
1547
+ * ```ts
1548
+ * scanEmphasis('*em*', 0, 4)
1549
+ * // { node: { element: 'emphasis', strong: false, children: [...] }, end: 4 }
1550
+ * ```
1551
+ */
1552
+ export declare function scanEmphasis(source: string, start: number, to: number, depth?: number): {
1553
+ readonly node: EmphasisNode;
1554
+ readonly end: number;
1555
+ } | undefined;
1556
+
1557
+ /**
1558
+ * Scan the window `[from, to)` of `source` into inline nodes - the single recursive
1559
+ * engine the inline phase runs on (emphasis, link text, and image alternative
1560
+ * content recurse through it). Linear:
1561
+ * each character is consumed once; a failed construct emits its opening character as
1562
+ * text and advances by one, so there is no re-scan (no ReDoS).
1563
+ *
1564
+ * @param source - The inline source text
1565
+ * @param from - The inclusive start of the scan window
1566
+ * @param to - The exclusive end of the scan window
1567
+ * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1568
+ * incremented by one on every recursive descent through {@link scanLink} /
1569
+ * {@link scanEmphasis}. At {@link MAX_DEPTH} the window is never scanned for markup -
1570
+ * it emits as a single literal text node - so pathological nesting (`[[[[…`,
1571
+ * `****…`) cannot exhaust the call stack.
1572
+ * @returns The parsed inline nodes (NOT yet coalesced)
1573
+ *
1574
+ * @example
1575
+ * ```ts
1576
+ * scanInline('hi *there*', 0, 10) // [{ element: 'text', value: 'hi ' }, { element: 'emphasis', ... }]
1577
+ * ```
1578
+ */
1579
+ export declare function scanInline(source: string, from: number, to: number, depth?: number): readonly InlineNode[];
1580
+
1581
+ /**
1582
+ * Scan a link `[text](href)` at `start` - the text runs to a BALANCED `]`, then `(`
1583
+ * must immediately follow and the destination runs to the matching `)` (both respect
1584
+ * nested delimiters + escapes). Returns the link node, or `undefined` when the shape
1585
+ * does not hold (it then degrades to a literal `[`).
1586
+ *
1587
+ * @param source - The inline source text
1588
+ * @param start - The index of the opening `[`
1589
+ * @param to - The exclusive end of the scan window
1590
+ * @param depth - The current inline-recursion depth (defaults to 0 at the entry point);
1591
+ * at {@link MAX_DEPTH} the link's text children degrade to literal text instead of
1592
+ * recursing further
1593
+ * @returns The parsed {@link LinkNode} + end index, or `undefined`
1594
+ *
1595
+ * @example
1596
+ * ```ts
1597
+ * scanLink('[text](url)', 0, 11)
1598
+ * // { node: { element: 'link', href: 'url', children: [...] }, end: 11 }
1599
+ * ```
1600
+ */
1601
+ export declare function scanLink(source: string, start: number, to: number, depth?: number): {
1602
+ readonly node: LinkNode;
1603
+ readonly end: number;
1604
+ } | undefined;
1605
+
1606
+ /**
1607
+ * Normalize line endings to `\n` and split a markdown document into its lines - CRLF
1608
+ * (`\r\n`) and bare CR (`\r`) both collapse to `\n` first, so a Windows-origin
1609
+ * document parses identically. A single trailing newline does not yield a final
1610
+ * empty line.
1611
+ *
1612
+ * @param markdown - The raw markdown source
1613
+ * @returns The document's lines, line-terminators stripped
1614
+ *
1615
+ * @example
1616
+ * ```ts
1617
+ * splitLines('a\r\nb\nc') // ['a', 'b', 'c']
1618
+ * ```
1619
+ */
1620
+ export declare function splitLines(markdown: string): readonly string[];
1621
+
1622
+ /**
1623
+ * Split one GFM table row into its cell strings - outer pipes are optional, an escaped
1624
+ * pipe (`\|`) inside a cell is NOT a separator (it becomes a literal `|`), and the
1625
+ * empty leading / trailing cell produced by an outer `|` is dropped.
1626
+ *
1627
+ * @param row - The raw table row line
1628
+ * @returns The row's cells, in column order
1629
+ *
1630
+ * @example
1631
+ * ```ts
1632
+ * splitTableRow('|a|b|') // ['a', 'b']
1633
+ * ```
1634
+ */
1635
+ export declare function splitTableRow(row: string): readonly string[];
1636
+
1637
+ /**
1638
+ * Whether the line at `index` starts a NEW block kind (heading / fence / thematic
1639
+ * break / blockquote / list / table) - the paragraph collector stops at such a line
1640
+ * so a block following a paragraph without a blank line still parses (a trusted-input
1641
+ * caller writing a `##` heading directly under a paragraph, with no intervening blank
1642
+ * line).
1643
+ *
1644
+ * @param lines - The document's lines
1645
+ * @param index - The line index to test
1646
+ * @returns `true` when the line begins a different block
1647
+ *
1648
+ * @example
1649
+ * ```ts
1650
+ * startsBlock(['text', '## Heading'], 1) // true
1651
+ * ```
1652
+ */
1653
+ export declare function startsBlock(lines: readonly string[], index: number): boolean;
1654
+
1655
+ /**
1656
+ * Strip one level of blockquote marker (`>` plus one optional following space) from a
1657
+ * blockquote line, so the de-quoted lines re-parse as nested blocks.
1658
+ *
1659
+ * @param line - A blockquote line (per {@link isQuote})
1660
+ * @returns The line with its leading `>` (and one space) removed
1661
+ *
1662
+ * @example
1663
+ * ```ts
1664
+ * stripQuote('> text') // 'text'
1665
+ * ```
1666
+ */
1667
+ export declare function stripQuote(line: string): string;
1668
+
1669
+ /**
1670
+ * The horizontal alignment of a GFM table column, as declared by its delimiter row
1671
+ * (`:---` left, `---:` right, `:---:` center). A bare `---` delimiter is represented
1672
+ * by `null` in {@link TableNode.align}: the positional array requires one entry per
1673
+ * column, JSON cannot carry `undefined` in an array, and the bare delimiter is an
1674
+ * explicit no-alignment marker rather than an omitted value.
1675
+ */
1676
+ export declare type TableAlign = 'left' | 'right' | 'center';
1677
+
1678
+ /**
1679
+ * The shape of a {@link TableAlign} - the per-column GFM table alignment
1680
+ * literal.
1681
+ *
1682
+ * @example
1683
+ * ```ts
1684
+ * import { createContract } from '@orkestrel/contract'
1685
+ * import { tableAlignShape } from '@src/core'
1686
+ *
1687
+ * const tableAlign = createContract(tableAlignShape)
1688
+ * tableAlign.is('left') // true
1689
+ * tableAlign.is('center') // true
1690
+ * tableAlign.is('top') // false
1691
+ * ```
1692
+ */
1693
+ export declare const tableAlignShape: LiteralShape<readonly ["left", "right", "center"]>;
1694
+
1695
+ /**
1696
+ * A GFM table - `header` the inline content of each header cell, `rows` the body
1697
+ * rows (each a list of cells, each cell inline content), `align` the per-column
1698
+ * alignment from the delimiter row. A short body row is padded with empty cells; an
1699
+ * over-long one is truncated to the header's column count.
1700
+ */
1701
+ export declare interface TableNode {
1702
+ readonly element: 'table';
1703
+ /** The header row - one cell of inline content per column. */
1704
+ readonly header: readonly (readonly InlineNode[])[];
1705
+ /** The body rows - each a list of cells, each cell inline content. */
1706
+ readonly rows: readonly (readonly (readonly InlineNode[])[])[];
1707
+ /**
1708
+ * The per-column alignment from the delimiter row, in column order. `null`
1709
+ * represents a bare `---` delimiter because this positional array requires one
1710
+ * entry per column, JSON cannot carry `undefined` in an array, and the delimiter
1711
+ * is an explicit no-alignment marker rather than an omitted value.
1712
+ */
1713
+ readonly align: readonly (TableAlign | null)[];
1714
+ }
1715
+
1716
+ /**
1717
+ * A run of plain text - the leaf inline node. `value` is the decoded text with
1718
+ * markdown escapes (`\*`, `\_`, …) already resolved to their literal characters;
1719
+ * html's text encoder escapes `&`, `<`, `>` on the way out; `"` and `'` stay literal
1720
+ * in character data.
1721
+ */
1722
+ export declare interface TextNode {
1723
+ readonly element: 'text';
1724
+ /** The literal text content (escapes resolved, NOT yet HTML-escaped). */
1725
+ readonly value: string;
1726
+ }
1727
+
1728
+ /**
1729
+ * The shape of a {@link TextNode} - a plain-text leaf inline run.
1730
+ *
1731
+ * @example
1732
+ * ```ts
1733
+ * import { createContract } from '@orkestrel/contract'
1734
+ * import { textShape } from '@src/core'
1735
+ *
1736
+ * const text = createContract(textShape)
1737
+ * text.is({ element: 'text', value: 'hi' }) // true
1738
+ * ```
1739
+ */
1740
+ export declare const textShape: ObjectShape<{
1741
+ element: LiteralShape<readonly ["text"]>;
1742
+ value: StringShape;
1743
+ }, false>;
1744
+
1745
+ /** A thematic break - a horizontal rule (`---` / `***` / `___` on its own line). */
1746
+ export declare interface ThematicBreakNode {
1747
+ readonly element: 'thematicBreak';
1748
+ }
1749
+
1750
+ /**
1751
+ * The shape of a {@link ThematicBreakNode} - a horizontal rule. Carries no
1752
+ * fields beyond its `element` discriminant.
1753
+ *
1754
+ * @example
1755
+ * ```ts
1756
+ * import { createContract } from '@orkestrel/contract'
1757
+ * import { thematicBreakShape } from '@src/core'
1758
+ *
1759
+ * const thematicBreak = createContract(thematicBreakShape)
1760
+ * thematicBreak.is({ element: 'thematicBreak' }) // true
1761
+ * ```
1762
+ */
1763
+ export declare const thematicBreakShape: ObjectShape<{
1764
+ element: LiteralShape<readonly ["thematicBreak"]>;
1765
+ }, false>;
1766
+
1767
+ /**
1768
+ * Trim the whitespace at the two ends of an inline run - the leading whitespace of a
1769
+ * leading text node and the trailing whitespace of a trailing one - dropping either
1770
+ * node when nothing survives.
1771
+ *
1772
+ * @remarks
1773
+ * Markdown trims every line of a paragraph, a heading's text, and a table cell, so an
1774
+ * untrimmed run would come back from a re-parse a different AST. Expects a coalesced
1775
+ * run (see {@link coalesceText}): only the outermost node on each side is examined.
1776
+ *
1777
+ * @param nodes - The inline run to trim
1778
+ * @returns The run with its edge whitespace removed
1779
+ *
1780
+ * @example
1781
+ * ```ts
1782
+ * trimInlines([{ element: 'text', value: ' a ' }]) // [{ element: 'text', value: 'a' }]
1783
+ * ```
1784
+ */
1785
+ export declare function trimInlines(nodes: readonly InlineNode[]): readonly InlineNode[];
1786
+
1787
+ /**
1788
+ * Resolve backslash escapes in a raw string to their literal characters - used for a
1789
+ * link `href` (which is not otherwise inline-parsed) and any plain text run.
1790
+ *
1791
+ * @param text - The raw text possibly carrying `\x` escapes
1792
+ * @returns The text with escapable `\x` reduced to `x`
1793
+ *
1794
+ * @example
1795
+ * ```ts
1796
+ * unescapeText('\\*hi\\*') // '*hi*'
1797
+ * ```
1798
+ */
1799
+ export declare function unescapeText(text: string): string;
1800
+
1801
+ /**
1802
+ * Depth-first, pre-order, root-inclusive traversal of a {@link MarkdownNode} - yields
1803
+ * the node itself, then recurses into its children (block children, list items,
1804
+ * image/link inline children, table header/row cells' inline nodes) in walk order.
1805
+ *
1806
+ * @remarks
1807
+ * Total: never throws. Descent stops at {@link MAX_DEPTH} (the node at the cap is
1808
+ * still yielded; its children are not) so pathologically deep input cannot exhaust
1809
+ * the call stack.
1810
+ *
1811
+ * @param node - The AST node to walk (a full document, or any sub-node)
1812
+ * @returns A generator yielding every visited node, pre-order
1813
+ *
1814
+ * @example
1815
+ * ```ts
1816
+ * const doc = { element: 'document', children: [{ element: 'thematicBreak' }] } as const
1817
+ * [...walkNodes(doc)].map((node) => node.element) // ['document', 'thematicBreak']
1818
+ * ```
1819
+ */
1820
+ export declare function walkNodes(node: MarkdownNode): Generator<MarkdownNode>;
1821
+
1822
+ export { }