/** * Shared text-fitting primitives built on `@chenglou/pretext`. * * Given a fixed-size text box, find the largest uniform font factor at which * the content still fits — by measuring wrapped line count with pretext, then * painting that factor as *actual font sizes* (Excel shrink-to-fit). Never a * CSS zoom of the authored layout: zoom keeps the original wraps and scales * paragraph gaps, which leaves a large empty region under the last line. * * pretext uses canvas, so this only does real work in a DOM/canvas runtime; * callers wrap usage so a non-DOM context falls back gracefully. */ import { prepare as pretextPrepare } from '@chenglou/pretext'; /** ProseMirror's default text size (assets/styles/prosemirror.scss). */ export declare const DEFAULT_TEXT_FONT_SIZE = 16; /** * Live text boxes use `$textElementFont` when the theme/element family is * empty. Canvas wrap must use the same stack — generic `sans-serif` is a * different face and packs a different number of words per line. */ export declare const DEFAULT_TEXT_FONT_FAMILY = "Calibri, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\""; /** @deprecated Use DEFAULT_TEXT_FONT_FAMILY */ export declare const DEFAULT_FIT_FONT_FAMILY = "Calibri, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\""; export declare function resolvePaintFontFamily(family?: string | null): string; /** Matches `ul, ol { padding-inline-start: 1em }` when the list has no inline pad. */ export declare const DEFAULT_LIST_PADDING_EM = 1; /** Matches `li { padding-inline-start: 0.5333333333em }` (marker-to-text gap). */ export declare const LIST_MARKER_GAP_EM: number; /** * @deprecated Prefer per-block `listIndentPx` from the imported list padding. * Kept as a fallback indent (px) when a list item has no padding of its own. */ export declare const BULLET_INDENT = 40; export interface TextFitRun { text: string; size: number; bold?: boolean; italic?: boolean; underline?: boolean; strikethrough?: boolean; color?: string; fontFamily?: string; /** Authored TeX from `span.fika-math[data-latex]` — painted as a raster chip. */ mathLatex?: string; mathDisplay?: boolean; /** Chip width at authored `size` (not yet fit-scaled). */ extraWidth?: number; /** Chip height at authored `size` (fractions are taller than the font). */ mathHeight?: number; } /** A single measurable text block (one paragraph or one list item). */ export interface TextFitBlock { /** Plain text of the block (markers stripped). */ text: string; /** Explicit preformatted text and hard breaks retain authored whitespace. */ whiteSpace?: 'pre-wrap'; /** Authored font size in px (its largest run, to stay safe). */ size: number; bold?: boolean; italic?: boolean; fontFamily: string; /** When true, a list marker indent is subtracted from the text column. */ listItem?: boolean; /** `ul`/`ol` padding-inline-start in px, when authored as px. */ listIndentPx?: number; /** `ul`/`ol` padding-inline-start in em, when authored as em. */ listIndentEm?: number; /** * Mixed-size runs inside one paragraph, measured exactly: each run gets its * own font and pretext's rich-inline line breaker, so wrapping and per-line * heights match what the browser paints (a 57px body with one 120px word * wraps like 57px and only that one line is 120px tall). */ runs?: TextFitRun[]; /** Canvas/static-paint metadata. It does not affect fitting math. */ align?: 'left' | 'center' | 'right' | 'justify'; listMarker?: string; } export interface MeasureBlocksOptions { /** Box content width in px (insets already removed). */ innerWidth: number; /** Line height multiplier (e.g. 1.5). */ lineHeight: number; /** * Vertical gap between consecutive blocks, in px. CSS `--paragraphSpace` is * a fixed px value on the box — it does **not** shrink with the font (Excel * cell padding / our fittedContent paint). Do not scale this with sizeScale. */ blockSpace?: number; /** Fallback list gutter in px when a list item has no `listIndentPx`/`Em`. */ bulletIndent?: number; letterSpacing?: number; /** Multiply every block's font size by this factor before measuring (default 1). */ sizeScale?: number; } /** Extra inner-height reserved so overflow:hidden does not clip glyph ink that * sits outside the CSS line box (PowerPoint lnSpcReduction / line-height < 1). * Sized from the font, not a fixed slab — large pads look like over-shrink. */ export declare function fitClipPadding(maxFontSize: number, lineHeight: number): number; export declare const MIN_FIT_SCALE = 0.2; /** * Excel-style shrink-to-fit from a *real* laid-out height: the largest scale * `<= 1` at which `contentHeight * scale <= innerHeight`. Never grows past 100%. * When content already fits, returns 1 (empty space in a tall box is authored). */ export declare function fitScaleFromContentHeight(contentHeight: number, innerHeight: number, options?: { minScale?: number; }): number; /** * Natural (unzoomed) scroll height of a text-fit host. Forces zoom to 1 for * one synchronous layout read (no clone — cloning a focused ProseMirror can * under-measure and snap the scale back to 100% on click). Width is locked to * the box inner width because Chrome `zoom` shrinks `clientWidth`. */ /** * Content-box size of the clipped text frame (padding already removed). * Prefers the live DOM box so shrink-to-fit tracks applyLiveSize / resize * instead of the last committed store width/height. */ export declare function innerBoxFromContentElement(box: HTMLElement | null | undefined, fallback: { innerWidth: number; innerHeight: number; }): { innerWidth: number; innerHeight: number; }; /** * Inner box from applyLiveSize inline styles + authored inset. * No getComputedStyle / clientWidth — safe on the resize hot path. */ export declare function innerBoxFromLiveStyles(box: HTMLElement | null | undefined, el: { width: number; height: number; inset?: [number, number, number, number]; }): { innerWidth: number; innerHeight: number; }; export declare function contentBoxOfHost(host: HTMLElement | null | undefined): HTMLElement | null; export declare function measureUnzoomedScrollHeight(host: HTMLElement, innerWidth: number): number; /** Line-box size for a run: math chips use the typeset height, not the font. */ export declare const runVisualSize: (run: TextFitRun) => number; /** * Line box for the runs sharing one line. Text costs `size × lineHeight`; a * math chip is taller than its font but gets the *same* leading as the words * beside it — never leading scaled by its own height — so a fraction widens * the line by its excess height only, not by a 3 em gap. */ export declare const lineBoxHeight: (runs: TextFitRun[], sizeScale: number, lineHeight: number) => number; /** * Stand-in grapheme for a math chip. pretext drops items whose text is empty * or whitespace-only, so a chip must carry one real `text`-kind grapheme to * exist in the flow; its glyph advance is subtracted from `extraWidth` so the * item occupies exactly the chip width. */ export declare const MATH_PLACEHOLDER = "\uFFFC"; /** * Live ProseMirror uses `white-space: break-spaces`: the space after the last * word on a line still occupies width, so the wrap column is one space * narrower than the content box. pretext is `white-space: normal` and would * otherwise pack an extra word per line (thumb vs slide). */ export declare const measureSpaceWidth: (font: string) => number; export declare const breakSpacesWrapWidth: (width: number, spaceWidth: number) => number; export declare const editorWrapWidth: (width: number, font: string) => number; export declare const richInlineFromRun: (run: TextFitRun, font: string, sizeScale: number, letterSpacing?: number) => { text: string; font: string; letterSpacing?: number; break?: 'never'; extraWidth?: number; }; export declare function measureTextBlocksHeight(blocks: TextFitBlock[], options: MeasureBlocksOptions): number; export interface FitFontScaleOptions { innerWidth: number; innerHeight: number; lineHeight: number; blockSpace?: number; bulletIndent?: number; letterSpacing?: number; /** Smallest font factor to fall back to before clipping takes over (default 0.1). */ minScale?: number; } /** * Largest uniform font factor in `[minScale, 1]` at which `blocks` fit * `innerHeight`, found by binary search over pretext measurements. The factor * multiplies the authored font sizes; because wrapping changes as the type * shrinks, this re-measures at each candidate (not a single geometric divide). * Returns 1 when content already fits (or measurement isn't possible). */ export declare function fitFontScaleForBlocks(blocks: TextFitBlock[], options: FitFontScaleOptions): number; /** * Uniform zoom factor `innerHeight / height(authored)` — wrapping stays at the * authored size. Only for the live editor (authored HTML + CSS zoom). Fitted * static/thumbnail paint must use {@link fitFontScaleForBlocks} instead. */ export declare function fitZoomScaleForBlocks(blocks: TextFitBlock[], options: FitFontScaleOptions): number; export type FitMeasureSession = { key: string; lineHeight: number; maxFont: number; letterSpacing?: number; items: Array<{ block: TextFitBlock; size: number; handle: ReturnType; }>; }; export declare function fitSessionKey(html: string, fontFamily: string, lineHeight: number, letterSpacing: number, locale: string, defaultSize?: number): string; /** * One-time pretext `prepare()` for authored font sizes. Resize then only * calls `layout()` with the new width (no canvas, no DOM, no re-prepare). */ export declare function createFitMeasureSession(html: string, options: { key: string; defaultFontFamily: string; lineHeight: number; letterSpacing?: number; /** Painted size for runs with no inline `font-size` (placeholder body is 20, not 16). */ defaultSize?: number; }): FitMeasureSession | null; /** Resize hot path: pure `layout()` over cached prepare handles. */ export declare function measureSessionHeight(session: FitMeasureSession, innerWidth: number, blockSpace?: number, bulletIndent?: number): number; export declare function fitZoomScaleFromSession(session: FitMeasureSession, innerWidth: number, innerHeight: number, blockSpace?: number): number; /** Rendered spans may carry calc(var(--text-fit-scale,...)); measurement wants authored px. */ declare const stripFitScale: (html: string) => string; export declare const stripFitScaleForProbe: typeof stripFitScale; type FitScaleOptions = { innerWidth: number; innerHeight: number; defaultFontFamily?: string; defaultSize?: number; lineHeight: number; letterSpacing?: number; blockSpace?: number; locale?: string; /** Pre-extracted blocks (e.g. after MathLive refined math chip sizes). */ blocks?: TextFitBlock[]; }; /** * Persist a DOM-verified correction: the editor measured the REAL rendered * height and found the searched scale overflowing. Remembered so the rail, * later applies, and export reuse the corrected value instead of re-overflow. */ export declare function rememberFitScale(html: string, options: FitScaleOptions, scale: number): void; /** * One-shot locked-box scale for a rich-text HTML string. Editor resize keeps a * cached session; preview/export call this so they shrink with the same math. */ export declare function textFitScaleForHtml(html: string, options: FitScaleOptions): number; /** Replace estimated math chip boxes with a real typeset measure. */ export declare function applyMeasuredMathBoxes(blocks: TextFitBlock[], measure: (latex: string, fontSize: number, display: boolean) => { width: number; height: number; } | null): void; export interface ExtractedContent { blocks: TextFitBlock[]; } export interface ExtractOptions { defaultFontFamily: string; defaultSize?: number; } /** * Parse a Fika rich-text HTML string into measurable blocks. Each list item * and top-level paragraph/quote becomes one block; a block's representative font * size is the largest inline size in it (so the clip pad never under- * estimates), while wrapping measures the mixed-size run profile. * Empty paragraphs occupy one native line. List items are tagged * so the marker indent is accounted for. */ export declare function extractFitBlocksFromHtml(html: string, options: ExtractOptions): ExtractedContent; /** * Return a copy of `html` with every inline `font-size:Npx` multiplied by * `scale` (rounded to 0.1px). Text without an explicit size is untouched here — * the renderer scales that through the `--text-fit-base-size` CSS variable. A * scale >= 1 (or a non-DOM context) returns the HTML unchanged. */ export declare function scaleHtmlFontSizes(html: string, scale: number): string; export {};