/** * How declared font sizes map into the editor. `"off"` is the default and * today's behaviour: sizes are not recovered. */ type PasteFontSizes = "off" | "relative" | "absolute"; /** * paste/index.ts — the paste pipeline. * * Intercepts paste events on the editable region and normalizes * clipboard HTML from Word and Google Docs to the supported set: * bold/italic/lists/links/paragraphs survive; fonts, colors, and * vendor markup are stripped to clean defaults; Word's fake lists * (mso-list paragraphs) are reconstructed as real ul/ol; tables stay * first-class structured nodes; images become a placeholder. The * final word on structure belongs to the editor boundary: * normalizeContent + serialize run at the end of the pipeline, so * output is always inside the whitelist. Never garbage. */ /** What one clipboard flavor is worth, once cleaned to the supported set. */ interface FlavorScore { /** * Elements that carry meaning plain text CANNOT express: headings, lists, * tables, code, links, and every emphasis run. This is the number that * decides the paste. */ structure: number; /** * Paragraph and line-break elements. Scored separately and used only as the * tiebreak, because the plain-text path mints these for free — counting them * as structure would let a hard-wrapped `text/plain` outrank a Word payload * that carries real bold and lists. */ lines: number; } /** Which flavor won, and what every candidate scored. */ interface PasteDecision { chosen: "html" | "rtf" | "text"; scores: { html?: FlavorScore; rtf?: FlavorScore; text?: FlavorScore; }; } /** * license/ types — shared across verify, entitlements, and the badge. */ type Tier = "free" | "starter" | "business" | "enterprise"; /** The signed payload carried inside a license key (wire schema v1). */ interface LicensePayload { /** * Payload schema version. This build understands v1 semantics; a higher * version is accepted and read as v1 — the validator is deliberately * permissive so a future key still verifies on an installed bundle. Typed * `number`, not `1`, so that stays true rather than being a lie the * compiler enforces. */ v: number; /** Paid tier. Free is never issued a key (absence of a key IS Free). */ tier: Exclude; /** Licensed application origins — exact or `*.wildcard` (see verify). */ origins: string[]; /** Entitlement: render the "Powered by Sqwig" badge? */ badge: boolean; /** ISO date (UTC, date-only) the subscription window opens. */ validFrom: string; /** ISO date (UTC, date-only) the subscription window closes. */ validUntil: string; /** Optional, informational only. */ customer?: string; /** Optional, informational only. */ issued?: string; /** * Optional per-key override of the plan's custom-dictionary capacity * (how many `customWords` the editor honors). Absent means the plan * default. Non-integer or negative values are ignored, never rejected — * a mint typo must not invalidate a paying customer's key. */ customWords?: number; } /** Why verification fell back to Free (drives the single console.info). */ type LicenseFailReason = "malformed" | "bad-signature" | "bad-payload" | "origin" /** Running inside a packaged app shell (Capacitor / Cordova / Electron). * Not a licensable application in v1. */ | "packaged-app" | "expired"; interface VerifyContext { /** Defaults to `location.origin`; injectable for tests / SSR. */ origin?: string; /** * Today, as a UTC date-only ISO string, for the term check. Defaults to the * real current date; injectable for tests and SSR. Licences are term-bound: * the key's window is compared against the calendar, never the build. */ now?: string; /** Override the bundled public-key trust set (tests only). */ publicKeys?: readonly Uint8Array[]; } interface LicenseResult { /** Resolved tier — always "free" on any failure. */ tier: Tier; /** True for Free and for any verification failure (fail-open). */ badge: boolean; /** Did a signed paid key pass every check? */ valid: boolean; /** Present only when a check failed (never when there was simply no key). */ reason?: LicenseFailReason; /** Present only when `valid`. */ payload?: LicensePayload; } interface Entitlements { tier: Tier; /** Display name: "Free" | "Starter" | "Business" | "Enterprise". */ tierName: string; /** Render the "Powered by Sqwig" badge? */ badge: boolean; /** * True when no license choice is in effect: nothing passed, non-key * garbage, or a key that failed verification (incl. expired). Drives the * "· unlicensed" badge marker — entitlements are otherwise identical to * free (never MORE; the fail-open invariant is unchanged). False for * `license: "free"` (assent to the Free License) and for valid paid keys. */ unlicensed: boolean; /** Informational only — NOT enforced in code (no metering, ever). null = unlimited. */ maxApps: number | null; /** * Is a paid plan in effect? The single gate for every paid-only feature. * Derived from the verified tier — NOT carried in the key — so a key already * issued picks up every future paid feature with no reissue and no schema bump. */ paid: boolean; /** * How many `customWords` entries the editor honors: 0 outside a valid paid * plan, the plan default otherwise, or the key's own override when one was * issued. A capacity fixed at purchase — nothing here ever meters usage. */ customWords: number; } /** * Export options — SHARED BY BOTH FORMATS (pdf-export-spec §8.1). * * ============================ PUBLISH GATE ============================ * This shape freezes into every installed bundle, exactly like `PRICING_URL` * and `originMatches`. A caller passing `{ pageSize: "a4" }` today gets that * meaning forever on the version they installed, and renaming or re-typing a * field reaches new installs only. It changes before a publish or not at all. * ====================================================================== * * `exportDocx` takes the same object as `exportPdf` — the founder's call, so * the two formats stay consistent — and every field is optional, so the * existing no-argument call is unchanged. Where a field cannot apply to a * format, it is IGNORED rather than rejected: an export never fails over an * option it does not use. */ /** * TrueType bytes for a font the host already has the rights to (§5.3). * * The image-handler pattern applied to typography. Base-14 covers CP1252 — * all of Western European — and nothing else, so a candidate named Łukasz or * Dvořák is unrenderable without this. Shipping a Unicode-wide font ourselves * would add 350-750KB to a package that advertises its size, which is the * dictionary problem again; host-supplied means zero bundle cost, no network * request, and no font licensing exposure for us. * * A style that is not supplied falls back to the REGULAR face, not to base-14: * mixing an embedded roman with a base-14 bold inside one paragraph reads as a * rendering fault, and one font in two weights is the common case for what a * host actually has on hand. * * PDF only. Word embeds fonts by a different mechanism, and the host's font is * usually installed on the reader's machine anyway. */ interface ExportFonts { /** Label used in the PDF's subset font name. Cosmetic. */ name?: string; regular: Uint8Array; bold?: Uint8Array; italic?: Uint8Array; bolditalic?: Uint8Array; } interface ExportImageReport { src: string; path: "jpeg-passthrough" | "canvas-rgb" | "canvas-rgb+smask" | "placeholder"; bytes?: number; reason?: string; } /** * One entry per face, always — `regular`, `bold`, `italic`, `bolditalic`. * * `embedded` means exactly one thing: **a font programme backs this face**. It * does not mean the face is its own programme. When `reason` is present * alongside `embedded: true`, the face is a stand-in — either it was never * supplied, or the file supplied for it could not be read — and `reason` says * which. `embedded: false` means nothing was embedded and the text is drawn * with the built-in fonts. */ interface ExportFontReport { style: string; embedded: boolean; reason?: string; } /** * What the export could not do, handed to {@link ExportOptions.onExportReport}. * * COUNTS AND CHARACTERS ONLY, NEVER DOCUMENT TEXT — the same discipline as * `onPasteDecision`. This is diagnostic, so an app can warn its user before * they mail the file; it is not a channel for the document itself. */ interface ExportReport { pages: number; /** Characters with no glyph in the built-in fonts; each written as "?". */ unsupported: string[]; /** Right-to-left characters, which lay out left-to-right and read wrong. */ rtl: string[]; /** * Relative hrefs written verbatim because no `baseUrl` was supplied. They * resolve in a browser and reach nothing from a file on disk. */ unresolvedLinks: string[]; images: ExportImageReport[]; fonts: ExportFontReport[]; } interface ExportOptions { /** * Paper size. Default "letter", matching the `sectPr` the Word exporter * writes, so the two formats agree. * * Deliberately NOT detected from locale: A4 is 18mm narrower and 18mm taller * than Letter, enough that a document laid out for one prints wrong on the * other, and a silent guess that changes the artifact's geometry produces a * support ticket nobody can reproduce. */ pageSize?: "letter" | "a4"; /** * "Page N of M" in the bottom margin. Default true, in BOTH formats. * * The mechanism differs and the difference is the interesting part: WE * paginate, so the PDF writes literal numbers; WORD paginates, so the .docx * carries PAGE and NUMPAGES fields it computes on open. */ pageNumbers?: boolean; /** PDF only — see {@link ExportFonts}. Ignored by Word export. */ fonts?: ExportFonts; /** * Where the document's content lives, used to resolve RELATIVE hrefs. * * `` works on the page it was authored on and reaches * nothing from an exported file, which has no base URL. Supply the public * home of the content — `"https://acme.com/"` — and relative links resolve * against it. Absolute links, including `mailto:` and a LinkedIn URL on a * résumé, are never touched. * * Deliberately NOT defaulted to the editor's own origin: an editor at * `localhost` or inside an applicant tracking system has nothing to do with * where the content publishes, and guessing would bake a wrong-but-plausible * URL into somebody's document. Without it, relative hrefs are written * verbatim and listed in `unresolvedLinks` on the report. * * Applies to BOTH formats. */ baseUrl?: string; /** * Called once after the export with what it could not do (§5.2, §7.4). * * Diagnostic, in the shape of `onPasteDecision`: counts and characters * only, never document text. Throwing from it cannot break an export, and * a slow handler cannot stall one — it is called after the bytes exist. * * PDF only today. Word export computes no report, and adding one later is a * behaviour change rather than a shape change, so it needs no decision now. */ onExportReport?: (report: ExportReport) => void; } /** * Shared contracts for editor/ — pinned before implementation so the * modules (normalize, serializer, commands, toolbar, trust, spellcheck) * compose without circular knowledge. */ type CommandName = "bold" | "italic" | "underline" | "strikethrough" | "unorderedList" | "orderedList" | "link" /** arg: "p" | "h1" | "h2" | "h3" */ | "heading" /** arg: font family stack, or "" to reset to inherit */ | "fontFamily" /** step through FONT_SIZES_PX; no arg */ | "fontSizeIncrease" | "fontSizeDecrease" /** arg: "disc" | "circle" | "square" (ul) | "decimal" | "lower-alpha" | "upper-alpha" | "lower-roman" | "upper-roman" (ol) */ | "listStyle" | "indent" | "outdent" /** arg: explicit px from FONT_SIZES_PX (e.g. "16") */ | "fontSize" /** arg: "left" | "center" | "right" | "justify" ("left" clears to default) */ | "align" /** arg: unitless line-height "1" | "1.15" | "1.5" | "2" */ | "lineSpacing" /** arg: "upper" | "lower" | "title" | "sentence" */ | "changeCase" | "subscript" | "superscript" /** Semantic inline toggle (dev bundle). */ | "inlineCode" /**
 block toggle; serializes as 
. */
 | "codeBlock"
/** 
block toggle, one level. */ | "blockquote" | "clearFormatting" /** Recover paragraphs/lists/headings from hard-wrapped plain text. */ | "cleanUpText" /** Insert an empty table after the caret's block; arg "RxC" (rows x columns, 1-8 each, e.g. "3x4"). */ | "insertTable" /** Add a row below the caret's row. */ | "tableAddRow" /** Add a column to the right of the caret's cell. */ | "tableAddColumn" /** Delete the caret's row; deleting the last remaining row deletes the table. */ | "tableDeleteRow" /** Delete the caret's column; deleting the last remaining column deletes the table. */ | "tableDeleteColumn" /** Delete the table containing the caret. */ | "tableDelete" | "tableAddRowAbove" | "tableAddColumnLeft" /** Toggle the first row td ↔ th (free — spec §14). */ | "tableHeaderRow" /** arg: "plain" | "banded" | "tinted" — sets/clears the closed-enum * data-sqwig-style preset (paid — spec §15; "plain" clears). */ | "tableStyle" /** Sort data rows by the caret's column (paid — spec §16; header pinned). */ | "tableSortAsc" | "tableSortDesc" /** Table ↔ tab-delimited paragraphs (paid — spec §16). */ | "tableConvertToText" | "tableConvertToTable" /** arg: lowercase #rrggbb, or "" for Automatic (clears the color). */ | "fontColor" /** arg: lowercase #rrggbb (palette of 15, spec §2.3), or "" for No Color. */ | "highlight" /** Download the document as .docx (paid plans). */ | "exportDocx" /** Download the document as .pdf (paid plans). */ | "exportPdf" /** Open the toolbar-layer image insert panel through the ctx seam * (the findReplace shape); the insert itself runs in image-ui.ts. */ | "insertImage" /** Insert a horizontal rule at the caret ("---" + Enter autoformat). */ | "horizontalRule" /** arg: start value "0"-"9999" for the enclosing
    ; "" restarts at 1. */ | "listStart" | "fullscreen" | "findReplace" | "undo" | "redo"; /** * Stable, published toolbar item ids — kebab-case, one per control, stamped on * the mounted DOM as `data-sqwig-control` (both halves of a split button carry * their id). `"|"` is the group separator. These are the vocabulary of * `EditorOptions.toolbar` arrays and `ToolbarSpec.items`; an unknown string is * ignored with one console note, never a throw. */ type ToolbarItemId = "undo" | "redo" | "heading" | "font-family" | "font-size" | "bold" | "italic" | "underline" | "strikethrough" | "inline-code" | "subscript" | "superscript" | "change-case" | "clear-formatting" | "align-left" | "align-center" | "align-right" | "justify" | "line-spacing" | "bullet-list" | "numbered-list" | "outdent" | "indent" | "blockquote" | "code-block" | "table" | "link" | "image" | "horizontal-rule" | "clean-up" | "font-color" | "highlight" | "export-word" | "export-pdf" | "find-replace" | "fullscreen" | "|"; /** * Object form of `EditorOptions.toolbar` (toolbar-config-spec §1). A bare * `ToolbarItemId[]` is shorthand for `{ mode: "fixed", items: [...] }`. */ interface ToolbarSpec { /** * "fixed" (default) or "floating". Floating keeps its own fixed six-control * balloon (§5) — `items` shapes the fixed bar touch devices fall back to, * never the balloon. */ mode?: "fixed" | "floating"; /** * "full" (today's bar, verbatim), "minimal" (the form-field trim), or an * explicit ordered id list — `"|"` separates groups. */ items: "full" | "minimal" | readonly ToolbarItemId[]; } /** * The named presets, frozen, so developers compose instead of retyping: * `toolbar: [...TOOLBAR_PRESETS.minimal, "|", "table"]`. * * `full` is today's bar in published-id form (paid ids ride the mount * tri-state exactly as always — a never-paid mount skips them). `minimal` is * the form-field trim; it excludes `font-color` / `highlight` because a form * field does not want them, NOT because they are paid — they went free on * 2026-08-07. Add them by id if you want them. `full` is also the registry of * every known id, separator included. */ declare const TOOLBAR_PRESETS: { readonly full: readonly ToolbarItemId[]; readonly minimal: readonly ToolbarItemId[]; }; interface CommandState { active: boolean; enabled: boolean; /** Current value for dropdown-style commands (heading tag, font family, list style). */ value?: string; } /** * Structural subset of spell/'s checker the editor depends on. Async: the * default checker runs the engine in a Web Worker, so classification and * suggestions cross a postMessage boundary. `check` is batched — one * round-trip per text node, not per word. */ interface SpellCheckerLike { /** Classify a text node's words at once → indices of the misspelled ones. */ check(words: readonly string[]): Promise>; /** Up to `limit` corrections for a flagged word, best first. */ suggestionsFor(word: string, limit: number): Promise; /** "Add to dictionary": optimistic (treated correct immediately), persists. */ addWord(word: string): void; /** * Application-level vocabulary: treated correct immediately, suggested for * near-misses, never persisted. Optional — a custom checker without it * simply doesn't receive the editor's `customWords`. */ primeWords?(words: readonly string[]): void; } type SpellState = "checking-on-device" | "plain-input"; type EditorEventName = "change" | "selectionchange" | "spell-state"; interface EditorOptions { placeholder?: string; initialHTML?: string; /** * Batteries-included toolbar. `true` ≡ "fixed" (default). "floating" mounts * a compact balloon (B/I/U/S, inline code, link) over non-collapsed * selections instead — a DESKTOP affordance: it takes effect only where * `(hover: hover) and (pointer: fine)` matches; touch devices always get * the fixed toolbar. `false` = no toolbar. * * Per-button configuration: pass an ordered `ToolbarItemId[]` (shorthand * for `{ mode: "fixed", items: [...] }`) or a `ToolbarSpec`. Items render * in your order; `"|"` separates groups; `TOOLBAR_PRESETS.full` / * `.minimal` compose. Unknown ids are ignored with one console note, * duplicates keep their first occurrence, and an empty result mounts no * toolbar — exactly `toolbar: false`. Hiding a button hides chrome, not * capability: `exec()` and keyboard shortcuts still work, and paid ids * obey the license exactly as when unlisted. */ toolbar?: boolean | "fixed" | "floating" | readonly ToolbarItemId[] | ToolbarSpec; /** * Trust indicator footer (default true); object customizes text / word * count. HIDING it is a paid-plan option (since 1.1.0): * `false` (whole footer) and `text: ""` (blank the label) are honoured on * paid plans only — Free-tier editors keep the footer/default label plus * one console note. REWORDING the label (`text: "…"`) works on every tier. */ trustIndicator?: boolean | { text?: string; wordCount?: boolean; }; /** * Built-in color themes. "dark" stamps `data-sqwig-theme="dark"` on the * component root. Host apps that toggle dark mode at runtime can instead * set that same attribute on any ancestor (e.g. ) — both selectors * activate the identical preset, and every value stays overridable via the * `--sqwig-*` custom properties. Default "light". */ theme?: "light" | "dark"; /** * Spell checker instance or pending promise; null disables spellcheck. * A rejected promise degrades to plain-input — it must never throw * out of editor code paths. */ spell?: SpellCheckerLike | Promise | null; /** * Your application's dictionary: words your product considers correct for * every user — product names, jargon, acronyms. Single tokens only * (entries containing whitespace are skipped); matching is * case-insensitive; duplicates count once. The words are session-scoped * and never written to storage — users' own "Add to dictionary" words * layer on top. Available on paid plans up to the plan's word capacity; * entries beyond the capacity stay inactive, noted once in the console. */ customWords?: readonly string[]; /** Live squiggles as you type (default) or manual via checkNow(). */ mode?: "live" | "manual"; /** * Start read-only: region uneditable, toolbar disabled, spellcheck and * link-edit popovers suppressed. Toggle at runtime via `setReadOnly`. */ readOnly?: boolean; /** * Maximum visible-character count (region.textContent.length). Typing past * it is blocked, pasted content is truncated to fit, and — when the count * footer is on — the counter shows `N / max` and warns as it fills. */ maxLength?: number; /** * REQUIRED license choice — it never gates function (the editor always * works, and licensing never throws or phones home): * * - `"free"` — the free tier. Setting it constitutes acceptance of the * Sqwig Free License (https://sqwig.com/free-license). Clean * "Powered by Sqwig" badge. * - A signed key (`"sqwig-lk1.…"`) — your tier's entitlements, * offline-verified in the bundle; the badge is removed on paid tiers. * * Anything else — including omitting it (possible from plain JS) or an * expired/invalid key — runs the free tier with the badge reading * "Powered by Sqwig · unlicensed" plus one console note explaining the * fix. See https://sqwig.com/docs for details. */ license: "free" | (string & {}); /** * Recover DECLARED font sizes from rich pastes. Default `"off"` — pasted * sizes strip to the clean default, exactly as before this option existed. * * - `"relative"` (recommended): sizes map in proportion to the document's * own body size, so a 2× heading pastes at 2× the editor default. The * body itself stays the clean default. * - `"absolute"`: sizes keep their declared point values (px = pt × 4/3, * snapped to the size ladder). Faithful, but a document whose body is * under 12pt pastes smaller than the editor default. * * Only ever reads what the producer declared (`\fs` in RTF, inline * `font-size` in HTML) — "paste never guesses" holds in every mode. */ pasteFontSizes?: PasteFontSizes; /** * Recover DECLARED text colour from rich pastes. Default `false` — pasted * colour strips to the clean default, exactly as before this option * existed. When on, declared colours normalize to * `#rrggbb`; the document's default ink (black/windowtext/automatic) is * never emitted. Reads only what the producer declared — never guesses. */ pasteColors?: boolean; /** * Parse plain-text pastes as Markdown (CommonMark/GFM subset). Default * `false` — today's behavior, byte-for-byte. When on, EVERY plain-text * paste runs the parser — there is no "does this look like markdown?" * guess: text with no markdown syntax comes out identical to the plain * path. The stated cost: a `.txt` paste whose lines start with `#` or * `-` converts too. Never applies to paste-as-plain-text * (Ctrl+Shift+V), pastes into table cells or code blocks, or any rich * clipboard flavor that won the paste. */ pasteMarkdown?: boolean; /** * Upload pasted or file-picked image bytes and return their hosted URL. * Sqwig never hosts or uploads anything itself: your handler receives the * bytes and resolves to an http(s) URL, which is re-validated before it * enters the document (http/https only — a `data:` return never survives). * A rejecting or throwing handler, or an invalid return, degrades safely: * the uploading placeholder is removed with one console note, and the * editor never throws. Without a handler, insert-by-URL still works; * pasted inline images and the file picker fall back to the `[image]` * placeholder. */ imageUpload?: (file: { bytes: Uint8Array; type: string; name?: string; }) => Promise; /** * Diagnostic: called once per rich paste with the clipboard flavor that won * and what every flavor offered scored. * * Clipboards vary by source application, and a paste report rarely comes * with the clipboard that caused it. A report that includes `{chosen:"html", * scores:{html:{structure:0,lines:1},rtf:{structure:73,lines:33}}}` lets us * pinpoint the flavor decision from the report alone, with no capture * round-trip. * * Off unless you pass it, reports nothing about the text itself (counts only), * and throwing from it cannot break a paste. */ onPasteDecision?(decision: PasteDecision): void; /** * Show the bottom-edge resize grabber. Default `false`, so no installed * build changes appearance on upgrade. * * DESKTOP ONLY: it mounts where `(hover: hover) and (pointer: fine)` * matches, the same guard `toolbar: "floating"` uses. A 4px bar is a poor * touch target, and taking it to the ≥40px rule would spend a fortieth of a * phone screen on a control that competes with the platform's own scroll * gesture. `maxHeight` still applies everywhere. */ resizable?: boolean; /** * Cap the component's height in px. Past it the region scrolls internally * instead of growing and pushing the host page around. Default: none, which * is today's unbounded growth. * * This is the option that bounds growth; `resizable` only makes the boundary * adjustable. Note it sits beside `maxLength`, which is unrelated — that one * counts characters. */ maxHeight?: number; /** * Initial height in px, for hosts restoring a height they persisted * themselves. Clamped like any dragged height, so a restored value that is * too small cannot produce an editor nobody can use. */ height?: number; /** * Called after a resize SETTLES (not per pointermove) with the new height in * px. Sqwig stores nothing: persist it yourself if you want it to survive a * reload. Throwing from it cannot break the editor. */ onResize?(height: number): void; } interface Editor { getHTML(): string; setHTML(html: string): void; getText(): string; /** * The document as Markdown (GFM), from the same canonical HTML as * `getHTML()`. Lossy exactly by the published set (/docs): span styles * (font, size, color, highlight), alignment, line spacing, indent, * list-style variants and table style presets drop — never structure, * never text. Never throws. */ getMarkdown(): string; /** * The document as a Word (.docx) file — assembled entirely in the browser, * nothing leaves the machine. A paid-plan feature: * resolves `null` on the Free tier (one console note names the gate) and * on any pipeline failure (one console.error). Never throws or rejects, * never touches the document. The toolbar's Export button is a thin * consumer of the same path plus a download. */ exportDocx(options?: ExportOptions): Promise; /** * The document as a PDF — paginated, laid out and written entirely in the * browser, nothing leaves the machine. The only network requests are GETs * for images the page is already displaying, to your own image host. * * A paid-plan feature with the same contract as {@link exportDocx}: resolves * `null` on the Free tier (one console note names the gate) and on any * pipeline failure (one console.error). Never throws or rejects, never * touches the document. The toolbar's Export to PDF button is a thin * consumer of the same path plus a download. * * Text is real text — selectable, searchable, and extractable by an ATS — * and the document carries a structure tree, so a screen reader gets * headings, lists and tables rather than a flat blob. */ exportPdf(options?: ExportOptions): Promise; exec(command: CommandName, arg?: string): void; queryState(command: CommandName): CommandState; on(event: "spell-state", handler: (state: SpellState) => void): () => void; on(event: EditorEventName, handler: () => void): () => void; /** Manual-mode sweep; no-op when mode is "live" (already continuous). */ checkNow(): void; focus(): void; /** Toggle read-only: uneditable region, disabled toolbar, no edit popovers. */ setReadOnly(readOnly: boolean): void; isReadOnly(): boolean; destroy(): void; /** The contenteditable element (advanced integrations, tests). */ readonly region: HTMLElement; } declare function createEditor(host: HTMLElement, options: EditorOptions): Editor; /** * Pluggable suggestion-source contract: the popover renders ANY source * through this interface. Dictionary (v1) and LLM (v2 premium) are internal * details behind it — never leak the source type into popover code. */ interface SuggestionSource { /** * Up to `limit` replacement candidates for a flagged word, best first. * Async because premium sources are; the dictionary source resolves * synchronously in practice. */ suggestionsFor(word: string, limit: number): Promise; } /** Structural view of the wasm engine — the seam tests fake. */ interface EngineLike { load_chunk(chunk: string): number; add_word(word: string): boolean; is_correct(word: string): boolean; suggest(word: string, max: number): string[]; term_count(): number; } type WasmInput = string | URL | BufferSource; /** * spell/ public API — client-side dictionary spellcheck. * * The engine runs in a Web Worker by default (`createSpellChecker()`), so the * multi-second SymSpell index build and every query stay off the main * thread. When no Worker is available (SSR, exotic * hosts) or when a caller injects an `engineFactory` (tests, the chunk * validator), it transparently falls back to the in-thread engine, which keeps * the richer synchronous surface (`isCorrect`/`suggest`/`termCount`). * * Split load: the core chunk gates readiness; the long-tail chunk merges in * the background and its failure is never fatal. The component above this * layer must keep working if createSpellChecker rejects (non-blocking, * always — that fallback lives in editor/, not here). * * PRIVACY: text never leaves the browser. The worker is same-origin * and in-process; postMessage never touches the network. The only network * activity anywhere in spell/ is static-asset GETs (wasm + dict chunks). */ type SpellReadiness = "core" | "full"; interface SpellCheckerOptions { /** * Base URL/dir the spell assets are served from — the drop-in escape hatch for * bundlers (Vite/Angular) and server stacks (ASP.NET/Blazor) where the worker * and dict/wasm don't sit beside the imported module. Copy `@sqwig/core/dist`'s * `spell_wasm_bg.wasm`, `spell.worker.js`, and `dict/` into a served folder and * point this at it (e.g. "/sqwig/"). May be absolute or root-relative. Any of * the specific asset options below still override per-file. When omitted, assets * resolve beside the module (import.meta.url) — the default that "just works" * when they're co-located (e.g. the demo). */ assetBaseUrl?: string | URL; /** Explicit worker URL; overrides assetBaseUrl. Default: ./spell.worker.js beside this module. */ workerUrl?: string | URL; /** Explicit wasm source; defaults to the asset beside this module. */ wasm?: WasmInput; /** Core dictionary chunk URL. Default: ./dict/en_US.core.txt beside this module. */ coreDict?: string | URL; /** Tail chunk URL; null disables the tail load. Default: ./dict/en_US.tail.txt. */ tailDict?: string | URL | null; /** localStorage key for the user dictionary; null disables persistence. */ userDictionaryStorageKey?: string | null; /** Injection seams for tests / exotic hosts. Presence forces the in-thread engine. */ fetchImpl?: typeof fetch; engineFactory?: (wasm?: WasmInput) => Promise; /** Escape hatch: false forces the in-thread engine even in a Worker-capable host. */ worker?: boolean; } /** * The editor-facing async surface — satisfied by both the worker-backed and * in-thread checkers. Async because the worker is; the popover's * SuggestionSource seam is already async, so v2's LLM source drops in here. */ interface SpellCheckerAsync extends SuggestionSource { /** Batch classify: resolves to the indices of `words` that are misspelled. */ check(words: readonly string[]): Promise>; /** "Add to dictionary": optimistic (correct immediately), persists locally. */ addWord(word: string): void; /** * Application-level vocabulary for this session: every word is treated * correct immediately and can be suggested for near-misses, but nothing is * ever written to storage — the words are re-supplied by configuration on * each load. This is the layer the editor's `customWords` option feeds; * users' own "Add to dictionary" words layer on top of it. */ primeWords(words: readonly string[]): void; /** "core" once usable; "full" after the tail chunk merges. */ readonly readiness: SpellReadiness; /** Resolves when the tail settles (merged, disabled, or failed). */ readonly whenFull: Promise; } /** * In-thread checker — the async surface plus the synchronous conveniences the * chunk validator and integration tests rely on. Only `createInlineSpellChecker` * (and `createSpellChecker` when an `engineFactory` is injected) returns this. */ interface SpellChecker extends SpellCheckerAsync { /** True when the (case-insensitive) word is in the dictionary. */ isCorrect(word: string): boolean; /** Up to `max` (default 3) corrections, best first. */ suggest(word: string, max?: number): readonly string[]; /** Dictionary lines accepted so far (diagnostics). */ readonly termCount: number; } /** * Boot the checker. Resolves as soon as checking is usable; rejects only if * the engine or core chunk cannot load (the caller treats spellcheck as * unavailable and stays a plain input). * * Default (browser, no injected engine): runs in a Web Worker. When a Worker * cannot be used it falls back to the in-thread engine transparently. */ declare function createSpellChecker(options: SpellCheckerOptions & { engineFactory: NonNullable; }): Promise; declare function createSpellChecker(options?: SpellCheckerOptions): Promise; /** The in-thread checker — synchronous engine wrapped in the async surface. */ declare function createInlineSpellChecker(options?: SpellCheckerOptions): Promise; /** * The worker-backed checker (default). Resolves once the worker reports the * core chunk is loaded (checking usable); rejects on a fatal worker error so * the caller can fall back or degrade to plain-input. */ declare function createWorkerSpellChecker(options?: SpellCheckerOptions): Promise; /** * Third-party open-source notices, exported programmatically so applications * can surface them if they wish (promised by LICENSE.txt §3 — no action is * required to remain compliant; the notices also travel as a banner comment * in the built bundle and as dist/THIRD_PARTY_LICENSES.txt beside the wasm). */ /** Full text of this package's THIRD_PARTY_LICENSES.txt. */ declare const LICENSES: string; /** * Verify a license key offline. Returns Free entitlements on anything less than * a fully valid, in-window, origin-matched paid key. */ declare function verifyLicense(key: string | null | undefined, ctx?: VerifyContext): LicenseResult; /** * license/ entitlements — pure resolution of a verified result into what the * editor is allowed to do. Today the only behavioral entitlement is the badge; * `maxApps` is informational (we never count apps in code — no metering, ever). */ declare function entitlementsFor(result: LicenseResult, opts?: { /** * True when the caller holds an explicit free-tier acceptance * (`license: "free"`). A valid key is always assented (the purchase was * the acceptance); everything else is unlicensed. */ assented?: boolean; }): Entitlements; export { type CommandName, type CommandState, type Editor, type EditorOptions, type EngineLike, type Entitlements, type FlavorScore, LICENSES, type LicenseFailReason, type LicensePayload, type LicenseResult, type PasteDecision, type SpellChecker, type SpellCheckerAsync, type SpellCheckerLike, type SpellCheckerOptions, type SpellReadiness, type SpellState, type SuggestionSource, TOOLBAR_PRESETS, type Tier, type ToolbarItemId, type ToolbarSpec, type VerifyContext, type WasmInput, createEditor, createInlineSpellChecker, createSpellChecker, createWorkerSpellChecker, entitlementsFor, verifyLicense };