@vectojs/markdown 0.14.0 → 0.15.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.
- package/README.md +79 -17
- package/dist/Markdown.d.ts +58 -154
- package/dist/index.js +1462 -1208
- package/dist/index.mjs +1448 -1202
- package/dist/markdown-code.d.ts +102 -0
- package/dist/markdown-entities.d.ts +33 -0
- package/dist/markdown-image.d.ts +126 -0
- package/dist/markdown-inline.d.ts +58 -0
- package/dist/markdown-math.d.ts +169 -0
- package/dist/theme.d.ts +153 -0
- package/package.json +8 -7
|
@@ -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,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 — `[](dest)` is
|
|
25
|
+
* `paragraph > link > image` and `- item ` 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 {};
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color, typography and spacing tokens for `@vectojs/markdown`.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `Markdown.ts` so `CodeBlock` and the inline renderer can take a
|
|
5
|
+
* theme without importing the whole component, which would make the module
|
|
6
|
+
* graph cyclic. See `forge/decisions/file-decomposition-2026-08.md`.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Color, typography and spacing theme for Markdown rendering.
|
|
10
|
+
*
|
|
11
|
+
* The shape is deliberately **flat**, not a nested token tree. Every key is
|
|
12
|
+
* optional and merged over {@link DEFAULT_THEME} by a single spread, so adding a
|
|
13
|
+
* key is backward compatible and a caller may override exactly one value
|
|
14
|
+
* without restating a group. A nested `{ colors: {...}, spacing: {...} }` shape
|
|
15
|
+
* would need a deep merge and would break every existing caller.
|
|
16
|
+
*
|
|
17
|
+
* Sizes and spacing are **numbers in px**, not CSS strings, because the values
|
|
18
|
+
* are consumed by canvas layout arithmetic rather than by a stylesheet.
|
|
19
|
+
*/
|
|
20
|
+
export interface MarkdownTheme {
|
|
21
|
+
/** Body text color. */
|
|
22
|
+
textColor?: string;
|
|
23
|
+
/** Heading text color. */
|
|
24
|
+
headingColor?: string;
|
|
25
|
+
/** Code text color (inline + block). */
|
|
26
|
+
codeColor?: string;
|
|
27
|
+
/** Code block background color. */
|
|
28
|
+
codeBgColor?: string;
|
|
29
|
+
/** Blockquote border/accent color. */
|
|
30
|
+
quoteBorderColor?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Blockquote text color. Defaults to {@link MarkdownTheme.textColor} so
|
|
33
|
+
* blockquote body text matches surrounding prose unless overridden.
|
|
34
|
+
*/
|
|
35
|
+
quoteTextColor?: string;
|
|
36
|
+
/** Horizontal-rule color. */
|
|
37
|
+
hrColor?: string;
|
|
38
|
+
/** Table background color. */
|
|
39
|
+
tableBgColor?: string;
|
|
40
|
+
/** Table header background color. */
|
|
41
|
+
tableHeaderBgColor?: string;
|
|
42
|
+
/** Link text color. */
|
|
43
|
+
linkColor?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Color for TeX source shown verbatim when a formula could not be typeset.
|
|
46
|
+
* Deliberately distinct from body text so an untypeset formula is visible as
|
|
47
|
+
* a failure rather than reading as prose.
|
|
48
|
+
*/
|
|
49
|
+
mathFallbackColor?: string;
|
|
50
|
+
/** Code-block keyword color. */
|
|
51
|
+
syntaxKeywordColor?: string;
|
|
52
|
+
/** Code-block string-literal color. */
|
|
53
|
+
syntaxStringColor?: string;
|
|
54
|
+
/** Code-block comment color. */
|
|
55
|
+
syntaxCommentColor?: string;
|
|
56
|
+
/** Code-block numeric-literal color. */
|
|
57
|
+
syntaxNumberColor?: string;
|
|
58
|
+
/** Body font. */
|
|
59
|
+
bodyFont?: string;
|
|
60
|
+
/** Monospace font for code. */
|
|
61
|
+
codeFont?: string;
|
|
62
|
+
/** Base font size in px. */
|
|
63
|
+
fontSize?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Font sizes in px for heading depths 1-6. A shorter array is padded by
|
|
66
|
+
* repeating its last entry; depths past the end clamp to the last entry.
|
|
67
|
+
*/
|
|
68
|
+
headingSizes?: readonly number[];
|
|
69
|
+
/** Code-block font size in px. */
|
|
70
|
+
codeFontSize?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Table cell font size in px.
|
|
73
|
+
*
|
|
74
|
+
* Left `undefined` by default and **derived** as `fontSize - 2` (clamped to
|
|
75
|
+
* at least 1) rather than defaulted to a literal, because that is how it was
|
|
76
|
+
* hardcoded before it became a key: a caller who raises only `fontSize` must
|
|
77
|
+
* keep getting a proportionally larger table, which a fixed default would
|
|
78
|
+
* silently break.
|
|
79
|
+
*/
|
|
80
|
+
tableFontSize?: number;
|
|
81
|
+
/** Code-block line height in px. */
|
|
82
|
+
codeLineHeight?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Line height in px for body text drawn through the plain-`Text` fallback
|
|
85
|
+
* path (an unrecognised block token that still carries `text`).
|
|
86
|
+
*/
|
|
87
|
+
bodyLineHeight?: number;
|
|
88
|
+
/** Vertical gap in px between top-level blocks. */
|
|
89
|
+
blockGap?: number;
|
|
90
|
+
/** Inner padding in px of a code block. */
|
|
91
|
+
codePadding?: number;
|
|
92
|
+
/** Corner radius in px of a code block. */
|
|
93
|
+
codeRadius?: number;
|
|
94
|
+
/** Vertical gap in px between list items. */
|
|
95
|
+
listGap?: number;
|
|
96
|
+
/** Vertical gap in px between blocks inside one multi-block list item. */
|
|
97
|
+
listItemGap?: number;
|
|
98
|
+
/** Left indent in px of a blockquote's contents. */
|
|
99
|
+
quoteIndent?: number;
|
|
100
|
+
/** Width in px of a blockquote's accent border. */
|
|
101
|
+
quoteBorderWidth?: number;
|
|
102
|
+
/** Vertical gap in px between blocks inside a blockquote. */
|
|
103
|
+
quoteInnerGap?: number;
|
|
104
|
+
/** Corner radius in px of an image. */
|
|
105
|
+
imageRadius?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Height of an image that renders *inline*, as a multiple of the run's font
|
|
108
|
+
* size.
|
|
109
|
+
*
|
|
110
|
+
* Applies only where an image shares a line with text — a heading or a table
|
|
111
|
+
* cell. An image that is its own block (the ordinary `` paragraph)
|
|
112
|
+
* still renders at its natural size capped to the available width, and ignores
|
|
113
|
+
* this.
|
|
114
|
+
*
|
|
115
|
+
* A cap rather than the natural size, because an inline object's box is fixed
|
|
116
|
+
* when the span is collected while the natural size is known only after the
|
|
117
|
+
* decode, and because a 512px logo written into an `h1` would otherwise tower
|
|
118
|
+
* over its own heading. The width follows the natural aspect ratio, so a wide
|
|
119
|
+
* badge stays wide. `1.15` keeps a square icon a little shorter than the line it
|
|
120
|
+
* sits on, which is where a cap-height glyph puts its own ink.
|
|
121
|
+
*/
|
|
122
|
+
inlineImageScale?: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolved defaults. Every key of {@link MarkdownTheme} has an entry here, so a
|
|
126
|
+
* resolved theme is `Required<MarkdownTheme>` and no consumer needs a fallback.
|
|
127
|
+
*
|
|
128
|
+
* Two entries are placeholders that {@link resolveTheme} overwrites when the
|
|
129
|
+
* caller did not set them: `quoteTextColor` (follows `textColor`) and
|
|
130
|
+
* `tableFontSize` (derived from `fontSize`). They still carry a literal so this
|
|
131
|
+
* object satisfies `Required<MarkdownTheme>`; read {@link resolveTheme} for the
|
|
132
|
+
* value a caller actually gets.
|
|
133
|
+
*/
|
|
134
|
+
export declare const DEFAULT_THEME: Required<MarkdownTheme>;
|
|
135
|
+
/**
|
|
136
|
+
* Merge a caller's partial theme over {@link DEFAULT_THEME}.
|
|
137
|
+
*
|
|
138
|
+
* `tableFontSize` is **derived** from the resolved `fontSize` when the caller
|
|
139
|
+
* did not set it explicitly, so raising only `fontSize` still scales tables.
|
|
140
|
+
* A plain spread cannot express that: `DEFAULT_THEME` has to satisfy
|
|
141
|
+
* `Required<MarkdownTheme>`, so its literal would always win over the
|
|
142
|
+
* derivation.
|
|
143
|
+
*/
|
|
144
|
+
export declare function resolveTheme(theme?: MarkdownTheme): Required<MarkdownTheme>;
|
|
145
|
+
/**
|
|
146
|
+
* Font size in px for a 1-based heading depth, clamping past the end of
|
|
147
|
+
* `headingSizes` and tolerating a short or empty array.
|
|
148
|
+
*
|
|
149
|
+
* Extracted so the heading renderer and any caller inspecting the scale agree;
|
|
150
|
+
* an inline `sizes[Math.min(depth - 1, 5)]` silently yields `undefined` for a
|
|
151
|
+
* theme that supplied fewer than six sizes.
|
|
152
|
+
*/
|
|
153
|
+
export declare function headingSize(theme: Required<MarkdownTheme>, depth: number): number;
|