@vectojs/markdown 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ import { type ContentProjection, type ContentProjectionHint, GlyphRasterAtlas, type GlyphRasterAtlasStats, IRenderer } from '@vectojs/core';
2
+ import { UIComponent } from '@vectojs/ui';
3
+ import { type MarkdownTheme } from './theme';
4
+ /**
5
+ * A single self-rendering entity for fenced code blocks.
6
+ *
7
+ * Replaces the old N×M child-entity explosion (Container → Stack → Text per
8
+ * segment per line) with a flat leaf that draws its own background + text.
9
+ */
10
+ export declare class CodeBlock extends UIComponent {
11
+ private lines;
12
+ private grid;
13
+ /** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
14
+ private rawLines;
15
+ private cellWidth;
16
+ private source;
17
+ /** Bumped by {@link buildLines} and {@link setSelectable}; read by `Scene`. */
18
+ private contentEpoch;
19
+ private lang;
20
+ private theme;
21
+ /**
22
+ * Assigned in the constructor rather than as a field initializer: both come
23
+ * from `theme`, and a field initializer runs before the constructor body has
24
+ * a `theme` to read.
25
+ */
26
+ private lineH;
27
+ private pad;
28
+ private codeFont;
29
+ selectable: boolean;
30
+ /**
31
+ * @param theme Any subset of {@link MarkdownTheme}; missing keys fall back to
32
+ * `DEFAULT_THEME` in `./theme`. Accepting a partial theme keeps callers that were
33
+ * written against an earlier, smaller `MarkdownTheme` working — this class
34
+ * is public API, and a hand-built theme literal would otherwise start
35
+ * throwing `lineHeight must be a positive finite number` the moment a new
36
+ * size key was added.
37
+ */
38
+ constructor(code: string, lang: string, maxWidth: number, theme: MarkdownTheme, selectable?: boolean);
39
+ /** Re-parse code content (e.g. for live editing). */
40
+ setCode(code: string, lang?: string): this;
41
+ /** Enable or disable browser-native selection for this code block. */
42
+ setSelectable(selectable: boolean): this;
43
+ getContentEpoch(): number;
44
+ /**
45
+ * Change the block's box width.
46
+ *
47
+ * Deliberately does **not** rebuild the grid or the highlight, because code does
48
+ * not reflow: lines are placed on a fixed monospace grid at `col × cellWidth` and
49
+ * a long line overflows rather than wrapping, so `height` is a function of line
50
+ * *count* alone. The width only sizes the rounded background. Anything that would
51
+ * change the glyph geometry — the source, the language, the font — goes through
52
+ * {@link setCode} and invalidates the grid there.
53
+ *
54
+ * @returns `this` for chaining.
55
+ */
56
+ setWidth(width: number): this;
57
+ getContentProjection(hint?: ContentProjectionHint): ContentProjection | null;
58
+ /**
59
+ * Re-highlight the code, reusing the highlight of any unchanged line prefix.
60
+ *
61
+ * Streaming appends to the END of a block, so all but the last line or two are
62
+ * byte-identical to the previous call — yet this used to re-highlight every
63
+ * line on every chunk, making a streamed block O(N) per append and O(N^2)
64
+ * overall. Reusing the stable prefix makes an append proportional to what
65
+ * actually changed.
66
+ *
67
+ * The last previously-seen line is deliberately NOT reused: a chunk usually
68
+ * lands mid-line, so that line's text (and therefore its tokenization) changes.
69
+ */
70
+ private buildLines;
71
+ private ensureGrid;
72
+ /** Code blocks are decorative — not interactive. */
73
+ isPointInside(): boolean;
74
+ render(r: IRenderer): void;
75
+ }
76
+ /**
77
+ * Instrumentation for the code-block glyph atlas in use, or `null` before first
78
+ * use.
79
+ *
80
+ * Exposed so an app or benchmark can confirm the atlas is actually active and
81
+ * reusing slots. Watch `resets`: a steadily climbing count means the glyph set is
82
+ * unbounded for the atlas size, so every reset re-rasterizes everything and the
83
+ * atlas is doing net harm rather than saving work.
84
+ *
85
+ * Reports the *most recently used* atlas, which after a zoom is the one now being
86
+ * blitted — see {@link codeAtlas}.
87
+ */
88
+ export declare function codeAtlasStats(): GlyphRasterAtlasStats | null;
89
+ /**
90
+ * The code-block atlas most recently blitted from, or `null` before first use.
91
+ *
92
+ * For instrumentation that must map a traced `drawImage` back to the glyph it
93
+ * painted — a blit carries only a source rect, so `slotAt()` is the only way to
94
+ * recover the cluster and its metrics. Used by `e2e/text-projection.e2e.ts` to
95
+ * keep the code-grid positioning assertions working on the blit path.
96
+ *
97
+ * "Most recently used" rather than "the one" because atlases are pooled per DPR:
98
+ * a caller resolving a traced blit wants the atlas that produced it, which is the
99
+ * one the last render selected. Compare its {@link GlyphRasterAtlas.pixelRatio}
100
+ * against {@link IRenderer.pixelRatio} to assert the blit is 1:1.
101
+ */
102
+ export declare function codeAtlas(): GlyphRasterAtlas | null;
@@ -0,0 +1,33 @@
1
+ import { Entity, IRenderer } from '@vectojs/core';
2
+ /**
3
+ * Leaf entities the Markdown renderer composes: the `<hr>` rule, the blockquote
4
+ * accent bar, and a bare container for nested layouts.
5
+ *
6
+ * These live outside `Markdown.ts` because `MathBlock` extends
7
+ * `MarkdownContainer`. With the base class declared in `Markdown.ts`, a math
8
+ * module importing it back forms a cycle that resolves the binding to
9
+ * `undefined` when the `extends` clause is evaluated, throwing `TypeError:
10
+ * Class extends value undefined is not a constructor or null` on import.
11
+ * Verified by reintroducing it deliberately. 22 of 27 test files enter through
12
+ * `../src/Markdown`, which is the order that trips it. See
13
+ * `forge/decisions/file-decomposition-2026-08.md`.
14
+ */
15
+ /** A thin horizontal line (for `<hr>`). */
16
+ export declare class HorizontalRule extends Entity {
17
+ color: string;
18
+ constructor(w: number, color: string);
19
+ isPointInside(): boolean;
20
+ render(r: IRenderer): void;
21
+ }
22
+ /** A vertical accent bar for blockquotes. */
23
+ export declare class QuoteBorder extends Entity {
24
+ color: string;
25
+ constructor(height: number, color: string, width?: number);
26
+ isPointInside(): boolean;
27
+ render(r: IRenderer): void;
28
+ }
29
+ /** A simple concrete container entity for nested layouts. */
30
+ export declare class MarkdownContainer extends Entity {
31
+ isPointInside(_globalX: number, _globalY: number): boolean;
32
+ render(_r: any): void;
33
+ }
@@ -0,0 +1,133 @@
1
+ import type { TokenizerAndRendererExtension } from 'marked';
2
+ /**
3
+ * GFM footnotes: the two `marked` tokenizer extensions, their token types, and
4
+ * the marker text a reference renders as.
5
+ *
6
+ * This module is the **single** registration source for both `marked.use` call
7
+ * sites — `Markdown.ts` and `MarkdownWorker.ts`. They must agree exactly: a
8
+ * worker lexing with a different extension set than the main thread produces
9
+ * tokens the renderer has no arm for, which is why `MarkdownWorker.ts` already
10
+ * carries a comment demanding lockstep for the math tokenizers. Sharing one
11
+ * array makes the divergence impossible rather than merely discouraged, and it
12
+ * costs nothing in the worker bundle: `scripts/build-worker.js` runs esbuild
13
+ * with `bundle: true`, so this import is inlined into the emitted worker source.
14
+ *
15
+ * Deliberately holds **no** entity, theme or `@vectojs/*` import. The whole
16
+ * module is inlined into the worker string, so a dependency on the render layer
17
+ * would drag the UI packages into a worker that only lexes text.
18
+ */
19
+ /**
20
+ * A footnote reference — `[^1]` or `[^note]` — as it appears mid-sentence.
21
+ *
22
+ * Carries `label`, not `text`. That is load-bearing in two places: `Markdown`'s
23
+ * `producesEntity` falls back to `'text' in token` for an unknown type and
24
+ * `renderToken`'s `default:` arm renders `.text` as a plain block, so a token
25
+ * spelling its payload `text` would silently route through the fallback and
26
+ * reproduce the very defect this replaces. A distinct field name makes an
27
+ * unhandled footnote token render nothing instead of rendering wrongly.
28
+ */
29
+ export interface FootnoteRefToken {
30
+ type: 'footnoteRef';
31
+ raw: string;
32
+ /** The label exactly as written between `[^` and `]`, e.g. `1` or `note`. */
33
+ label: string;
34
+ }
35
+ /** A footnote definition line — `[^1]: The note.` — as its own block. */
36
+ export interface FootnoteDefToken {
37
+ type: 'footnoteDef';
38
+ raw: string;
39
+ /** The label exactly as written, matching a {@link FootnoteRefToken}'s. */
40
+ label: string;
41
+ /** The note body, source text with inline markup left unparsed. */
42
+ body: string;
43
+ }
44
+ /**
45
+ * The two extensions, in the order they are registered.
46
+ *
47
+ * ## Neither supplies `start()`, and the block one must not
48
+ *
49
+ * `start()` is not the harmless optimisation it looks like. `Lexer.blockTokens`
50
+ * clips the text handed to the paragraph tokenizer whenever any extension's
51
+ * `startBlock` hook reports a position, and sets a flag that merges the *next*
52
+ * paragraph into the clipped one — so a `[^` anywhere ahead retroactively
53
+ * re-groups paragraphs already emitted.
54
+ *
55
+ * Measured against marked 18.0.7 on `incrementalLex.ts`'s own probe string,
56
+ * `'Term\n: definition-ish\n| partial | table |\n| --- |\n\nAfter.\n'` followed
57
+ * by `'\n[^1]: n\n'`:
58
+ *
59
+ * - with a block `start()`: `[paragraph, space, paragraph, space, footnoteDef]`
60
+ * — four content tokens collapse to three and the `Term` paragraph is **lost**
61
+ * - without it: `[paragraph, paragraph, space, paragraph, space, footnoteDef]`,
62
+ * identical to the no-extension baseline
63
+ *
64
+ * It also breaks the invariant every incremental offset is derived from — that
65
+ * the tokens' `raw` strings tile their source. With a block `start()`,
66
+ * `'A[^1] B[^2].\n\n[^1]: One.\n[^2]: Two.\n'` yields a paragraph whose `raw` is
67
+ * `'A\n[^1] B[^2].'`: a newline the source does not contain. A 2x2 matrix over
68
+ * (block `start()`, inline `start()`) attributes both symptoms to the block one
69
+ * alone.
70
+ *
71
+ * The inline `start()` is simply not load-bearing — with and without it the
72
+ * inline token sequence is identical — so it is omitted for the smaller surface.
73
+ *
74
+ * Omitting the block `start()` costs one spec-adjacent behaviour, and it is the
75
+ * behaviour that is already correct: a definition on the line directly after a
76
+ * paragraph line, with no blank line between, is absorbed into that paragraph.
77
+ * CommonMark says the same of link reference definitions (they cannot interrupt
78
+ * a paragraph) and GFM says it of footnote definitions.
79
+ *
80
+ * ## Why an extension is enough
81
+ *
82
+ * A `marked.use` extension **runs before** the built-in tokenizers, not after:
83
+ * `Lexer.blockTokens` and `Lexer.inlineTokens` both begin with
84
+ * `this.options.extensions?.<level>?.some(...)`, and `Marked.use` inserts with
85
+ * `unshift`. So `footnoteRef` claims `[^1]` ahead of the built-in `link` rule
86
+ * and `footnoteDef` claims the definition line ahead of the `def` rule. Both are
87
+ * necessary. Without them, marked 18.0.7 splits on whether the note body
88
+ * contains a space, because a link destination cannot:
89
+ *
90
+ * - `[^1]: The note.` → `[paragraph, space, paragraph]`, the definition showing
91
+ * as a stray body paragraph and `Here[^1] is text.` printing its raw syntax
92
+ * - `[^1]: Note.` → `[paragraph, space, def]`, where the reference becomes a
93
+ * real inline `link` with `href: 'Note.'` — a **clickable link to a garbage
94
+ * URL** — and the definition line vanishes from the output entirely
95
+ *
96
+ * A test written only against the first case passes while the second still ships.
97
+ *
98
+ * Claiming the definition before the `def` rule has a second effect worth
99
+ * naming: `tokens.links` stays empty, so a footnoted document no longer trips
100
+ * the permanent `'link-definition'` degrade in `incrementalLex.ts`.
101
+ *
102
+ * ## `renderer` is required but unreachable
103
+ *
104
+ * `marked.use` demands one for a custom token, and `@vectojs/markdown` never
105
+ * calls `marked.parse` — it renders from the token tree. Returning `raw`
106
+ * matches what the math extensions do, so an HTML round-trip is lossless rather
107
+ * than silently dropping the note.
108
+ */
109
+ export declare const FOOTNOTE_EXTENSIONS: TokenizerAndRendererExtension[];
110
+ /**
111
+ * The visible marker for a label: `1` → `[1]`.
112
+ *
113
+ * Brackets rather than a raised superscript because {@link TextStyle} has no
114
+ * baseline shift, and `InlineObjectSurface` exposes only `drawImage` — so a
115
+ * genuinely raised marker would mean rasterizing text, at a cost far past what a
116
+ * reference marker is worth. Size alone carries the signal instead
117
+ * (`theme.footnoteMarkerScale`).
118
+ *
119
+ * Unicode superscript digits (`¹`) were the obvious alternative and are wrong
120
+ * here: they exist only for digits, so `[^note]` could not use them, and a
121
+ * document mixing numeric and named labels would render two different marker
122
+ * styles. Font coverage for the full set is also uneven.
123
+ *
124
+ * The `^` is dropped — `[^1]` is source syntax, `[1]` is the conventional
125
+ * printed marker — and the label is printed **as written** rather than
126
+ * renumbered to GFM's 1, 2, 3 by order of first reference. Renumbering needs
127
+ * document-wide state, which is exactly the non-local dependency that makes
128
+ * incremental lexing unsound: a reference arriving late would renumber markers
129
+ * already on screen. incremark reaches the same conclusion from the other
130
+ * direction, patching micromark to stop checking whether a definition exists so
131
+ * a reference can parse before its definition arrives.
132
+ */
133
+ export declare function footnoteMarker(label: string): string;
@@ -0,0 +1,126 @@
1
+ import type { InlineObjectBox, InlineObjectSurface } from '@vectojs/core';
2
+ import type { Token, Tokens } from 'marked';
3
+ /**
4
+ * Image predicates over a `marked` token tree, plus the raster store for images
5
+ * that render *inline* rather than as their own block.
6
+ *
7
+ * Still a leaf — it imports no other module of this package, so nothing here has
8
+ * an edge back into `Markdown.ts`. The predicates are pure; the raster store below
9
+ * is not, and is here rather than in `markdown-inline.ts` so that everything
10
+ * deciding how an image reaches the screen lives in one file. The predicates are
11
+ * kept together because they decide, between them, whether a paragraph renders as
12
+ * one `RichText` or as a `Stack` of runs and images — a disagreement among them
13
+ * silently drops a picture. See
14
+ * `forge/decisions/file-decomposition-2026-08.md`.
15
+ */
16
+ /**
17
+ * Whether a paragraph renders as a `Stack` of runs and images rather than one
18
+ * `RichText`.
19
+ *
20
+ * The same test the `paragraph` render arm uses, so the reconciler and the
21
+ * renderer cannot disagree about which shape a token produces.
22
+ *
23
+ * The search is over **descendants, not direct children**. `marked` nests an
24
+ * image as deeply as the source does — `[![a](u)](dest)` is
25
+ * `paragraph > link > image` and `- item ![a](u)` is
26
+ * `list_item > text > [text, image]` — so a direct-children test failed every
27
+ * nested form, sent the run to `inlineRunRichText`, which has no image support,
28
+ * and dropped the image with no warning. Recursing costs one walk of an inline
29
+ * run and removes the whole class rather than the two shapes that were reported.
30
+ */
31
+ export declare function paragraphHasImage(token: Tokens.Paragraph): boolean;
32
+ /**
33
+ * Whether any token in this inline run, at any depth, is an image.
34
+ *
35
+ * The one place the question is answered, so the predicate above, the list-item
36
+ * tier check and the flattening the render arms do cannot drift apart.
37
+ */
38
+ export declare function containsImage(tokens: Token[] | undefined): boolean;
39
+ /**
40
+ * An inline run with nested images lifted to the top level, in source order.
41
+ *
42
+ * The paragraph arm splits a run into one `Stack` child per image plus one per
43
+ * maximal run of non-image tokens, which requires every image to be a direct
44
+ * member of the array it iterates. An image inside a link or an emphasis is not,
45
+ * so the run is flattened first.
46
+ *
47
+ * A wrapper is replaced by its children rather than dropped, so the text inside a
48
+ * link that also holds an image survives. Only wrappers **containing** an image
49
+ * are opened: a plain link keeps its own token, and therefore keeps the styling
50
+ * and click handling `renderInlineToRichText` gives it.
51
+ */
52
+ /**
53
+ * Every image in an inline run, at any depth, in source order.
54
+ *
55
+ * Pairs with `stripImages`: together they partition a run into the prose a
56
+ * `RichText` can render and the images it cannot.
57
+ */
58
+ export declare function imagesOf(tokens: Token[] | undefined): Tokens.Image[];
59
+ /**
60
+ * The same token with every nested image removed, prose intact.
61
+ *
62
+ * A wrapper that held only an image is dropped; one that also held text keeps the
63
+ * text. Used for a list item's lead run, which must show its marker and its prose
64
+ * while its images render as blocks beneath.
65
+ */
66
+ export declare function stripImages<T extends Token>(token: T): T;
67
+ export declare function liftNestedImages(tokens: Token[]): Token[];
68
+ /** Index of the last `image` token in an inline run, or -1 if there is none. */
69
+ export declare function lastIndexOfImage(tokens: Token[]): number;
70
+ /**
71
+ * How many `Stack` children the paragraph render arm builds for an inline run.
72
+ *
73
+ * One child per image, plus one per *maximal run* of consecutive non-image
74
+ * tokens — the arm merges those into a single `RichText` via `flushText`, so this
75
+ * is not `tokens.length`. Kept in lockstep with that arm; it is what
76
+ * `updateImageParagraph` checks to confirm the entity it was handed is the one
77
+ * built for the old tokens.
78
+ */
79
+ /**
80
+ * A decoding raster for one inline image, keyed by URL.
81
+ *
82
+ * Separate from the paragraph path's `Image` entity, which owns its own bitmap and
83
+ * resizes itself in `onLoad`. An inline image cannot do that: it is an
84
+ * `InlineObject`, and the box it occupies is fixed when the span is collected, so
85
+ * the natural size has to be readable from here before the next layout rather than
86
+ * applied to an entity afterwards.
87
+ */
88
+ export interface InlineImageRaster {
89
+ /** `undefined` when this environment has no `Image` (SSR, plain unit tests). */
90
+ bitmap?: HTMLImageElement;
91
+ decoded: boolean;
92
+ /** Natural size, known only once decoded. */
93
+ naturalWidth?: number;
94
+ naturalHeight?: number;
95
+ /** Set when the decode failed, so a broken URL is not retried every frame. */
96
+ failed?: boolean;
97
+ }
98
+ /** Subscribe `notify` to inline-image decodes. Idempotent per closure. */
99
+ export declare function subscribeInlineImageRaster(notify: () => void): void;
100
+ /**
101
+ * Unsubscribe `notify`.
102
+ *
103
+ * Must be called on teardown: the set is module-level and lives as long as the
104
+ * page, so a retained closure retains the whole entity tree that created it.
105
+ */
106
+ export declare function unsubscribeInlineImageRaster(notify: () => void): void;
107
+ /**
108
+ * Ensure the raster for `src` is decoding, and return it.
109
+ *
110
+ * Synchronous and idempotent: the span collector calls it while measuring and the
111
+ * paint path calls it on every visible frame, and only the first call starts a
112
+ * decode. Exported because the span collector needs the natural size to size its
113
+ * box, which is the whole reason this store reports one.
114
+ */
115
+ export declare function ensureInlineImageRaster(src: string): InlineImageRaster;
116
+ /**
117
+ * Paint one inline image into the box the layout engine reserved for it.
118
+ *
119
+ * Draws nothing until the raster decodes — one frame of empty box, then a repaint
120
+ * through {@link inlineImageRasterWaiters}. Mirrors `paintInlineMath`; a
121
+ * placeholder slab would flash a grey rectangle mid-sentence on every first paint.
122
+ */
123
+ export declare function paintInlineImage(src: string, surface: InlineObjectSurface, box: InlineObjectBox): void;
124
+ /** Drop every cached raster. Tests only — a decode is process-wide state. */
125
+ export declare function clearInlineImageRasters(): void;
126
+ export declare function expectedImageParagraphChildren(tokens: Token[]): number;
@@ -0,0 +1,58 @@
1
+ import { type StyledSpan, type TextStyle } from '@vectojs/core';
2
+ import { RichText } from '@vectojs/ui';
3
+ import type { Token } from 'marked';
4
+ import type { MarkdownTheme } from './theme';
5
+ /**
6
+ * Inline tokens to `RichText` spans: the entity decoder, the span collector and
7
+ * its switch, the unclosed-delimiter scanner for optimistic streaming, and the
8
+ * `RichText` factory.
9
+ *
10
+ * Depends one way on `./markdown-math` (inline formulas become object spans) and
11
+ * takes the theme as a type only, so nothing here imports the component. See
12
+ * `forge/decisions/file-decomposition-2026-08.md`.
13
+ */
14
+ /** Decode basic HTML entities that `marked` emits in token text. */
15
+ export declare function decodeEntities(text: string): string;
16
+ /**
17
+ * Recursively walk the inline token tree, accumulating {@link StyledSpan}s
18
+ * with inherited style overrides (bold, italic, etc.).
19
+ */
20
+ export declare function collectSpans(tokens: Token[], inherited: TextStyle, theme: Required<MarkdownTheme>, out: StyledSpan[],
21
+ /**
22
+ * The size the enclosing block is drawn at, when it is not the theme body size.
23
+ *
24
+ * Only inline math uses it: `ex` is font-relative, so a formula's reserved box
25
+ * has to be resolved against the size of the run it sits in. A heading carries
26
+ * its size in its `font` string rather than in any span style, so it cannot be
27
+ * recovered from `inherited`.
28
+ */
29
+ blockFontSize?: number): void;
30
+ /**
31
+ * One trailing inline construct that has opened but not closed yet.
32
+ *
33
+ * `at` is the index in the scanned text of the construct's first syntax
34
+ * character, so the caller can split there: everything before it keeps whatever
35
+ * `marked` already decided, everything after it is the construct's content.
36
+ */
37
+ export interface UnclosedInline {
38
+ kind: 'strong' | 'em' | 'codespan' | 'link';
39
+ /** Index of the opening marker's first character. */
40
+ at: number;
41
+ /** Index just past the opening marker, where the content starts. */
42
+ contentAt: number;
43
+ }
44
+ /**
45
+ * Find the last unclosed inline construct in one trailing text run.
46
+ *
47
+ * Only ever called with the text of the FINAL inline token of the document's
48
+ * final paragraph. That is the only place an unclosed construct can be: a
49
+ * construct that closed is already its own `strong`/`em`/`codespan`/`link`
50
+ * token, so whatever syntax characters survive into a plain text token are
51
+ * exactly the ones `marked` could not pair up.
52
+ *
53
+ * Returns `null` when nothing plausible is open, which is the common case and
54
+ * must stay cheap — this runs once per streamed chunk.
55
+ */
56
+ export declare function findUnclosedInline(text: string): UnclosedInline | null;
57
+ /** Parse inline markdown tokens and produce a {@link RichText} entity. */
58
+ export declare function renderInlineToRichText(tokens: Token[] | undefined, fallbackText: string, font: string, color: string, maxWidth: number, theme: Required<MarkdownTheme>, selectable: boolean, onLinkClick?: (url: string) => void): RichText;
@@ -0,0 +1,169 @@
1
+ import { type InlineObjectBox, type InlineObjectSurface, type DevtoolsDescriptor } from '@vectojs/core';
2
+ import type { Token, Tokens } from 'marked';
3
+ import { MarkdownContainer } from './markdown-entities';
4
+ /**
5
+ * Begin (or join) loading the math engine, resolving once formulas typeset
6
+ * synchronously.
7
+ *
8
+ * Idempotent and safe to call from anywhere: the promise is cached, so N callers
9
+ * and N documents share one module load. Rejection is swallowed deliberately —
10
+ * a failed load must degrade to TeX source in a CodeBlock, not reject a caller's
11
+ * `await close()` or leave an unhandled rejection on the page. `mathConverter`
12
+ * simply stays null and every formula keeps rendering as source.
13
+ */
14
+ export declare function preloadMathJax(): Promise<void>;
15
+ /** Whether formulas can be typeset without waiting. Exposed for tests. */
16
+ export declare function isMathJaxReady(): boolean;
17
+ /** Convert an `ex` measurement to px at a given font size. */
18
+ export declare function exToPx(ex: number, fontSize: number): number;
19
+ /**
20
+ * The px size out of a CSS font shorthand (`'bold 28px Inter, sans-serif'` → 28).
21
+ *
22
+ * `undefined` when there is no `px` size to read, so the caller can fall back to
23
+ * the theme rather than silently substituting a wrong number. `@vectojs/ui` has an
24
+ * equivalent `fontSizePx` in `measure.ts`, but it is not re-exported from that
25
+ * package's barrel and it returns a hardcoded 16 on failure, which would hide a
26
+ * malformed font behind a plausible-looking box.
27
+ *
28
+ * Deliberately not a regex. The obvious `/(\d+(?:\.\d+)?)px/` is polynomial: the
29
+ * digit run can backtrack from every start position when no `px` follows, so a
30
+ * font string of many digits costs O(n^2) — CodeQL flagged exactly that here
31
+ * (`js/polynomial-redos`, high), and `font` comes from caller-supplied theme
32
+ * input. Anchoring on `px` first and walking back over the digits is linear.
33
+ */
34
+ export declare function fontSizeFromFont(font: string): number | undefined;
35
+ /**
36
+ * A converted formula: its SVG data URI and the intrinsic box scraped off it.
37
+ *
38
+ * The box is in **`ex` units**, not px, because `ex` is font-relative and one
39
+ * cached conversion is reused across runs of different sizes (inline math in a
40
+ * heading versus body prose). Callers resolve to px with {@link exToPx} at the
41
+ * size of the run the formula actually sits in.
42
+ */
43
+ interface MathRender {
44
+ uri: string;
45
+ /** Intrinsic width in `ex`. */
46
+ widthEx: number;
47
+ /** Intrinsic height in `ex`, ascent + descent. */
48
+ heightEx: number;
49
+ /**
50
+ * How far the box descends below the text baseline, in `ex`, as a positive
51
+ * number.
52
+ *
53
+ * MathJax emits this as `style="vertical-align:-N ex"` on the root `<svg>`.
54
+ * Measured on 8 formulas spanning subscripts, superscripts, fractions, big
55
+ * operators and radicals, it equals the viewBox-derived depth exactly, so it
56
+ * is read straight off the attribute rather than computed from the viewBox.
57
+ */
58
+ depthEx: number;
59
+ }
60
+ /**
61
+ * Subscribe `notify` to inline-formula raster decodes.
62
+ *
63
+ * A function rather than exporting the `Set`, so the collection stays private to
64
+ * this module once the math cluster moves out of `Markdown.ts`. Idempotent per
65
+ * closure, since `Set` de-duplicates.
66
+ */
67
+ export declare function subscribeInlineMathRaster(notify: () => void): void;
68
+ /**
69
+ * Unsubscribe `notify`.
70
+ *
71
+ * Must be called on teardown: the set is module-level and lives as long as the
72
+ * page, so a retained closure retains the whole entity tree that created it.
73
+ */
74
+ export declare function unsubscribeInlineMathRaster(notify: () => void): void;
75
+ /**
76
+ * Paint one inline formula into the box the layout engine reserved for it.
77
+ *
78
+ * Draws nothing until the raster has decoded — one frame of blank box, then a
79
+ * repaint via {@link inlineMathRasterWaiters}. Drawing a placeholder slab instead
80
+ * would flash a grey rectangle mid-sentence on every first paint.
81
+ */
82
+ export declare function paintInlineMath(uri: string, surface: InlineObjectSurface, box: InlineObjectBox): void;
83
+ export declare const MATH_LANGS: Set<string>;
84
+ /**
85
+ * Whether a token subtree contains an `inlineMath` token.
86
+ *
87
+ * Recursive because inline math nests: inside `strong`/`em`, a link's children, a
88
+ * list item's tokens, a blockquote, or a table cell. Used only to decide whether
89
+ * to start the lazy MathJax load, so a false negative delays typesetting rather
90
+ * than corrupting output — but a missed nesting site means a formula in, say, a
91
+ * table cell never typesets at all.
92
+ */
93
+ export declare function containsInlineMath(token: Token): boolean;
94
+ /**
95
+ * Whether a fenced-code token's source actually contains its closing fence.
96
+ *
97
+ * `marked` lexes an unterminated fence as a COMPLETE `code` token as soon as the
98
+ * info string is read, so a formula streamed a few characters at a time arrives
99
+ * as a long run of whole tokens, nearly all of them syntactically invalid TeX.
100
+ * The token carries no "closed" flag (probed against marked 18.0.7: the keys are
101
+ * exactly `type`, `raw`, `lang`, `text` whether or not the fence is closed), so
102
+ * `raw` is the only signal. Per CommonMark a closing fence is a line of at least
103
+ * as many of the SAME fence character as the opening, indented at most three
104
+ * spaces, followed by nothing but whitespace.
105
+ */
106
+ export declare function isFenceClosed(raw: string): boolean;
107
+ /**
108
+ * Whether this `code` token renders as a typeset formula rather than a CodeBlock.
109
+ *
110
+ * Single source of truth for that decision, because three places have to agree
111
+ * on it: the render arm, the top-level in-place update path, and the blockquote
112
+ * tail path. If the two update paths disagreed with the renderer they would call
113
+ * `setCode` on an entity that is not a CodeBlock, or leave a CodeBlock on screen
114
+ * where a formula belongs.
115
+ *
116
+ * An empty closed fence is deliberately NOT math: it renders as the empty
117
+ * CodeBlock any other empty fence would, rather than as a zero-width image.
118
+ */
119
+ export declare function rendersAsMath(token: Tokens.Code): boolean;
120
+ /**
121
+ * A cached formula render, or null when one is not available *yet*.
122
+ *
123
+ * Null deliberately means two things at once — "MathJax is not loaded" and "the
124
+ * conversion failed" — because the caller's response to both is identical: show
125
+ * the TeX source in a CodeBlock. Keeping them one signal is what let the render
126
+ * arm stay unchanged when loading became lazy. A cache hit is answered even
127
+ * before MathJax loads, so a formula already converted once (the common case on
128
+ * a re-render, and for the closed fence whose `raw` grows by a trailing newline)
129
+ * never waits on the module.
130
+ *
131
+ * The cache lookup being ahead of the `mathConverter` check is intentional but
132
+ * currently unobservable: `mathConverter` only ever goes null -> set, so nothing
133
+ * can be in the cache while it is still null. Swapping the two lines changes no
134
+ * behaviour today (confirmed by mutation: no test fails). It is written this way
135
+ * so the cache stays authoritative if the converter ever becomes resettable.
136
+ */
137
+ export declare function renderMathToSVGDataURI(formula: string, displayMode: boolean, color: string): MathRender | null;
138
+ /**
139
+ * One display formula: a `$$..$$` block or a closed ```` ```math ```` fence.
140
+ *
141
+ * A named class rather than a bare {@link MarkdownContainer} because the formula
142
+ * needs a stable handle, and after the switch to an inline object it has none:
143
+ * the typeset raster lives in a `paint` closure captured by the span, so removing
144
+ * the `Image` entity left nothing exposing either the source or the SVG bytes.
145
+ * Devtools, tests, and anything auditing what a formula actually rendered all
146
+ * want that. `markstream-vue` reaches the same conclusion from the DOM side and
147
+ * publishes `data-markstream-mode` on its math node for the same reason.
148
+ *
149
+ * Deliberately carries no typeset-vs-source flag. A formula MathJax has not
150
+ * converted yet renders as a bare {@link CodeBlock} of its TeX, which this class
151
+ * does not wrap — wrapping it would put a container between `content` and a
152
+ * `CodeBlock` that the streamed `setCode` path locates by type. So a flag would
153
+ * have exactly one reachable value, which is the dead-API trap that cost CTX-0208
154
+ * a debugging pass. Add it together with wrapping the fallback, or not at all.
155
+ */
156
+ export declare class MathBlock extends MarkdownContainer {
157
+ /**
158
+ * The TeX source, exactly as written between the delimiters.
159
+ *
160
+ * Also the projected text and the accessible name, so this is the one string a
161
+ * reader can find, select, and copy.
162
+ */
163
+ readonly formula: string;
164
+ /** The `data:image/svg+xml` URI of the typeset glyphs. */
165
+ readonly svgUri: string;
166
+ constructor(formula: string, svgUri: string);
167
+ getDevtoolsDescriptor(): DevtoolsDescriptor;
168
+ }
169
+ export {};