@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
package/README.md
CHANGED
|
@@ -4,12 +4,11 @@ Canvas-native Markdown (with TeX math) rendering for [VectoJS](https://github.co
|
|
|
4
4
|
|
|
5
5
|
`Markdown` is a high-level entity that parses Markdown with
|
|
6
6
|
[`marked`](https://marked.js.org/), renders TeX math to SVG with
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
exports `CodeBlock`.
|
|
7
|
+
`@vectojs/tex`, and lays the result out using `@vectojs/ui` components
|
|
8
|
+
(`RichText`, `Stack`, `Table`, `Text`, `Image`). It also exports `CodeBlock`.
|
|
10
9
|
|
|
11
10
|
This package was split out of `@vectojs/ui` so that the heavy `marked` +
|
|
12
|
-
`
|
|
11
|
+
`@vectojs/tex` dependencies are only pulled in by apps that actually render
|
|
13
12
|
Markdown. Because it depends on `@vectojs/ui` components, it sits **above** `ui`
|
|
14
13
|
in the dependency graph — install it alongside `@vectojs/ui` and `@vectojs/core`.
|
|
15
14
|
|
|
@@ -135,26 +134,32 @@ converted once no matter how many documents or instances render it.
|
|
|
135
134
|
Inline `$...$` math is a separate path: it is currently shown as styled source
|
|
136
135
|
text, not typeset.
|
|
137
136
|
|
|
138
|
-
####
|
|
137
|
+
#### The math engine is loaded on demand
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
against a browser bundle of a consumer that renders
|
|
139
|
+
TeX math is typeset by `@vectojs/tex`, which is imported dynamically the first
|
|
140
|
+
time a document actually has a formula. It is by far the heaviest thing this
|
|
141
|
+
package can pull in — measured against a browser bundle of a consumer that renders
|
|
142
|
+
only prose, built with code splitting and minification:
|
|
143
143
|
|
|
144
|
-
| prose-only consumer
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
144
|
+
| prose-only consumer | raw | gzip | chunks |
|
|
145
|
+
| ---------------------- | --------: | ------: | -----: |
|
|
146
|
+
| `mathjax-full` | 2,199,869 | 748,713 | 19 |
|
|
147
|
+
| `@vectojs/tex` (now) | 758,249 | 273,754 | 3 |
|
|
148
|
+
| no math at all (floor) | 379,224 | 118,670 | 3 |
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
Against that floor the math path itself is 630,043 gzip under `mathjax-full` and
|
|
151
|
+
155,033 under `@vectojs/tex` — **4.06x smaller**. The eagerly-downloaded entry
|
|
152
|
+
chunk a prose-only consumer actually pays for is 117,889 gzip, within 1 KB of the
|
|
153
|
+
no-math floor.
|
|
154
|
+
|
|
155
|
+
Your bundler needs code splitting enabled to see this; without it the bytes are
|
|
156
|
+
still in the output, just not evaluated until first use.
|
|
153
157
|
|
|
154
158
|
The tradeoff is that **the first formula on a page cannot be typeset
|
|
155
159
|
synchronously.** It renders as a code block of TeX source — the same state an
|
|
156
160
|
unclosed fence already shows — and is replaced once the module resolves. Every
|
|
157
|
-
formula after that is synchronous again.
|
|
161
|
+
formula after that is synchronous again. (The engine itself is synchronous; the
|
|
162
|
+
lazy import is what defers it, and it is kept for the bundle size above.)
|
|
158
163
|
|
|
159
164
|
While streaming this is invisible: the load starts as soon as an _opening_ math
|
|
160
165
|
fence appears, several chunks before the closing one, so the formula is typeset on
|
|
@@ -176,6 +181,63 @@ from several places starts one load. `isMathJaxReady()` reports whether formulas
|
|
|
176
181
|
currently typeset without waiting. If the load fails, formulas keep rendering as
|
|
177
182
|
TeX source rather than throwing.
|
|
178
183
|
|
|
184
|
+
Both names are historical: they date from when `mathjax-full` was the engine and
|
|
185
|
+
mean "the math engine", whichever one that is. They keep those names because they
|
|
186
|
+
are public API and a rename would break every consumer for cosmetics.
|
|
187
|
+
|
|
188
|
+
A formula containing a symbol outside the engine's shipped glyph corpus also
|
|
189
|
+
renders as TeX source rather than being drawn with that symbol missing.
|
|
190
|
+
|
|
191
|
+
## Images
|
|
192
|
+
|
|
193
|
+
An image renders in one of two ways, decided by where it is written.
|
|
194
|
+
|
|
195
|
+
**On its own, or in a paragraph, blockquote or list item**, the paragraph splits
|
|
196
|
+
into blocks and the image becomes an `Image` entity at its natural size, capped
|
|
197
|
+
to the available width. This is the ordinary `` case.
|
|
198
|
+
|
|
199
|
+
**On a line it shares with text** — in a heading, or in a table cell — it renders
|
|
200
|
+
as an inline box in the text run, so the prose flows around it and selection and
|
|
201
|
+
the accessible name still work. Its height is a multiple of the run's font size
|
|
202
|
+
(`theme.inlineImageScale`, default `1.15`) and its width follows the image's
|
|
203
|
+
natural aspect ratio, so a badge stays wide and a square icon stays square:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const md = new Markdown("# Build ");
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
This is a deliberate departure from HTML, which would render an inline image at
|
|
210
|
+
its intrinsic size. A 512px logo written into an `h1` would otherwise tower over
|
|
211
|
+
its own heading, and an inline box has to be sized before the image has decoded.
|
|
212
|
+
The height is fixed up front for the same reason: the line box never moves, and
|
|
213
|
+
only the width settles once the aspect ratio is known.
|
|
214
|
+
|
|
215
|
+
The `alt` text is the accessible name and the copied text — never painted as
|
|
216
|
+
visible prose. If the image fails to load, the box is replaced by the alt text
|
|
217
|
+
rather than left as an invisible gap.
|
|
218
|
+
|
|
219
|
+
## Syntax coverage
|
|
220
|
+
|
|
221
|
+
Everything in [CommonMark](https://spec.commonmark.org/) plus the
|
|
222
|
+
[GFM](https://github.github.com/gfm/) extensions this renderer draws: tables,
|
|
223
|
+
strikethrough, task lists, autolinks, plus `$…$` / `$$…$$` TeX math and
|
|
224
|
+
` ```math ` fences.
|
|
225
|
+
|
|
226
|
+
Two constructs are deliberately **not** supported, and both are pinned by tests
|
|
227
|
+
so the behaviour cannot drift silently:
|
|
228
|
+
|
|
229
|
+
- **Definition lists.** `Term` then `: definition` renders as the two literal
|
|
230
|
+
lines the source contains, colon included.
|
|
231
|
+
- **Raw HTML blocks.** `<details>`, `<div>`, `<iframe>` and HTML comments render
|
|
232
|
+
nothing at all.
|
|
233
|
+
|
|
234
|
+
Definition lists are neither CommonMark nor GFM; when they arrive it will be
|
|
235
|
+
through the same syntax-extension mechanism footnotes need. Raw HTML blocks
|
|
236
|
+
cannot work in a zero-DOM renderer — there is no DOM to hand markup to. `<svg>`
|
|
237
|
+
is the one exception, because a self-contained SVG document can be rasterized.
|
|
238
|
+
|
|
239
|
+
Footnotes (`[^1]`) are **not yet parsed** and currently render as literal source.
|
|
240
|
+
|
|
179
241
|
> Migrating from `@vectojs/ui` ≤ 1.x? `Markdown` and `CodeBlock` used to be
|
|
180
242
|
> exported from `@vectojs/ui`. As of `@vectojs/ui@2.0.0` they live here — change
|
|
181
243
|
> `import { Markdown } from '@vectojs/ui'` to `from '@vectojs/markdown'`.
|
package/dist/Markdown.d.ts
CHANGED
|
@@ -1,159 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Entity, type DevtoolsDescriptor, IRenderer } from '@vectojs/core';
|
|
2
2
|
import { type Token } from 'marked';
|
|
3
3
|
import { type StreamController, type StreamControllerOptions } from './StreamController';
|
|
4
|
+
export { isMathJaxReady, MathBlock, preloadMathJax } from './markdown-math';
|
|
5
|
+
export { CodeBlock, codeAtlas, codeAtlasStats } from './markdown-code';
|
|
6
|
+
import { type MarkdownTheme } from './theme';
|
|
7
|
+
export type { MarkdownTheme } from './theme';
|
|
4
8
|
import { Stack, UIComponent } from '@vectojs/ui';
|
|
5
|
-
export declare function preloadMathJax(): Promise<void>;
|
|
6
|
-
/** Whether formulas can be typeset without waiting. Exposed for tests. */
|
|
7
|
-
export declare function isMathJaxReady(): boolean;
|
|
8
|
-
/** Color and typography theme for Markdown rendering. */
|
|
9
|
-
export interface MarkdownTheme {
|
|
10
|
-
/** Body text color. */
|
|
11
|
-
textColor?: string;
|
|
12
|
-
/** Heading text color. */
|
|
13
|
-
headingColor?: string;
|
|
14
|
-
/** Code text color (inline + block). */
|
|
15
|
-
codeColor?: string;
|
|
16
|
-
/** Code block background color. */
|
|
17
|
-
codeBgColor?: string;
|
|
18
|
-
/** Blockquote border/accent color. */
|
|
19
|
-
quoteBorderColor?: string;
|
|
20
|
-
/** Blockquote text color. */
|
|
21
|
-
quoteTextColor?: string;
|
|
22
|
-
/** Horizontal-rule color. */
|
|
23
|
-
hrColor?: string;
|
|
24
|
-
/** Table background color. */
|
|
25
|
-
tableBgColor?: string;
|
|
26
|
-
/** Table header background color. */
|
|
27
|
-
tableHeaderBgColor?: string;
|
|
28
|
-
/** Body font. */
|
|
29
|
-
bodyFont?: string;
|
|
30
|
-
/** Monospace font for code. */
|
|
31
|
-
codeFont?: string;
|
|
32
|
-
/** Base font size in px. */
|
|
33
|
-
fontSize?: number;
|
|
34
|
-
}
|
|
35
|
-
/** A simple concrete container entity for nested layouts. */
|
|
36
|
-
declare class MarkdownContainer extends Entity {
|
|
37
|
-
isPointInside(_globalX: number, _globalY: number): boolean;
|
|
38
|
-
render(_r: any): void;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* One display formula: a `$$..$$` block or a closed ```` ```math ```` fence.
|
|
42
|
-
*
|
|
43
|
-
* A named class rather than a bare {@link MarkdownContainer} because the formula
|
|
44
|
-
* needs a stable handle, and after the switch to an inline object it has none:
|
|
45
|
-
* the typeset raster lives in a `paint` closure captured by the span, so removing
|
|
46
|
-
* the `Image` entity left nothing exposing either the source or the SVG bytes.
|
|
47
|
-
* Devtools, tests, and anything auditing what a formula actually rendered all
|
|
48
|
-
* want that. `markstream-vue` reaches the same conclusion from the DOM side and
|
|
49
|
-
* publishes `data-markstream-mode` on its math node for the same reason.
|
|
50
|
-
*
|
|
51
|
-
* Deliberately carries no typeset-vs-source flag. A formula MathJax has not
|
|
52
|
-
* converted yet renders as a bare {@link CodeBlock} of its TeX, which this class
|
|
53
|
-
* does not wrap — wrapping it would put a container between `content` and a
|
|
54
|
-
* `CodeBlock` that the streamed `setCode` path locates by type. So a flag would
|
|
55
|
-
* have exactly one reachable value, which is the dead-API trap that cost CTX-0208
|
|
56
|
-
* a debugging pass. Add it together with wrapping the fallback, or not at all.
|
|
57
|
-
*/
|
|
58
|
-
export declare class MathBlock extends MarkdownContainer {
|
|
59
|
-
/**
|
|
60
|
-
* The TeX source, exactly as written between the delimiters.
|
|
61
|
-
*
|
|
62
|
-
* Also the projected text and the accessible name, so this is the one string a
|
|
63
|
-
* reader can find, select, and copy.
|
|
64
|
-
*/
|
|
65
|
-
readonly formula: string;
|
|
66
|
-
/** The `data:image/svg+xml` URI of the typeset glyphs. */
|
|
67
|
-
readonly svgUri: string;
|
|
68
|
-
constructor(formula: string, svgUri: string);
|
|
69
|
-
getDevtoolsDescriptor(): DevtoolsDescriptor;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* A single self-rendering entity for fenced code blocks.
|
|
73
|
-
*
|
|
74
|
-
* Replaces the old N×M child-entity explosion (Container → Stack → Text per
|
|
75
|
-
* segment per line) with a flat leaf that draws its own background + text.
|
|
76
|
-
*/
|
|
77
|
-
export declare class CodeBlock extends UIComponent {
|
|
78
|
-
private lines;
|
|
79
|
-
private grid;
|
|
80
|
-
/** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
|
|
81
|
-
private rawLines;
|
|
82
|
-
private cellWidth;
|
|
83
|
-
private source;
|
|
84
|
-
/** Bumped by {@link buildLines} and {@link setSelectable}; read by `Scene`. */
|
|
85
|
-
private contentEpoch;
|
|
86
|
-
private lang;
|
|
87
|
-
private theme;
|
|
88
|
-
private lineH;
|
|
89
|
-
private pad;
|
|
90
|
-
private codeFont;
|
|
91
|
-
selectable: boolean;
|
|
92
|
-
constructor(code: string, lang: string, maxWidth: number, theme: Required<MarkdownTheme>, selectable?: boolean);
|
|
93
|
-
/** Re-parse code content (e.g. for live editing). */
|
|
94
|
-
setCode(code: string, lang?: string): this;
|
|
95
|
-
/** Enable or disable browser-native selection for this code block. */
|
|
96
|
-
setSelectable(selectable: boolean): this;
|
|
97
|
-
getContentEpoch(): number;
|
|
98
|
-
/**
|
|
99
|
-
* Change the block's box width.
|
|
100
|
-
*
|
|
101
|
-
* Deliberately does **not** rebuild the grid or the highlight, because code does
|
|
102
|
-
* not reflow: lines are placed on a fixed monospace grid at `col × cellWidth` and
|
|
103
|
-
* a long line overflows rather than wrapping, so `height` is a function of line
|
|
104
|
-
* *count* alone. The width only sizes the rounded background. Anything that would
|
|
105
|
-
* change the glyph geometry — the source, the language, the font — goes through
|
|
106
|
-
* {@link setCode} and invalidates the grid there.
|
|
107
|
-
*
|
|
108
|
-
* @returns `this` for chaining.
|
|
109
|
-
*/
|
|
110
|
-
setWidth(width: number): this;
|
|
111
|
-
getContentProjection(hint?: ContentProjectionHint): ContentProjection | null;
|
|
112
|
-
/**
|
|
113
|
-
* Re-highlight the code, reusing the highlight of any unchanged line prefix.
|
|
114
|
-
*
|
|
115
|
-
* Streaming appends to the END of a block, so all but the last line or two are
|
|
116
|
-
* byte-identical to the previous call — yet this used to re-highlight every
|
|
117
|
-
* line on every chunk, making a streamed block O(N) per append and O(N^2)
|
|
118
|
-
* overall. Reusing the stable prefix makes an append proportional to what
|
|
119
|
-
* actually changed.
|
|
120
|
-
*
|
|
121
|
-
* The last previously-seen line is deliberately NOT reused: a chunk usually
|
|
122
|
-
* lands mid-line, so that line's text (and therefore its tokenization) changes.
|
|
123
|
-
*/
|
|
124
|
-
private buildLines;
|
|
125
|
-
private ensureGrid;
|
|
126
|
-
/** Code blocks are decorative — not interactive. */
|
|
127
|
-
isPointInside(): boolean;
|
|
128
|
-
render(r: IRenderer): void;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Instrumentation for the code-block glyph atlas in use, or `null` before first
|
|
132
|
-
* use.
|
|
133
|
-
*
|
|
134
|
-
* Exposed so an app or benchmark can confirm the atlas is actually active and
|
|
135
|
-
* reusing slots. Watch `resets`: a steadily climbing count means the glyph set is
|
|
136
|
-
* unbounded for the atlas size, so every reset re-rasterizes everything and the
|
|
137
|
-
* atlas is doing net harm rather than saving work.
|
|
138
|
-
*
|
|
139
|
-
* Reports the *most recently used* atlas, which after a zoom is the one now being
|
|
140
|
-
* blitted — see {@link codeAtlas}.
|
|
141
|
-
*/
|
|
142
|
-
export declare function codeAtlasStats(): GlyphRasterAtlasStats | null;
|
|
143
|
-
/**
|
|
144
|
-
* The code-block atlas most recently blitted from, or `null` before first use.
|
|
145
|
-
*
|
|
146
|
-
* For instrumentation that must map a traced `drawImage` back to the glyph it
|
|
147
|
-
* painted — a blit carries only a source rect, so `slotAt()` is the only way to
|
|
148
|
-
* recover the cluster and its metrics. Used by `e2e/text-projection.e2e.ts` to
|
|
149
|
-
* keep the code-grid positioning assertions working on the blit path.
|
|
150
|
-
*
|
|
151
|
-
* "Most recently used" rather than "the one" because atlases are pooled per DPR:
|
|
152
|
-
* a caller resolving a traced blit wants the atlas that produced it, which is the
|
|
153
|
-
* one the last render selected. Compare its {@link GlyphRasterAtlas.pixelRatio}
|
|
154
|
-
* against {@link IRenderer.pixelRatio} to assert the blit is 1:1.
|
|
155
|
-
*/
|
|
156
|
-
export declare function codeAtlas(): GlyphRasterAtlas | null;
|
|
157
9
|
export interface MarkdownOptions {
|
|
158
10
|
maxWidth?: number;
|
|
159
11
|
theme?: MarkdownTheme;
|
|
@@ -300,6 +152,20 @@ export declare class Markdown extends UIComponent {
|
|
|
300
152
|
* field only so {@link destroy} can remove the exact closure it added.
|
|
301
153
|
*/
|
|
302
154
|
private inlineMathRepaint?;
|
|
155
|
+
/**
|
|
156
|
+
* This instance's entry in the inline-image decode waiters, or `undefined` if it
|
|
157
|
+
* has never rendered an image. Held as a field only so {@link destroy} can remove
|
|
158
|
+
* the exact closure it added.
|
|
159
|
+
*/
|
|
160
|
+
private inlineImageRemeasure?;
|
|
161
|
+
/**
|
|
162
|
+
* URLs whose decoded aspect ratio this document has already reserved a box for.
|
|
163
|
+
*
|
|
164
|
+
* The guard that makes the re-measure fire once per image rather than once per
|
|
165
|
+
* decode-notification-per-image: the waiter set is module-level, so a page of
|
|
166
|
+
* many documents tells all of them about all decodes.
|
|
167
|
+
*/
|
|
168
|
+
private readonly inlineImagesMeasured;
|
|
303
169
|
/**
|
|
304
170
|
* True while this document is waiting on the lazy MathJax load.
|
|
305
171
|
*
|
|
@@ -487,6 +353,45 @@ export declare class Markdown extends UIComponent {
|
|
|
487
353
|
* one closure per instance.
|
|
488
354
|
*/
|
|
489
355
|
private subscribeInlineMathRepaint;
|
|
356
|
+
/**
|
|
357
|
+
* Re-measure this document when an inline image's raster finishes decoding.
|
|
358
|
+
*
|
|
359
|
+
* Inline images differ from inline formulas in one way that matters: a formula's
|
|
360
|
+
* box is known synchronously the moment it typesets, while an image's aspect
|
|
361
|
+
* ratio arrives only with the decode. The span reserved a square until then, so a
|
|
362
|
+
* decode that reports anything else has invalidated a WIDTH, and a repaint into
|
|
363
|
+
* the old box would letterbox or stretch the picture.
|
|
364
|
+
*
|
|
365
|
+
* So this rebuilds through {@link retypesetFromTokens} — the same late-arrival
|
|
366
|
+
* path MathJax uses — but only when a reserved width actually changed. Every live
|
|
367
|
+
* document is notified for every decode, including images it does not contain, so
|
|
368
|
+
* an unconditional rebuild here would be O(documents x images) full re-renders
|
|
369
|
+
* for a page of many blocks.
|
|
370
|
+
*
|
|
371
|
+
* Subscribed lazily and held as a field for the same two reasons as its math
|
|
372
|
+
* counterpart: a document with no images costs nothing, and `destroy` must remove
|
|
373
|
+
* the exact closure it added.
|
|
374
|
+
*/
|
|
375
|
+
private subscribeInlineImageRemeasure;
|
|
376
|
+
/**
|
|
377
|
+
* Whether any inline image in this document has just learned it is not square.
|
|
378
|
+
*
|
|
379
|
+
* An inline image's span reserves a square box before its raster decodes, because
|
|
380
|
+
* that is the only shape available without a natural size. The decode supplies the
|
|
381
|
+
* real aspect ratio, so a non-square image needs one rebuild to reserve the right
|
|
382
|
+
* width — and exactly one. Every live document is notified of every decode on the
|
|
383
|
+
* page, including images it does not contain, so this has to answer "did MY
|
|
384
|
+
* geometry just change" and not merely "did something decode".
|
|
385
|
+
*
|
|
386
|
+
* Walks the tokens rather than the entity tree: the reserved box is a function of
|
|
387
|
+
* the raster's aspect ratio, which is available here, and a token walk cannot be
|
|
388
|
+
* confused by an entity a previous rebuild already corrected.
|
|
389
|
+
*
|
|
390
|
+
* Only headings and table cells are inspected. Every other context splits an image
|
|
391
|
+
* into its own block whose `Image` entity resizes itself in `onLoad`, so a rebuild
|
|
392
|
+
* for one of those would be pure cost.
|
|
393
|
+
*/
|
|
394
|
+
private inlineImageBoxesStale;
|
|
490
395
|
destroy(): void;
|
|
491
396
|
/**
|
|
492
397
|
* Streaming and parse state — the markdown streaming inspector.
|
|
@@ -949,4 +854,3 @@ export declare class Markdown extends UIComponent {
|
|
|
949
854
|
/** Structural — children draw themselves. */
|
|
950
855
|
render(_r: IRenderer): void;
|
|
951
856
|
}
|
|
952
|
-
export {};
|