@vectojs/markdown 0.20.3 → 0.21.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/dist/Markdown.d.ts +56 -0
- package/dist/index.js +160 -0
- package/dist/index.mjs +169 -1
- package/package.json +1 -1
package/dist/Markdown.d.ts
CHANGED
|
@@ -83,6 +83,23 @@ export interface MarkdownOptions {
|
|
|
83
83
|
* download behaviour to observe.
|
|
84
84
|
*/
|
|
85
85
|
saveFile?: (filename: string, content: string, mimeType: string) => void;
|
|
86
|
+
/**
|
|
87
|
+
* Materialize only the top-level blocks near the viewport of a very large
|
|
88
|
+
* document, representing off-screen height as numeric offsets rather than
|
|
89
|
+
* entities. Off by default; pass `true` or `{ overscan }` to opt in.
|
|
90
|
+
*
|
|
91
|
+
* The host must drive it with {@link Markdown.setVisibleRange} each scroll
|
|
92
|
+
* frame (a `ScrollView` whose content is this `Markdown` does so
|
|
93
|
+
* automatically). Off-screen height is estimated from the token source and
|
|
94
|
+
* refined to the exact measured height when a block first mounts, so the
|
|
95
|
+
* scrollbar stays correct while only a window of blocks exists as entities.
|
|
96
|
+
*
|
|
97
|
+
* Not supported together with streaming (`createStream` / `appendMarkdown`):
|
|
98
|
+
* a document that virtualizes must be rendered whole.
|
|
99
|
+
*/
|
|
100
|
+
virtualize?: boolean | {
|
|
101
|
+
overscan?: number;
|
|
102
|
+
};
|
|
86
103
|
}
|
|
87
104
|
/**
|
|
88
105
|
* Renders Markdown content into a VectoJS entity tree using {@link marked}.
|
|
@@ -138,6 +155,19 @@ export declare class Markdown extends UIComponent {
|
|
|
138
155
|
/** File saver used by the download controls. */
|
|
139
156
|
saveFile: (filename: string, content: string, mimeType: string) => void;
|
|
140
157
|
private activeBlockMetrics;
|
|
158
|
+
/** Whether `opts.virtualize` enabled viewport-culled block materialization. */
|
|
159
|
+
private readonly virtualizeBlocks;
|
|
160
|
+
/** Overscan margin (px) added above/below the visible window before mounting. */
|
|
161
|
+
private readonly virtualOverscan;
|
|
162
|
+
/** Top-level tokens that produce a child entity, in document order. */
|
|
163
|
+
private virtualTokens;
|
|
164
|
+
/** Fenwick tree over per-block strides (height + blockGap) when virtualizing. */
|
|
165
|
+
private virtualHeights;
|
|
166
|
+
/** token index → mounted entity, for blocks currently inside `content`. */
|
|
167
|
+
private readonly virtualMounted;
|
|
168
|
+
/** Visible window top (scroll offset) and height, last set via setVisibleRange. */
|
|
169
|
+
private virtualScrollY;
|
|
170
|
+
private virtualViewportH;
|
|
141
171
|
/**
|
|
142
172
|
* Called after this entity's own `width`/`height` changed because the document
|
|
143
173
|
* was re-laid-out. **This is the hook to wire up when a host has to move or
|
|
@@ -388,6 +418,32 @@ export declare class Markdown extends UIComponent {
|
|
|
388
418
|
*/
|
|
389
419
|
get frontMatterFields(): Readonly<Record<string, string>>;
|
|
390
420
|
private renderMarkdown;
|
|
421
|
+
/**
|
|
422
|
+
* Drive the virtualized window from the host's scroll state.
|
|
423
|
+
*
|
|
424
|
+
* Only the blocks intersecting `[scrollY − overscan, scrollY + viewportHeight +
|
|
425
|
+
* overscan]` stay materialized; every other block is a numeric offset in the
|
|
426
|
+
* height tree, so the scrollbar still reports the full document height while
|
|
427
|
+
* only a window of blocks exists as entities. A no-op when `virtualize` was
|
|
428
|
+
* not enabled.
|
|
429
|
+
*/
|
|
430
|
+
setVisibleRange(scrollY: number, viewportHeight: number): void;
|
|
431
|
+
/**
|
|
432
|
+
* Cheap per-block height estimate from the token source, refined to the exact
|
|
433
|
+
* measured height when the block first mounts. Coarse by design: the estimate
|
|
434
|
+
* only has to keep the scrollbar plausible before a block is first seen; the
|
|
435
|
+
* Fenwick tree corrects the rest.
|
|
436
|
+
*/
|
|
437
|
+
private estimateBlockHeight;
|
|
438
|
+
/** Estimate a wrapped line count from raw character length against the max width. */
|
|
439
|
+
private estimateTextHeight;
|
|
440
|
+
/**
|
|
441
|
+
* Mount the blocks in the current window and unmount the rest. The `content`
|
|
442
|
+
* Stack lays the mounted window contiguously from y = 0; `content.y` is then
|
|
443
|
+
* set to the skipped prefix sum so the first mounted block lands at its true
|
|
444
|
+
* y — a numeric spacer, not a wrapper entity.
|
|
445
|
+
*/
|
|
446
|
+
private reconcileVirtual;
|
|
391
447
|
/** Create a frame-coalesced stream bound to this Markdown instance. */
|
|
392
448
|
createStream(options?: StreamControllerOptions): StreamController;
|
|
393
449
|
/**
|
package/dist/index.js
CHANGED
|
@@ -3499,6 +3499,19 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3499
3499
|
/** File saver used by the download controls. */
|
|
3500
3500
|
saveFile;
|
|
3501
3501
|
activeBlockMetrics = null;
|
|
3502
|
+
/** Whether `opts.virtualize` enabled viewport-culled block materialization. */
|
|
3503
|
+
virtualizeBlocks;
|
|
3504
|
+
/** Overscan margin (px) added above/below the visible window before mounting. */
|
|
3505
|
+
virtualOverscan;
|
|
3506
|
+
/** Top-level tokens that produce a child entity, in document order. */
|
|
3507
|
+
virtualTokens = null;
|
|
3508
|
+
/** Fenwick tree over per-block strides (height + blockGap) when virtualizing. */
|
|
3509
|
+
virtualHeights = null;
|
|
3510
|
+
/** token index → mounted entity, for blocks currently inside `content`. */
|
|
3511
|
+
virtualMounted = /* @__PURE__ */ new Map();
|
|
3512
|
+
/** Visible window top (scroll offset) and height, last set via setVisibleRange. */
|
|
3513
|
+
virtualScrollY = 0;
|
|
3514
|
+
virtualViewportH = 0;
|
|
3502
3515
|
/**
|
|
3503
3516
|
* Called after this entity's own `width`/`height` changed because the document
|
|
3504
3517
|
* was re-laid-out. **This is the hook to wire up when a host has to move or
|
|
@@ -3784,6 +3797,9 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3784
3797
|
this.showCodeLanguage = opts.showCodeLanguage ?? false;
|
|
3785
3798
|
this.writeClipboard = opts.writeClipboard ?? defaultWriteClipboard;
|
|
3786
3799
|
this.saveFile = opts.saveFile ?? defaultSaveFile;
|
|
3800
|
+
const virt = opts.virtualize;
|
|
3801
|
+
this.virtualizeBlocks = virt === true || typeof virt === "object" && virt !== null;
|
|
3802
|
+
this.virtualOverscan = typeof virt === "object" && virt !== null && typeof virt.overscan === "number" ? virt.overscan : 800;
|
|
3787
3803
|
this.content = new import_ui4.Stack({
|
|
3788
3804
|
direction: "vertical",
|
|
3789
3805
|
gap: this.theme.blockGap
|
|
@@ -3896,6 +3912,20 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3896
3912
|
const tokens = lexMarkdown(text, this._userTiming);
|
|
3897
3913
|
this.setTokens(tokens);
|
|
3898
3914
|
this.abbreviations = collectAbbreviations(tokens);
|
|
3915
|
+
if (this.virtualizeBlocks) {
|
|
3916
|
+
this.virtualTokens = tokens.filter((token) => this.producesEntity(token));
|
|
3917
|
+
const n = this.virtualTokens.length;
|
|
3918
|
+
this.virtualHeights = new import_ui4.RowHeights(n, 0);
|
|
3919
|
+
for (let i = 0; i < n; i++) {
|
|
3920
|
+
this.virtualHeights.set(
|
|
3921
|
+
i,
|
|
3922
|
+
this.estimateBlockHeight(this.virtualTokens[i]) + this.theme.blockGap
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
this.virtualMounted.clear();
|
|
3926
|
+
this.reconcileVirtual();
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3899
3929
|
for (const token of tokens) {
|
|
3900
3930
|
const el = this.renderToken(token);
|
|
3901
3931
|
if (el) {
|
|
@@ -3905,8 +3935,123 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3905
3935
|
this.width = this.content.width;
|
|
3906
3936
|
this.height = this.content.height;
|
|
3907
3937
|
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Drive the virtualized window from the host's scroll state.
|
|
3940
|
+
*
|
|
3941
|
+
* Only the blocks intersecting `[scrollY − overscan, scrollY + viewportHeight +
|
|
3942
|
+
* overscan]` stay materialized; every other block is a numeric offset in the
|
|
3943
|
+
* height tree, so the scrollbar still reports the full document height while
|
|
3944
|
+
* only a window of blocks exists as entities. A no-op when `virtualize` was
|
|
3945
|
+
* not enabled.
|
|
3946
|
+
*/
|
|
3947
|
+
setVisibleRange(scrollY, viewportHeight) {
|
|
3948
|
+
if (!this.virtualizeBlocks) return;
|
|
3949
|
+
this.virtualScrollY = Math.max(0, scrollY);
|
|
3950
|
+
this.virtualViewportH = Math.max(0, viewportHeight);
|
|
3951
|
+
this.reconcileVirtual();
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Cheap per-block height estimate from the token source, refined to the exact
|
|
3955
|
+
* measured height when the block first mounts. Coarse by design: the estimate
|
|
3956
|
+
* only has to keep the scrollbar plausible before a block is first seen; the
|
|
3957
|
+
* Fenwick tree corrects the rest.
|
|
3958
|
+
*/
|
|
3959
|
+
estimateBlockHeight(token) {
|
|
3960
|
+
const t = this.theme;
|
|
3961
|
+
switch (token.type) {
|
|
3962
|
+
case "heading": {
|
|
3963
|
+
const size = headingSize(t, token.depth);
|
|
3964
|
+
return Math.ceil(size * 1.5) + 8;
|
|
3965
|
+
}
|
|
3966
|
+
case "paragraph":
|
|
3967
|
+
return this.estimateTextHeight(token.text, t.bodyLineHeight);
|
|
3968
|
+
case "code": {
|
|
3969
|
+
const raw = token.text;
|
|
3970
|
+
const lines = Math.max(1, raw.split("\n").length - (raw.endsWith("\n") ? 1 : 0));
|
|
3971
|
+
let h = lines * t.codeLineHeight + t.codePadding * 2;
|
|
3972
|
+
if (this.showCodeLanguage) h += t.codeLangFontSize + 8;
|
|
3973
|
+
return h;
|
|
3974
|
+
}
|
|
3975
|
+
case "hr":
|
|
3976
|
+
return 24;
|
|
3977
|
+
case "blockquote":
|
|
3978
|
+
case "container": {
|
|
3979
|
+
const inner = token.tokens ?? [];
|
|
3980
|
+
let sum = 0;
|
|
3981
|
+
for (const tk of inner) sum += this.estimateBlockHeight(tk) + t.blockGap;
|
|
3982
|
+
return sum + t.quoteInnerGap * 2 + 8;
|
|
3983
|
+
}
|
|
3984
|
+
case "list": {
|
|
3985
|
+
const items = token.items ?? [];
|
|
3986
|
+
let sum = 0;
|
|
3987
|
+
for (const item of items) {
|
|
3988
|
+
for (const tk of item.tokens ?? []) sum += this.estimateBlockHeight(tk) + t.listItemGap;
|
|
3989
|
+
}
|
|
3990
|
+
return sum + t.listGap;
|
|
3991
|
+
}
|
|
3992
|
+
case "table": {
|
|
3993
|
+
const table = token;
|
|
3994
|
+
const rows = (table.rows?.length ?? 0) + (table.header?.length ?? 0);
|
|
3995
|
+
return rows * (t.tableFontSize + 16) + 16;
|
|
3996
|
+
}
|
|
3997
|
+
case "footnoteDef":
|
|
3998
|
+
return this.estimateTextHeight(token.text ?? "", t.bodyLineHeight);
|
|
3999
|
+
case "html":
|
|
4000
|
+
return 200;
|
|
4001
|
+
default:
|
|
4002
|
+
return t.bodyLineHeight;
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
/** Estimate a wrapped line count from raw character length against the max width. */
|
|
4006
|
+
estimateTextHeight(text, lineHeight) {
|
|
4007
|
+
const charsPerLine = Math.max(20, Math.floor(this.maxWidth / (this.theme.fontSize * 0.5)));
|
|
4008
|
+
const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
|
|
4009
|
+
return lines * lineHeight;
|
|
4010
|
+
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Mount the blocks in the current window and unmount the rest. The `content`
|
|
4013
|
+
* Stack lays the mounted window contiguously from y = 0; `content.y` is then
|
|
4014
|
+
* set to the skipped prefix sum so the first mounted block lands at its true
|
|
4015
|
+
* y — a numeric spacer, not a wrapper entity.
|
|
4016
|
+
*/
|
|
4017
|
+
reconcileVirtual() {
|
|
4018
|
+
const tree = this.virtualHeights;
|
|
4019
|
+
const tokens = this.virtualTokens;
|
|
4020
|
+
if (!tree || !tokens) return;
|
|
4021
|
+
const first = tree.indexAt(Math.max(0, this.virtualScrollY - this.virtualOverscan));
|
|
4022
|
+
const last = tree.indexAt(
|
|
4023
|
+
Math.max(0, this.virtualScrollY + this.virtualViewportH + this.virtualOverscan)
|
|
4024
|
+
);
|
|
4025
|
+
let changed = false;
|
|
4026
|
+
for (const [idx, el] of this.virtualMounted) {
|
|
4027
|
+
if (idx < first || idx > last) {
|
|
4028
|
+
el.destroy();
|
|
4029
|
+
this.virtualMounted.delete(idx);
|
|
4030
|
+
changed = true;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
for (let i = first; i <= last; i++) {
|
|
4034
|
+
if (this.virtualMounted.has(i)) continue;
|
|
4035
|
+
const el = this.renderToken(tokens[i]);
|
|
4036
|
+
if (!el) continue;
|
|
4037
|
+
this.content.add(el);
|
|
4038
|
+
tree.set(i, el.height + this.theme.blockGap);
|
|
4039
|
+
this.virtualMounted.set(i, el);
|
|
4040
|
+
changed = true;
|
|
4041
|
+
}
|
|
4042
|
+
if (changed) this.content.layout();
|
|
4043
|
+
this.content.y = tree.prefix(first);
|
|
4044
|
+
const nextHeight = Math.max(0, tree.total() - this.theme.blockGap);
|
|
4045
|
+
const heightChanged = nextHeight !== this.height;
|
|
4046
|
+
this.width = this.content.width;
|
|
4047
|
+
this.height = nextHeight;
|
|
4048
|
+
if (heightChanged) this.notifyLayoutUpdated();
|
|
4049
|
+
}
|
|
3908
4050
|
/** Create a frame-coalesced stream bound to this Markdown instance. */
|
|
3909
4051
|
createStream(options = {}) {
|
|
4052
|
+
if (this.virtualizeBlocks) {
|
|
4053
|
+
throw new Error("Markdown.createStream is not supported when virtualize is enabled");
|
|
4054
|
+
}
|
|
3910
4055
|
if (this.streamController) {
|
|
3911
4056
|
throw new Error("Markdown already has an active StreamController");
|
|
3912
4057
|
}
|
|
@@ -3979,6 +4124,18 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3979
4124
|
const next = Math.max(0, maxWidth);
|
|
3980
4125
|
if (next === this.maxWidth) return this;
|
|
3981
4126
|
this.maxWidth = next;
|
|
4127
|
+
if (this.virtualizeBlocks && this.virtualTokens && this.virtualHeights) {
|
|
4128
|
+
const tokens = this.virtualTokens;
|
|
4129
|
+
const tree = this.virtualHeights;
|
|
4130
|
+
for (const el of this.virtualMounted.values()) el.destroy();
|
|
4131
|
+
this.virtualMounted.clear();
|
|
4132
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
4133
|
+
tree.set(i, this.estimateBlockHeight(tokens[i]) + this.theme.blockGap);
|
|
4134
|
+
}
|
|
4135
|
+
this.reconcileVirtual();
|
|
4136
|
+
this.scene?.markDirty();
|
|
4137
|
+
return this;
|
|
4138
|
+
}
|
|
3982
4139
|
let childIndex = 0;
|
|
3983
4140
|
const children = this.content.children;
|
|
3984
4141
|
for (const token of this.tokens) {
|
|
@@ -4513,6 +4670,9 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
4513
4670
|
return this.appendMarkdownCore(chunk);
|
|
4514
4671
|
}
|
|
4515
4672
|
appendMarkdownCore(chunk) {
|
|
4673
|
+
if (this.virtualizeBlocks) {
|
|
4674
|
+
throw new Error("Markdown.appendMarkdown is not supported when virtualize is enabled");
|
|
4675
|
+
}
|
|
4516
4676
|
const before = this.rawMarkdown.length;
|
|
4517
4677
|
this.consumeFrontMatter(chunk);
|
|
4518
4678
|
this.streamStats.appends++;
|
package/dist/index.mjs
CHANGED
|
@@ -2948,7 +2948,15 @@ function renderFencedBlock(source, lang, options) {
|
|
|
2948
2948
|
}
|
|
2949
2949
|
|
|
2950
2950
|
// src/Markdown.ts
|
|
2951
|
-
import {
|
|
2951
|
+
import {
|
|
2952
|
+
RichText as RichText2,
|
|
2953
|
+
RowHeights,
|
|
2954
|
+
Stack,
|
|
2955
|
+
Table,
|
|
2956
|
+
Text,
|
|
2957
|
+
Image,
|
|
2958
|
+
UIComponent as UIComponent3
|
|
2959
|
+
} from "@vectojs/ui";
|
|
2952
2960
|
|
|
2953
2961
|
// src/blockAffordances.ts
|
|
2954
2962
|
import { Button, measureText as measureText2, UIComponent as UIComponent2 } from "@vectojs/ui";
|
|
@@ -3445,6 +3453,19 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3445
3453
|
/** File saver used by the download controls. */
|
|
3446
3454
|
saveFile;
|
|
3447
3455
|
activeBlockMetrics = null;
|
|
3456
|
+
/** Whether `opts.virtualize` enabled viewport-culled block materialization. */
|
|
3457
|
+
virtualizeBlocks;
|
|
3458
|
+
/** Overscan margin (px) added above/below the visible window before mounting. */
|
|
3459
|
+
virtualOverscan;
|
|
3460
|
+
/** Top-level tokens that produce a child entity, in document order. */
|
|
3461
|
+
virtualTokens = null;
|
|
3462
|
+
/** Fenwick tree over per-block strides (height + blockGap) when virtualizing. */
|
|
3463
|
+
virtualHeights = null;
|
|
3464
|
+
/** token index → mounted entity, for blocks currently inside `content`. */
|
|
3465
|
+
virtualMounted = /* @__PURE__ */ new Map();
|
|
3466
|
+
/** Visible window top (scroll offset) and height, last set via setVisibleRange. */
|
|
3467
|
+
virtualScrollY = 0;
|
|
3468
|
+
virtualViewportH = 0;
|
|
3448
3469
|
/**
|
|
3449
3470
|
* Called after this entity's own `width`/`height` changed because the document
|
|
3450
3471
|
* was re-laid-out. **This is the hook to wire up when a host has to move or
|
|
@@ -3730,6 +3751,9 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3730
3751
|
this.showCodeLanguage = opts.showCodeLanguage ?? false;
|
|
3731
3752
|
this.writeClipboard = opts.writeClipboard ?? defaultWriteClipboard;
|
|
3732
3753
|
this.saveFile = opts.saveFile ?? defaultSaveFile;
|
|
3754
|
+
const virt = opts.virtualize;
|
|
3755
|
+
this.virtualizeBlocks = virt === true || typeof virt === "object" && virt !== null;
|
|
3756
|
+
this.virtualOverscan = typeof virt === "object" && virt !== null && typeof virt.overscan === "number" ? virt.overscan : 800;
|
|
3733
3757
|
this.content = new Stack({
|
|
3734
3758
|
direction: "vertical",
|
|
3735
3759
|
gap: this.theme.blockGap
|
|
@@ -3842,6 +3866,20 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3842
3866
|
const tokens = lexMarkdown(text, this._userTiming);
|
|
3843
3867
|
this.setTokens(tokens);
|
|
3844
3868
|
this.abbreviations = collectAbbreviations(tokens);
|
|
3869
|
+
if (this.virtualizeBlocks) {
|
|
3870
|
+
this.virtualTokens = tokens.filter((token) => this.producesEntity(token));
|
|
3871
|
+
const n = this.virtualTokens.length;
|
|
3872
|
+
this.virtualHeights = new RowHeights(n, 0);
|
|
3873
|
+
for (let i = 0; i < n; i++) {
|
|
3874
|
+
this.virtualHeights.set(
|
|
3875
|
+
i,
|
|
3876
|
+
this.estimateBlockHeight(this.virtualTokens[i]) + this.theme.blockGap
|
|
3877
|
+
);
|
|
3878
|
+
}
|
|
3879
|
+
this.virtualMounted.clear();
|
|
3880
|
+
this.reconcileVirtual();
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3845
3883
|
for (const token of tokens) {
|
|
3846
3884
|
const el = this.renderToken(token);
|
|
3847
3885
|
if (el) {
|
|
@@ -3851,8 +3889,123 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3851
3889
|
this.width = this.content.width;
|
|
3852
3890
|
this.height = this.content.height;
|
|
3853
3891
|
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Drive the virtualized window from the host's scroll state.
|
|
3894
|
+
*
|
|
3895
|
+
* Only the blocks intersecting `[scrollY − overscan, scrollY + viewportHeight +
|
|
3896
|
+
* overscan]` stay materialized; every other block is a numeric offset in the
|
|
3897
|
+
* height tree, so the scrollbar still reports the full document height while
|
|
3898
|
+
* only a window of blocks exists as entities. A no-op when `virtualize` was
|
|
3899
|
+
* not enabled.
|
|
3900
|
+
*/
|
|
3901
|
+
setVisibleRange(scrollY, viewportHeight) {
|
|
3902
|
+
if (!this.virtualizeBlocks) return;
|
|
3903
|
+
this.virtualScrollY = Math.max(0, scrollY);
|
|
3904
|
+
this.virtualViewportH = Math.max(0, viewportHeight);
|
|
3905
|
+
this.reconcileVirtual();
|
|
3906
|
+
}
|
|
3907
|
+
/**
|
|
3908
|
+
* Cheap per-block height estimate from the token source, refined to the exact
|
|
3909
|
+
* measured height when the block first mounts. Coarse by design: the estimate
|
|
3910
|
+
* only has to keep the scrollbar plausible before a block is first seen; the
|
|
3911
|
+
* Fenwick tree corrects the rest.
|
|
3912
|
+
*/
|
|
3913
|
+
estimateBlockHeight(token) {
|
|
3914
|
+
const t = this.theme;
|
|
3915
|
+
switch (token.type) {
|
|
3916
|
+
case "heading": {
|
|
3917
|
+
const size = headingSize(t, token.depth);
|
|
3918
|
+
return Math.ceil(size * 1.5) + 8;
|
|
3919
|
+
}
|
|
3920
|
+
case "paragraph":
|
|
3921
|
+
return this.estimateTextHeight(token.text, t.bodyLineHeight);
|
|
3922
|
+
case "code": {
|
|
3923
|
+
const raw = token.text;
|
|
3924
|
+
const lines = Math.max(1, raw.split("\n").length - (raw.endsWith("\n") ? 1 : 0));
|
|
3925
|
+
let h = lines * t.codeLineHeight + t.codePadding * 2;
|
|
3926
|
+
if (this.showCodeLanguage) h += t.codeLangFontSize + 8;
|
|
3927
|
+
return h;
|
|
3928
|
+
}
|
|
3929
|
+
case "hr":
|
|
3930
|
+
return 24;
|
|
3931
|
+
case "blockquote":
|
|
3932
|
+
case "container": {
|
|
3933
|
+
const inner = token.tokens ?? [];
|
|
3934
|
+
let sum = 0;
|
|
3935
|
+
for (const tk of inner) sum += this.estimateBlockHeight(tk) + t.blockGap;
|
|
3936
|
+
return sum + t.quoteInnerGap * 2 + 8;
|
|
3937
|
+
}
|
|
3938
|
+
case "list": {
|
|
3939
|
+
const items = token.items ?? [];
|
|
3940
|
+
let sum = 0;
|
|
3941
|
+
for (const item of items) {
|
|
3942
|
+
for (const tk of item.tokens ?? []) sum += this.estimateBlockHeight(tk) + t.listItemGap;
|
|
3943
|
+
}
|
|
3944
|
+
return sum + t.listGap;
|
|
3945
|
+
}
|
|
3946
|
+
case "table": {
|
|
3947
|
+
const table = token;
|
|
3948
|
+
const rows = (table.rows?.length ?? 0) + (table.header?.length ?? 0);
|
|
3949
|
+
return rows * (t.tableFontSize + 16) + 16;
|
|
3950
|
+
}
|
|
3951
|
+
case "footnoteDef":
|
|
3952
|
+
return this.estimateTextHeight(token.text ?? "", t.bodyLineHeight);
|
|
3953
|
+
case "html":
|
|
3954
|
+
return 200;
|
|
3955
|
+
default:
|
|
3956
|
+
return t.bodyLineHeight;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
/** Estimate a wrapped line count from raw character length against the max width. */
|
|
3960
|
+
estimateTextHeight(text, lineHeight) {
|
|
3961
|
+
const charsPerLine = Math.max(20, Math.floor(this.maxWidth / (this.theme.fontSize * 0.5)));
|
|
3962
|
+
const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
|
|
3963
|
+
return lines * lineHeight;
|
|
3964
|
+
}
|
|
3965
|
+
/**
|
|
3966
|
+
* Mount the blocks in the current window and unmount the rest. The `content`
|
|
3967
|
+
* Stack lays the mounted window contiguously from y = 0; `content.y` is then
|
|
3968
|
+
* set to the skipped prefix sum so the first mounted block lands at its true
|
|
3969
|
+
* y — a numeric spacer, not a wrapper entity.
|
|
3970
|
+
*/
|
|
3971
|
+
reconcileVirtual() {
|
|
3972
|
+
const tree = this.virtualHeights;
|
|
3973
|
+
const tokens = this.virtualTokens;
|
|
3974
|
+
if (!tree || !tokens) return;
|
|
3975
|
+
const first = tree.indexAt(Math.max(0, this.virtualScrollY - this.virtualOverscan));
|
|
3976
|
+
const last = tree.indexAt(
|
|
3977
|
+
Math.max(0, this.virtualScrollY + this.virtualViewportH + this.virtualOverscan)
|
|
3978
|
+
);
|
|
3979
|
+
let changed = false;
|
|
3980
|
+
for (const [idx, el] of this.virtualMounted) {
|
|
3981
|
+
if (idx < first || idx > last) {
|
|
3982
|
+
el.destroy();
|
|
3983
|
+
this.virtualMounted.delete(idx);
|
|
3984
|
+
changed = true;
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
for (let i = first; i <= last; i++) {
|
|
3988
|
+
if (this.virtualMounted.has(i)) continue;
|
|
3989
|
+
const el = this.renderToken(tokens[i]);
|
|
3990
|
+
if (!el) continue;
|
|
3991
|
+
this.content.add(el);
|
|
3992
|
+
tree.set(i, el.height + this.theme.blockGap);
|
|
3993
|
+
this.virtualMounted.set(i, el);
|
|
3994
|
+
changed = true;
|
|
3995
|
+
}
|
|
3996
|
+
if (changed) this.content.layout();
|
|
3997
|
+
this.content.y = tree.prefix(first);
|
|
3998
|
+
const nextHeight = Math.max(0, tree.total() - this.theme.blockGap);
|
|
3999
|
+
const heightChanged = nextHeight !== this.height;
|
|
4000
|
+
this.width = this.content.width;
|
|
4001
|
+
this.height = nextHeight;
|
|
4002
|
+
if (heightChanged) this.notifyLayoutUpdated();
|
|
4003
|
+
}
|
|
3854
4004
|
/** Create a frame-coalesced stream bound to this Markdown instance. */
|
|
3855
4005
|
createStream(options = {}) {
|
|
4006
|
+
if (this.virtualizeBlocks) {
|
|
4007
|
+
throw new Error("Markdown.createStream is not supported when virtualize is enabled");
|
|
4008
|
+
}
|
|
3856
4009
|
if (this.streamController) {
|
|
3857
4010
|
throw new Error("Markdown already has an active StreamController");
|
|
3858
4011
|
}
|
|
@@ -3925,6 +4078,18 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3925
4078
|
const next = Math.max(0, maxWidth);
|
|
3926
4079
|
if (next === this.maxWidth) return this;
|
|
3927
4080
|
this.maxWidth = next;
|
|
4081
|
+
if (this.virtualizeBlocks && this.virtualTokens && this.virtualHeights) {
|
|
4082
|
+
const tokens = this.virtualTokens;
|
|
4083
|
+
const tree = this.virtualHeights;
|
|
4084
|
+
for (const el of this.virtualMounted.values()) el.destroy();
|
|
4085
|
+
this.virtualMounted.clear();
|
|
4086
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
4087
|
+
tree.set(i, this.estimateBlockHeight(tokens[i]) + this.theme.blockGap);
|
|
4088
|
+
}
|
|
4089
|
+
this.reconcileVirtual();
|
|
4090
|
+
this.scene?.markDirty();
|
|
4091
|
+
return this;
|
|
4092
|
+
}
|
|
3928
4093
|
let childIndex = 0;
|
|
3929
4094
|
const children = this.content.children;
|
|
3930
4095
|
for (const token of this.tokens) {
|
|
@@ -4459,6 +4624,9 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
4459
4624
|
return this.appendMarkdownCore(chunk);
|
|
4460
4625
|
}
|
|
4461
4626
|
appendMarkdownCore(chunk) {
|
|
4627
|
+
if (this.virtualizeBlocks) {
|
|
4628
|
+
throw new Error("Markdown.appendMarkdown is not supported when virtualize is enabled");
|
|
4629
|
+
}
|
|
4462
4630
|
const before = this.rawMarkdown.length;
|
|
4463
4631
|
this.consumeFrontMatter(chunk);
|
|
4464
4632
|
this.streamStats.appends++;
|