/** * Heading-id derivation for the unified markdown engine. * * PURE BY CONSTRUCTION. The previous design was a stateful generator whose * per-pass counters were reset in the engine's render body, and it was * unsound in two independent ways: * - memoized streaming blocks do NOT re-render on the pass that resets the * counter, so mid-stream a document with two `## Setup` headings emitted * `id="setup"` TWICE (block 0 kept its id; the re-rendered tail * re-derived from an empty counter) — invalid HTML, and `#setup` * resolved to the wrong node for the rest of the stream; * - React StrictMode double-invokes render, and heading renderers are * element types re-rendered independently of the parent that did the * reset, so every id gained a `-2` suffix in dev and extractor↔renderer * parity broke. * * Instead the ids are computed ONCE from the processed markdown source, in a * `useMemo`, into a `line → id` map. The heading renderer is a pure lookup * by `node.position.start.line`; nothing is derived from render order, so * memo bails and StrictMode double-invocation are both no-ops. * * The slug algorithm itself lives in the server-safe SSOT * `utils/markdown-heading-id` (shared with `utils/markdown-section-extractor`, * the producer of `sectionIds`); the title normalization mirrors the * extractor's `stripInlineMarkdown` so `## **Setup**` slugs identically on * both sides. */ import type { Element } from 'hast'; import { type ReactNode } from 'react'; export interface HeadingSection { id: string; title: string; level: number; } /** `line (1-based, document-wide) → heading id`. */ export type HeadingIdMap = ReadonlyMap; /** * Line offset added to unit-relative hast positions. Non-zero only on the * streaming path, where each atomic unit is parsed by its own * `ReactMarkdown` (see `StreamingBlock.startLine`). */ export declare const HeadingLineOffsetContext: import("react").Context; /** * The document's heading-id map, delivered by CONTEXT rather than baked into * the react-markdown `components` map on purpose: the map is rebuilt on every * streamed token (its input is the growing `processedContent`), so listing it * as a `components` memo dep would change that map's identity each token and * make every completed streaming block's `memo` bail — the exact regression * `NO_BROKEN_LINKS` was introduced to fix. Through context only the heading * renderers re-render. */ export declare const HeadingIdMapContext: import("react").Context; /** Look up the pre-computed id for a heading hast node. Pure. */ export declare function useHeadingId(node: Element | undefined): string | undefined; /** * Build the document's `line → id` map. Pure: same content + same * `sectionIds` always yields the same map. */ export declare function buildHeadingIdMap(content: string, sectionIds?: HeadingSection[]): HeadingIdMap; /** * Memoized `buildHeadingIdMap` for the engine, with the map's IDENTITY held * stable across content changes that don't touch any heading. * * `content` changes on every streamed token, so the `useMemo` alone rebuilt * a fresh `Map` per token; that map is the context value, so EVERY heading * consumer in every already-completed block re-rendered on every token — * the same identity-churn class `NO_BROKEN_LINKS` exists to prevent, just * one layer down. Rebuilding is cheap; re-rendering the consumers is not, so * the newly built map is compared to the previous one and the previous * object is returned when the entries match. */ export declare function useHeadingIdMap(content: string, sectionIds?: HeadingSection[]): HeadingIdMap; /** * Fallback id for a heading the source scan could NOT see — in practice only * a heading synthesized by a caller remark/rehype plugin, which carries no * source position. (Setext, blockquote/list-nested ATX and multiple raw * ``s on one line are all scanned now, so they no longer land here.) * * Routed through the SAME dedupe shape as the map so a fallback id can never * collide with an assigned one. It stays PURE — `taken` is derived from the * map, not from render order — which is why two position-less headings with * IDENTICAL text still collide with each other: disambiguating those would * require the shared mutable counter this module deleted. */ export declare function resolveFallbackHeadingId(base: string, taken: ReadonlySet): string; /** * The ids already assigned by the document's heading-id map. Cached per map * IDENTITY, which `useHeadingIdMap` now keeps stable across heading-free * token growth — so this is O(headings) once, not per heading per token. */ export declare function useAssignedHeadingIds(): ReadonlySet; /** Extract plain text from React children (headings receive mixed nodes). */ export declare function extractText(node: ReactNode): string; //# sourceMappingURL=heading-ids.d.ts.map