import { BooleanShape } from '@orkestrel/contract'; import type { CommentNode } from '@orkestrel/html'; import type { ContractInterface } from '@orkestrel/contract'; import type { DoctypeNode } from '@orkestrel/html'; import type { ElementNode } from '@orkestrel/html'; import type { Guard } from '@orkestrel/contract'; import type { HTMLDocument } from '@orkestrel/html'; import type { HTMLNode } from '@orkestrel/html'; import { LiteralShape } from '@orkestrel/contract'; import { NumberShape } from '@orkestrel/contract'; import { ObjectShape } from '@orkestrel/contract'; import { OptionalShape } from '@orkestrel/contract'; import { StringShape } from '@orkestrel/contract'; import type { TextNode as TextNode_2 } from '@orkestrel/html'; /** Represents a node that can appear at the block level of a document (or inside a list item / blockquote). */ export declare type BlockNode = HeadingNode | ParagraphNode | ListNode | TableNode | CodeBlockNode | BlockquoteNode | ThematicBreakNode; /** Represents a blockquote — `>`-prefixed lines; `children` the block content parsed from the de-quoted lines (so quotes nest). */ export declare interface BlockquoteNode { readonly element: 'blockquote'; /** Holds the block content of the quote (the `>`-stripped lines, re-parsed as blocks). */ readonly children: readonly BlockNode[]; } /** * Merges adjacent text nodes into one — the inline scanner emits a text node per * unrecognized character, so coalescing keeps the AST clean and assertion-friendly. * * @param nodes - The inline nodes (possibly with adjacent text runs) * @param spans - The optional operation-owned node span recorder * @returns The nodes with consecutive text nodes concatenated * * @example * ```ts * coalesceText([{ element: 'text', value: 'a' }, { element: 'text', value: 'b' }]) * // [{ element: 'text', value: 'ab' }] * ``` */ export declare function coalesceText(nodes: readonly InlineNode[], spans?: Map): readonly InlineNode[]; /** * Represents a fenced code block — ```` ```lang ````. `code` is the verbatim block content (no * inner markdown; the closing fence and the trailing newline are stripped), `lang` * the info-string language tag (the first word after the opening fence), absent when * none was given. */ export declare interface CodeBlockNode { readonly element: 'codeBlock'; /** Holds the info-string language tag (first word after the opening fence), if any. */ readonly lang?: string; /** Holds the verbatim code content (no inner markdown; HTML-escaped at render). */ readonly code: string; } /** * Describes the shape of a {@link CodeBlockNode} — a fenced code block. `lang` is * optional (absent when the opening fence carries no info-string). * * @example * ```ts * import { createContract } from '@orkestrel/contract' * import { codeBlockShape } from '@src/core' * * const codeBlock = createContract(codeBlockShape) * codeBlock.is({ element: 'codeBlock', code: 'x' }) // true * codeBlock.is({ element: 'codeBlock', code: 'x', lang: 'ts' }) // true * ``` */ export declare const codeBlockShape: ObjectShape< { element: LiteralShape; lang: OptionalShape; code: StringShape; }, false>; /** * Represents the located extent of one inline code span — the value the inline phase's code * scanner returns for a matched backtick run. */ export declare interface CodeSpanMatch { /** Holds the span's literal text, with one padding space stripped from each end. */ readonly value: string; /** Holds the index one past the span's closing backtick run, exclusive. */ readonly end: number; } /** * Represents an inline code span — `` `code` ``. `value` is the verbatim span text; no inner * markdown is parsed (code is literal), and the renderer HTML-escapes it inside a * `` element. */ export declare interface CodeSpanNode { readonly element: 'codeSpan'; /** Holds the verbatim code text (no inner markdown; HTML-escaped at render). */ readonly value: string; } /** * Describes the shape of a {@link CodeSpanNode} — an inline code span (`` `code` ``). * * @example * ```ts * import { createContract } from '@orkestrel/contract' * import { codeSpanShape } from '@src/core' * * const codeSpan = createContract(codeSpanShape) * codeSpan.is({ element: 'codeSpan', value: 'const x = 1' }) // true * ``` */ export declare const codeSpanShape: ObjectShape< { element: LiteralShape; value: StringShape; }, false>; /** * Collects a list starting at the first item, gathering sibling items at the * same indent/ordering and recursing into each item's own block content. * * @param lines - The markdown lines to scan. * @param start - The index of the first list item. * @param depth - The current recursion depth (each item recurses at `depth + 1`). * @param spans - The optional operation-owned node span recorder. * @param end - The original-source end of this line run, including a removed terminator. * @returns The parsed list node and the index of the first line after it. * * @example * ```ts * collectList(splitLines('- item'), 0, 0) // { node: { element: 'list', ... }, next: 1 } * ``` */ export declare function collectList(lines: readonly MarkdownSource[], start: number, depth: number, spans?: Map, end?: number): ListCollection; /** * Collects a GFM table starting at a header row, parsing the header, the * alignment row, and every contiguous body row that follows. * * @param lines - The markdown lines to scan. * @param start - The index of the header row. * @param spans - The optional operation-owned node span recorder. * @returns The parsed table node and the index of the first line after it. * * @example * ```ts * collectTable(splitLines('| a |\n| - |'), 0) // { node: { element: 'table', ... }, next: 2 } * ``` */ export declare function collectTable(lines: readonly MarkdownSource[], start: number, spans?: Map): TableCollection; /** * Counts the leading space / tab characters on `line` (a tab counts as one) — the * indent that decides whether a list item's continuation belongs to the item. * * @param line - The line to measure * @returns The number of leading space / tab characters * * @example * ```ts * countIndent(' text') // 2 * ``` */ export declare function countIndent(line: string): number; /** * Compiles the {@link codeBlockShape} into a {@link ContractInterface} for * {@link CodeBlockNode} — a guard, coercing parser, JSON Schema, and seeded * generator from one shape declaration. * * @returns A `CodeBlockNode` contract bundling `schema` / `is` / `parse` / `generate` * * @example * ```ts * import { createCodeBlockContract } from '@src/core' * * const codeBlock = createCodeBlockContract() * codeBlock.is({ element: 'codeBlock', code: 'x' }) // true * ``` */ export declare function createCodeBlockContract(): ContractInterface; /** * Compiles the {@link codeSpanShape} into a {@link ContractInterface} for * {@link CodeSpanNode} — a guard, coercing parser, JSON Schema, and seeded * generator from one shape declaration. * * @returns A `CodeSpanNode` contract bundling `schema` / `is` / `parse` / `generate` * * @example * ```ts * import { createCodeSpanContract } from '@src/core' * * const codeSpan = createCodeSpanContract() * codeSpan.is({ element: 'codeSpan', value: 'const x = 1' }) // true * ``` */ export declare function createCodeSpanContract(): ContractInterface; /** * Compiles the {@link lineBreakShape} into a {@link ContractInterface} for * {@link LineBreakNode}. * * @returns A `LineBreakNode` contract bundling `schema` / `is` / `parse` / `generate` * * @example * ```ts * import { createLineBreakContract } from '@src/core' * * createLineBreakContract().is({ element: 'break' }) // true * ``` */ export declare function createLineBreakContract(): ContractInterface; /** * Creates a stateful markdown handle from a markdown string or an already-parsed * {@link MarkdownDocument} — a typed AST plus the query, rewrite, and fold operations * {@link MarkdownInterface} exposes. * * @remarks * Given a `string`, runs a block phase (headings / paragraphs / lists / GFM tables / * fenced code / blockquotes / thematic breaks) then an inline phase (emphasis / * inline code / links / images / hard breaks) to build a render-agnostic * {@link MarkdownDocument}. Given a * {@link MarkdownDocument}, adopts it as-is without re-validation — gate an untrusted * value with `isMarkdownDocument` first. Pure + total parse (malformed markdown * degrades to text, never throws) and zero-dependency — a hand-written scanner, no * regex-only structural parse, linear-time (no ReDoS). * * @param input - A markdown string to parse, or an already-parsed {@link MarkdownDocument} * @returns A working {@link MarkdownInterface} * * @example * ```ts * import { createMarkdown } from '@src/core' * * const markdown = createMarkdown('# Hi\n\nRead the [guide](./guide.md).') * markdown.document.children[0] // { element: 'heading', ... } * ``` */ export declare function createMarkdown(input: string | MarkdownDocument): MarkdownInterface; /** * Builds an HTML-to-markdown projection with absent fields defaulted from * {@link EMPTY_PROJECTION} and the block/inline exclusivity invariant enforced. * * @remarks * A block-bearing projection cannot also expose inline content. Callers may provide * both views, but `inlines` is flushed whenever `blocks` is non-empty. * * @param parts - The projection fields to provide * @returns A complete invariant-preserving projection * * @example * ```ts * createProjection({ * blocks: [{ element: 'thematicBreak' }], * inlines: [{ element: 'text', value: 'discarded' }], * }) * // { blocks: [{ element: 'thematicBreak' }], inlines: [], text: '', cells: [], rows: [] } * ``` */ export declare function createProjection(parts?: Partial): MarkdownProjection; /** * Compiles the {@link textShape} into a {@link ContractInterface} for * {@link TextNode} — a guard, coercing parser, JSON Schema, and seeded * generator from one shape declaration. * * @returns A `TextNode` contract bundling `schema` / `is` / `parse` / `generate` * * @example * ```ts * import { createTextContract } from '@src/core' * * const text = createTextContract() * text.is({ element: 'text', value: 'hi' }) // true * ``` */ export declare function createTextContract(): ContractInterface; /** * Compiles the {@link thematicBreakShape} into a {@link ContractInterface} for * {@link ThematicBreakNode} — a guard, coercing parser, JSON Schema, and * seeded generator from one shape declaration. * * @returns A `ThematicBreakNode` contract bundling `schema` / `is` / `parse` / `generate` * * @example * ```ts * import { createThematicBreakContract } from '@src/core' * * const thematicBreak = createThematicBreakContract() * thematicBreak.is({ element: 'thematicBreak' }) // true * ``` */ export declare function createThematicBreakContract(): ContractInterface; /** * Derives the per-column {@link TableAlign} list from a GFM delimiter row — `:---` * left, `---:` right, `:---:` center, and `---` as the explicit no-alignment * marker represented by `null`. * * @param delimiter - The table's delimiter row * @returns One alignment per column, in column order * * @example * ```ts * delimiterToAlignments('| :--- | ---: |') // ['left', 'right'] * ``` */ export declare function delimiterToAlignments(delimiter: string): ReadonlyArray; /** * Represents the located content and syntax bounds of one emphasis run — the value the inline * phase's emphasis locator returns for a matched marker run. */ export declare interface EmphasisBounds { /** Holds `true` for a doubled marker (`**strong**`), `false` for a single one (`*em*`). */ readonly strong: boolean; /** Holds the index of the run's first content character. */ readonly open: number; /** Holds the index of the closing marker run's first character. */ readonly close: number; /** Holds the index one past the closing marker run, exclusive. */ readonly end: number; } /** * Represents emphasized inline content — `*italic*` / `_italic_` (`strong: false`) or * `**bold**` / `__bold__` (`strong: true`). `children` are the nested inline nodes, * so emphasis composes (a `**bold _and italic_**` is a strong node wrapping a text * node and an emphasis node). */ export declare interface EmphasisNode { readonly element: 'emphasis'; /** Holds `true` for strong (`**` / `__`, → ``); `false` for ordinary emphasis (`*` / `_`, → ``). */ readonly strong: boolean; /** Holds the emphasized inline content. */ readonly children: readonly InlineNode[]; } /** * Represents the scanned result of one emphasis run — the node the inline phase's emphasis * scanner built from {@link EmphasisBounds} and where the scan resumes. */ export declare interface EmphasisScan { /** Holds the scanned emphasis run, its content already scanned into inline children. */ readonly node: EmphasisNode; /** Holds the index one past the closing marker run, exclusive. */ readonly end: number; } /** * Holds the frozen empty HTML-to-markdown projection from which projection factories * default every absent field. * * @example * ```ts * EMPTY_PROJECTION.blocks // [] * Object.isFrozen(EMPTY_PROJECTION) // true * ``` */ export declare const EMPTY_PROJECTION: MarkdownProjection; /** * Extracts a fenced-code opening line (```` ``` ```` or `~~~`, optionally with an info * string) into its `{ marker, lang }`, or `undefined` when `line` is not a fence * opener. `marker` is the exact fence run (the closer must match the same character + * at least the same length); `lang` is the first word of the info string. * * @param line - The candidate line * @returns The fence marker run and its language tag, or `undefined` * * @example * ```ts * extractFence('```ts') // { marker: '```', lang: 'ts' } * ``` */ export declare function extractFence(line: string): FenceMatch | undefined; /** * Extracts an ATX heading line (`#` … `######` followed by text) into its level, * trimmed text, and the text's offset inside the line. A run of more than 6 `#`s, or * `#`s not followed by whitespace + text, is not a heading; an optional closing * `###` run is stripped. * * @param line - The candidate line * @returns The heading level (1–6), raw inline text, and text offset, or `undefined` * * @example * ```ts * extractHeading('## Title') // { level: 2, text: 'Title', offset: 3 } * ``` */ export declare function extractHeading(line: string): HeadingMatch | undefined; /** * Extracts a list-item line (`-` / `*` / `+` bullet, or `1.` / `1)` ordinal, followed by * a space) into its {@link ListItemMatch}, or `undefined` when `line` is not a list * item. `content` is the text after the marker; `marker` is the full marker-plus-space * width (for measuring a continuation's indent). * * @param line - The candidate line * @returns The list-item parts, or `undefined` when not a list item * * @example * ```ts * extractListItem('- item') // { ordered: false, start: 1, content: 'item', indent: 0, marker: 2 } * ``` */ export declare function extractListItem(line: string): ListItemMatch | undefined; /** * Represents the parsed parts of a fenced-code opening line — the value the block phase's fence * detector returns for a ```` ``` ```` or `~~~` opener. */ export declare interface FenceMatch { /** Holds the exact fence run; a closer must repeat the same character at least as long. */ readonly marker: string; /** Holds the first word of the info string, or `undefined` when the fence declares none. */ readonly lang: string | undefined; } /** * Concatenates the `value` / `code` content of every descendant text / code-span / * code-block node under `node`, including image alternative content, in walk order — * the plain-text projection of an AST (search indexing, word counts, a text-only * preview). * * @remarks * Total: never throws. Descent stops at {@link MAX_DEPTH} (contributes `''` past the * cap instead of recursing further). * * @param node - The AST node to flatten (a full document, or any sub-node) * @returns The concatenated text content * * @example * ```ts * flattenText({ element: 'paragraph', children: [ * { element: 'text', value: 'a ' }, * { element: 'codeSpan', value: 'b' }, * ] }) * // 'a b' * ``` */ export declare function flattenText(node: MarkdownNode): string; /** * Folds a {@link MarkdownNode} into a `T` through a total catamorphism — children are * folded first (post-order), then the node's own {@link MarkdownHandler} is invoked * with the already-folded children. * * @remarks * **Table contract.** A {@link TableNode} has no single `children` array — its cells * live in `header` (one inline-node list per column) and `rows` (a list of such * rows). The `table` handler receives one folded `T` per inline node, flattened in * walk order across all cells — every header cell's inline nodes (column order), then * every body row's cells' inline nodes (row order, then column order) — and reads * `node.header[c].length` / `node.rows[r][c].length` off the table node itself to * recover cell boundaries within the flat list. * * Total: never throws. At `depth >= {@link MAX_DEPTH}` the node's handler is invoked * with an empty children list instead of recursing further. * * @param node - The AST node to fold * @param handlers - The total {@link MarkdownHandlerMap} table, one handler per element * @param depth - The starting recursion depth (pass `0` at the entry point) * @returns The folded `T` * * @example * ```ts * const countHandlers: MarkdownHandlerMap = { * document: (_, children) => children.reduce((a, b) => a + b, 1), * // ...one handler per element, each summing its folded children * } * foldNode(document, countHandlers, 0) // total node count * ``` */ export declare function foldNode(node: MarkdownNode, handlers: MarkdownHandlerMap, depth: number): T; /** * Represents the parsed parts of a single ATX heading line — the value the block phase's heading * detector returns for a `#` … `######` line. */ export declare interface HeadingMatch { /** Holds the heading's level, 1 to 6. */ readonly level: number; /** Holds the heading's raw inline text, with an optional closing `#` run stripped. */ readonly text: string; /** Holds the offset of {@link HeadingMatch.text} inside the original line. */ readonly offset: number; } /** * Represents an ATX heading — `#` … `######`. `level` is 1–6 (the number of leading `#`), * `children` the inline content of the heading text. */ export declare interface HeadingNode { readonly element: 'heading'; /** Holds the heading level, 1 (`#`) through 6 (`######`). */ readonly level: number; /** Holds the inline content of the heading text. */ readonly children: readonly InlineNode[]; } /** * Projects an `@orkestrel/html` {@link HTMLNode} into a {@link MarkdownDocument} — the * HTML→markdown direction, and the inverse of {@link markdownToHTML}. * * @remarks * **Engine.** One total handler table — {@link projectHTMLNode} for the containers, * {@link projectHTMLLeaf} for the leaves — folded by `@orkestrel/html`'s own `foldNode`, so * depth capping, cycle safety, and bottom-up ordering are inherited rather than * rebuilt. Total: hostile, cyclic, and pathologically deep input degrades instead of * throwing. * * **Composed depth.** Both packages cap recursion at 64, and html's cap is reached * first: a document nested past it projects to a chain bounded by that cap, with the * content below it truncated before markdown ever sees it. Since the projected chain * can be a level or two deeper than {@link MAX_DEPTH}, the serializer's own cap can * then truncate again — so the anchor law that follows is a law within the depth budget, and * beyond it only totality is promised. * * **Safety.** Every `href` and `src` is re-sanitized through * `sanitizeURL(value, SAFE_URL_SCHEMES)` whether or not the AST was ever sanitized, * because a hand-built one never was. A refused destination empties to `''` and the * link or image is kept — `[text]()` — because a bad URL is no reason to lose the words * around it. An `UNSAFE_ELEMENTS` subtree contributes nothing at all, text included, so * a `script` body can never resurface as prose. * * **The anchor law.** HTML→markdown is lossy, so the fixpoint that matters is the * projected AST, not the input bytes: * `parseDocument(renderMarkdown(htmlToMarkdown(x)))` deep-equals `htmlToMarkdown(x)`. * The projection therefore emits canonical markdown shapes rather than literal * translations — whitespace collapsed, edges trimmed, a blank paragraph dropped, a hard * break only where a line can end — because a shape markdown cannot write back is a * shape this projection has no business producing. * * @param node - The HTML document or bare node to project * @returns The projected markdown document * * @example * ```ts * import { parseDocument } from '@orkestrel/html' * * htmlToMarkdown(parseDocument('

Title

')) * // { element: 'document', children: [{ element: 'heading', level: 1, children: [...] }] } * ``` */ export declare function htmlToMarkdown(node: HTMLNode): MarkdownDocument; /** * Represents an inline image — `![alt](src)`. `children` are the inline nodes of the * alternative content and `src` is the image destination. */ export declare interface ImageNode { readonly element: 'image'; /** Holds the image destination. */ readonly src: string; /** Holds the inline alternative content. */ readonly children: readonly InlineNode[]; } /** Represents a node that can appear inside inline content (a heading / paragraph / cell / list item / link text). */ export declare type InlineNode = TextNode | EmphasisNode | CodeSpanNode | LineBreakNode | LinkNode | ImageNode; /** * Checks whether `line` is blank — empty, or containing only whitespace — the markdown * definition of a blank line that block parsing uses to separate paragraphs, skip * gaps, and end list continuations. * * @param line - The candidate line * @returns True if the line is blank; false otherwise * * @example * ```ts * isBlankLine(' ') // true * ``` */ export declare function isBlankLine(line: string): boolean; /** * Determines whether an arbitrary value is a valid {@link BlockNode} — a * heading, paragraph, list, table, code block, blockquote, or thematic break, * recursively validated. * * @remarks * Total: never throws, even on cyclic or pathologically deep input — every * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is * throw-contained per the `@orkestrel/contract` guard contract. * A list item's shape is inlined here (and in {@link isMarkdownNode}) rather * than named separately — it is used at exactly these two sites. * * @param value - The value to test * @returns True if `value` is a well-formed {@link BlockNode}; false otherwise * * @example * ```ts * import { isBlockNode } from '@orkestrel/markdown' * * isBlockNode({ element: 'thematicBreak' }) // true * isBlockNode({ element: 'heading' }) // false - missing `level` / `children` * ``` */ export declare const isBlockNode: Guard; /** * Determines whether a node is a blockquote block. * * @param node - The AST node to test * @returns True if the node is a {@link BlockquoteNode}; false otherwise * * @example * ```ts * isBlockquoteNode({ element: 'blockquote', children: [] }) // true * ``` */ export declare function isBlockquoteNode(node: MarkdownNode): node is BlockquoteNode; /** * Determines whether a node is a fenced code block. * * @param node - The AST node to test * @returns True if the node is a {@link CodeBlockNode}; false otherwise * * @example * ```ts * isCodeBlockNode({ element: 'codeBlock', code: 'x' }) // true * ``` */ export declare function isCodeBlockNode(node: MarkdownNode): node is CodeBlockNode; /** * Determines whether a node is an inline code span. * * @remarks * Narrows to {@link CodeSpanNode} — the node whose `element` discriminant is * `'codeSpan'`. * * @param node - The AST node to test * @returns True if the node is a {@link CodeSpanNode}; false otherwise * * @example * ```ts * isCodeSpanNode({ element: 'codeSpan', value: 'x' }) // true * ``` */ export declare function isCodeSpanNode(node: MarkdownNode): node is CodeSpanNode; /** * Determines whether a node is an emphasis run (`*em*` / `**strong**`). * * @param node - The AST node to test * @returns True if the node is an {@link EmphasisNode}; false otherwise * * @example * ```ts * isEmphasisNode({ element: 'emphasis', strong: false, children: [] }) // true * ``` */ export declare function isEmphasisNode(node: MarkdownNode): node is EmphasisNode; /** * Checks whether `character` is escapable by a leading backslash — the ASCII punctuation * markdown gives meaning to (so `\*` becomes `*` but `\.` stays `\.`). * * @param character - The single character after a backslash * @returns True if a backslash before it is an escape; false otherwise * * @example * ```ts * isEscapable('*') // true * isEscapable('a') // false * ``` */ export declare function isEscapable(character: string): boolean; /** * Checks whether `line` closes a fence opened by `marker` — the same fence character, a run * at least as long, and nothing else but surrounding whitespace. * * @param line - The candidate closing line * @param marker - The opening fence's marker run (from {@link extractFence}) * @returns True if `line` closes the fence; false otherwise * * @example * ```ts * isFenceClose('```', '```') // true * ``` */ export declare function isFenceClose(line: string, marker: string): boolean; /** * Checks whether `character` is a regex-`\s`-equivalent whitespace character — the * character class {@link isFenceClose}'s scan treats as surrounding padding. * * @param character - The single character to test, or `undefined` past the end of a line * @returns True if it is whitespace; false otherwise * * @example * ```ts * isFenceWhitespace(' ') // true * isFenceWhitespace(undefined) // false * ``` */ export declare function isFenceWhitespace(character: string | undefined): boolean; /** * Checks whether `character` is whitespace under the emphasis flanking rule — a space, a * tab, or a newline. * * @param character - The character to test * @returns True if the flanking rule counts it as whitespace; false otherwise * * @example * ```ts * isFlankingWhitespace(' ') // true * isFlankingWhitespace('a') // false * ``` */ export declare function isFlankingWhitespace(character: string): boolean; /** * Determines whether a node is a heading block. * * @param node - The AST node to test * @returns True if the node is a {@link HeadingNode}; false otherwise * * @example * ```ts * isHeadingNode({ element: 'heading', level: 1, children: [] }) // true * ``` */ export declare function isHeadingNode(node: MarkdownNode): node is HeadingNode; /** * Determines whether a node is an image. * * @param node - The AST node to test * @returns True if the node is an {@link ImageNode}; false otherwise * * @example * ```ts * isImageNode({ element: 'image', src: 'x.png', children: [] }) // true * ``` */ export declare function isImageNode(node: MarkdownNode): node is ImageNode; /** * Determines whether an arbitrary value is a valid {@link InlineNode} — a text * run, emphasis, code span, hard break, link, or image, recursively validated. * * @remarks * Total: never throws, even on cyclic or pathologically deep input — every * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is * throw-contained per the `@orkestrel/contract` guard contract. * * @param value - The value to test * @returns True if `value` is a well-formed {@link InlineNode}; false otherwise * * @example * ```ts * import { isInlineNode } from '@orkestrel/markdown' * * isInlineNode({ element: 'text', value: 'hi' }) // true * isInlineNode({ element: 'text' }) // false - missing `value` * ``` */ export declare const isInlineNode: Guard; /** * Determines whether a node is a GFM hard line break. * * @param node - The AST node to test * @returns True if the node is a {@link LineBreakNode}; false otherwise * * @example * ```ts * isLineBreakNode({ element: 'break' }) // true * ``` */ export declare function isLineBreakNode(node: MarkdownNode): node is LineBreakNode; /** * Determines whether a node is a link. * * @param node - The AST node to test * @returns True if the node is a {@link LinkNode}; false otherwise * * @example * ```ts * isLinkNode({ element: 'link', href: 'https://example.dev', children: [] }) // true * ``` */ export declare function isLinkNode(node: MarkdownNode): node is LinkNode; /** * Determines whether a node is a list block. * * @param node - The AST node to test * @returns True if the node is a {@link ListNode}; false otherwise * * @example * ```ts * isListNode({ element: 'list', ordered: false, start: 1, items: [] }) // true * ``` */ export declare function isListNode(node: MarkdownNode): node is ListNode; /** * Determines whether an arbitrary value is a valid {@link MarkdownDocument} — * the parsed-AST root {@link parseDocument} returns, recursively * validated. * * @remarks * Total: never throws, even on cyclic or pathologically deep input — every * combinator involved (`recordOf`, `arrayOf`) is throw-contained per the * `@orkestrel/contract` guard contract. * * @param value - The value to test * @returns True if `value` is a well-formed {@link MarkdownDocument}; false otherwise * * @example * ```ts * import { isMarkdownDocument } from '@orkestrel/markdown' * * isMarkdownDocument({ element: 'document', children: [] }) // true * isMarkdownDocument({ element: 'document' }) // false - missing `children` * ``` */ export declare const isMarkdownDocument: Guard; /** * Determines whether an arbitrary value is a valid {@link MarkdownNode} — the * {@link MarkdownDocument} root, a {@link BlockNode}, a {@link ListItemNode}, or * an {@link InlineNode}, recursively validated. * * @remarks * Total: never throws, even on cyclic or pathologically deep input — every * combinator involved (`unionOf`, `recordOf`, `arrayOf`, `lazyOf`) is * throw-contained per the `@orkestrel/contract` guard contract. * A list item's shape is inlined here (and in {@link isBlockNode}) rather than * named separately — it is used at exactly these two sites. * * @param value - The value to test * @returns True if `value` is a well-formed {@link MarkdownNode}; false otherwise * * @example * ```ts * import { isMarkdownNode } from '@orkestrel/markdown' * * isMarkdownNode({ element: 'text', value: 'hi' }) // true * isMarkdownNode({ element: 'bogus' }) // false * ``` */ export declare const isMarkdownNode: Guard; /** * Determines whether a node is a paragraph block. * * @param node - The AST node to test * @returns True if the node is a {@link ParagraphNode}; false otherwise * * @example * ```ts * isParagraphNode({ element: 'paragraph', children: [] }) // true * ``` */ export declare function isParagraphNode(node: MarkdownNode): node is ParagraphNode; /** * Checks whether `line` is a blockquote line (`>` optionally indented up to three spaces) — * its content is de-quoted by {@link stripQuote}. * * @param line - The candidate line * @returns True if the line begins a blockquote; false otherwise * * @example * ```ts * isQuote('> quoted') // true * ``` */ export declare function isQuote(line: string): boolean; /** * Determines whether a node is a GFM table block. * * @param node - The AST node to test * @returns True if the node is a {@link TableNode}; false otherwise * * @example * ```ts * isTableNode({ element: 'table', header: [], rows: [], align: [] }) // true * ``` */ export declare function isTableNode(node: MarkdownNode): node is TableNode; /** * Checks whether the pair (`header`, `delimiter`) opens a GFM table — `delimiter` is a row of * `|`-separated cells each matching `:?-+:?`, the GFM rule that a table requires a * header row immediately followed by a delimiter row. * * @param header - The candidate header line * @param delimiter - The line after it (the candidate delimiter) * @returns True if the two lines open a table; false otherwise * * @example * ```ts * isTableStart('| a |', '| - |') // true * ``` */ export declare function isTableStart(header: string, delimiter: string | undefined): boolean; /** * Determines whether a node is a plain text run. * * @param node - The AST node to test * @returns True if the node is a {@link TextNode}; false otherwise * * @example * ```ts * isTextNode({ element: 'text', value: 'hi' }) // true * ``` */ export declare function isTextNode(node: MarkdownNode): node is TextNode; /** * Checks whether `line` is a thematic break (horizontal rule) — three or more of the same * marker `-`, `*`, or `_` (optionally space-separated) and nothing else (`---`, * `***`, `___`, `- - -`). * * @param line - The candidate line * @returns True if the line is a thematic break; false otherwise * * @example * ```ts * isThematicBreak('---') // true * ``` */ export declare function isThematicBreak(line: string): boolean; /** * Determines whether a node is a thematic break (horizontal rule) block. * * @param node - The AST node to test * @returns True if the node is a {@link ThematicBreakNode}; false otherwise * * @example * ```ts * isThematicBreakNode({ element: 'thematicBreak' }) // true * ``` */ export declare function isThematicBreakNode(node: MarkdownNode): node is ThematicBreakNode; /** * Joins offset-bearing markdown sources while mapping a separator to the original * region between adjacent mapped sources. * * @param sources - The sources to join * @param separator - The derived text inserted between sources * @returns The joined text and every source-backed segment * * @example * ```ts * joinSources(splitLines('a\nb'), '\n') * // { text: 'a\nb', segments: [...] } * ``` */ export declare function joinSources(sources: readonly MarkdownSource[], separator: string): MarkdownSource; /** * Represents a GFM hard line break — two or more trailing spaces before a newline in * markdown source, a `br` element in HTML. */ export declare interface LineBreakNode { readonly element: 'break'; } /** * Describes the shape of a {@link LineBreakNode} — a GFM hard line-break leaf. * * @example * ```ts * import { createContract } from '@orkestrel/contract' * import { lineBreakShape } from '@src/core' * * const lineBreak = createContract(lineBreakShape) * lineBreak.is({ element: 'break' }) // true * ``` */ export declare const lineBreakShape: ObjectShape< { element: LiteralShape; }, false>; /** * Represents the located syntax bounds of one `[text](href)` link — the value the inline phase's * link locator returns for a balanced label followed by a destination. */ export declare interface LinkBounds { /** Holds the index of the label's closing `]`. */ readonly close: number; /** Holds the index one past the destination's closing `)`, exclusive. */ readonly end: number; } /** * Represents an inline link — `[text](href)`. `children` are the inline nodes of the link text. * At render, html's floor removes a refused `href` attribute and the link keeps its * text; {@link htmlToMarkdown} instead stores a refused destination as `''`. */ export declare interface LinkNode { readonly element: 'link'; /** Holds the link destination (sanitized + attribute-escaped at render). */ readonly href: string; /** Holds the inline content of the link text. */ readonly children: readonly InlineNode[]; } /** * Represents the scanned result of one `[text](href)` link — the node the inline phase's link * scanner built from {@link LinkBounds} and where the scan resumes. */ export declare interface LinkScan { /** Holds the scanned link, its text already scanned into inline children. */ readonly node: LinkNode; /** Holds the index one past the destination's closing `)`, exclusive. */ readonly end: number; } /** * Represents the result of collecting one list — the node the construct scanner built and where * the block phase resumes. */ export declare interface ListCollection { /** Holds the collected list. */ readonly node: ListNode; /** Holds the index of the first line after the list. */ readonly next: number; } /** * Represents the parsed parts of a single list-item line — the value the block phase's * list detector returns for a `-` / `*` / `+` bullet or a `1.` / `1)` ordinal line. */ export declare interface ListItemMatch { /** Holds `true` for an ordered (`1.` / `1)`) item, `false` for a bullet (`-` / `*` / `+`). */ readonly ordered: boolean; /** Holds the ordinal of an ordered item (its number); `1` for a bullet. */ readonly start: number; /** Holds the item's text after the marker. */ readonly content: string; /** Holds the leading-space indent of the marker. */ readonly indent: number; /** Holds the full marker width (indent + bullet/ordinal + the following space) — the continuation indent. */ readonly marker: number; } /** * Describes the shape of {@link ListItemMatch} — the parsed parts of a single list-item * line the block phase's list detector returns. Fully non-recursive (no * nested node fields), so every field shapes directly. * * @example * ```ts * import { createContract } from '@orkestrel/contract' * import { listItemMatchShape } from '@src/core' * * const listItemParts = createContract(listItemMatchShape) * listItemParts.is({ ordered: false, start: 1, content: 'hi', indent: 0, marker: 2 }) // true * ``` */ export declare const listItemMatchShape: ObjectShape< { ordered: BooleanShape; start: NumberShape; content: StringShape; indent: NumberShape; marker: NumberShape; }, false>; /** Represents one item of a {@link ListNode} — `children` the block content of the item (typically one paragraph, plus any nested list). */ export declare interface ListItemNode { readonly element: 'listItem'; /** Holds the block content of the list item (its text as a paragraph, plus any nested list). */ readonly children: readonly BlockNode[]; } /** * Represents a list — bulleted (`-` / `*` / `+`, `ordered: false`) or numbered (`1.` / `1)`, * `ordered: true`). `start` is the first ordinal of an ordered list (usually `1`). * Nesting is expressed by a {@link ListNode} appearing in a {@link ListItemNode}'s * `children`. */ export declare interface ListNode { readonly element: 'list'; /** Holds `true` for an ordered (numbered) list (→ `
    `); `false` for a bulleted list (→ `
      `). */ readonly ordered: boolean; /** Holds the starting ordinal of an ordered list (the first item's number); `1` for a bulleted list. */ readonly start: number; /** Holds the list's items, in order. */ readonly items: readonly ListItemNode[]; } /** * Locates an emphasis run at `start` (`*` / `_`, doubled for strong) — finds the nearest * matching closing run of the same marker + width while skipping complete nested * runs from the other marker family, and requires non-space immediately inside both * delimiters (the CommonMark flanking simplification that blocks `* x *`). Returns * the content and syntax bounds, or `undefined` when no valid closer exists (it then degrades to * a literal marker). * * @param source - The inline source text * @param start - The index of the opening marker * @param to - The exclusive end of the scan window * @returns The content and syntax bounds, or `undefined` * * @example * ```ts * locateEmphasis('*em*', 0, 4) // { strong: false, open: 1, close: 3, end: 4 } * ``` */ export declare function locateEmphasis(source: string, start: number, to: number): EmphasisBounds | undefined; /** * Locates a link `[text](href)` at `start` — the text runs to a balanced `]`, then `(` * must immediately follow and the destination runs to the matching `)` (both respect * nested delimiters + escapes). Returns the label close and syntax end, or `undefined` when the shape * does not hold (it then degrades to a literal `[`). * * @param source - The inline source text * @param start - The index of the opening `[` * @param to - The exclusive end of the scan window * @returns The label close and syntax end indices, or `undefined` * * @example * ```ts * locateLink('[text](url)', 0, 11) // { close: 5, end: 11 } * ``` */ export declare function locateLink(source: string, start: number, to: number): LinkBounds | undefined; /** * Wraps a typed {@link MarkdownDocument} AST as a stateful, parsed markdown document * with the query (`find` / `filter` / `reduce` / iteration), rewrite (`map`), fold, and * streaming operations {@link MarkdownInterface} declares. * * @remarks * - **Construction.** Given a `string`, the constructor runs {@link parseProvenance} (the * block phase then the inline phase) once, keeping the AST and a copy of the span map * that parse recorded. Given a {@link MarkdownDocument}, the document is adopted as-is * and is not re-validated — gate an untrusted value with `isMarkdownDocument` first. * - **Provenance.** {@link span} reads the region of the original constructor string a * node was produced from, and it is handle-relative: a string-constructed handle exposes * the regions of the nodes it parsed, an adopted document exposes none, and a node from * another handle reports `undefined` here whatever that handle reports. Each call * returns a fresh value. A node reports the region this handle holds for its identity, * else the region of the direct input a rewrite named for it, else `undefined`: a text * run the parse joined from adjacent scanner output reports the region enclosing its * parts, and only a rewrite output that holds no region of its own and was assembled * from separate source nodes reports `undefined`. * {@link map} carries provenance across the rewrite: an unchanged node keeps its * region, a one-source replacement takes the region of the node it replaced, and a * rebuilt parent takes its original's. * - **Immutable.** {@link map} never mutates the stored AST — it returns a new `Markdown` * instance; the document root invariant (`element: 'document'`) always holds. An * identity rewrite still returns a new handle, over the same document tree. * - **Traversal order.** {@link walk} and the `find` / `filter` / `reduce` queries built * on it walk the AST depth-first, pre-order, root-inclusive (through {@link walkNodes}); * `stream` is shallow — only the document's direct block children. * * @example Construct from a string and narrow with a guard * ```ts * import { Markdown, isHeadingNode } from '@orkestrel/markdown' * * const markdown = new Markdown('# Title\n\nA **bold** [link](https://x.dev).') * markdown.document.children[0] // { element: 'heading', level: 1, children: [...] } * * const heading = markdown.find(isHeadingNode) // HeadingNode | undefined, narrowed * if (heading !== undefined) heading.level // number — narrowed to HeadingNode * ``` */ export declare class Markdown implements MarkdownInterface { #private; constructor(input: string | MarkdownDocument); /** Holds the stored {@link MarkdownDocument} AST root. */ get document(): MarkdownDocument; /** * Reads the region of the original markdown string a node of this handle's tree was * produced from. * * @param node - The node whose provenance to read * @returns A fresh {@link MarkdownSpan}, or `undefined` when this handle holds no * region for the node * * @example * ```ts * const source = '# Title\n\npara' * const markdown = new Markdown(source) * const heading = markdown.find(isHeadingNode) * const span = heading && markdown.span(heading) * span && source.slice(span.start, span.end) // '# Title' * ``` */ span(node: MarkdownNode): MarkdownSpan | undefined; /** * Returns the deep traversal — a lazy, depth-first, pre-order, root-inclusive generator * over every {@link MarkdownNode} in the document. `find` / `filter` / `reduce` * all iterate this single traversal. * * @example * ```ts * for (const node of markdown.walk()) { * // every node, depth-first, pre-order, root-inclusive * } * * // also consumable by for-await - JS accepts a sync iterable in for-await * for await (const node of markdown.walk()) { * // same sequence, no separate async iterator needed * } * ``` */ walk(): Generator; find(guard: (node: MarkdownNode) => node is T): T | undefined; find(predicate: (node: MarkdownNode) => boolean): MarkdownNode | undefined; filter(guard: (node: MarkdownNode) => node is T): readonly T[]; filter(predicate: (node: MarkdownNode) => boolean): readonly MarkdownNode[]; /** * Rewrites the AST bottom-up (copy-on-write) and returns a new {@link Markdown}, * carrying each output node's provenance across the rewrite. A rewrite that returns * its node unchanged shares that subtree instead of copying it, so an identity * rewrite copies no node and still returns a new handle. * * @param rewrite - The bottom-up node rewrite * @returns A new handle over the rewritten document */ map(rewrite: MarkdownRewriteHandler): MarkdownInterface; /** Folds the AST depth-first, pre-order into an accumulator. */ reduce(callback: (accumulator: T, node: MarkdownNode) => T, initial: T): T; /** Runs a total catamorphism over the document using a {@link MarkdownHandlerMap} table. */ fold(handlers: MarkdownHandlerMap): T; /** * Returns a web-standard {@link ReadableStream} over the document's top-level block nodes * (shallow, source order) — a fresh, pull-based source per call: one block is * enqueued per `pull`, so a slow reader's backpressure is respected. Cancellable, * async-iterable wherever the platform supports it (Node, Deno), and pipeable * through any {@link TransformStream} / {@link WritableStream}. * * @example * ```ts * // universal - works in every ReadableStream-supporting environment * const reader = markdown.stream().getReader() * for (let result = await reader.read(); !result.done; result = await reader.read()) { * console.log(result.value) // one BlockNode * } * * // Node / Deno / Firefox support async iteration of ReadableStream natively; * // other environments use the reader loop shown earlier. * for await (const block of markdown.stream()) { * console.log(block) * } * ``` */ stream(): ReadableStream; } /** * Represents one projected table cell — the inline content and alignment of a `th` / `td`. * * @remarks * {@link htmlToMarkdown} derives a projected table's header row from the source HTML * structure rather than from any flag a cell carries: the header is the first * `th`-bearing row, and every row after it becomes a body row. */ export declare interface MarkdownCell { /** Holds the alignment the cell's `align` attribute declared; `undefined` when it declared none. */ readonly align: TableAlign | undefined; /** Holds the cell's inline content — a table cell is inline-only, so block content flattens to text. */ readonly inlines: readonly InlineNode[]; } /** * Pairs a rewritten value with the input node each rewritten node was produced from — * what `rewriteDocument` returns, so provenance survives a rewrite instead of ending at * it. `T` is the rewritten value: the document for a whole-document rewrite. * * @remarks * `derivations` is keyed by the nodes of the output, and each entry names the direct * input the rewrite drew that output from. {@link MarkdownInterface.map} resolves each * output node against the source handle's own spans in a fixed order, and follows no * second derivation edge: * * - the output identity's own span in the source handle wins, whatever the map says, * so an identity the rewrite reused — one node returned for several inputs, or a * node the handler moved elsewhere in the tree — keeps the region it already had; * - otherwise the span of the direct input the entry names, where that input has one; * - otherwise none. Where the output identity holds no region of its own, a node mapped * to `undefined`, a node whose direct input has no span, and a node with no entry at * all each report `undefined`. Own-region resolution runs first, so an identity that * does hold a region keeps it in every one of those cases. * * An absent entry does not by itself mean the output node kept its identity. A node * the handler synthesized beneath its replacement is absent too, and it reports no * span because the rewrite named no input for it. */ export declare type MarkdownDerivation = readonly [ value: T, derivations: ReadonlyMap ]; /** * Represents the root of a parsed markdown AST — the ordered block children of the whole * document. The value {@link MarkdownInterface.document} holds. */ export declare interface MarkdownDocument { readonly element: 'document'; /** Holds the document's top-level block nodes, in source order. */ readonly children: readonly BlockNode[]; } /** * Represents a fold handler for one AST element — receives the node and its children * already folded to `T`, and produces the node's own `T`. The building block of a * {@link MarkdownHandlerMap} catamorphism table. */ export declare type MarkdownHandler = (node: TNode, children: readonly T[]) => T; /** * Represents the total catamorphism table for {@link MarkdownInterface.fold} — one * {@link MarkdownHandler} per AST element, keyed by its `element` discriminant. Every * key is required: a fold is total over the AST, so there is no element it can skip. */ export declare interface MarkdownHandlerMap { /** Folds a {@link MarkdownDocument} root from its already-folded block children. */ readonly document: MarkdownHandler; /** Folds a {@link HeadingNode} from its already-folded inline children. */ readonly heading: MarkdownHandler; /** Folds a {@link ParagraphNode} from its already-folded inline children. */ readonly paragraph: MarkdownHandler; /** Folds a {@link ThematicBreakNode} (leaf — always called with an empty children list). */ readonly thematicBreak: MarkdownHandler; /** Folds a {@link BlockquoteNode} from its already-folded block children. */ readonly blockquote: MarkdownHandler; /** Folds a {@link CodeBlockNode} (leaf — always called with an empty children list). */ readonly codeBlock: MarkdownHandler; /** Folds a {@link ListNode} from its already-folded item children. */ readonly list: MarkdownHandler; /** Folds a {@link ListItemNode} from its already-folded block children. */ readonly listItem: MarkdownHandler; /** * Folds a {@link TableNode} from its cells' already-folded inline nodes, flattened * to one folded `T` per inline node — header cells first (column order), then body * rows' cells (row order, then column order). It is not a leaf: recover cell * boundaries from `node.header[c].length` / `node.rows[r][c].length` against the * flat `children` list. */ readonly table: MarkdownHandler; /** Folds a {@link TextNode} (leaf — always called with an empty children list). */ readonly text: MarkdownHandler; /** Folds an {@link EmphasisNode} from its already-folded inline children. */ readonly emphasis: MarkdownHandler; /** Folds a {@link CodeSpanNode} (leaf — always called with an empty children list). */ readonly codeSpan: MarkdownHandler; /** Folds a {@link LineBreakNode} (leaf — always called with an empty children list). */ readonly break: MarkdownHandler; /** Folds a {@link LinkNode} from its already-folded inline children. */ readonly link: MarkdownHandler; /** Folds an {@link ImageNode} from its already-folded alternative content. */ readonly image: MarkdownHandler; } /** * Represents a stateful, parsed markdown document: the typed {@link MarkdownDocument} AST plus * the query, rewrite, and fold operations over it. * * @remarks * - **Immutable.** {@link MarkdownInterface.map} never mutates the stored AST — it * returns a new {@link MarkdownInterface} instance; the document root invariant * (`element: 'document'`) always holds. * - **Traversal order.** `walk` / `find` / `filter` / `reduce` walk the AST * depth-first, pre-order, root-inclusive; `stream` is shallow — only the * document's direct block children. * - **`stream`.** Returns a web-standard {@link ReadableStream} over the top-level * blocks — a fresh, pull-based source per call: exactly one block is enqueued per * `pull`, so a slow consumer's backpressure is respected and no work happens ahead * of demand. Cancellable through the returned stream's own `cancel()`, async-iterable * wherever the platform supports it (Node, Deno, and browsers that ship the * proposal), and pipeable through any {@link TransformStream} / {@link WritableStream}. * - **The surface.** `document` (the AST root), `walk` (the deep traversal), `find` / * `filter` / `reduce` (queries built on `walk`), `span` (the region of the original * markdown a node was parsed from), `map` (the bottom-up rewrite), `fold` (the * total catamorphism), and `stream` (the shallow, backpressured top-level source). */ export declare interface MarkdownInterface { /** Holds the stored {@link MarkdownDocument} AST root. */ readonly document: MarkdownDocument; /** * Returns the deep traversal — a lazy, depth-first, pre-order, root-inclusive * {@link Generator} over every {@link MarkdownNode} in the document. The sync * `for (const node of markdown.walk())` surface is also consumable by * `for await (const node of markdown.walk())` (JavaScript accepts a sync * iterable in a `for await`), so async pipelines need no separate iterator. * Contrast with {@link stream}: `walk` is deep, every-node, and sync; `stream` * is shallow (top-level blocks only) and backpressure-respecting. */ walk(): Generator; /** * Finds the first node (depth-first, pre-order) narrowed by a type guard, and returns * `undefined` when no node matches; a second overload takes a plain predicate. */ find(guard: (node: MarkdownNode) => node is T): T | undefined; /** Finds the first node (depth-first, pre-order) matching a predicate. */ find(predicate: (node: MarkdownNode) => boolean): MarkdownNode | undefined; /** * Collects every node (depth-first, pre-order) narrowed by a type guard; a second * overload takes a plain predicate. */ filter(guard: (node: MarkdownNode) => node is T): readonly T[]; /** Collects every node (depth-first, pre-order) matching a predicate. */ filter(predicate: (node: MarkdownNode) => boolean): readonly MarkdownNode[]; /** * Reads the region of the original markdown string a node was produced from. * * @param node - A node of this handle's document. * @returns A fresh {@link MarkdownSpan}, or `undefined` when this handle holds no * region for the node. * * @remarks * Provenance is per handle and per node identity, so a node reports a region only * where this handle holds coordinates for it. A handle constructed from an adopted * {@link MarkdownDocument} reports `undefined` for every node: it parsed no string, * so no coordinates exist to report. A text run the parse joined from adjacent * scanner output reports the region enclosing its parts rather than `undefined`; * only a rewrite output that holds no region of its own and was assembled from * separate source nodes reports `undefined`. The region a node does report is the * original source it was produced from, which can include syntax its value drops * and characters that normalization removed. Each call returns a fresh value rather * than the stored one. */ span(node: MarkdownNode): MarkdownSpan | undefined; /** Rewrites the AST bottom-up (copy-on-write) and returns a new {@link MarkdownInterface}. */ map(rewrite: MarkdownRewriteHandler): MarkdownInterface; /** Folds the AST depth-first, pre-order into an accumulator through a reducer callback. */ reduce(callback: (accumulator: T, node: MarkdownNode) => T, initial: T): T; /** Runs a total catamorphism over the document using a {@link MarkdownHandlerMap} table. */ fold(handlers: MarkdownHandlerMap): T; /** * Returns a web-standard {@link ReadableStream} over the document's top-level block nodes * (shallow, source order) — a lazy, pull-based, backpressure-respecting source. A * fresh, independently-replayable stream every call; never mutates the document. */ stream(): ReadableStream; } /** * Represents any node in a markdown AST — the {@link MarkdownDocument} root, a {@link BlockNode}, * a {@link ListItemNode}, or an {@link InlineNode}. The exhaustive set every * projection's `switch` covers. */ export declare type MarkdownNode = MarkdownDocument | BlockNode | ListItemNode | InlineNode; /** * Pairs a parsed document with the {@link MarkdownSpan} of each of its nodes — what * `parseProvenance` returns, and what `parseDocument` projects the document out of. * * @remarks * `spans` is keyed by node identity, so it addresses the nodes of that document and * no other. A node the parse merged from adjacent scanner output — the text run * `coalesceText` joins — is present and carries the region enclosing its parts, from * the first part's `start` to the last part's `end`, which can include original text * lying between them. Absence means the parse recorded no region for the node, not * that the node was assembled from more than one region. Destructure it as * `const [document, spans] = parseProvenance(markdown)`. */ export declare type MarkdownParseResult = readonly [ document: MarkdownDocument, spans: ReadonlyMap ]; /** * Represents what one HTML node projects to on the way to markdown — the fold value * `htmlToMarkdown` carries up the AST. * * @remarks * A node projects to several things at once because markdown decides late what a * given HTML subtree becomes: a `td`'s content is inline in a table and a paragraph * outside one, and a `code` body is a code span in prose and a verbatim code block * under a `pre`. Rather than guess, each node reports every view its ancestors could * need, and the ancestor that knows the context takes the one it wants. * * - `blocks` / `inlines` — the block and inline views. They are exclusive by * construction: as soon as a node contributes a block, the inline runs around it * are wrapped into paragraphs, so `blocks` being non-empty means `inlines` is * empty and no interleaving is ever lost. * - `text` — the raw, uncollapsed, unescaped subtree text a code span and a * `pre > code` body need verbatim. An `UNSAFE_ELEMENTS` subtree contributes none * of it, so a script body can never resurface as prose. * - `cells` / `rows` — table structure in flight. A cell travels up to its `tr` and a * row up to its `table`, passing through the `thead` / `tbody` wrappers between * them untouched; whatever never reaches a table degrades to paragraphs. */ export declare interface MarkdownProjection { /** Holds the node's block content, with any surrounding inline runs already wrapped into paragraphs. */ readonly blocks: readonly BlockNode[]; /** Holds the node's inline content; empty whenever `blocks` is not. */ readonly inlines: readonly InlineNode[]; /** Holds the raw subtree text, whitespace uncollapsed and escapes unresolved. */ readonly text: string; /** Holds the cells this node contributes to an enclosing row. */ readonly cells: readonly MarkdownCell[]; /** Holds the rows this node contributes to an enclosing table — each its cells, in column order. */ readonly rows: ReadonlyArray; } /** * Represents a copy-on-write node rewrite applied bottom-up by {@link MarkdownInterface.map} — * receives one node (its own children already rewritten) and returns its * replacement (the same node, unchanged, or a new node). */ export declare type MarkdownRewriteHandler = (node: MarkdownNode) => MarkdownNode; /** * Maps one run of a {@link MarkdownSource} back to the region of the original * markdown string it was taken from. * * @remarks * `offset` addresses {@link MarkdownSource.text}; `start` and `end` address the * original string. The run's original length derives from `end - start` rather than * being stored beside them, so no length member exists to drift. The run's derived * extent ends where the next segment's `offset` begins, so a run may cover more of the * original than it holds derived: the separator run `joinSources` records over a * normalized `\r\n` terminator is one derived code unit over a two-unit original * region. * * `projectSpan` resolves a derived position `p` against that shape by the following * rules rather than by a single affine relation: * * - strictly inside the run, `p` projects to `start + (p - offset)`; * - at the run's derived end, `p` projects to `end`, so the boundary claims the run's * whole original region instead of the prefix an affine step would reach — which is * how the one-unit `\r\n` separator run above reports its two-unit region; * - a zero-width `p` that coincides with a later segment's `offset` resolves through the * last segment whose `offset` equals `p`, skipping every earlier segment at that * position whatever its extent, so a discontinuous abutment reports that final run's * `start` rather than the earlier run's `end`. * * The mapping is therefore affine strictly inside a run and clamped at its end. */ export declare interface MarkdownSegment { /** Holds the first code unit of the run inside {@link MarkdownSource.text}. */ readonly offset: number; /** Holds the first code unit of the original-string region the run was produced from, inclusive. */ readonly start: number; /** Holds the code unit one past that region's last, exclusive. */ readonly end: number; } /** * Pairs a piece of derived markdown text with the runs mapping it back to the * original string — what `splitLines` returns per line, so every phase downstream of * it keeps original coordinates instead of reconstructing them from node values. * * @remarks * `text` is the line a parser reads: its terminator, `>` quote marker, or leading * indent already removed. `segments` run in ascending `offset` order, one run per * contiguous stretch of the original; a piece assembled from separate stretches * carries one segment per stretch. * * The runs need not cover every position of `text`. `joinSources` records a segment * for its separator only where the two sides leave a gap in the original, so joining * two abutting regions with a separator leaves that separator's derived position * uncovered. `projectSpan` resolves a range's two boundaries against the runs * independently: it reports `undefined` when either boundary lands in an uncovered * position, and it bridges an uncovered interior when both boundaries resolve. Test * coverage with `projectSpan` rather than assuming it. */ export declare interface MarkdownSource { /** Holds the derived text a parser reads. */ readonly text: string; /** Holds the runs mapping `text` back to the original string, in ascending `offset` order. */ readonly segments: readonly MarkdownSegment[]; } /** * Addresses a half-open region of the original markdown string, in UTF-16 code units — * `start` inclusive, `end` exclusive. The provenance a parse records for a node and * {@link MarkdownInterface.span} reads back. * * @remarks * The coordinates address the string the handle was constructed from, never the line * text a later phase walks, so `markdown.slice(span.start, span.end)` returns the * original source region the node was produced from. That region is not the node's * value: it carries the syntax the value drops, such as a `\` escape marker, and the * characters that normalization removed, such as a trailing space the paragraph phase * trimmed. The text node of `'a \nb'` has the `value` `a\nb` and reports * `{ start: 0, end: 4 }`, which slices the whole `a \nb`. Read a value off the node * and a region off the source; never derive either from the other. The region's length * is `end - start`; no length member exists to drift from the two offsets. */ export declare interface MarkdownSpan { /** Holds the first code unit of the region, inclusive. */ readonly start: number; /** Holds the code unit one past the region's last, exclusive. */ readonly end: number; } /** * Projects a {@link MarkdownNode} into an unsanitized {@link HTMLDocument}. * * @remarks * The projection is pure and iterative. Text and attribute values remain literal for * `@orkestrel/html` to encode, and URL values remain unsanitized so callers can choose * their own HTML policy. Projected HTML element depth, including generated `pre > code` * and table scaffolding, never exceeds {@link MAX_DEPTH}. At the cap a node carrying a * string `value` degrades to a text node and a structural node contributes nothing. * * @param node - The markdown document or bare node to project * @returns An unsanitized HTML document wrapping the projected node or nodes * * @example * ```ts * markdownToHTML({ element: 'text', value: 'a & b' }) * // { category: 'document', children: [{ category: 'text', value: 'a & b' }] } * ``` */ export declare function markdownToHTML(node: MarkdownNode): HTMLDocument; /** * Caps the recursion depth the parse pipeline (`parseDocument` and its * `parsers.ts` helpers), the `helpers.ts` traversal / projection functions * (`markdownToHTML`, `renderMarkdown`, `walkNodes`, `foldNode`, `rewriteDocument`), * and the `compilers.ts` renderer (`renderHTML`) honor before degrading, at 64. It bounds blockquote nesting, inline * nesting (emphasis / links), and traversal / projection recursion so pathological * or hostile input cannot exhaust the call stack. {@link htmlToMarkdown} is the * inherited exception: its fold and depth cap belong to `@orkestrel/html`. */ export declare const MAX_DEPTH = 64; /** * Combines the projections of one node's children into the projection of that node — * the single place inline runs become paragraphs, so no ancestor has to decide it * twice. * * @remarks * A child is either inline or block, never both, so merging preserves source order * exactly: an inline run is held pending until a block arrives, then written out as a * paragraph before it. That is what keeps `
      lead

      a

      ` two paragraphs in * the order they were written rather than two lists that lost their interleaving. A * pending run carrying no text is dropped rather than becoming a blank paragraph. * Direct cells become one row before a later row, while cells/rows before a block * materialize as paragraphs at that exact source position. * * @param children - The children's projections, in source order * @returns Their combined projection * * @example * ```ts * mergeProjections([ * createProjection({ inlines: [{ element: 'text', value: 'a' }], text: 'a' }), * createProjection({ blocks: [{ element: 'thematicBreak' }] }), * ]).blocks * // [{ element: 'paragraph', children: [...] }, { element: 'thematicBreak' }] * ``` */ export declare function mergeProjections(children: readonly MarkdownProjection[]): MarkdownProjection; /** * Reduces an inline run to the shape markdown can actually write back: adjacent text * coalesced, empty text dropped, and every hard break either kept as a real line * ending or spent as a space. * * @remarks * A hard break is ` \n` in markdown source, so it survives a re-parse only between * two lines of content and only with no whitespace touching it: a leading or trailing * break has no line to end, a run of breaks reads as one blank line (which would end * the paragraph), and a space beside one is eaten by the parser's line trimming. Where * a break cannot be written at all — a heading and a table cell are one line each — it * becomes the space it stood for. * * @param nodes - The inline run to normalize * @param breaks - If `true`, keeps each hard break as a real line ending; if `false`, spends * every break as the space it stood for, as a heading or a table cell requires * @returns The normalized run * * @example * ```ts * normalizeInlines([{ element: 'break' }, { element: 'text', value: 'a' }], true) * // [{ element: 'text', value: 'a' }] - a leading break has no line to end * ``` */ export declare function normalizeInlines(nodes: readonly InlineNode[], breaks: boolean): readonly InlineNode[]; /** * Normalizes one paragraph line while retaining the full source run consumed by a * trailing-space hard break. * * @param source - The offset-bearing paragraph line * @param breaks - If `true`, preserves a trailing run of at least two spaces as the * scanner's two-space hard-break syntax; if `false`, trims the line normally * @returns The normalized line and its original-string segments * * @example * ```ts * normalizeParagraphLine(splitLines('text \nnext')[0], true).text // 'text ' * ``` */ export declare function normalizeParagraphLine(source: MarkdownSource, breaks: boolean): MarkdownSource; /** Represents a paragraph — a run of non-blank lines that is not another block; `children` its inline content. */ export declare interface ParagraphNode { readonly element: 'paragraph'; /** Holds the inline content of the paragraph. */ readonly children: readonly InlineNode[]; } /** * Parses a run of markdown lines into a block AST, recursing into nested * blockquotes, list items, and depth-capped degrade paragraphs. * * @param lines - The markdown lines to parse. * @param depth - The current recursion depth (blockquotes/lists increment it). * @param spans - The optional operation-owned node span recorder. * @param end - The original-source end of this line run, including a removed terminator. * @returns The parsed block nodes. * * @example * ```ts * parseBlocks(splitLines('# Hi'), 0) // [{ element: 'heading', level: 1, children: [...] }] * ``` */ export declare function parseBlocks(lines: readonly MarkdownSource[], depth: number, spans?: Map, end?: number): readonly BlockNode[]; /** * Parses a markdown string into a typed {@link MarkdownDocument} AST through the * block phase — the document half of what {@link parseProvenance} returns. Malformed * markdown degrades to literal text, so the parse never throws. * * @param markdown - The markdown source to parse. * @returns The parsed document. * * @example * ```ts * parseDocument('# Hi') // { element: 'document', children: [{ element: 'heading', ... }] } * ``` */ export declare function parseDocument(markdown: string): MarkdownDocument; /** * Parses inline markdown text (emphasis, code spans, links, images, and hard * breaks) into inline AST nodes, coalescing adjacent text runs and reading no block * structure. Malformed markdown degrades to literal text, so the parse never throws. * * @param text - The inline markdown text to parse. * @returns The parsed inline nodes. * * @example * ```ts * parseInline('a *b*') // [{ element: 'text', value: 'a ' }, { element: 'emphasis', ... }] * ``` */ export declare function parseInline(text: string): readonly InlineNode[]; /** * Parses a markdown string into a document and its original-source spans. Malformed * markdown degrades to literal text, so the parse never throws. * * @param markdown - The markdown source to parse. * @returns The parsed document and its node-identity span map. * * @example * ```ts * const [document, spans] = parseProvenance('# Hi') * spans.get(document) // { start: 0, end: 4 } * ``` */ export declare function parseProvenance(markdown: string): MarkdownParseResult; /** * Projects one HTML leaf — a text node, a comment, or a doctype — to its * {@link MarkdownProjection}. * * @remarks * Text collapses each whitespace run to one space, which is both what HTML means by it * and all markdown can write back; the raw value travels on in `text` for the two * places that need it verbatim, a code span and a `pre > code` body. A comment and a * doctype carry nothing into markdown and project to nothing. * * @param leaf - The leaf node to project * @returns Its projection * * @example * ```ts * projectHTMLLeaf({ category: 'text', value: 'a\n b' }).inlines * // [{ element: 'text', value: 'a b' }] * ``` */ export declare function projectHTMLLeaf(leaf: CommentNode | DoctypeNode | TextNode_2): MarkdownProjection; /** * Projects one HTML container — the document root or an element — from its children's * already-computed projections. The element mapping, and the only place that decides * what an HTML tag becomes in markdown. * * @remarks * `h1`-`h6` become headings; `p` a paragraph; `strong` / `b` and `em` / `i` emphasis; * `code` a code span; `pre` a code block, verbatim through a first `code` element child * (its `language-` class naming the language) and through `renderText` otherwise; `a` * and `img` a link and an image, each destination re-sanitized; `br` and `hr` a hard * break and a thematic break; `blockquote` and `li` their block content, with bare * inline runs wrapped in paragraphs; `ul` / `ol` a list, ordered from the tag and * numbered from `start`; `th` / `td`, `tr`, and `table` a GFM table whose column * alignment comes from each header-position cell's `align` attribute. Every * `UNSAFE_ELEMENTS` subtree contributes nothing at all, text included. Every other * element unwraps to its children, so wrapper soup melts while its content keeps its * shape — `

      a

      b

      ` stays two paragraphs. * * Three mappings read their own node rather than only their children's projections, * because HTML puts the fact in a position rather than in a value: a `pre` takes its * body from its `code` child's raw text, and a list takes one item per `li` child — so * an empty `
    • ` is still an item, while the whitespace between two of them is not. * A `tr` accepts only its own direct cells, and a table derives the first `th`-bearing * row from its own source structure. * * @param node - The document root or element to project * @param children - Its children's projections, in source order * @returns Its projection * * @example * ```ts * projectHTMLNode({ category: 'element', name: 'hr', attributes: [], children: [] }, []).blocks * // [{ element: 'thematicBreak' }] * ``` */ export declare function projectHTMLNode(node: ElementNode | HTMLDocument, children: readonly MarkdownProjection[]): MarkdownProjection; /** * Reads a projection as block content — the view a document, a blockquote, and a list * item each need. * * @remarks * A bare inline run becomes one paragraph, and a run carrying no text becomes nothing * at all, because a blank paragraph is unwritable in markdown. A cell or a row that * never reached a table is unwrapped here rather than dropped: a stray `` is still * someone's content. * * @param projection - The projection to read * @returns Its block content * * @example * ```ts * projectionToBlocks(createProjection({ inlines: [{ element: 'text', value: 'a' }], text: 'a' })) * // [{ element: 'paragraph', children: [{ element: 'text', value: 'a' }] }] * ``` */ export declare function projectionToBlocks(projection: MarkdownProjection): readonly BlockNode[]; /** * Reads a projection as inline content — the view a link, an emphasis, and a table cell * each need. * * @remarks * Inline content passes through as itself. Block content cannot: markdown has no way to * put a paragraph inside a table cell, so it flattens to one text node of its own words, * joined and whitespace-collapsed. Content that carries no text flattens to nothing * rather than to an empty text node, which is a shape the parser never produces. * * @param projection - The projection to read * @returns Its inline content * * @example * ```ts * projectionToInlines(createProjection({ inlines: [{ element: 'break' }] })) * // [{ element: 'break' }] * ``` */ export declare function projectionToInlines(projection: MarkdownProjection): readonly InlineNode[]; /** * Projects a derived text range through its segments to a half-open region of the * original markdown string. * * @param source - The offset-bearing source carrying the range * @param from - The inclusive derived-text boundary * @param to - The exclusive derived-text boundary * @returns The original-string span, or `undefined` when either boundary is unmapped * * @example * ```ts * projectSpan({ text: 'a', segments: [{ offset: 0, start: 4, end: 5 }] }, 0, 1) * // { start: 4, end: 5 } * ``` */ export declare function projectSpan(source: MarkdownSource, from: number, to: number): MarkdownSpan | undefined; /** * Renders a {@link MarkdownNode} to sanitized canonical HTML. * * @remarks * Sanitization is unconditional: the function takes one argument and declares no * options, so no call shape opts out of it. * * Markdown widens `@orkestrel/html`'s attribute floor by exactly `src`, because image * syntax is meaningless without its source. `src` is still a URL attribute, so the * floor refuses `javascript:`, `data:`, `vbscript:`, and `file:` values. A stricter * consumer can compose {@link markdownToHTML} with `@orkestrel/html`'s `HTML` class * directly. * * @param node - The markdown document or bare node to render * @returns Sanitized canonical HTML * * @example * ```ts * renderHTML({ element: 'paragraph', children: [{ element: 'text', value: 'a & b' }] }) * // '

      a & b

      ' * ``` */ export declare function renderHTML(node: MarkdownNode): string; /** * Renders a {@link MarkdownNode} to its canonical markdown source — the inverse * projection of `renderHTML`. It is the serializer a `parse(renderMarkdown(doc))` * round-trip is built on. Canonical forms: `*` / `**` emphasis at even emphasis * nesting depths and `_` / `__` at odd depths, `- ` bullets, `N. ` sequential * ordinals (from the list's `start`), `---` thematic breaks, fenced code blocks * (backtick run widened past any 3+ backtick run inside the body), ATX headings, * `> `-prefixed blockquote lines, GFM tables (1-space-padded cells, a backslash * before each literal pipe, an alignment delimiter row), `[text](href)` links, * `![alt](src)` images, and two-space hard breaks. A `text` node's literal content is backslash-escaped * wherever it would otherwise re-parse as markup, so parsing the rendered source * returns the node it was rendered from. * * @remarks * Total: never throws. At {@link MAX_DEPTH} a value-bearing node degrades to its * escaped `value`; any other node degrades to `''`. Blocks are joined by exactly one * blank line; a document with zero blocks renders `''`. * * @param node - The AST node to render (a full document, or any sub-node) * @returns The canonical markdown source * * @example * ```ts * renderMarkdown({ element: 'document', children: [ * { element: 'heading', level: 2, children: [{ element: 'text', value: 'Hi' }] }, * ] }) * // '## Hi' * ``` */ export declare function renderMarkdown(node: MarkdownNode): string; /** * Rewrites a {@link MarkdownDocument} bottom-up (copy-on-write) — each node's children * are rewritten first (post-order), then `rewrite` is applied to the node itself; the * document root is never passed to `rewrite` (the `element: 'document'` invariant * always holds). A table's inline cells and a list's items are rewritten too. * * @remarks * Never mutates `document`. An unchanged subtree keeps its input identity. A parent * is rebuilt only when an accepted child changes, and the returned derivation map * associates each rebuilt output with its input node. When `rewrite` returns a node * whose `element` does not fit the slot it was called for (a block slot handed a * non-{@link BlockNode}, an inline slot handed a non-{@link InlineNode}, a list-item * slot handed a non-`listItem`), the ill-fitting result is discarded and the accepted * input child is reused — `rewriteDocument` stays total and never produces a * structurally invalid document. * * Descent is capped at {@link MAX_DEPTH}, the same cap {@link walkNodes} and * {@link foldNode} observe: at `depth >= MAX_DEPTH` the subtree is passed through * unchanged (by reference, not rebuilt, and `rewrite` is not invoked on it) instead of * recursing further, so a pathologically deep adopted document cannot exhaust the * call stack. {@link MarkdownInterface.map} inherits this cap since it delegates here. * * @param document - The document AST to rewrite * @param rewrite - The bottom-up {@link MarkdownRewriteHandler} * @returns The rewritten document and its output-to-input derivations * * @example * ```ts * const [rewritten, derivations] = rewriteDocument(document, (node) => * node.element === 'text' ? { element: 'text', value: node.value.toUpperCase() } : node, * ) * ``` */ export declare function rewriteDocument(document: MarkdownDocument, rewrite: MarkdownRewriteHandler): MarkdownDerivation; /** * Scans an inline code span at `start` (a `` ` ``-run … a matching `` ` ``-run of the * same length, the CommonMark rule that lets a span contain backticks). Returns the * span's literal text + end index, or `undefined` when no matching closer exists (it * then degrades to literal backticks). * * @param source - The inline source text * @param start - The index of the opening backtick * @param to - The exclusive end of the scan window * @returns The span text + end index, or `undefined` * * @example * ```ts * scanCode('`code`', 0, 6) // { value: 'code', end: 6 } * ``` */ export declare function scanCode(source: string, start: number, to: number): CodeSpanMatch | undefined; /** * Scans an emphasis run at `start` (`*` / `_`, doubled for strong) — finds the nearest * matching closing run of the same marker + width while skipping complete nested runs * from the other marker family, and requires non-space immediately inside both * delimiters (the CommonMark flanking simplification that blocks `* x *`) through * {@link locateEmphasis}, and returns the parsed node and end index. Returns * `undefined` when no valid closer exists (it then degrades to a literal marker). * * @param source - The inline source text * @param start - The index of the opening marker * @param to - The exclusive end of the scan window * @param depth - The current inline-recursion depth, forwarded to {@link scanInline} * incremented by one for the run's children. At {@link MAX_DEPTH} that recursion * emits the content as a single literal text node instead of scanning it. * @returns The parsed emphasis and end index, or `undefined` when no closer exists * * @example * ```ts * scanEmphasis('*em*', 0, 4) * // { node: { element: 'emphasis', strong: false, children: [{ element: 'text', value: 'em' }] }, end: 4 } * ``` */ export declare function scanEmphasis(source: string, start: number, to: number, depth?: number): EmphasisScan | undefined; /** * Scans the window `[from, to)` of `source` into inline nodes — the single recursive * engine the inline phase runs on (emphasis, link text, and image alternative * content recurse through it). Linear: * each character is consumed once; a failed construct emits its opening character as * text and advances by one, so there is no re-scan (no ReDoS). * * @param source - The inline source text * @param from - The inclusive start of the scan window * @param to - The exclusive end of the scan window * @param depth - The current inline-recursion depth (defaults to 0 at the entry point); * incremented by one on every recursive descent {@link scanInlineSource} makes into * itself for a link's text, an image's alternative content, or an emphasis run's * children. At {@link MAX_DEPTH} the window is never scanned for markup — it emits as * a single literal text node — so pathological nesting (`[[[[…`, `****…`) cannot * exhaust the call stack. * @returns The parsed inline nodes (not yet coalesced) * * @example * ```ts * scanInline('hi *there*', 0, 10) // [{ element: 'text', value: 'hi ' }, { element: 'emphasis', ... }] * ``` */ export declare function scanInline(source: string, from: number, to: number, depth?: number): readonly InlineNode[]; /** * Scans an offset-bearing inline window with the same engine as {@link scanInline} * and records each emitted node against the original markdown string. * * @param source - The offset-bearing inline source * @param from - The inclusive start of the scan window * @param to - The exclusive end of the scan window * @param spans - The operation-owned node span recorder * @param depth - The current inline-recursion depth, incremented by one on every * recursive descent this function makes into itself for a link's text, an image's * alternative content, or an emphasis run's children * @returns The parsed inline nodes before adjacent text coalescing * * @example * ```ts * scanInlineSource( * { text: 'hi *there*', segments: [{ offset: 0, start: 0, end: 10 }] }, * 0, * 10, * new Map(), * ) * // [{ element: 'text', value: 'hi ' }, { element: 'emphasis', ... }] * ``` */ export declare function scanInlineSource(source: MarkdownSource, from: number, to: number, spans: Map, depth?: number): readonly InlineNode[]; /** * Scans a link `[text](href)` at `start` — the text runs to a balanced `]`, then `(` * must immediately follow and the destination runs to the matching `)` (both respect * nested delimiters + escapes) through {@link locateLink}, and returns the parsed node * and end index. Returns `undefined` when the shape does not hold (it then degrades to * a literal `[`). * * @param source - The inline source text * @param start - The index of the opening `[` * @param to - The exclusive end of the scan window * @param depth - The current inline-recursion depth, forwarded to {@link scanInline} * incremented by one for the link text's children. At {@link MAX_DEPTH} that * recursion emits the text as a single literal text node instead of scanning it. * @returns The parsed link and end index, or `undefined` when the shape does not hold * * @example * ```ts * scanLink('[text](url)', 0, 11) * // { node: { element: 'link', href: 'url', children: [{ element: 'text', value: 'text' }] }, end: 11 } * ``` */ export declare function scanLink(source: string, start: number, to: number, depth?: number): LinkScan | undefined; /** * Slices derived markdown text and narrows each intersecting source segment to the * same text-relative range. * * @param source - The offset-bearing source to slice * @param from - The inclusive text offset * @param to - The exclusive text offset * @returns The sliced text and its narrowed original-string segments * * @example * ```ts * sliceSource({ text: 'abc', segments: [{ offset: 0, start: 4, end: 7 }] }, 1, 3) * // { text: 'bc', segments: [{ offset: 0, start: 5, end: 7 }] } * ``` */ export declare function sliceSource(source: MarkdownSource, from: number, to: number): MarkdownSource; /** * Splits a markdown document into offset-bearing lines while normalizing CRLF and * bare CR terminators at the line boundary. A single trailing terminator does not * yield a final empty line. * * @param markdown - The raw markdown source * @returns The document's lines with their original-string coordinates * * @example * ```ts * splitLines('a\r\nb') // [{ text: 'a', segments: [{ offset: 0, start: 0, end: 1 }] }, ...] * ``` */ export declare function splitLines(markdown: string): readonly MarkdownSource[]; /** * Splits one GFM table row into its cell strings — outer pipes are optional, a pipe * escaped by a leading backslash inside a cell is not a separator (it becomes a literal * pipe character), and the empty leading / trailing cell an outer pipe produces is * dropped. Derives the string form from {@link splitTableSources}, which owns the * escaped-pipe splitting rule. * * @param row - The raw table row line * @returns The row's cells, in column order * * @example * ```ts * splitTableRow('|a|b|') // ['a', 'b'] * ``` */ export declare function splitTableRow(row: string): readonly string[]; /** * Splits an offset-bearing GFM table row into offset-bearing cells, retaining the * complete source spelling of an escaped pipe while exposing its literal value. * * @param row - The offset-bearing table row * @returns The row's cells with their original-string coordinates * * @example * ```ts * splitTableSources(splitLines('| a\\|b |')[0]).map((cell) => cell.text) // [' a|b '] * ``` */ export declare function splitTableSources(row: MarkdownSource): readonly MarkdownSource[]; /** * Checks whether the line at `index` starts a new block kind (heading / fence / thematic * break / blockquote / list / table) — the paragraph collector stops at such a line * so a block following a paragraph without a blank line still parses (a trusted-input * caller writing a `##` heading directly under a paragraph, with no intervening blank * line). * * @param lines - The document's lines * @param index - The line index to test * @returns True if the line begins a different block; false otherwise * * @example * ```ts * startsBlock(['text', '## Heading'], 1) // true * ``` */ export declare function startsBlock(lines: readonly string[], index: number): boolean; /** * Strips one level of blockquote marker (`>` plus one optional following space) from * an offset-bearing blockquote line, so the de-quoted source re-parses as nested * blocks without losing its original coordinates. * * @param source - A blockquote line (per {@link isQuote}) * @returns The source with its leading `>` and optional space removed * * @example * ```ts * stripQuote({ text: '> text', segments: [{ offset: 0, start: 0, end: 6 }] }) * // { text: 'text', segments: [{ offset: 0, start: 2, end: 6 }] } * ``` */ export declare function stripQuote(source: MarkdownSource): MarkdownSource; /** * Names the horizontal alignment of a GFM table column, as declared by its delimiter row * (`:---` left, `---:` right, `:---:` center). A bare `---` delimiter is represented * by `null` in {@link TableNode.align}: the positional array requires one entry per * column, JSON cannot carry `undefined` in an array, and the bare delimiter is an * explicit no-alignment marker rather than an omitted value. * * @remarks * The other place absence appears is {@link MarkdownCell.align}, which holds * `undefined` when the projected cell declared no alignment. That member is a plain * optional property on one cell rather than an entry in a positional array, so it * takes the ordinary `undefined` instead of the in-band `null` marker. */ export declare type TableAlign = 'left' | 'right' | 'center'; /** * Describes the shape of a {@link TableAlign} — the per-column GFM table alignment * literal. Absence is no member of it, so the shape refuses the `null` a bare `---` * delimiter takes in a `TableNode`'s `align` list. * * @example * ```ts * import { createContract } from '@orkestrel/contract' * import { tableAlignShape } from '@src/core' * * const tableAlign = createContract(tableAlignShape) * tableAlign.is('left') // true * tableAlign.is('center') // true * tableAlign.is('top') // false * ``` */ export declare const tableAlignShape: LiteralShape; /** * Represents the result of collecting one GFM table — the node the construct scanner built and * where the block phase resumes. */ export declare interface TableCollection { /** Holds the collected table. */ readonly node: TableNode; /** Holds the index of the first line after the table. */ readonly next: number; } /** * Represents a GFM table — `header` the inline content of each header cell, `rows` the body * rows (each a list of cells, each cell inline content), `align` the per-column * alignment from the delimiter row. A short body row is padded with empty cells; an * over-long one is truncated to the header's column count. */ export declare interface TableNode { readonly element: 'table'; /** Holds the header row — one cell of inline content per column. */ readonly header: ReadonlyArray; /** Holds the body rows — each a list of cells, each cell inline content. */ readonly rows: ReadonlyArray>; /** * Holds the per-column alignment from the delimiter row, in column order. `null` * represents a bare `---` delimiter because this positional array requires one * entry per column, JSON cannot carry `undefined` in an array, and the delimiter * is an explicit no-alignment marker rather than an omitted value. */ readonly align: ReadonlyArray; } /** * Represents a run of plain text — the leaf inline node. `value` is the decoded text with * markdown escapes (`\*`, `\_`, …) already resolved to their literal characters; * html's text encoder escapes `&`, `<`, `>` on the way out; `"` and `'` stay literal * in character data. */ export declare interface TextNode { readonly element: 'text'; /** Holds the literal text content (escapes resolved, not yet HTML-escaped). */ readonly value: string; } /** * Describes the shape of a {@link TextNode} — a plain-text leaf inline run. * * @example * ```ts * import { createContract } from '@orkestrel/contract' * import { textShape } from '@src/core' * * const text = createContract(textShape) * text.is({ element: 'text', value: 'hi' }) // true * ``` */ export declare const textShape: ObjectShape< { element: LiteralShape; value: StringShape; }, false>; /** Represents a thematic break — a horizontal rule (`---` / `***` / `___` on its own line). */ export declare interface ThematicBreakNode { readonly element: 'thematicBreak'; } /** * Describes the shape of a {@link ThematicBreakNode} — a horizontal rule. Carries no * fields beyond its `element` discriminant. * * @example * ```ts * import { createContract } from '@orkestrel/contract' * import { thematicBreakShape } from '@src/core' * * const thematicBreak = createContract(thematicBreakShape) * thematicBreak.is({ element: 'thematicBreak' }) // true * ``` */ export declare const thematicBreakShape: ObjectShape< { element: LiteralShape; }, false>; /** * Trims the whitespace at the two ends of an inline run — the leading whitespace of a * leading text node and the trailing whitespace of a trailing one — dropping either * node when nothing survives. * * @remarks * Markdown trims every line of a paragraph, a heading's text, and a table cell, so an * untrimmed run would come back from a re-parse a different AST. Expects a coalesced * run (see {@link coalesceText}): only the outermost node on each side is examined. * * @param nodes - The inline run to trim * @returns The run with its edge whitespace removed * * @example * ```ts * trimInlines([{ element: 'text', value: ' a ' }]) // [{ element: 'text', value: 'a' }] * ``` */ export declare function trimInlines(nodes: readonly InlineNode[]): readonly InlineNode[]; /** * Trims an offset-bearing source without losing the coordinates of its retained text. * * @param source - The source to trim * @returns The trimmed text and its narrowed original-string segments * * @example * ```ts * trimSource({ text: ' a ', segments: [{ offset: 0, start: 4, end: 7 }] }) * // { text: 'a', segments: [{ offset: 0, start: 5, end: 6 }] } * ``` */ export declare function trimSource(source: MarkdownSource): MarkdownSource; /** * Resolves backslash escapes in a raw string to their literal characters — used for a * link `href` (which is not otherwise inline-parsed) and any plain text run. * * @param text - The raw text possibly carrying `\x` escapes * @returns The text with escapable `\x` reduced to `x` * * @example * ```ts * unescapeText('\\*hi\\*') // '*hi*' * ``` */ export declare function unescapeText(text: string): string; /** * Walks a {@link MarkdownNode} depth-first, pre-order, root-inclusive — yields * the node itself, then recurses into its children (block children, list items, * image/link inline children, table header/row cells' inline nodes) in walk order. * * @remarks * Total: never throws. Descent stops at {@link MAX_DEPTH} (the node at the cap is * still yielded; its children are not) so pathologically deep input cannot exhaust * the call stack. * * @param node - The AST node to walk (a full document, or any sub-node) * @returns A generator yielding every visited node, pre-order * * @example * ```ts * const doc = { element: 'document', children: [{ element: 'thematicBreak' }] } as const * [...walkNodes(doc)].map((node) => node.element) // ['document', 'thematicBreak'] * ``` */ export declare function walkNodes(node: MarkdownNode): Generator; export { }