import * as react from 'react'; import { JSX, ComponentType, PropsWithChildren, CSSProperties, FC } from 'react'; import { SmoothStreamPacing, SmoothStreamOptions, Registry, AIMDContentPreprocessor, UrlTransform, SanitizeSchema, AIMarkdownEnginePlugin } from '@ai-react-markdown/engine'; export { AIMDContentPreprocessor, AIMarkdownEnginePlugin, AIMarkdownEnginePluginName, ChunkData, FootnoteDef, LinkDef, RefKind, RefRecord, Registry, RemendPreprocessorOptions, SMOOTH_STREAM_PACING_PRESETS, SanitizeSchema, SmoothStreamController, SmoothStreamOptions, SmoothStreamPacing, SmoothStreamPacingParams, UrlTransform, createRemendPreprocessor, createSmoothStreamController, defaultUrlTransform, extendSanitizeSchema } from '@ai-react-markdown/engine'; import { Element } from 'hast'; /** * Hook for referential stability of deep-equal values. * * @module hooks/useStableValue */ /** * Returns a referentially stable version of `value`. * * On each render the new value is deep-compared (via `lodash/isEqual`) against * the previous one. If they are structurally equal the *previous* reference is * returned, preventing unnecessary re-renders in downstream `useMemo` / `useEffect` * consumers that depend on reference equality. * * The ref is updated in a layout effect (not during render) so that the cached * reference only advances on COMMITTED renders. In concurrent mode a render * may be discarded (e.g. by Suspense); writing to the ref during render would * let values from discarded renders pollute the cache and leak into subsequent * committed renders. Layout effects run synchronously right after commit, which * closes the window where a same-tick re-render could otherwise observe a stale * `ref.current` and hand back an outdated reference. * * @typeParam T - The value type. * @param value - The potentially new value to stabilize. * @returns The previous reference when deep-equal, otherwise the new value. * * @example * ```tsx * const stableConfig = useStableValue(config); * // stableConfig keeps the same reference as long as config is deep-equal. * ``` */ declare function useStableValue(value: T): T; /** * Public types for the local Markdown wrapper — the React half after * boundary action ③: the pipeline-facing fields live in the engine's * `PipelineOptions`, and {@link Options} extends it with the React-only * `components` field. Pure types are re-exported so this module's surface * is unchanged for existing importers. * * Ported 1:1 from react-markdown v10's lib/index.js JSDoc. * * @module components/markdown/types */ /** Extra fields the wrapper passes to user-supplied tag components. */ interface ExtraProps { node?: Element | undefined; } /** Map tag names to user components or other tag names. */ type Components = { [Key in keyof JSX.IntrinsicElements]?: ComponentType | keyof JSX.IntrinsicElements; }; /** * Core type definitions for ai-react-markdown: metadata, typography * theming, and the custom-component override surface. Engine/behavior * configuration lives on the flat prop surface (see `index.tsx`) and the * sealed plugin catalog (`plugins/`). * * @module defs */ /** * Custom component overrides for the markdown renderer. * Alias for the local Markdown wrapper's `Components` type (a vendored fork of * react-markdown's), re-exported under the library's `AIMarkdown` naming * convention so consumers don't need a direct `react-markdown` dependency * for type imports. */ type AIMarkdownCustomComponents = Components; /** * Arbitrary metadata that consumers can pass through a dedicated React context. * Custom renderers can access this via the {@link useAIMarkdownMetadata} hook. */ interface AIMarkdownMetadata extends Record { } /** * Typography variant identifier. Built-in variant is `'default'`; * consumers may define additional variants via custom typography components. */ type AIMarkdownVariant = 'default' | (string & {}); /** * Color scheme identifier. Built-in schemes are `'light'` and `'dark'`; * consumers may define additional schemes via custom typography CSS. */ type AIMarkdownColorScheme = 'light' | 'dark' | (string & {}); /** Props accepted by a typography wrapper component. */ interface AIMarkdownTypographyProps extends PropsWithChildren { /** Resolved CSS font-size value (e.g. `'14px'`, `'0.875rem'`). */ fontSize: string; /** Active typography variant. */ variant?: AIMarkdownVariant; /** Active color scheme. */ colorScheme?: AIMarkdownColorScheme; /** * Inline styles injected by the core renderer. Custom Typography implementations * **must** merge this object into their root element's `style` to ensure CSS * custom properties set by the core are available to all descendant nodes. * * ### Currently injected variables * * | Variable | Value | Purpose | * |-------------------------|----------------|----------------------------------------------------------| * | `--aim-font-size-root` | `fontSize` prop | Absolute font-size anchor for the component instance. | * * #### Why `--aim-font-size-root`? * * Markdown content frequently nests elements that use relative `em` units * (blockquotes, lists, code blocks). Each nesting level compounds the * effective size — a `0.875em` code span inside a `1.125em` blockquote * becomes `0.984375em` of the parent, not `0.875em` of the root. * * `--aim-font-size-root` provides the component-level root font-size as an * absolute reference so that inner CSS rules can use * `font-size: var(--aim-font-size-root)` to opt out of `em` compounding * when a stable size is needed. * * The built-in `default` variant consumes this variable: its spacing, * font-size, and heading tokens are defined as `calc(var(--aim-font-size-root) * k)`, * so the `fontSize` prop proportionally scales every rendered dimension. * * @example * ```tsx * const MyTypography: AIMarkdownTypographyComponent = ({ children, fontSize, style }) => ( *
* {children} *
* ); * ``` */ style?: CSSProperties; } /** React component type for the typography wrapper. */ type AIMarkdownTypographyComponent = ComponentType; /** Props accepted by an optional extra style wrapper component. */ interface AIMarkdownExtraStylesProps extends PropsWithChildren { } /** React component type for an optional extra style wrapper. */ type AIMarkdownExtraStylesComponent = ComponentType; /** Payload of the document context — entirely derived invariants (closed to extension). */ interface AIMarkdownDocumentInfo { /** Resolved logical-document id (consumer-supplied or `useId()` fallback). */ documentId: string; /** Whether `documentId` was explicitly supplied — the cross-chunk coordination signal. */ documentIdExplicit: boolean; /** Canonical URI-safe id prefix for clobberable attributes; never reconstruct locally. */ clobberPrefix: string; } /** Payload of the theme context (value tier of the Theme system). */ interface AIMarkdownThemeInfo { /** Resolved CSS font-size value (number props already normalized to `px`). */ fontSize: string; variant: AIMarkdownVariant; colorScheme: AIMarkdownColorScheme; } /** Core (locked) keys of the state context. */ interface AIMarkdownStateCore { streaming: boolean; } /** Core (locked) keys of the behaviors context — the three engine switches. */ interface AIMarkdownBehaviorsCore { /** Output-invariant engine strategy: block-level memoization. */ blockMemo: boolean; /** Output-invariant engine strategy: incremental (prefix-freeze) parsing. */ incrementalParse: boolean; /** Orphan-reference policy for incomplete/streaming documents (affects output). */ preserveOrphanReferences: boolean; } /** * Extension groups accepted by {@link AIMarkdownStateProvider}. Core keys are * type-forbidden (`never`) — they are owned by ``'s resolution * and would be overwritten at the innermost merge anyway. * * **Frequency contract**: group members must change at message-lifecycle * frequency (aborted, reasoning, tool-call-in-progress, …). Frame-rate data * (per-token progress etc.) goes through metadata's stable-container * pattern instead — one context, every subscriber re-renders per change. */ type AIMarkdownStateGroups = { [group: string]: object; } & { streaming?: never; }; /** * Extension groups accepted by {@link AIMarkdownBehaviorsProvider} — * wrapper component-behavior parameter groups (e.g. mantine's `codeBlock`). * Core keys are type-forbidden (`never`). */ type AIMarkdownBehaviorGroups = { [group: string]: object; } & { blockMemo?: never; incrementalParse?: never; preserveOrphanReferences?: never; }; /** * Opaque extension-group record seen by consumers. Values are typed * `object | undefined` so a mistyped group key does not type as present; * wrapper narrow hooks perform the single type assertion. */ type AIMarkdownExtensionGroups = Record; /** * Additive (stackable) Provider for state extension groups. Stack it * *outside* ``; the value should be firewall output (or any * reference-stable record) used directly: * * ```tsx * * * * ``` * * Group members obey the message-lifecycle frequency contract — see * {@link AIMarkdownStateGroups}. `streaming` is core-locked. */ declare const AIMarkdownStateProvider: FC>; /** * Additive (stackable) Provider for behavior extension groups (wrapper * component parameters, e.g. mantine's `codeBlock`). Stack it *outside* * ``. The three core switches are core-locked; group defaults * are applied inside the wrapper's narrow hook, never at read sites. */ declare const AIMarkdownBehaviorsProvider: FC>; /** * Document-system narrow hook: resolved `documentId`, the * `documentIdExplicit` coordination signal, and the canonical * `clobberPrefix`. Payload is derived invariants — this context is closed * to extension (a forgeable `clobberPrefix` would break the anchor system). * * @throws When called outside an `` tree. */ declare function useAIMarkdownDocument(): AIMarkdownDocumentInfo; /** * Theme-system narrow hook: `fontSize`, `variant`, `colorScheme`. * @throws When called outside an `` tree. */ declare function useAIMarkdownTheme(): AIMarkdownThemeInfo; /** * State-system narrow hook. `streaming` is the most frequently flipping * field in the library — this hook's subscribers are the ONLY components * that re-render on a flip. * Extension state groups contributed via {@link AIMarkdownStateProvider} * appear as additional keys, typed `object | undefined`. * * @throws When called outside an `` tree — a stacked additive * Provider alone does not satisfy the guard (core keys come only from * ``'s internal provider). */ declare function useAIMarkdownState(): AIMarkdownStateCore & AIMarkdownExtensionGroups; /** * Behaviors-system narrow hook: the three core engine switches plus an * opaque extension record. Non-generic by design — the caller-asserted * `TConfig` generic is retired; wrapper narrow hooks (e.g. * `useMantineCodeBlockOptions()`) perform the single type assertion and * apply group defaults inside. * * @throws When called outside an `` tree — a stacked additive * Provider alone does not satisfy the guard (core keys come only from * ``'s internal provider). */ declare function useAIMarkdownBehaviors(): AIMarkdownBehaviorsCore & AIMarkdownExtensionGroups; /** Aggregate payload returned by {@link useAIMarkdown}. */ interface AIMarkdownAggregate { document: AIMarkdownDocumentInfo; metadata: TMetadata | undefined; state: AIMarkdownStateCore & AIMarkdownExtensionGroups; theme: AIMarkdownThemeInfo; behaviors: AIMarkdownBehaviorsCore & AIMarkdownExtensionGroups; } /** * Aggregate hook over all five systems: * * ```ts * const { document, metadata, state, theme, behaviors } = useAIMarkdown(); * ``` * * **Price**: subscribes to all five contexts and re-renders on ANY change * (including every `streaming` flip). Performance-sensitive components * should use the narrow hooks; the aggregate serves teaching and * low-frequency components. */ declare function useAIMarkdown(): AIMarkdownAggregate; /** * Access the current metadata from within the `` tree. * * Metadata lives in its own React context so that changes to metadata do * not cause re-renders in components that only consume the other systems * (e.g. the internal `MarkdownContent` renderer). * * ### `TMetadata` is a caller-asserted type * * The generic is an assertion about the `metadata` prop passed to the * provider above, not a value TypeScript can derive. Metadata has no * runtime fallback: if the provider received no `metadata`, the hook returns * `undefined` regardless of the asserted type. Prefer wrapping this hook in * a project-local hook that pins `TMetadata` next to the call site that * actually provides the metadata. * * @typeParam TMetadata - Caller-asserted metadata shape (defaults to * {@link AIMarkdownMetadata}). Caller is responsible for ensuring the * provider's `metadata` prop matches this shape. * @returns The current metadata, or `undefined` if none was provided. * * @see `@ai-react-markdown/mantine` — `useMantineAIMarkdownMetadata` applies * the wrapper pattern to this hook, pinning `MantineAIMarkdownMetadata` in * a single location. */ declare function useAIMarkdownMetadata(): TMetadata | undefined; /** * `define*` factories — the packaging unit for integration-time * configuration (EXECUTION-PLAN §3.3). * * Factory output is a frozen, fully typed, reference-stable flat prop * fragment, spread into ``: * * ```tsx * const THEME = defineTheme({ fontSize: 15, variant: 'default' }); * const BEHAVIORS = defineBehaviors({ blockMemo: false }); * const PIPELINE = definePipeline({ contentPreprocessors: [...], sanitizeSchema: mySchema }); * * // runtime-varying fields go AFTER the spreads; later props win * * ``` * * Three disciplines (binding): * * 1. A factory is identity + types + `Object.freeze`, zero logic. No * default-filling (defaults live only in the component's destructuring), * no merging, no side effects. `define` (vs `create`) signals "declares * data", not "manufactures a live object". * 2. Factories package values decided at integration time. Data, State, and * Document members change per frame/message and get no factory; * runtime-switchable fields may enter a factory as initial values, with * runtime overrides as post-spread props (see `colorScheme` above). * 3. Factories are sugar, not gates — passing bare flat props is always legal. * * Core factories accept core prop types only. Wrappers re-export widened * factories for their extension fields (e.g. `defineMantineBehaviors`) — * still identity + own types + freeze. * * @module define */ /** Theme-system props packagable at integration time. */ type AIMarkdownThemeProps = Pick; /** Behavior-system core switches. */ type AIMarkdownBehaviorProps = Pick; /** Engine payloads + engine plugin selection. */ type AIMarkdownPipelineProps = Pick; /** Freeze a theme fragment. Identity + types + freeze; zero logic. */ declare function defineTheme(values: AIMarkdownThemeProps): Readonly; /** Freeze a behaviors fragment. Identity + types + freeze; zero logic. */ declare function defineBehaviors(values: AIMarkdownBehaviorProps): Readonly; /** Freeze a pipeline fragment. Identity + types + freeze; zero logic. */ declare function definePipeline(values: AIMarkdownPipelineProps): Readonly; /** * The stability firewall: single-boundary, table-driven reference * stabilization of object-valued props (EXECUTION-PLAN §3.9). * * Replaces scattered per-prop `useStableValue` calls with ONE hook whose * behavior per key is declared in a module-scope policy table. Layering * rule: below the firewall, internal code never stabilizes again — * reference equality is trusted outright. * * The table is the complete roster of object-valued props: * {@link AIMarkdownStabilityTable} is `Required>`, so * adding an object prop without registering it is a compile error. * Exemption ({@link AIMarkdownStabilityPolicy.PASS_THROUGH}) and omission * (missing row) are thereby distinguishable — exemption is a decision, * a missing row is an omission. * * @module hooks/useStableRecord */ /** * Stabilization policy tiers. Publicly exported; wrappers reuse it for * their own tables (a wrapper builds a table only for object props it * *terminates* — forwarded props ride core's firewall untouched). */ declare enum AIMarkdownStabilityPolicy { /** * Deep-equal backstop: when the reference changed, deep-compare; if the * contents are deep-equal, reuse the previous reference. For * structurally comparable data values. In dev, high-frequency * "deep-equal restores" warn — the caller is inlining objects; caches * stay warm, but every render pays one deep comparison. */ DEEP_EQUAL = "DEEP_EQUAL", /** * No comparison; dev-mode flip-rate probe only. For function and * component values — deep-comparing closures is meaningless; the * contract requires the caller to hold a stable reference. */ WARN_ONLY = "WARN_ONLY", /** * Explicit pass-through: no comparison, no probe. For deliberately * exempted props (e.g. `metadata`: opaque shape, potentially huge, * unbounded comparison cost — stabilization is the consumer's * responsibility). */ PASS_THROUGH = "PASS_THROUGH" } /** * Policy table for a record of object-valued props. `Required` makes the * table the complete roster: every key of `T` must carry a policy row. */ type AIMarkdownStabilityTable = Required>; /** * Returns a stabilized version of `record` according to `table`. * * - The loop lives inside the hook (static hook count — rules-compliant * regardless of table size). * - The previous-value ref advances in a post-commit layout effect, * identical to `useStableValue`'s concurrent-safety discipline: * discarded renders never pollute the cache. * - When no key changed, the output object keeps its identity. * * `table` must be a module-scope constant: the roster (and each key's * policy) is fixed for the lifetime of the call site. */ declare function useStableRecord(record: T, table: AIMarkdownStabilityTable): T; /** Props the shell injects into the indicator (the actual visual layer). */ interface AIMarkdownStreamingIndicatorProps { /** * Rendered height (px) of the last character at the anchor. Indicators * use it to match the current line's text size — taller on headings, * shorter on body text. */ height: number; /** * Rendered width (px) of the last character at the anchor. Block-style * cursors (e.g. `▍`) can use it to align with the character cell. */ width: number; /** Timestamp of the most recent content mutation (`performance.now()` basis). */ lastMutationAt: number; } type AIMarkdownStreamingIndicatorComponent = ComponentType; interface AIMarkdownStreamingCursorProps { /** * The indicator component the shell positions and feeds with * {@link AIMarkdownStreamingIndicatorProps}. Defaults to the built-in * blinking circle. Bind a custom one at module scope: * * ```tsx * const MyCursor = () => ; * * ``` */ indicator?: AIMarkdownStreamingIndicatorComponent; } /** * Positioner shell. Renders a zero-height wrapper (the coordinate base and * the handle to the content root via `parentElement`), detects the anchor, * and keeps the indicator positioned through three pre-paint signal * sources: a MutationObserver on the content root (tokens, tail-block * morphs), a ResizeObserver (reflow from container width changes), and * `document.fonts.ready` (one re-measure after font swap). Detection * failure hides the indicator; the next mutation re-detects. * * SSR renders the (invisible) wrapper only — detection needs a real DOM, so * the indicator appears after the first client-side measurement. */ declare const AIMarkdownStreamingCursor: ({ indicator: Indicator, }: AIMarkdownStreamingCursorProps) => react.JSX.Element; /** * React binding for the smooth-stream controller. * * The return value is deliberately props-shaped: `{ content, streaming }` * mirrors the `` props of the same names, so the result * spreads into the base component or ANY wrapper (mantine included) in * one line: * * ```tsx * const smooth = useSmoothStream({ content, streaming: !done }); * return ; * ``` * * Semantics: * - Mount snaps to the current `content` — SSR hydration and mid-stream * remounts (route return, list virtualization) show the full text * instantly instead of replaying the typewriter from empty. * - While `streaming` is true, content growth animates. * - On the `streaming` true→false transition the controller drains the * remaining backlog within its rate-continuity drain window (at most * `3 × drainMs`); the RETURNED `streaming` stays true until drained so * the cursor slot doesn't unmount mid-animation. * - Content changes while not streaming snap (no animation). * * @module components/smoothStream/useSmoothStream */ interface UseSmoothStreamOptions { /** Full accumulated markdown source (the same value you would pass to `content`). */ content: string; /** Whether the stream is still producing (the same value you would pass to `streaming`). */ streaming?: boolean; /** * Named pacing preset — the whole tuning surface at this level: * `'smooth'` (extra buffer, never runs dry), `'balanced'` (default, * minimal lag that still bridges typical bursts), `'responsive'` * (lowest lag, accepts occasional pauses). Numeric parameters live on * {@link createSmoothStreamController} for advanced hosts. */ pacing?: SmoothStreamPacing; /** * Fires when the post-stream drain completes — the message is now * fully visible. In practice this is end-of-stream only (the held-back * trailing grapheme keeps the reveal one step short of a LIVE stream, * so mid-stream catch-ups never count as drained); multi-round flows * fire it once per round. Content replacement (regeneration) does NOT * fire it — the replaced message never "completed". Read through a * latest-ref: identity changes are safe and never re-trigger anything. */ onDrained?: () => void; /** @internal test seam — injectable clock, see {@link SmoothStreamOptions.now}. */ now?: SmoothStreamOptions['now']; /** @internal test seam — injectable scheduler, see {@link SmoothStreamOptions.schedule}. */ schedule?: SmoothStreamOptions['schedule']; } interface UseSmoothStreamResult { /** The paced (revealed-prefix) content — pass as `content`. */ content: string; /** True while pixels should still read as streaming — pass as `streaming`. */ streaming: boolean; /** * Reveals everything pending immediately (a "skip animation" button). * Identity-stable; safe to pass down memoized trees. Spreading it onto * a component is harmless — the base components ignore unknown props. */ flush: () => void; } declare const useSmoothStream: ({ content, streaming, pacing, onDrained, now, schedule, }: UseSmoothStreamOptions) => UseSmoothStreamResult; /** * `useSmoothStream` with document-level turn-taking. * * Inside ``, chunks sharing a `documentId` should read * as ONE typewriter: chunk N reveals completely before chunk N+1 starts, * with a single cursor throughout. This hook wraps an UNCHANGED * `useSmoothStream` with that gate: * * - A chunk that mounts with EMPTY content queues behind its earlier-mounted * siblings and renders nothing (no cursor) until every one of them is * done (source stopped AND reveal drained). * - A chunk that mounts with non-empty content passes through ungated — * the mount snap presents it instantly, exactly like `useSmoothStream` * (hydration, virtualization scroll-back, and mid-stream remounts must * not blank out or replay; see the gate note below). It still occupies * its queue slot, so later empty-mounted chunks wait for it. * - Without a `documentId`, or outside `` (or with * `smoothTurnTaking={false}` on the wrapper), behavior is identical to * `useSmoothStream`. * * The result is props-shaped and spreads into `` or any * wrapper, same as `useSmoothStream`. `documentId` must match the one the * rendered component receives (the hook cannot cross-check; a mismatch * silently loses coordination) and must be mount-stable. * * Contract shifts relative to `useSmoothStream`, both gate-inherent: * `onDrained` fires after the chunk's TURN completes — potentially long * after its source stream ended — and a content replacement that happens * entirely while gated DOES fire it (the reveal only ever saw empty → * final text, so the finally-revealed message genuinely completed). * `flush()` while gated is a no-op: nothing is playing yet, and skipping * the whole document's queue is deliberately not a per-chunk power. * * @module components/smoothStream/useDocumentSmoothStream */ interface UseDocumentSmoothStreamOptions extends UseSmoothStreamOptions { /** * The document this chunk belongs to — the same value the rendered * `` receives. Coordination engages only when this is a * non-empty string AND the component sits under `` * with turn-taking enabled; otherwise the hook degrades to plain * `useSmoothStream`. Must be mount-stable: changing it mid-life is * undefined behavior. */ documentId?: string; /** Hold this queue slot before input starts, without showing a streaming * cursor. Clear when input starts OR when an empty result completes. * `streaming: false` alone still means a completed source. */ waiting?: boolean; } declare const useDocumentSmoothStream: ({ documentId, content, streaming, waiting, pacing, onDrained, now, schedule, }: UseDocumentSmoothStreamOptions) => UseSmoothStreamResult; /** * Optional outer wrapper enabling cross-chunk coordination for any * `` instances rendered as descendants. Each unique * `documentId` partitions its own Registry. * * Without this wrapper, `` instances render independently * (current behavior). With it, multiple chunks sharing a `documentId` * coordinate footnote numbering, linkReference/imageReference resolution, * and anchor jumps across chunks. * * @module components/AIMarkdownDocuments */ interface AIMarkdownDocumentsProps extends PropsWithChildren { /** * Default `true`. Unconditionally controls orphan-reference protection * for all chunks under this wrapper, overriding each chunk's own * `preserveOrphanReferences` prop. Does not control cross-chunk * coordination itself (that's gated by wrapper presence + `documentId`). */ preserveOrphanReferences?: boolean; /** * Default `true`. Turn-taking for smooth-streaming chunks: chunks that * share a `documentId` and stream from empty reveal sequentially (chunk * N fully revealed before chunk N+1 starts — one typewriter, one * cursor). `false` disables coordination wholesale; every * `AIMarkdownSmoothStream` / `useDocumentSmoothStream` under this * wrapper then behaves like plain `useSmoothStream`. Per-chunk opt-out * exists too (`smoothCoordination={false}` on the shell). Has no effect * on chunks that don't smooth-stream. */ smoothTurnTaking?: boolean; } declare const AIMarkdownDocuments: FC; /** * Returns the registry for the given `documentId`, or `null` if: * - `` is not inside an `` wrapper, OR * - `documentId` is undefined / empty string, OR * - `documentIdExplicit` is `false` — i.e. the id was auto-generated rather * than supplied by the consumer. * * The `documentIdExplicit` gate is the crux of the standalone-vs-coordinated * decision. An auto-generated id (`useId()` fallback) is non-empty and unique * by construction, so a chunk carrying one has nothing to coordinate with even * inside the wrapper — it must run standalone. Without this gate, the mere * presence of `` would drag every uncoordinated chunk * onto the cross-chunk registry path (subscribe / allocate / evict overhead) * for no behavioral gain. The flag defaults to `true` so external callers who * pass an id directly are treated as explicit (passing an id IS the intent to * coordinate); the internal renderer threads through `state.documentIdExplicit`. * * Callers should treat `null` as "no coordination; run standalone path." */ declare function useDocumentRegistry(documentId: string | undefined, documentIdExplicit?: boolean): Registry | null; /** * Props for the `` component. * * @typeParam TMetadata - Custom metadata type (extends {@link AIMarkdownMetadata}). */ interface AIMarkdownProps { /** * Arbitrary consumer data delivered to custom components through the * metadata context (`useAIMarkdownMetadata`). Deliberately never * stabilized by the library — see the firewall table (`PASS_THROUGH`). */ metadata?: TMetadata; /** * Whether content is actively being streamed (e.g. token-by-token from an LLM). * When `true`, the flag is propagated via context so custom components can adapt * their behavior (show cursors, disable copy buttons, skip animations, etc.). * Defaults to `false`. `null` (from untyped/serialized callers) counts as absent. */ streaming?: boolean; /** * Base font size for the rendered output. * Accepts a CSS length string (e.g. `'14px'`, `'0.875rem'`) or a number * which is treated as pixels. Defaults to `'0.9375rem'`. `null` (from * untyped/serialized callers) and `''` count as absent. */ fontSize?: number | string; /** Raw markdown content to render. */ content: string; /** * Additional preprocessors to run on the raw markdown before rendering. * These run *after* the built-in LaTeX preprocessor. The package ships an * optional streaming tail-repair factory for this slot — * {@link createRemendPreprocessor} (see docs/content-preprocessors.md). */ contentPreprocessors?: AIMDContentPreprocessor[]; /** * Custom `react-markdown` component overrides. * Use this to replace the default renderers for specific HTML elements * (e.g. code blocks, links, images). */ customComponents?: AIMarkdownCustomComponents; /** * Typography wrapper component. Receives `fontSize`, `variant`, and `colorScheme`. * Defaults to the built-in {@link DefaultTypography}; `null` counts as absent. * * `children` may be a Fragment (the rendered content plus the optional * `streamingCursor` slot) — implementations must render `children` * verbatim; `Children.only` / `cloneElement`-style handling will break. */ Typography?: AIMarkdownTypographyComponent; /** * Optional extra style wrapper component rendered between the typography * wrapper and the markdown content. Useful for injecting additional * CSS scope or theme providers. * * Same `children` contract as `Typography`: may be a Fragment; render it * verbatim. */ ExtraStyles?: AIMarkdownExtraStylesComponent; /** Typography variant name. Defaults to `'default'`; `null` counts as absent. */ variant?: AIMarkdownVariant; /** Color scheme name. Defaults to `'light'`; `null` counts as absent. */ colorScheme?: AIMarkdownColorScheme; /** * Stable identifier for the *logical markdown document* this `` * is rendering. Used as the id namespace for all clobberable attributes * (`id`, hash hrefs) so two documents on the same page do not cross-link — * e.g. clicking a footnote `[^1]` in message A will not scroll to the * `[^1]` definition in message B. * * Why `documentId` and not `instanceId`: when one logical document is * split across multiple `` instances (chunked / streamed * rendering), every chunk should share the SAME `documentId` so their * id-prefixes line up. The id is per-document, not per-React-instance. * * When omitted, an id is auto-generated via React's `useId()` (SSR-safe * and stable across re-renders). `null` and the empty string `''` both * count as omitted — they fall to the auto-generated id AND opt the * instance out of cross-chunk coordination (an auto id never opens a * registry), so `documentId={maybeEmpty}` silently renders standalone. * Pass an explicit value when you need deterministic ids (snapshot * tests, cross-component deep links) or when multiple instances render * the same logical document. * * Consumer-supplied values pass through `encodeURIComponent` at the prefix * construction site, so any string is safe — including ids with reserved * characters like `:`, `/`, or spaces. Even ill-formed UTF-16 (an unpaired * surrogate from a string truncated mid-emoji upstream) is accepted: such * ids are hashed into the prefix, keeping distinct ids on distinct * prefixes (up to the same 2^32 hash bound long ids always had). * Dev builds log a warning when this happens, since * the corruption usually indicates an upstream bug worth fixing. */ documentId?: string; /** * This chunk's position in the DOCUMENT, when several `` * instances share a `documentId` inside ``. * * Cross-chunk state (footnote numbering, which chunk renders the * aggregate footer) follows the order chunks register in. Without this * prop that is MOUNT order, which is correct as long as chunks mount once * in document order — the common case, so the prop is optional and the * default behaviour is unchanged. * * Pass it when chunks can mount out of order or remount: a **virtualized * transcript** that unmounts messages scrolled out of view re-registers * them at the end when they scroll back, which renumbers footnotes and * moves the aggregate footer under whichever chunk registered last. Any * stable per-chunk ordinal works (the message's index in your list). * * Ignored outside `` (a standalone document is its * own chunk 0). */ documentIndex?: number; /** * Override the per-attribute URL rewriter (Gate 2 of the two-gate model). * Runs at render time during the hast traversal in `renderHastSubtree`, * after Gate 1 (`rehype-sanitize` schema) has already filtered URLs by * protocol allowlist in the rehype plugin chain. * * The default allowlist mirrors `react-markdown` / GitHub: `http`, * `https`, `irc`, `ircs`, `mailto`, `xmpp`. Anything else is rewritten * to `''`. * * **Recommended pattern**: compose with the exported * {@link defaultUrlTransform} so the built-in XSS protections survive, * and define the result at module scope so its identity is stable across * renders: * * ```ts * import AIMarkdown, { defaultUrlTransform } from '@ai-react-markdown/core'; * * const ALLOWED = /^(myapp|tel):/i; * const URL_TRANSFORM = (url, key, node) => * ALLOWED.test(url) ? url : defaultUrlTransform(url, key, node); * * function App() { * return ; * } * ``` * * **Regex-escaping**: scheme names per RFC 3986 may contain `+`, `-`, and * `.` (e.g. `web+app`, `coap+tcp`). All three are regex metacharacters, * so write `/^web\+app:/i` rather than `/^web+app:/i`. The latter would * match URLs starting with `we`, `wee`, `weee`, … and silently broaden * the allowlist. * * **Reference stability matters.** The block-memo cache treats this prop * as a dependency. Defining the function inline (`urlTransform={(url) => * …}`) creates a new closure on every parent render, discards the cache * for the entire markdown document on each render, and effectively * disables block-level memoization. In development the library will * `console.warn` if it detects this pattern. * * Allowing a protocol here is necessary but **not sufficient** to render * a link — Gate 1 (`rehype-sanitize` schema) also enforces its own * protocol allowlist and runs first. See the `sanitizeSchema` prop on * this component and the {@link extendSanitizeSchema} helper for Gate 1. * * **API stability**: the `UrlTransform` type tracks the upstream * `react-markdown` shape and may change with its major versions. */ urlTransform?: UrlTransform | null; /** * Override the `rehype-sanitize` schema applied to the rendered output. * The library default extends `rehype-sanitize`'s own `defaultSchema` * with the `` tag, the `math-inline` / `math-display` className * markers `remark-math` emits on `` (KaTeX's own output classes * survive separately because `rehype-katex` runs after `rehype-sanitize`), * and the cross-chunk coordination tags (`cross-chunk-link`, * `cross-chunk-image`, `footnote-sup`). The default is not exported as a * value — see {@link extendSanitizeSchema} for how to inspect or extend * it safely. * * **Recommended pattern**: build the schema with {@link extendSanitizeSchema} * (mutate-and-return form) so those library additions stay intact, and * define the result at module scope: * * ```ts * import AIMarkdown, { extendSanitizeSchema } from '@ai-react-markdown/core'; * * const SCHEMA = extendSanitizeSchema((s) => { * s.protocols.href.push('myapp'); * s.protocols.src.push('myapp'); * }); * * function App() { * return ; * } * ``` * * **Footgun**: hand-rolling a schema (e.g. spreading from * `rehype-sanitize`'s `defaultSchema` directly) silently drops the * cross-chunk tag allowlist — coordinated multi-chunk rendering will then * lose its placeholders. Prefer the helper unless you have a specific * reason to opt out. * * **Reference stability matters.** An inline call * (`sanitizeSchema={extendSanitizeSchema((s) => { … })}`) is mitigated by * the stability firewall's `DEEP_EQUAL` policy (`useStableRecord` + * `CORE_STABILITY_TABLE`: an equal-but-new object is replaced by the * previous reference), but the safer pattern is still module-scope. * Development builds will `console.warn` on repeated identity flips. * * **API stability**: the `SanitizeSchema` type tracks the upstream * `rehype-sanitize` shape and may change with its major versions. */ sanitizeSchema?: SanitizeSchema; /** * Streaming cursor slot. While `streaming === true`, the given component * is rendered after the markdown content — inside the typography wrapper * and both providers — and unmounted when `streaming` is `false` or the * prop is omitted. No props are injected; the slot only controls WHEN and * WHERE the component renders. * * Pass the exported {@link AIMarkdownStreamingCursor} for the built-in * inline self-positioning cursor (optionally wrapped at module scope to * bind a custom `indicator`): * * ```tsx * import AIMarkdown, { AIMarkdownStreamingCursor } from '@ai-react-markdown/core'; * * * ``` * * **Reference stability matters.** Like `Typography`, this is compared by * identity in the memo wrapper — define the component at module scope, not * inline. */ streamingCursor?: ComponentType; /** * Sealed engine plugin selection (v2 input surface; Engine-plugins system). * Accepts core-exported sealed plugins only — import them from * `@ai-react-markdown/core/plugins`. Third-party content extension goes * through `contentPreprocessors` + `customComponents`. * * - Absent → `defaultEnginePlugins` (all five, parity with the shipped * config defaults). Passing an array replaces the set wholesale * (array-atomic semantics). * - Each plugin's position in the produced chain comes from canonical * per-stage tables keyed by name; the order of this array is * irrelevant. Duplicates are deduplicated with a dev warning. * - Turn one off: `enginePlugins={defaultEnginePlugins.filter((p) => p !== pangu)}`. * * "Explicit" is `v != null` — passing `null` counts as absent (guards * serialization boundaries materializing "not passed" as `null`). */ enginePlugins?: readonly AIMarkdownEnginePlugin[]; /** * Behaviors system: block-level memoization. Output-invariant in * standalone rendering — flipping it changes no rendered byte (orphan * protection included). When `true` (default), the renderer splits the * document into per-block units and memoizes each block's subtree by * source identity, so unchanged blocks skip render work during streaming. * When `false`, the legacy bare flow runs the full pipeline every render. * Cross-chunk coordination (``) is wired through the * block-memo path only — with `false`, refs across chunks do not resolve. * * `null` counts as absent (falls to the default). @default true */ blockMemo?: boolean; /** * Behaviors system: incremental (prefix-freeze) parsing for streaming * content. Output-invariant (enforced by the splice-equivalence suites). * When the content grows by appends, the engine freezes the verified * prefix and re-parses only the tail; a per-frame gate chain silently * falls back to the full parse whenever splicing is not provably safe. * Effective only while `blockMemo` is `true`; SSR always full-parses. * * `null` counts as absent (falls to the default). @default true */ incrementalParse?: boolean; /** * Behaviors system: protect orphan reference definitions (footnote/link * defs with no matching reference yet) in incomplete/streaming documents. * Affects output. Override chain: an `` wrapper's * same-named prop (omission ≡ explicit `true`) unconditionally wins for * all chunks under it > this prop > the shipped default. * * `null` counts as absent (falls to the default). @default true */ preserveOrphanReferences?: boolean; } /** * Root component that preprocesses markdown content and renders it through * a configurable remark/rehype pipeline wrapped in typography and style layers. */ declare const AIMarkdownComponent: ({ streaming, content, fontSize, contentPreprocessors, customComponents, metadata, Typography, ExtraStyles, variant, colorScheme, documentId, documentIndex, urlTransform, sanitizeSchema, streamingCursor, enginePlugins, blockMemo, incrementalParse, preserveOrphanReferences, }: AIMarkdownProps) => react.JSX.Element; declare const _default: typeof AIMarkdownComponent; /** * Props for {@link AIMarkdownSmoothStream}: the full `` * surface plus `smooth*`-prefixed pacing knobs (prefixed so future base * props can never collide with the shell's additions). */ interface AIMarkdownSmoothStreamProps extends AIMarkdownProps { /** * Pacing preset — the whole tuning surface at this level: `'smooth'` * (extra buffer, almost never runs dry between server flushes), * `'balanced'` (default: minimal lag that still bridges typical * bursts), `'responsive'` (lowest lag, accepts occasional pauses). * The reveal rate itself is adaptive — it tracks the source's measured * arrival cadence — so there are no numeric speed props here; advanced * numeric overrides live on `createSmoothStreamController`. */ smoothPacing?: SmoothStreamPacing; /** * Fires when the post-stream drain completes (once per stream round); * content replacement never fires it. Correctness is identity-insensitive * (read through a latest-ref), but an inline closure still defeats this * shell's `memo` — prefer a stable reference. * * Under document turn-taking, a gated chunk drains only after its turn: * this can fire long after the source stream ended — and a regeneration * that happened entirely while the chunk was still gated is invisible to * the reveal, so the eventual drain DOES fire it (the finally-revealed * message did complete). */ onSmoothDrained?: () => void; /** * Default `true`. Inside ``, chunks sharing a * `documentId` take turns revealing (one typewriter, one cursor); see * {@link useDocumentSmoothStream}. Set `false` to keep this chunk's * reveal independent — it neither waits for predecessors nor blocks * successors (the escape hatch for chunks inserted out of mount order, * e.g. a regenerated middle message). Coordination also requires an * explicit `documentId` prop; without one this flag is moot. */ smoothCoordination?: boolean; /** Reserve the document queue slot while awaiting input; clear on start * or completion of an empty result. Does not display a streaming cursor. */ smoothWaiting?: boolean; } /** * `` with built-in typewriter pacing: the incoming `content` * is revealed grapheme-by-grapheme at a backlog-adaptive rate, and the * revealed prefix is what actually renders. Because the prefix grows * append-only, every frame rides the incremental-parse fast path. * * `streaming` semantics shift one step: the value you pass marks the * SOURCE stream's liveness; the inner component (and so the cursor slot * and context consumers) sees `streaming === true` until the reveal has * also drained — the cursor must not unmount while pixels still move. * * Footgun: `blockMemo={false}` also disables incremental parsing, which * turns per-frame reveals into per-frame full reparses. Leave block-memo * on (the default) when smoothing. * * For custom composition (mantine wrapper, skip-animation buttons), use * {@link useDocumentSmoothStream} (coordinated) or {@link useSmoothStream} * (standalone) directly — either result spreads into any wrapper. * * @example * ```tsx * * ``` */ declare const AIMarkdownSmoothStreamComponent: ({ smoothPacing, onSmoothDrained, smoothCoordination, smoothWaiting, content, streaming, ...rest }: AIMarkdownSmoothStreamProps) => react.JSX.Element; declare const AIMarkdownSmoothStream: typeof AIMarkdownSmoothStreamComponent; export { type AIMarkdownAggregate, type AIMarkdownBehaviorGroups, type AIMarkdownBehaviorProps, type AIMarkdownBehaviorsCore, AIMarkdownBehaviorsProvider, type AIMarkdownColorScheme, type AIMarkdownCustomComponents, type AIMarkdownDocumentInfo, AIMarkdownDocuments, type AIMarkdownDocumentsProps, type AIMarkdownExtensionGroups, type AIMarkdownExtraStylesComponent, type AIMarkdownExtraStylesProps, type AIMarkdownMetadata, type AIMarkdownPipelineProps, type AIMarkdownProps, AIMarkdownSmoothStream, type AIMarkdownSmoothStreamProps, AIMarkdownStabilityPolicy, type AIMarkdownStabilityTable, type AIMarkdownStateCore, type AIMarkdownStateGroups, AIMarkdownStateProvider, AIMarkdownStreamingCursor, type AIMarkdownStreamingCursorProps, type AIMarkdownStreamingIndicatorComponent, type AIMarkdownStreamingIndicatorProps, type AIMarkdownThemeInfo, type AIMarkdownThemeProps, type AIMarkdownTypographyComponent, type AIMarkdownTypographyProps, type AIMarkdownVariant, type UseDocumentSmoothStreamOptions, type UseSmoothStreamOptions, type UseSmoothStreamResult, _default as default, defineBehaviors, definePipeline, defineTheme, useAIMarkdown, useAIMarkdownBehaviors, useAIMarkdownDocument, useAIMarkdownMetadata, useAIMarkdownState, useAIMarkdownTheme, useDocumentRegistry, useDocumentSmoothStream, useSmoothStream, useStableRecord, useStableValue };