import type { DesignDocument, DocumentNode, DocumentValue } from './contracts.js'; /** * Rich text for `kind: 'text'` nodes. * * The plain string `properties.text` stays the canonical content that bindings and the * visual source compiler read. `properties.richText` is an optional sidecar that holds * per-range character styles and per-paragraph styles; `normalizeTextProperties` is the * single function that keeps the two in sync and it is the only writer either path uses. * * Style that applies to the whole layer is stored on node-level properties, never in runs. * Normalization hoists any key shared by every run (or every paragraph) to the node level * and drops the sidecar when nothing per-range remains, so a stored `richText` always * means "this layer has mixed styling" — the exact condition that blocks plain source output. */ export type RichTextCase = 'none' | 'upper' | 'lower' | 'title'; export type RichTextList = 'none' | 'bullet' | 'numbered'; export type RichTextAlign = 'start' | 'center' | 'end' | 'justify'; /** The component measures; the document records only the mode. */ export type TextResizeMode = 'fixed' | 'auto-width' | 'auto-height'; export interface RichTextCharacterStyle { fontFamily?: string; /** CSS numeric weight, 1–1000. */ fontWeight?: number; /** Pixels. Node-level `fontSize` may also be a permitted CSS token string. */ fontSize?: number; italic?: boolean; underline?: boolean; strikethrough?: boolean; textCase?: RichTextCase; color?: string; /** A link target; only http(s), mailto, tel, and relative targets validate. */ link?: string; } export interface RichTextParagraphStyle { align?: RichTextAlign; /** Unitless multiplier. */ lineHeight?: number; /** Pixels; negative values tighten. */ letterSpacing?: number; list?: RichTextList; /** Indent level, 0–10. */ indent?: number; } export interface RichTextRun { text: string; style: RichTextCharacterStyle; } export interface RichTextParagraph { runs: RichTextRun[]; style: RichTextParagraphStyle; } export interface RichText { version: 1; paragraphs: RichTextParagraph[]; } /** Offsets into the plain projection, where paragraphs are joined by one `\n`. */ export interface RichTextRange { start: number; end: number; } /** Node-level defaults; `fontSize` may be a CSS token the visual compiler already accepts. */ export interface TextNodeCharacterStyle extends Omit { fontSize?: number | string; } export interface TextNodeStyle { character: TextNodeCharacterStyle; paragraph: RichTextParagraphStyle; } export declare const TEXT_CHARACTER_KEYS: readonly ["fontFamily", "fontWeight", "fontSize", "italic", "underline", "strikethrough", "textCase", "color", "link"]; export declare const TEXT_PARAGRAPH_KEYS: readonly ["align", "lineHeight", "letterSpacing", "list", "indent"]; export type RichTextCharacterKey = typeof TEXT_CHARACTER_KEYS[number]; export type RichTextParagraphKey = typeof TEXT_PARAGRAPH_KEYS[number]; export declare const TEXT_RESIZE_MODES: readonly TextResizeMode[]; export declare const RICH_TEXT_CASES: readonly RichTextCase[]; export declare const RICH_TEXT_LISTS: readonly RichTextList[]; export declare const RICH_TEXT_ALIGNMENTS: readonly RichTextAlign[]; /** Node property names for paragraph keys; alignment is `textAlign` so it never reads as layout alignment. */ export declare const TEXT_PARAGRAPH_PROPERTY: Readonly>; /** Every node-level typography property, including the resize mode. */ export declare const TEXT_NODE_PROPERTY_KEYS: readonly string[]; export declare const isTextLink: (value: unknown) => value is string; export declare const isTextResizeMode: (value: unknown) => value is TextResizeMode; export declare const isRichTextCharacterValue: (key: RichTextCharacterKey, value: unknown) => boolean; export declare const isRichTextParagraphValue: (key: RichTextParagraphKey, value: unknown) => boolean; export declare function isRichTextCharacterStyle(value: unknown): value is RichTextCharacterStyle; export declare function isRichTextParagraphStyle(value: unknown): value is RichTextParagraphStyle; export declare function isRichTextRun(value: unknown): value is RichTextRun; export declare function isRichTextParagraph(value: unknown): value is RichTextParagraph; export declare function isRichText(value: unknown): value is RichText; export declare function richTextPlain(model: RichText): string; export declare function richTextLength(model: RichText): number; export declare function richTextFromPlain(text: string, style?: RichTextCharacterStyle, paragraphStyle?: RichTextParagraphStyle): RichText; /** Merges adjacent runs with equal styles and drops empty runs; an empty paragraph keeps one empty run for its pending style. */ export declare function normalizeRichText(model: RichText): RichText; /** Sets the defined keys of `style` on every character in the range; a collapsed range changes nothing. */ export declare function applyRichTextCharacterStyle(model: RichText, range: RichTextRange, style: RichTextCharacterStyle): RichText; export declare function clearRichTextCharacterStyle(model: RichText, range: RichTextRange, keys?: readonly RichTextCharacterKey[]): RichText; /** Sets the defined keys on every paragraph the range touches; a collapsed range targets its own paragraph. */ export declare function setRichTextParagraphStyle(model: RichText, range: RichTextRange, style: RichTextParagraphStyle): RichText; export declare function clearRichTextParagraphStyle(model: RichText, range: RichTextRange, keys?: readonly RichTextParagraphKey[]): RichText; /** The style typed text inherits at `offset`: the run before the caret, or the run after it at a paragraph start. */ export declare function richTextInheritedStyle(model: RichText, offset: number): RichTextCharacterStyle; /** Inserts plain text; each `\n` starts a new paragraph that copies the split paragraph's style. */ export declare function insertRichText(model: RichText, offset: number, text: string, style?: RichTextCharacterStyle): RichText; /** Deletes a range; a boundary inside the range merges its paragraphs under the first paragraph's style. */ export declare function deleteRichText(model: RichText, range: RichTextRange): RichText; /** Replaces a range with plain text that inherits the replaced text's style. */ export declare function replaceRichText(model: RichText, range: RichTextRange, text: string, style?: RichTextCharacterStyle): RichText; export declare function splitRichTextParagraph(model: RichText, offset: number): RichText; /** Merges paragraph `index` with the one after it. */ export declare function mergeRichTextParagraphs(model: RichText, index: number): RichText; /** * Applies a plain-text edit (the textarea result) while runs elsewhere keep their styles. When the paragraph * count is unchanged, every changed paragraph is one span replacement, so edits on different lines committed * together each inherit their own neighbour; otherwise the whole differing span is replaced once. */ export declare function applyPlainTextEdit(model: RichText, nextText: string): RichText; export interface RichTextStyleSummary { style: T; mixed: readonly (keyof T)[]; } /** Explicit run styles inside the range (the inherited style at a collapsed range); keys that differ are reported as mixed. */ export declare function richTextCharacterStyleAt(model: RichText, range: RichTextRange): RichTextStyleSummary; export declare function richTextParagraphStyleAt(model: RichText, range: RichTextRange): RichTextStyleSummary; /** Keys that vary across the whole model — the exact reason plain source output is blocked. */ export declare function richTextVaryingKeys(model: RichText): { character: readonly RichTextCharacterKey[]; paragraph: readonly RichTextParagraphKey[]; }; /** Reads the validated node-level typography; invalid values are ignored rather than rendered. */ export declare function textNodeStyle(properties: Readonly>): TextNodeStyle; export declare function textResizeMode(properties: Readonly>): TextResizeMode; /** The written string; bound content is projected by the canvas and never stored here. */ export declare function textNodePlain(properties: Readonly>): string; /** The editable model. A sidecar that disagrees with the written text is dropped, because the written text is what bindings and source read. */ export declare function textNodeModel(properties: Readonly>): RichText; export declare const isRichTextTrivial: (model: RichText) => boolean; /** Moves styles shared by the whole text to the node level and drops run keys that repeat the node default. */ export declare function hoistRichTextStyle(model: RichText, base: TextNodeStyle): { model: RichText; character: TextNodeCharacterStyle; paragraph: RichTextParagraphStyle; }; /** A JSON round trip drops `undefined` and prototypes so the engine accepts the value. */ export declare const richTextValue: (model: RichText) => DocumentValue; /** * The single writer for text properties. Given a model (or the stored sidecar), it hoists * whole-text styles to node properties, writes the plain projection to `text`, and keeps * `richText` only while per-range styling remains. Node-level defaults are removed rather * than written, so a layer without styling has no typography keys at all. */ export declare function normalizeTextProperties(properties: Readonly>, model?: RichText): Record; /** Bound text never carries runs: the projected value replaces the written text at render time. */ export declare function plainTextProperties(properties: Readonly>, text: string): Record; export interface TextFontRequirement { family: string; weights: readonly number[]; } export interface TextFontRequest { family: string; weight: number; } export interface TextFontLoader { load(request: TextFontRequest): Promise; } export interface TextFontLoadReport { loaded: readonly TextFontRequest[]; unavailable: readonly TextFontRequest[]; failed: readonly (TextFontRequest & { message: string; })[]; } /** Families and weights the text needs; runs without a family use the node's, and a missing family means the platform default. */ export declare function textFontRequirements(model: RichText, base?: TextNodeCharacterStyle): TextFontRequirement[]; export declare function documentFontRequirements(document: DesignDocument): TextFontRequirement[]; /** Resolves every required face through the injected loader; nothing here touches the network on its own. */ export declare function loadTextFonts(requirements: readonly TextFontRequirement[], loader: TextFontLoader): Promise; export declare function loadDocumentFonts(document: DesignDocument, loader: TextFontLoader): Promise; /** Adapts the browser `document.fonts` set when the host runs in a browser; returns null elsewhere. */ export declare function fontFaceSetLoader(fonts: { load(font: string): Promise; check(font: string): boolean; } | undefined | null): TextFontLoader | null; /** A convenience for text nodes: the node's own font needs. */ export declare function textNodeFontRequirements(node: DocumentNode): TextFontRequirement[]; /** An exact preparation request. A missing family remains unresolved; no default is guessed. */ export interface TextFontFaceRequest { readonly family: string | null; readonly weight: number; readonly style: 'normal' | 'italic'; /** Written sample in UTF-16. Rendered case or connected substitutions require explicit resolution. */ readonly sample: string; } /** Preserves family expressions and sample bytes, keeping weight/style/sample combinations distinct. */ export declare function textFontFaceRequests(model: RichText, base?: TextNodeCharacterStyle): readonly TextFontFaceRequest[];