/** * MarkdownEngine — THE one react-markdown pipeline for every surface * (chat messages, blog, docs, KB articles, legal, releases, admin previews). * * `SimpleMarkdownRenderer` and `RichMarkdownRenderer` are thin compositions * over this engine (see ./simple-markdown-renderer.tsx and ./rich/) — there * is exactly ONE parse path, ONE sanitizer stack, ONE mermaid, ONE * heading-id algorithm, ONE link-click model. Do not fork this pipeline; * extend via `componentOverrides` / `additionalRemarkPlugins` / * `preprocessContent` / `extraAllowedHtmlTags`. * * Pipeline order (each stage documented in ./sanitize.ts): * preprocessContent (composition hook, e.g. shortcodes) * → completeStreamingTail (streaming only — MUST precede the escape pass) * → escapeUnknownHtmlTags (text pre-pass, React 19 crash guard) * → remark: remarkGfm, remarkBreaks, ...additionalRemarkPlugins * → rehype: rehypeRaw → rehypeSanitize(schema) → rehypeStripUnsafe * → rehypeHighlight * → urlTransform: cardAwareUrlTransform * → components: buildBaseComponents(...) spread-last componentOverrides */ import React from 'react'; import { type Components } from 'react-markdown'; import type { PluggableList } from 'unified'; import type { ResolveLinkResult } from '../../../types/doc-source'; import { type TextSizeConfig } from './text-size'; import { type HeadingSection } from './heading-ids'; export type { ResolveLinkResult }; /** * Module-scope empty default for `brokenLinks`. * * A `brokenLinks = []` DEFAULT PARAMETER allocates a fresh array on every * render, so the `components` useMemo (which lists it as a dep) recomputed * every render, which changed `StreamingBlockRenderer`'s `components` prop * identity, which made its `memo` bail EVERY time — every completed block * re-parsed on every streamed token, defeating the entire atomic-block * optimization. Any new default that is an object/array/function MUST be * hoisted here for the same reason. * * Exported so the compositions that apply the SAME default before handing * props down (rich) share this ONE identity instead of declaring a second * module-scope empty array with a copy of this rationale. Deliberately NOT * re-exported from ./index — it is an internal identity contract between the * engine and its compositions, not public API. */ export declare const NO_BROKEN_LINKS: readonly string[]; export interface MarkdownEngineProps { content: string; className?: string; /** Backend-provided heading IDs for deep-link anchors */ sectionIds?: HeadingSection[]; /** When the page already has an H1, render markdown `#` as `

` */ demoteMarkdownH1ToH2?: boolean; /** List of broken link hrefs detected server-side (shown with [BROKEN] badge) */ brokenLinks?: readonly string[]; /** Callback for internal (non-http, non-anchor) link clicks */ onInternalLinkClick?: (path: string, options?: { expandFolder?: boolean; fromInternalLink?: boolean; }) => void; /** Current documentation path — enables internal-link mode when set */ currentPath?: string; /** Resolve an internal link href to a navigation path. */ onResolveLink?: (href: string, currentPath: string) => Promise; /** Pre-process the raw markdown string before rendering (e.g. shortcode expansion) */ preprocessContent?: (content: string) => string; /** Merge additional or override react-markdown component renderers (spread LAST — caller wins) */ componentOverrides?: Partial; /** Extra remark plugins appended after the built-in remarkGfm + remarkBreaks */ additionalRemarkPlugins?: PluggableList; /** Text sizing preset / per-element overrides */ textSize?: TextSizeConfig; /** * Extra raw-HTML tags this composition admits. Unioned into BOTH the * text pre-pass allowlist AND the sanitize schema together (the * coupled-allowlist invariant — see ./sanitize.ts). */ extraAllowedHtmlTags?: string[]; /** * Set true for the actively streaming message ONLY. Enables atomic-block * memoization + fence tail-completion + an aria-live wrapper. The caller * MUST flip back to false on completion — that final render is one * authoritative whole-document parse (block cache discarded), so * streaming can never permanently diverge from the settled output. */ streaming?: boolean; } /** * Memoized so a parent re-render with UNCHANGED props (same `content` * string, same memoized `componentOverrides`/plugins) does NOT re-parse the * markdown. Re-parsing rebuilds the entire react-markdown subtree, which * RE-MOUNTS any embedded inline entity cards — closing their open menus and * re-triggering their fetch on every chat re-render (streaming chunk AND * scroll). With stable props the renderer bails, so completed messages' * cards stay mounted. The streaming path additionally memoizes completed * atomic blocks so the in-flight message re-parses only its live tail. */ export declare const MarkdownEngine: React.NamedExoticComponent; //# sourceMappingURL=engine.d.ts.map