@vectojs/markdown 0.5.0 → 0.7.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 +119 -4
- package/dist/Markdown.d.ts +363 -0
- package/dist/MarkdownWorkerSource.d.ts +1 -1
- package/dist/StreamController.d.ts +53 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1199 -143
- package/dist/index.mjs +1187 -142
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -22,9 +22,9 @@ bun add @vectojs/markdown @vectojs/ui @vectojs/core
|
|
|
22
22
|
## Usage
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
|
-
import { Markdown } from
|
|
25
|
+
import { Markdown } from "@vectojs/markdown";
|
|
26
26
|
|
|
27
|
-
const md = new Markdown(
|
|
27
|
+
const md = new Markdown("# Hello\n\nInline math $E = mc^2$.");
|
|
28
28
|
scene.add(md);
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -35,14 +35,16 @@ parse/layout commit per animation frame. `write()` applies backpressure when its
|
|
|
35
35
|
bounded buffer is full; await it when consuming an async token source.
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
|
-
const md = new Markdown(
|
|
38
|
+
const md = new Markdown("");
|
|
39
39
|
scene.add(md);
|
|
40
40
|
|
|
41
41
|
const stream = md.createStream();
|
|
42
42
|
for await (const token of tokens) {
|
|
43
43
|
await stream.write(token);
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
// Commits the final text, then waits for the parse to be applied — once this
|
|
46
|
+
// resolves, the rendered document reflects everything written.
|
|
47
|
+
await stream.close();
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
Add fixed-rate typewriter pacing without changing producer code:
|
|
@@ -61,6 +63,119 @@ flags, and surrogate pairs stay intact across ordinary chunk/frame boundaries.
|
|
|
61
63
|
automatically. The existing `appendMarkdown()` API remains synchronous and
|
|
62
64
|
flushes submitted controller text before a direct append.
|
|
63
65
|
|
|
66
|
+
### Incomplete Markdown while streaming
|
|
67
|
+
|
|
68
|
+
Mid-stream, the trailing text of a paragraph is often an unclosed inline
|
|
69
|
+
construct — `**bo`, `` `cod ``, `[text](url`. `marked` has no signal that more
|
|
70
|
+
characters are coming, so it lexes those as plain text. `incompleteMode` chooses
|
|
71
|
+
what you show in the meantime:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const stream = md.createStream({ incompleteMode: "optimistic" });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Mode | Unclosed `**bo` renders as |
|
|
78
|
+
| --------------------- | ------------------------------------ |
|
|
79
|
+
| `'literal'` (default) | `**bo` — the literal characters |
|
|
80
|
+
| `'optimistic'` | **bo** — guessed bold, syntax hidden |
|
|
81
|
+
|
|
82
|
+
`'optimistic'` covers strong, emphasis, and inline code. An unclosed link shows
|
|
83
|
+
its label as plain text: with no closing `)` there is no URL yet, so nothing is
|
|
84
|
+
made clickable. Only the document's **last paragraph** is ever guessed at, and
|
|
85
|
+
only while the stream is open — headings, list items, table cells, and any
|
|
86
|
+
earlier paragraph are always literal.
|
|
87
|
+
|
|
88
|
+
The guess is display-only. `Markdown.tokens` is never affected, and `close()`
|
|
89
|
+
unwinds it, so a `'literal'` and an `'optimistic'` stream of the same source end
|
|
90
|
+
at an identical document. `'literal'` remains the default: it is what every prior
|
|
91
|
+
release rendered.
|
|
92
|
+
|
|
93
|
+
### Knowing when the document is final
|
|
94
|
+
|
|
95
|
+
`onStable` fires once, after `close()` has committed the last chunk _and_ the
|
|
96
|
+
parse has been applied. Use it for one-time work you do not want repeated against
|
|
97
|
+
content still in flight:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const stream = md.createStream({
|
|
101
|
+
onStable: (blocks) => {
|
|
102
|
+
for (const block of blocks) fadeIn(block);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
It never fires on `flush()`, `abort()`, or `destroy()` — none of those mean the
|
|
108
|
+
content stopped changing. It receives a snapshot array, not a live reference.
|
|
109
|
+
Calling `appendMarkdown()` or `setContent()` from inside the callback throws; a
|
|
110
|
+
throw from the callback rejects the `close()` promise. `onStable` is independent
|
|
111
|
+
of `incompleteMode` and works with the `'literal'` default.
|
|
112
|
+
|
|
113
|
+
### Streamed TeX math
|
|
114
|
+
|
|
115
|
+
A fenced math block is typeset only once its closing fence arrives:
|
|
116
|
+
|
|
117
|
+
````md
|
|
118
|
+
```math
|
|
119
|
+
\int_0^1 x\,dx = \frac{1}{2}
|
|
120
|
+
```
|
|
121
|
+
````
|
|
122
|
+
|
|
123
|
+
While the fence is still open the block renders as an ordinary code block showing
|
|
124
|
+
the TeX source, then becomes the formula on the chunk that closes it. This is
|
|
125
|
+
deliberate. `marked` lexes an unterminated fence as a _complete_ token as soon as
|
|
126
|
+
it reads the info string, so a formula streamed a few characters at a time
|
|
127
|
+
arrives as a long run of whole tokens, nearly all of them invalid TeX — typesetting
|
|
128
|
+
each one spends the most expensive call in this package rendering an error glyph
|
|
129
|
+
that is replaced by the next chunk. Showing the source is both cheaper and more
|
|
130
|
+
honest: the formula genuinely is not finished.
|
|
131
|
+
|
|
132
|
+
Converted formulas are cached (bounded, process-wide), so a repeated formula is
|
|
133
|
+
converted once no matter how many documents or instances render it.
|
|
134
|
+
|
|
135
|
+
Inline `$...$` math is a separate path: it is currently shown as styled source
|
|
136
|
+
text, not typeset.
|
|
137
|
+
|
|
138
|
+
#### MathJax is loaded on demand
|
|
139
|
+
|
|
140
|
+
MathJax is imported dynamically, the first time a document actually has a formula
|
|
141
|
+
to typeset. It is by far the heaviest thing this package can pull in — measured
|
|
142
|
+
against a browser bundle of a consumer that renders only prose:
|
|
143
|
+
|
|
144
|
+
| prose-only consumer | raw | gzip |
|
|
145
|
+
| ------------------------- | --------: | ------: |
|
|
146
|
+
| eagerly imported (before) | 2,157,295 | 725,012 |
|
|
147
|
+
| lazily imported (now) | 339,767 | 106,095 |
|
|
148
|
+
|
|
149
|
+
That is 85% of the bundle a document with no formulas used to carry, plus roughly
|
|
150
|
+
150 ms of module evaluation at startup. Your bundler needs code splitting enabled
|
|
151
|
+
to see this; without it the bytes are still in the output, just not evaluated
|
|
152
|
+
until first use.
|
|
153
|
+
|
|
154
|
+
The tradeoff is that **the first formula on a page cannot be typeset
|
|
155
|
+
synchronously.** It renders as a code block of TeX source — the same state an
|
|
156
|
+
unclosed fence already shows — and is replaced once the module resolves. Every
|
|
157
|
+
formula after that is synchronous again.
|
|
158
|
+
|
|
159
|
+
While streaming this is invisible: the load starts as soon as an _opening_ math
|
|
160
|
+
fence appears, several chunks before the closing one, so the formula is typeset on
|
|
161
|
+
the chunk that closes it. `await stream.close()` and `onStable` also wait for a
|
|
162
|
+
pending load, so a "final" document never hands you an untypeset formula.
|
|
163
|
+
|
|
164
|
+
If you need the very first formula typeset in the same tick — measuring layout
|
|
165
|
+
immediately after construction, for instance — preload it:
|
|
166
|
+
|
|
167
|
+
````ts
|
|
168
|
+
import { Markdown, preloadMathJax } from "@vectojs/markdown";
|
|
169
|
+
|
|
170
|
+
await preloadMathJax();
|
|
171
|
+
const md = new Markdown("```math\n\\int_0^1 x\\,dx\n```"); // typeset synchronously
|
|
172
|
+
````
|
|
173
|
+
|
|
174
|
+
`preloadMathJax()` is idempotent and shared across every document, so calling it
|
|
175
|
+
from several places starts one load. `isMathJaxReady()` reports whether formulas
|
|
176
|
+
currently typeset without waiting. If the load fails, formulas keep rendering as
|
|
177
|
+
TeX source rather than throwing.
|
|
178
|
+
|
|
64
179
|
> Migrating from `@vectojs/ui` ≤ 1.x? `Markdown` and `CodeBlock` used to be
|
|
65
180
|
> exported from `@vectojs/ui`. As of `@vectojs/ui@2.0.0` they live here — change
|
|
66
181
|
> `import { Markdown } from '@vectojs/ui'` to `from '@vectojs/markdown'`.
|
package/dist/Markdown.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { Entity, type DevtoolsDescriptor, GlyphRasterAtlas, type GlyphRasterAtla
|
|
|
2
2
|
import { type Token } from 'marked';
|
|
3
3
|
import { type StreamController, type StreamControllerOptions } from './StreamController';
|
|
4
4
|
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;
|
|
5
8
|
/** Color and typography theme for Markdown rendering. */
|
|
6
9
|
export interface MarkdownTheme {
|
|
7
10
|
/** Body text color. */
|
|
@@ -134,6 +137,51 @@ export declare class Markdown extends UIComponent {
|
|
|
134
137
|
onLayoutUpdated?: () => void;
|
|
135
138
|
private rawMarkdown;
|
|
136
139
|
private streamController;
|
|
140
|
+
/**
|
|
141
|
+
* Trailing-unclosed-syntax policy of the active stream, or `'literal'` when no
|
|
142
|
+
* stream is open.
|
|
143
|
+
*
|
|
144
|
+
* Held here rather than read back off the controller because it is a rendering
|
|
145
|
+
* concern: `StreamController` owns buffering and pacing and has no view of the
|
|
146
|
+
* entity tree, while the guess is a transform applied where spans are built.
|
|
147
|
+
*/
|
|
148
|
+
private streamIncompleteMode;
|
|
149
|
+
/** End-of-stream callback of the active stream, if it supplied one. */
|
|
150
|
+
private streamOnStable;
|
|
151
|
+
/**
|
|
152
|
+
* The trailing paragraph entity currently showing an optimistic guess, plus the
|
|
153
|
+
* token it was rendered from.
|
|
154
|
+
*
|
|
155
|
+
* Both halves are needed. The entity is what must be re-rendered to drop the
|
|
156
|
+
* guess; the token is what it must be re-rendered FROM, and it is the only
|
|
157
|
+
* copy — `this.tokens` has already moved on by the time an unwind is decided.
|
|
158
|
+
* `null` means no guess is live, which is the state every `'literal'` stream
|
|
159
|
+
* and every closed stream stays in.
|
|
160
|
+
*/
|
|
161
|
+
private optimisticTail;
|
|
162
|
+
/** Resolvers waiting for every in-flight worker append to have been applied. */
|
|
163
|
+
private appendSettledWaiters;
|
|
164
|
+
/** True only inside an `onStable` callback, to reject reentrant mutation. */
|
|
165
|
+
private inStableCallback;
|
|
166
|
+
/** Set by {@link destroy} so late settlement work skips a torn-down tree. */
|
|
167
|
+
private isDestroyed;
|
|
168
|
+
/**
|
|
169
|
+
* This instance's entry in {@link inlineMathRasterWaiters}, or `undefined` if it
|
|
170
|
+
* has never rendered inline math.
|
|
171
|
+
*
|
|
172
|
+
* Subscribed lazily so a document without formulas costs nothing, and held as a
|
|
173
|
+
* field only so {@link destroy} can remove the exact closure it added.
|
|
174
|
+
*/
|
|
175
|
+
private inlineMathRepaint?;
|
|
176
|
+
/**
|
|
177
|
+
* True while this document is waiting on the lazy MathJax load.
|
|
178
|
+
*
|
|
179
|
+
* Tracked per instance rather than read off the module state because it also
|
|
180
|
+
* gates settlement: `await close()` and `onStable` must not resolve while a
|
|
181
|
+
* formula is still showing TeX source, or a caller doing expensive one-time
|
|
182
|
+
* work on a "final" document would measure and export placeholder boxes.
|
|
183
|
+
*/
|
|
184
|
+
private mathLoadPending;
|
|
137
185
|
private _userTiming;
|
|
138
186
|
private tokens;
|
|
139
187
|
private appendInFlight;
|
|
@@ -195,6 +243,13 @@ export declare class Markdown extends UIComponent {
|
|
|
195
243
|
* the whole subtree alive until the worker replied), then recurse into the
|
|
196
244
|
* content subtree via `super.destroy()` so every block's resources are freed.
|
|
197
245
|
*/
|
|
246
|
+
/**
|
|
247
|
+
* Repaint this document when an inline formula's raster finishes decoding.
|
|
248
|
+
*
|
|
249
|
+
* Idempotent — called on every render of a math-bearing token, and the set holds
|
|
250
|
+
* one closure per instance.
|
|
251
|
+
*/
|
|
252
|
+
private subscribeInlineMathRepaint;
|
|
198
253
|
destroy(): void;
|
|
199
254
|
/**
|
|
200
255
|
* Streaming and parse state — the markdown streaming inspector.
|
|
@@ -232,6 +287,301 @@ export declare class Markdown extends UIComponent {
|
|
|
232
287
|
* whenever the worker reports it cannot trust what it holds (`needResync`).
|
|
233
288
|
*/
|
|
234
289
|
private dispatchAppend;
|
|
290
|
+
/**
|
|
291
|
+
* Spans for one paragraph token exactly as `marked` produced it.
|
|
292
|
+
*
|
|
293
|
+
* The literal baseline: what every release renders, and what an optimistic
|
|
294
|
+
* guess is unwound back to.
|
|
295
|
+
*/
|
|
296
|
+
private literalParagraphSpans;
|
|
297
|
+
/**
|
|
298
|
+
* Update a reused blockquote's tail child in place, or report that it cannot be.
|
|
299
|
+
*
|
|
300
|
+
* The render arm builds `container[border, innerStack]` where every inner block
|
|
301
|
+
* sits in its own single-child `wrapper`, so the tail entity is
|
|
302
|
+
* `innerStack.children.at(-1).children[0]`. Only the LAST inner block may be
|
|
303
|
+
* updated: the inner token list is prefix-stable exactly like the top level (a
|
|
304
|
+
* growing quote keeps its earlier blocks byte-identical), so anything before the
|
|
305
|
+
* tail is untouched and anything more complicated than a changed tail falls back
|
|
306
|
+
* to the caller's rebuild.
|
|
307
|
+
*
|
|
308
|
+
* Returns `false` without mutating anything when the shape is not the simple
|
|
309
|
+
* grow-the-tail case, which is the signal for the caller to rebuild. Every early
|
|
310
|
+
* return has to leave the entity untouched, or a rejected reuse would leave a
|
|
311
|
+
* half-updated quote on screen.
|
|
312
|
+
*/
|
|
313
|
+
/**
|
|
314
|
+
* Build one list item's spans: inline content plus its marker.
|
|
315
|
+
*
|
|
316
|
+
* Shared by the `list` render arm and the streamed-reuse path below, because
|
|
317
|
+
* the two must produce byte-identical spans — a reused list that disagreed with
|
|
318
|
+
* a rebuilt one about its marker or its entity decoding would make a streamed
|
|
319
|
+
* document differ from the same source pasted at once.
|
|
320
|
+
*/
|
|
321
|
+
/**
|
|
322
|
+
* Inline spans for one table cell.
|
|
323
|
+
*
|
|
324
|
+
* Always returns at least one span. A cell whose markup collapses to nothing —
|
|
325
|
+
* an empty cell, but also a bare `<span>`, an image, or an HTML comment, none
|
|
326
|
+
* of which `collectSpans` emits for — falls back to its decoded source text,
|
|
327
|
+
* which is what the previous string-returning path rendered. That guarantee is
|
|
328
|
+
* what lets every cell be a `RichText`: an empty cell would otherwise become a
|
|
329
|
+
* `Text`, and since `Text` has `setText` while `RichText` has `setSpans` and
|
|
330
|
+
* nothing converts between them, a cell that starts empty and later gains
|
|
331
|
+
* content could not be updated in place. A streamed table needs exactly that,
|
|
332
|
+
* because `marked` materializes a partial row as a full row of empty cells and
|
|
333
|
+
* then fills them one at a time.
|
|
334
|
+
*/
|
|
335
|
+
private tableCellSpans;
|
|
336
|
+
/**
|
|
337
|
+
* Spans for one run of consecutive non-image inline tokens.
|
|
338
|
+
*
|
|
339
|
+
* A paragraph holding an image renders as a `Stack` of alternating text runs
|
|
340
|
+
* and images, and this is one text run. Shared by the render arm and
|
|
341
|
+
* {@link updateImageParagraph} so a reused run cannot drift from a rebuilt one.
|
|
342
|
+
*
|
|
343
|
+
* The empty fallback mirrors `renderInlineToRichText('', …)`, which the render
|
|
344
|
+
* arm passed for these runs: a run is only created when it has at least one
|
|
345
|
+
* token, so the fallback is for tokens that emit no spans at all rather than
|
|
346
|
+
* for an empty run.
|
|
347
|
+
*/
|
|
348
|
+
private inlineRunSpans;
|
|
349
|
+
/** One text run of an image-bearing paragraph, as both paths build it. */
|
|
350
|
+
private inlineRunRichText;
|
|
351
|
+
/**
|
|
352
|
+
* One image inside a paragraph, sized by a guess until its bitmap decodes.
|
|
353
|
+
*
|
|
354
|
+
* Width and height start at a 16:10 guess because the intrinsic size is not
|
|
355
|
+
* known until the browser has the bitmap; `onLoad` corrects both from
|
|
356
|
+
* `naturalWidth`/`naturalHeight`. Extracted from the render arm so the streamed
|
|
357
|
+
* path reuses this exact entity rather than constructing a second variant.
|
|
358
|
+
*
|
|
359
|
+
* `markDirty()` is unconditional, matching the display-math sibling. It used
|
|
360
|
+
* to sit inside the `naturalWidth && naturalHeight` check, which meant a
|
|
361
|
+
* source that loads successfully while reporting a zero dimension left the
|
|
362
|
+
* scene un-notified. `Image` sets `loaded` before invoking this callback, so
|
|
363
|
+
* its `render()` starts drawing the bitmap either way — the cost was not a
|
|
364
|
+
* stale placeholder but a box frozen at the guess: measured on Chromium and
|
|
365
|
+
* Firefox, an `<svg width="0" height="0">` paragraph image kept 800x480 of
|
|
366
|
+
* reserved layout forever while a normal raster corrected to 80x60. An
|
|
367
|
+
* `onDemand` scene repaints only when marked, so nothing reclaimed it.
|
|
368
|
+
*
|
|
369
|
+
* The box is deliberately left at the guess when the bitmap reports zero.
|
|
370
|
+
* Collapsing it to 0x0 would make the paragraph reflow correctly but would
|
|
371
|
+
* also silently delete a reserved region on the strength of one browser
|
|
372
|
+
* quirk, and `Image.render()` still blits whatever the bitmap holds. Sizing
|
|
373
|
+
* policy for a zero-dimension source is a separate decision from notifying
|
|
374
|
+
* the scene, which is the actual defect here.
|
|
375
|
+
*/
|
|
376
|
+
private paragraphImage;
|
|
377
|
+
/** One table cell entity, shared by the render arm and the streamed-table path. */
|
|
378
|
+
private tableCellRichText;
|
|
379
|
+
private listItemSpans;
|
|
380
|
+
/** Construct the `RichText` for one list item. */
|
|
381
|
+
private listItemRichText;
|
|
382
|
+
/**
|
|
383
|
+
* Reuse a streamed list's `Stack` instead of rebuilding every item.
|
|
384
|
+
*
|
|
385
|
+
* Returns `false` to mean "rebuild instead", exactly like
|
|
386
|
+
* {@link updateBlockquoteTail}, and every rejection path leaves the entity
|
|
387
|
+
* untouched so a refused reuse cannot leave a half-updated list on screen.
|
|
388
|
+
*
|
|
389
|
+
* This is the shape a stream actually produces: items are APPENDED, and only
|
|
390
|
+
* the last one grows. That matters for the ordinal marker, which is
|
|
391
|
+
* position-derived (`start + index`) — under append an already-rendered item's
|
|
392
|
+
* index never changes, so its marker stays correct. A mid-list insertion would
|
|
393
|
+
* shift every later ordinal, but no stream produces one.
|
|
394
|
+
*
|
|
395
|
+
* Two traps this guards, both found by probing marked 18.0.7 rather than by
|
|
396
|
+
* reading:
|
|
397
|
+
*
|
|
398
|
+
* - **A retained item's `raw` is NOT stable.** `items[1].raw` goes `"- two"` ->
|
|
399
|
+
* `"- two\\n"` when item 3 arrives, so a byte-equality guard on `raw` fails on
|
|
400
|
+
* every chunk and the fast path would never fire. `text` is stable; compare
|
|
401
|
+
* that.
|
|
402
|
+
* - **A tight list can become loose.** Adding a blank line flips
|
|
403
|
+
* `token.loose`, which re-lexes every item's children from `text` to
|
|
404
|
+
* `paragraph`. Item 0's own `text` is unchanged, so a naive guard would reuse
|
|
405
|
+
* and keep stale spans. Bail when `loose` flips.
|
|
406
|
+
*/
|
|
407
|
+
private updateStreamedList;
|
|
408
|
+
/**
|
|
409
|
+
* Reuse a streamed image-bearing paragraph's `Stack` instead of rebuilding it.
|
|
410
|
+
*
|
|
411
|
+
* Returns `false` to mean "rebuild instead", and every rejection happens before
|
|
412
|
+
* any mutation, so a refused reuse leaves the entity exactly as it was.
|
|
413
|
+
*
|
|
414
|
+
* This was the last silent fallthrough in the in-place reuse path. A paragraph
|
|
415
|
+
* holding an image renders as a `Stack` of alternating text runs and images
|
|
416
|
+
* rather than one `RichText`, so it has no `setSpans` and failed the ordinary
|
|
417
|
+
* paragraph gate — with no `else`, which is what made the miss invisible:
|
|
418
|
+
* `inPlaceUpdates` stayed flat while `entitiesRebuilt` climbed. Measured on a
|
|
419
|
+
* six-chunk stream, `inPlaceUpdates` 0 / `entitiesRebuilt` 4 with an image
|
|
420
|
+
* against 4 / 0 for the identical shape without one. Every rebuild also
|
|
421
|
+
* re-created the `Image`, discarding its decoded bitmap and its corrected
|
|
422
|
+
* intrinsic size.
|
|
423
|
+
*
|
|
424
|
+
* It is *only* a performance path. The obvious worry — that a fresh `Image`
|
|
425
|
+
* starts at `loaded = false` and so repaints its placeholder slab — was
|
|
426
|
+
* measured and does not happen: sampling the real canvas pixel at the image
|
|
427
|
+
* centre in both Chromium and Firefox gives zero placeholder frames after the
|
|
428
|
+
* first paint, at 60ms and at 0ms between chunks, because a cached bitmap
|
|
429
|
+
* decodes before the next frame.
|
|
430
|
+
*
|
|
431
|
+
* The reuse is deliberately narrow: **only a growing trailing text run**. Probed
|
|
432
|
+
* against `marked@18.0.7`, that is the shape a stream actually produces once an
|
|
433
|
+
* image has closed — the image token's `raw` and its index are then stable while
|
|
434
|
+
* trailing prose grows, and the token list settles at
|
|
435
|
+
* `[…, image, text]` and stops changing length. Anything else (a new image
|
|
436
|
+
* arriving, an image token changing, a run appearing before the last image)
|
|
437
|
+
* falls through to the rebuild, which is correct and rare.
|
|
438
|
+
*
|
|
439
|
+
* Note the child list is not one entity per token: consecutive non-image tokens
|
|
440
|
+
* are merged into one `RichText` by the render arm's `flushText`, so
|
|
441
|
+
* `[text, text, image]` is two children, not three. The guards therefore compare
|
|
442
|
+
* *token runs* split at the last image, never token index against child index.
|
|
443
|
+
*/
|
|
444
|
+
private updateImageParagraph;
|
|
445
|
+
/**
|
|
446
|
+
* Reuse a streamed table's `Table` entity instead of rebuilding every cell.
|
|
447
|
+
*
|
|
448
|
+
* Returns `false` to mean "rebuild instead", and every rejection happens before
|
|
449
|
+
* any mutation, so a refused reuse leaves the entity exactly as it was.
|
|
450
|
+
*
|
|
451
|
+
* A `table` token carries every row, so the rebuild path costs Θ(C·N²)
|
|
452
|
+
* `RichText` constructions across a stream — and a further 2×, because
|
|
453
|
+
* `Table.layout()` re-runs `fitCell` on every cell. This was the last block
|
|
454
|
+
* type without an in-place path.
|
|
455
|
+
*
|
|
456
|
+
* Two shapes have to be handled, because of how `marked` lexes a growing table
|
|
457
|
+
* (probed against 18.0.7): a partial row is materialized immediately as a FULL
|
|
458
|
+
* row padded with empty cells, and its cells are then filled one at a time. A
|
|
459
|
+
* 2×2 table passes through eleven distinct row states, of which only two are
|
|
460
|
+
* clean row appends. So handling appends alone would reject most chunks and
|
|
461
|
+
* leave the quadratic cost essentially in place:
|
|
462
|
+
*
|
|
463
|
+
* 1. the last row's cells are rewritten in place via `setSpans`, and
|
|
464
|
+
* 2. genuinely new rows go through `Table.appendRows`.
|
|
465
|
+
*
|
|
466
|
+
* Cells are compared by `text`, never `raw` — a table cell has no `raw` at all
|
|
467
|
+
* (its keys are `text`/`tokens`/`header`/`align`).
|
|
468
|
+
*/
|
|
469
|
+
private updateStreamedTable;
|
|
470
|
+
private updateBlockquoteTail;
|
|
471
|
+
/**
|
|
472
|
+
* Spans for a heading being updated in place.
|
|
473
|
+
*
|
|
474
|
+
* Kept in lockstep with the `heading` arm of {@link renderToken}, which builds
|
|
475
|
+
* its `RichText` through `renderInlineToRichText`: same `collectSpans` call and
|
|
476
|
+
* the same `decodeEntities` fallback when a heading has no inline tokens (`##`
|
|
477
|
+
* with no text yet, which a stream produces before its first word arrives). A
|
|
478
|
+
* plain `token.text` fallback here would leave an entity-bearing heading
|
|
479
|
+
* undecoded on the in-place path but decoded on a fresh render.
|
|
480
|
+
*/
|
|
481
|
+
private headingSpans;
|
|
482
|
+
/**
|
|
483
|
+
* Spans for the trailing paragraph with its last unclosed inline construct
|
|
484
|
+
* rendered as though it had closed, or `null` when there is nothing to guess.
|
|
485
|
+
*
|
|
486
|
+
* `null` is the answer for every `'literal'` stream, every closed or absent
|
|
487
|
+
* stream, and any trailing paragraph whose syntax is all balanced — so the
|
|
488
|
+
* caller falls back to {@link literalParagraphSpans} and pays nothing.
|
|
489
|
+
*
|
|
490
|
+
* Only the paragraph's LAST inline token is scanned. An unclosed construct can
|
|
491
|
+
* only be there: anything that closed is already its own `strong`/`em`/
|
|
492
|
+
* `codespan`/`link` token, so a syntax character surviving into a trailing
|
|
493
|
+
* plain-text run is one `marked` could not pair. Scanning the whole raw string
|
|
494
|
+
* instead would re-find the markers of already-closed constructs.
|
|
495
|
+
*/
|
|
496
|
+
private optimisticParagraphSpans;
|
|
497
|
+
/** Display style for a guessed-closed construct. */
|
|
498
|
+
private optimisticStyle;
|
|
499
|
+
/**
|
|
500
|
+
* Re-render the paragraph currently showing a guess from its own tokens, with
|
|
501
|
+
* no overlay, and forget it.
|
|
502
|
+
*
|
|
503
|
+
* Idempotent and free when no guess is live, which is what lets `close()`,
|
|
504
|
+
* `abort()`, and a mid-stream staleness check all call it unconditionally.
|
|
505
|
+
*/
|
|
506
|
+
/**
|
|
507
|
+
* Start the MathJax load, and re-typeset this document once it resolves.
|
|
508
|
+
*
|
|
509
|
+
* Called from two places, for two different reasons:
|
|
510
|
+
*
|
|
511
|
+
* - When an OPEN math fence is rendered. This is a prefetch, and it is what
|
|
512
|
+
* makes the lazy load invisible while streaming: the module starts loading
|
|
513
|
+
* the moment a formula begins arriving, several chunks before its closing
|
|
514
|
+
* fence, so by the time the fence closes the converter is usually already
|
|
515
|
+
* installed and the formula typesets synchronously on the normal path.
|
|
516
|
+
* - When a CLOSED fence could not be typeset because the module is not ready.
|
|
517
|
+
* That is the case a rebuild actually exists for: a document constructed with
|
|
518
|
+
* math already complete, or a stream that closed a fence faster than the
|
|
519
|
+
* module loaded.
|
|
520
|
+
*
|
|
521
|
+
* Idempotent per instance. Concurrent callers coalesce onto the one cached
|
|
522
|
+
* module promise, and `mathLoadPending` keeps a second rebuild from being
|
|
523
|
+
* queued while the first is outstanding.
|
|
524
|
+
*/
|
|
525
|
+
private ensureMathJax;
|
|
526
|
+
/**
|
|
527
|
+
* Rebuild every block from the tokens already lexed, without re-lexing.
|
|
528
|
+
*
|
|
529
|
+
* Used only when MathJax arrives after a formula has already been rendered as
|
|
530
|
+
* source. Rebuilding wholesale rather than surgically replacing the math blocks
|
|
531
|
+
* is the deliberate choice: `tokenChildPrefix` maps token indices to child
|
|
532
|
+
* slots positionally, so swapping one child in place would have to keep that
|
|
533
|
+
* mapping, the `Stack`'s cached box, and every following sibling's position in
|
|
534
|
+
* agreement by hand. Re-rendering the same token list in the same order leaves
|
|
535
|
+
* the mapping trivially correct, and this runs at most once per document — the
|
|
536
|
+
* same cost as the `setContent` rebuild that already exists.
|
|
537
|
+
*
|
|
538
|
+
* The optimistic tail is dropped first. Its `entity` is about to be destroyed,
|
|
539
|
+
* so the pointer would dangle; unwinding restores literal spans, and if the
|
|
540
|
+
* stream is still open the next chunk re-applies a guess.
|
|
541
|
+
*/
|
|
542
|
+
private retypesetFromTokens;
|
|
543
|
+
private unwindOptimisticTail;
|
|
544
|
+
/**
|
|
545
|
+
* Drop a guess that is no longer on the document's trailing paragraph.
|
|
546
|
+
*
|
|
547
|
+
* A coalesced append can add a block after the paragraph that owns the guess,
|
|
548
|
+
* at which point the guess is frozen — the construct can never close, because
|
|
549
|
+
* no further text lands in that paragraph. Without this the stale styling would
|
|
550
|
+
* survive until `close()`.
|
|
551
|
+
*
|
|
552
|
+
* `writtenThisPass` is the entity whose spans this reconcile already rewrote,
|
|
553
|
+
* if any: for that one, literal spans are on screen already and re-rendering it
|
|
554
|
+
* would be wasted layout, so only the bookkeeping is cleared.
|
|
555
|
+
*/
|
|
556
|
+
private dropStaleOptimisticTail;
|
|
557
|
+
/**
|
|
558
|
+
* Resolve once every in-flight worker append has actually been applied.
|
|
559
|
+
*
|
|
560
|
+
* Committing text is not the same as the document reflecting it: `append()`
|
|
561
|
+
* reaches `dispatchAppend()`, which `postMessage()`s and returns, and the reply
|
|
562
|
+
* that runs `updateTokens()` lands later. Without waiting here, `close()` could
|
|
563
|
+
* resolve — and `onStable` fire — against a document missing its last chunk.
|
|
564
|
+
*
|
|
565
|
+
* An outstanding lazy MathJax load counts as unsettled for the same reason. A
|
|
566
|
+
* document whose formulas are still TeX source is not final in any sense a
|
|
567
|
+
* caller of `onStable` cares about: the boxes are the wrong size, so measuring
|
|
568
|
+
* or exporting there would capture placeholders.
|
|
569
|
+
*/
|
|
570
|
+
private waitForAppendSettled;
|
|
571
|
+
/**
|
|
572
|
+
* Release settlement waiters, but only once nothing is outstanding.
|
|
573
|
+
*
|
|
574
|
+
* Called at the very END of the worker callback, after its coalesced-re-dispatch
|
|
575
|
+
* check, rather than wherever `appendInFlight` goes false. Within that callback
|
|
576
|
+
* `appendInFlight` is cleared and then, if another chunk arrived while the
|
|
577
|
+
* request was in flight, set straight back to `true` by the re-dispatch — both
|
|
578
|
+
* synchronously, before anything watching the flag could observe the gap. Only
|
|
579
|
+
* checking here, after that, waits through the re-dispatch instead of resolving
|
|
580
|
+
* one chunk early.
|
|
581
|
+
*/
|
|
582
|
+
private flushAppendSettledWaiters;
|
|
583
|
+
/** Throw if a public mutation is attempted from inside an `onStable` callback. */
|
|
584
|
+
private assertNotInStableCallback;
|
|
235
585
|
private updateTokens;
|
|
236
586
|
/**
|
|
237
587
|
* Render one nested block with a temporary width/margin context while
|
|
@@ -250,6 +600,19 @@ export declare class Markdown extends UIComponent {
|
|
|
250
600
|
* `renderToken`'s null returns.
|
|
251
601
|
*/
|
|
252
602
|
protected producesEntity(token: Token): boolean;
|
|
603
|
+
/**
|
|
604
|
+
* Build a centered display-math block, or `null` if MathJax cannot typeset yet.
|
|
605
|
+
*
|
|
606
|
+
* Shared by the `$$..$$` block token and a closed ```` ```math ```` fence:
|
|
607
|
+
* both are display math and must render identically, differing only in how
|
|
608
|
+
* they were spelled in the source.
|
|
609
|
+
*
|
|
610
|
+
* `ex` is font-relative, so the intrinsic box is resolved against the theme's
|
|
611
|
+
* body size. This is what a previously hardcoded `* 8` got wrong -- exact only
|
|
612
|
+
* near fontSize 18.1px, so a formula was ~13% oversized at the 16px default
|
|
613
|
+
* and far worse at other sizes.
|
|
614
|
+
*/
|
|
615
|
+
private renderDisplayMath;
|
|
253
616
|
protected renderToken(token: Token): Entity | null;
|
|
254
617
|
/** Structural — children draw themselves. */
|
|
255
618
|
render(_r: IRenderer): void;
|