/**
* The rich-text grammar, in one place.
*
* A `richtext` field is stored as a markdown string, and until now three
* separate implementations parsed it: the block renderers (to React elements),
* the editor overlay (to an HTML string) and the Puck adapter (to HTML, and
* back). They had already drifted — the Puck adapter honoured heading depth
* while the page hard-coded `
` — so the same document rendered a different
* outline depending on which editor you had open.
*
* This module owns the grammar; the callers own presentation. Anything a
* particular surface does differently (HTML-escaping its input, skipping the
* paragraph wrapper for single-line values, choosing a heading tag) belongs in
* that surface's serializer, not here.
*
* The grammar is a subset of CommonMark, sized to exactly what the property
* panel's editor can produce — anything it can write must render, or the user
* sees their own formatting come back as punctuation:
* inline `**strong**`, `*em*`, `~~strike~~`, `` `code` ``, `[label](href)`,
* hard breaks (`\` or two trailing spaces)
* blocks ATX headings, bullet and ordered lists (nested), blockquotes,
* fenced code, thematic breaks, paragraphs
* Still outside it: tables, setext headings, reference links, footnotes, and
* inline images — the editor has no image node and deletes them, so an
* `` renders its alt text rather than promising an image the stack
* cannot keep. See the rich-text characterisation tests for each case.
*/
/** A span of text inside a block, already resolved to its mark. */
export type InlineToken = {
type: "text";
text: string;
at: number;
} | {
type: "strong";
text: string;
at: number;
} | {
type: "em";
text: string;
at: number;
} | {
type: "strike";
text: string;
at: number;
} | {
type: "code";
text: string;
at: number;
} | {
type: "link";
text: string;
href: string;
at: number;
}
/** An explicit line break inside a block — markdown's `\` or two trailing spaces. */
| {
type: "break";
at: number;
};
/** One item in a list, with an optional nested list under it. */
export type RichTextListItem = {
inline: InlineToken[];
children?: RichTextList;
};
export type RichTextList = {
type: "unordered-list" | "ordered-list";
items: RichTextListItem[];
};
/**
* One top-level block.
*
* `index` is the block's position in the source document. Serializers use it to
* key their output; it is stable for a given input.
*/
export type RichTextBlock = {
type: "heading";
index: number;
/** Hash count, 1–6, as written. Serializers decide which tag that maps to. */
level: number;
inline: InlineToken[];
/** Text that followed the heading inside the same block, if any. */
trailing?: InlineToken[];
} | {
type: "unordered-list";
index: number;
items: RichTextListItem[];
} | {
type: "ordered-list";
index: number;
items: RichTextListItem[];
} | {
type: "blockquote";
index: number;
children: RichTextBlock[];
} | {
type: "code";
index: number;
code: string;
language?: string;
} | {
type: "rule";
index: number;
} | {
type: "paragraph";
index: number;
inline: InlineToken[];
raw: string;
};
/**
* Map an authored hash count onto the heading level a body actually renders.
*
* Every serializer must use this, or the same document gets a different outline
* depending on which surface you are looking at — which is exactly the drift
* that made the page emit `` while Puck emitted ``.
*/
export declare function resolveRichTextHeadingLevel(level: number): number;
/**
* Rewrite heading markers so the source agrees with what will be rendered.
*
* Only `#` moves (to `##`); 2-6 are already renderable. Use it on a value
* entering an editor whose schema does not accept `h1`: without it the editor
* silently degrades the heading to a paragraph and saves that back, so a `#`
* that the page was rendering as `h2` disappears on the next edit.
*/
export declare function clampMarkdownHeadings(markdown: string): string;
/** Undo the serialiser's escaping of a text span. */
export declare function unescapeMarkdownText(text: string): string;
/** Split a line of text into marked spans. Unterminated markers stay literal. */
export declare function parseInline(text: string): InlineToken[];
/**
* Whitespace clean-up applied to a body before it is parsed.
*
* This used to also apply `/([.!?])([A-Z])/g → "$1 $2"`, a band-aid for
* planners that emitted `requested.Here's`. It fired on correct prose far more
* often than on the malformed kind: every `U.S.A.` became `U. S. A.`, and any
* URL with a capital after a dot (`/API.Reference`) gained a space and stopped
* resolving. It ran on every render of every page, so the damage was permanent
* from the reader's point of view even though the stored string was fine.
*
* If a planner starts emitting run-on sentences again, fix it in the prompt —
* not by rewriting everyone's prose at render time.
*/
export declare function normalizeRichTextBody(input: string): string;
/**
* Parse a body that has already been through `normalizeRichTextBody`.
*
* Callers that need to inspect or transform the normalized string first (the
* editor overlay checks it for newlines) use this; everyone else wants
* `parseRichText`.
*/
export declare function parseRichTextBlocks(normalizedBody: string): RichTextBlock[];
/** Parse a rich-text string into its blocks. */
export declare function parseRichText(input: string): RichTextBlock[];