/** * Pure parsing, rendering, cache, anchor, and highlight helpers shared by `` and * ``. Their common reactive lifecycle and DOM orchestration live in * `markdown-base.class.ts`; the concrete classes supply only their distinct Shiki loading * strategies and public reactive-property declarations. * * Nothing in this shared module may `import` a *value* from `code-loader.js`'s full-table half * (`loadShikiHighlighter`/`loadShikiLanguage`): ``'s build-leanness claim rests on * its own module graph never reaching that call, and this module is in that graph. */ import{type TemplateResult}from'lit';import{type TextQuoteIndex}from'../../../internal/text-quote.js';import{type HighlightHandle}from'../../../internal/text-highlights.js';import type{LyraAnchor,LyraHighlight}from'../../viewers/document-viewer/anchors.js';import{type ShikiHighlighter}from'../code-block/shiki-types.js';import type{ShikiTransformer}from'../code-block/shiki-types.js';import{type LyraMarkedParser,type MarkdownDeps,type MarkedModule}from'./markdown-loader.js';import{type KatexApi}from'./katex-loader.js'; /** Creates a parser owned by one component instance. Parser extensions can no longer leak into * sibling instances; a consumer mutates the instance it intends to refresh. */ export declare function createMarkdownParser(marked:MarkedModule|undefined):LyraMarkedParser|undefined; /** Owns one configurable parser and replaces it only when the resolved optional-peer module * changes. Full and core use the same controller without sharing instance configuration. */ export declare class MarkdownParserController{private module?;private parser?;get(marked:MarkedModule|undefined):LyraMarkedParser|undefined;} /** Converts tabs only in a line's indentation to spaces at real tab stops. Markdown treats four * leading spaces as an indented code block, so a fixed replacement is wrong after existing spaces; * the next stop depends on the current indentation column. Shared by both Markdown variants so * their source parsing differs only in the documented syntax-highlighting loader. */ export declare function normalizeMarkdownLeadingTabs(content:string,tabSize:number):string; /** Owns the single coalesced animation frame shared by each Markdown variant's streaming path. */ export declare class MarkdownOwnedAnimationFrameController{private pending?;get handle():number|undefined;get settled():Promise |undefined;request(owner:Window,callback:()=>void):number|undefined;cancel():boolean;}export declare function escapeHtml(text:string):string; /** * Mirrors marked's own default `link()` renderer's `cleanUrl()`: a malformed * percent-escape or lone UTF-16 surrogate in the raw href throws inside * `encodeURI`, and marked's default renderer responds by dropping the anchor * (rendering the link text alone) rather than emitting a broken `href` — * returning `null` here lets the caller do the same. The * `.replace(/%25/g, '%')` compensates for `encodeURI` re-escaping the `%` of * an href that was already percent-encoded (the common case for a real * markdown link) — without it, every existing `%XX` escape would become * `%25XX`, double-encoding it. */ export declare function cleanHref(href:string):string|null; /** One pending fenced-code block discovered during a `parseMarkdownDocument()` pass whose * `(lang, code)` pair wasn't already in the highlight cache -- collected as a side effect of the * `code()` renderer so the caller (`renderMarkdown()`) knows what to highlight next, without a * second pass over the source. `key` is the highlight cache's own lookup key for this pair. */ export interface PendingHighlight{key:string;lang:string;code:string;} /** One entry of `getHeadingTree()`'s document-ordered outline. `level` already reflects * `heading-offset` -- it always matches the rendered `` tag, not the source `#` count. */ export interface MarkdownHeadingItem{id:string;label:string;level:number;} /** Upper bound on a per-instance highlight cache's entries. Each entry holds a fully-highlighted * HTML string (potentially large for a long code block), and the cache is content-addressed -- * on a long-lived instance whose `content` keeps changing (a chat transcript, live docs), an * unbounded map would retain the highlighted HTML of every code block ever rendered. 100 far * exceeds the fenced-block count of any one document, so eviction only trims blocks that * scrolled out of the content long ago. */ export declare const HIGHLIGHT_CACHE_MAX=100;export declare const HIGHLIGHT_CACHE_MAX_BYTES:number;export declare const HIGHLIGHT_CACHE_ENTRY_MAX_BYTES:number;export declare const FAILED_HIGHLIGHT_MAX=256; /** Bounded content key: raw source never remains retained merely because it was used as a cache * key. Two 32-bit FNV streams plus source length make accidental collisions vanishingly unlikely. */ export declare function markdownHighlightKey(language:string,code:string):string;export declare function addFailedHighlightKey(failed:Set,key:string):void; /** Runs each unique highlight key once with bounded parallelism so one large document cannot * fan out hundreds of simultaneous grammar/tokenization jobs. */ export declare function processPendingHighlights(pending:readonly PendingHighlight[],worker:(item:PendingHighlight)=>Promise,concurrency?:number):Promise; /** LRU read: a hit is re-inserted so Map iteration order (insertion order) keeps the first key * the least recently used one -- the entry `setCachedHighlight()` evicts when full. */ export declare function getCachedHighlight(cache:Map,key:string):string|undefined;export declare function setCachedHighlight(cache:Map,key:string,html:string,max?:number,maxBytes?:number):boolean; /** Everything `parseMarkdownDocument()` needs that would otherwise come from `this` on either * `LyraMarkdown` or `LyraMarkdownCore` -- both components resolve identical inputs from their own * properties/state and pass them through unchanged, so this is the single parsing contract for * both. */ export interface ParseMarkdownOptions{marked:MarkedModule; /** Ordered snapshots of parser defaults. Later entries override earlier entries. */ markedConfigurations?:readonly(Record |undefined)[];content:string;gfm:boolean;linkTarget:string|null; /** Raw, possibly-unnormalized `headingOffset` property value -- `finiteInteger()`-guarded * internally, same as before extraction. */ headingOffset:number;escapeHtmlOption:boolean; /** `htmlMode === 'trusted'` -- a deliberate, fully-documented opt-out of every safety net this * parser applies, `link()`/`image()`'s scheme allowlist included. `sanitize` and `escape` both * validate; only `trusted` bypasses, matching `renderMarkdownDocument()`'s own DOMPurify skip. */ trustedHtmlOption:boolean; /** Already combines `highlightCode && !streaming` -- computed by the caller since that * combination differs by call site only in name, never in meaning. */ highlightCodeOption:boolean; /** Bound LRU accessor (not the raw map): reads must go through it so a hit refreshes its * recency, exactly as before extraction. */ getCachedHighlight:(key:string)=>string|undefined;failedHighlightKeys:Set;headingAnchorsOption:boolean;mathOption:boolean; /** Already-resolved katex module (or `null`) -- each component keeps its own katex-loading * singleton (unrelated to this shared parsing logic), so the caller resolves it before calling * in, exactly as `parseMarkdown()` did internally before extraction. */ cachedKatex:KatexApi|null;pendingKeys:PendingHighlight[];headingTreeOut:MarkdownHeadingItem[];} /** * Parses `options.content` into sanitizer-ready HTML via a fresh `marked` renderer, mirroring * ``'s original `parseMarkdown()` (now shared verbatim with ``). * Every `part="..."` injected into the output, the `heading-offset`/`link-target`/ * `internal-link-prefix`-driven behavior, and the math-token extension are documented on * `LyraMarkdown`'s own class doc -- this function's contract is exactly that doc. */ export declare function parseMarkdownDocument(options:ParseMarkdownOptions):{html:string;hadMathFallback:boolean;}; /** * One variant's `katex` bookkeeping. Deliberately *per-variant* rather than one process-wide * singleton: each of ``/`` creates exactly one of these at its own * module scope, reproducing the four module-level bindings each class file used to declare for * itself. Sharing a single instance across both would also share `loadStarted`, so a * `` that started the load would suppress a ``'s own * re-render-on-resolve on the same page (and vice versa) -- a behavior change this extraction has * no reason to make. The underlying `getKatex()` promise is page-cached anyway, so two states cost * one load. */ export interface MarkdownKatexState{ /** The katex module to render this pass's math with, or `null` for the literal-TeX fallback * (peer missing *or* still loading -- the fallback is the same either way). */ getIfLoaded():KatexApi|null; /** Whether the load has definitively finished with no peer available -- distinct from * `getIfLoaded()` returning falsy, which also covers a load that's merely still in flight. Used * only to decide whether a literal fallback should also report `lr-render-error`. */ isConfirmedMissing():boolean; /** Subscribes one instance to the shared page-wide `getKatex()` load and kicks that load off the * first time math needs it. Reusing the same callback is idempotent, so repeated renders while * the peer is pending do not produce duplicate completion work. * `onResolved` runs after the module lands; the caller still guards its own liveness. */ startLoad(onResolved:()=>void):void;}export declare function createMarkdownKatexState():MarkdownKatexState; /** * Rewrites shiki's generated `
`/`` hast nodes so the highlighted output keeps the
* markdown viewers' own `part="code-block"` hook and a `language-${lang}` class on `` --
* matching the plain-render output shape exactly, so existing consumer CSS targeting either keeps
* working whether or not a given block ended up highlighted. A separate, purpose-built function
* from `code-block-shared.ts`'s own `codeBlockLineTransformer` -- that one targets
* ``'s `part="pre'`/`part='code"`/line-numbers contract, which doesn't apply here.
*/
export declare function markdownCodeTransformer(lang:string):ShikiTransformer;
/**
* Tokenizes one pending fenced block with an already-resolved highlighter and returns the exact
* string to cache (trailing newline included, matching the plain `code()` renderer's own output).
* `null` means "leave this key uncached" -- the caller records it in `failedHighlightKeys` so the
* block keeps its plain fallback permanently rather than being rediscovered as pending forever.
* Shared by both variants' `highlightPending()`; only the *loading* half above it differs.
*/
export declare function tokenizeMarkdownHighlight(hl:ShikiHighlighter,pending:PendingHighlight):string|null;
/**
* `connectedCallback()`'s optional-peer load for both variants. A settled shared cache is always
* adopted synchronously; {@link loadMarkdownDeps} / `preloadMarkdown()` is the sole eager-loading
* API when a consumer wants the first instance to avoid the dynamic-import window.
*
* The (module-cached, page-lifetime) `loadMarkdownDeps()` promise can resolve after the instance
* was removed from the DOM -- e.g. a markdown viewer inside a conditionally-rendered chat message
* or a virtualized list. Without the `isConnected` guard, a detached instance would still have its
* deps applied and a render scheduled that no one will ever see. A per-host connection generation
* also invalidates the earlier subscription when the same instance reconnects before the shared
* promise settles, so one settlement cannot apply twice to the current connection.
*/
export declare function beginMarkdownDepsLoad(host:object&{readonly isConnected:boolean;},apply:(deps:MarkdownDeps)=>void):void;
/** Every property whose change means the document has to be reparsed. */
export declare function markdownNeedsReparse(changed:Map):boolean;
/** Whether the *highlighting* configuration changed, invalidating in-flight work and the
*  permanently-failed key set. */
export declare function markdownHighlightConfigChanged(changed:Map):boolean;
/** Whether the *grammar set* changed, additionally invalidating already-highlighted output. */
export declare function markdownLanguageSetChanged(changed:Map):boolean;
/** The rendered outcome of one `renderMarkdownDocument()` pass. `headingTree` is non-`null`
*  whenever the parse itself succeeded -- including the `fallback` produced by a missing
*  `dompurify`, which still computed a real outline before refusing to render. */
export type MarkdownRenderOutcome={status:'fallback';error:unknown;headingTree:MarkdownHeadingItem[]|null;}|{status:'rendered';html:string;headingTree:MarkdownHeadingItem[];
/** A math token rendered its literal TeX fallback *and* the peer is confirmed missing --
*  worth one `lr-render-error`. A fallback while the load is merely still in flight is the
*  same one-microtask transient window every other optional peer here has, and reporting it
*  would be a false positive. */
mathFailed:boolean;pendingKeys:PendingHighlight[];};export type MarkdownHtmlMode='sanitize'|'escape'|'trusted';export declare function normalizeMarkdownHtmlMode(value:unknown):MarkdownHtmlMode;export interface RenderMarkdownOptions{
/** This variant's own tag, for the peer-failure diagnostics below. */
tag:'lr-markdown'|'lr-markdown-core';deps:MarkdownDeps;htmlMode:MarkdownHtmlMode;math:boolean;
/** The instance's own `parseMarkdown()` -- see `ParseMarkdownOptions` for what it resolves. */
parse:(marked:MarkedModule,pendingKeys:PendingHighlight[],headingTreeOut:MarkdownHeadingItem[])=>{html:string;hadMathFallback:boolean;};
/** Runs immediately after a successful parse, before sanitization -- where each instance kicks
*  off its variant's katex load. */
onParsed:()=>void;isKatexConfirmedMissing:()=>boolean;}
/**
* The optional-peer load / parse / sanitize / fallback pipeline both variants run, returning what
* changed rather than mutating the instance -- the caller owns every `@state` assignment and every
* event, so this stays a pure function of its inputs.
*
* Rendering never ships unsanitized or broken markup silently: a missing/throwing `marked`, or a
* missing/throwing `dompurify` in `sanitize` mode both fall back to plain text plus
* `lr-render-error`.
*/
export declare function renderMarkdownDocument(options:RenderMarkdownOptions):MarkdownRenderOutcome;
/** The `lr-render-error` payload for a permanently-missing `katex` peer while `math` is set. */
export declare function markdownMathPeerError(tag:'lr-markdown'|'lr-markdown-core'):Error;
/** Mirrors "the document is not final" onto the host as `aria-busy`, so assistive technology knows
*  the rendered output is still resolving (peers loading, or `streaming` still on). */
export declare function applyMarkdownAriaBusy(host:Element,busy:boolean):void;
/**
* Scrolls a `fragment` anchor's heading into view. `headingAnchors` may be off, so the target
* heading might carry no `id` attribute in the DOM at all -- and even with it on, DOMPurify's
* DOM-clobbering protection strips a slug colliding with a real `document` property name. Both
* cases fall back to re-deriving the same slug order `getHeadingTree()` was built in and matching
* by position instead of by attribute.
*/
export declare function applyMarkdownFragmentAnchor(root:Element,anchor:Extract,headingTree:readonly MarkdownHeadingItem[]):boolean;
/** Scrolls a `text-quote` anchor's resolved range into view, centered. */
export declare function applyMarkdownTextQuoteAnchor(root:Element,anchor:Extract,
/** The component's resolved locale. Text-quote matching case-folds, and casing is
*  locale-sensitive -- under `lang="tr"` an unlocalized fold never matches "İSTANBUL". */
locale?:string,index?:TextQuoteIndex):boolean;export declare const MARKDOWN_PAINTED_HIGHLIGHT_LIMIT=100;
/** One `text-quote` highlight resolved against the currently rendered content. */
export interface ResolvedHighlightRange{id:string;range:Range;}
/**
* Re-resolves every `text-quote` highlight against the current rendered content and repaints via
* the caller's `acquireHighlightHandle()` handle -- resolution is always by quote text, never by
* node identity, so a highlight set before its quote exists in `content` yet (e.g. mid-`streaming`)
* simply paints nothing until a later render's text actually contains it. `fragment` highlights
* aren't painted (there is no literal span of text to wrap/underline for a whole section).
* Returns the resolved ranges for `hitTestHighlightRanges()` to activate against.
*/
export declare function repaintMarkdownHighlights(options:{root:Element;handle:HighlightHandle;highlights:readonly LyraHighlight[];activeHighlightId:string|null;index:TextQuoteIndex;
/** The component's resolved locale; see applyMarkdownTextQuoteAnchor. */
locale?:string;}):ResolvedHighlightRange[];
/**
* Hit-tests a click point against every currently-resolved highlight's `getClientRects()`, topmost
* (last-resolved) first. The CSS Custom Highlight API paints ranges without creating any DOM element
* to attach a click listener to, so this is the only activation path that works identically on both
* paint paths -- mirrors ``'s own coordinate-based `onPageClick()` hit-test for the
* same reason (its own painted highlights sit under a text layer that intercepts most pointer
* events).
*/
export declare function hitTestHighlightRanges(ranges:readonly ResolvedHighlightRange[],x:number,y:number):string|null;
/** Returns a genuine HTML anchor from a composed-path entry, rejecting hostile and structural
* lookalikes without relying on ambient-realm `instanceof` checks. The captured native calls
* brand-check foreign and adopted elements without trusting candidate-owned realm metadata. */
export declare function markdownAnchorFromTarget(target:unknown):HTMLAnchorElement|undefined;
/** The `href` of the rendered link a click landed on, when `internal-link-prefix` claims it --
*  `null` for every other click, including one on an ordinary external link.
*
*  Compared against the raw `href` *attribute*, not the `.href` IDL property: the property is
*  always browser-resolved to an absolute URL (e.g. "https://example.com/docs") even for a
*  relative/prefixed path, which would never match a relative `internal-link-prefix`. */
export declare function internalLinkHrefFrom(e:MouseEvent,prefix:string):string|null;export interface MarkdownContentOptions{
/** The Markdown source -- also the plain-text fallback rendering when `renderedHtml` is `null`. */
content:string;
/** Whether the rendered document passed through DOMPurify. Unsanitized content gets an explicit
*  paint-containment boundary in the shared stylesheet. */
sanitized:boolean;
/** Sanitized (or deliberately unsanitized) HTML, or `null` for the plain-text fallback: peers
*  still loading, or a render attempt just fell back after a failure. The two states look
*  identical on purpose -- a consumer distinguishes them via `lr-render-error`. */
renderedHtml:string|null;
/** The host's own `aria-label`, forwarded to the element that actually owns `role="document"` --
*  a host `aria-label` doesn't reach shadow internals on its own. */
hostAriaLabel:string|null;
/** Whether the host's *resolved* `--lr-color-*` palette is a dark scheme. Shiki's dual-theme
*  output carries its light colors as plain inline `color`/`background-color` and its dark ones
*  in `--shiki-dark`/`--shiki-dark-bg`; the stylesheet's `[data-dark-theme='true']` rule is what
*  swaps them, so without this flag every highlighted block paints light on a dark page. */
isDarkTheme:boolean;onClick:(e:MouseEvent)=>void;
/** `DocumentAnchorTarget`'s `renderAnchorLiveRegion()` output. */
liveRegion:unknown;
/** A CSS length (e.g. `"20rem"`); once set, `[part="content"]` scrolls internally past this
*  height instead of growing the page. Invalid values are ignored. */
maxHeight:string;}
/** The rendered tree both variants produce: one `[part="content"]` wrapper plus the anchor-target
*  live region. Only non-empty content is focusable -- an empty document is not a scrollable region
*  worth a tab stop. */
export declare function renderMarkdownContent(options:MarkdownContentOptions):TemplateResult;
/**
* Starts (and immediately applies) the resolved-theme watch both markdown variants need so
* Shiki's dual-theme output can be switched to its dark half. Returns the teardown; call it from
* `disconnectedCallback()`/`adoptedCallback()`.
*
* Keyed on the component's own resolved `--lr-color-text`/`--lr-color-surface` rather than the
* OS-level `prefers-color-scheme` query directly, so a consumer who sets `--lr-theme-color-*`
* explicitly gets the dark palette too -- identical to ``'s own handling.
*/
export declare function watchMarkdownDarkTheme(host:HTMLElement,apply:(isDarkTheme:boolean)=>void):()=>void;
/** Resolves the current Shiki palette half for shared ThemeWatcher callbacks and explicit refresh. */
export declare function resolveMarkdownDarkTheme(host:Element):boolean;