@vectojs/markdown 0.20.3 → 0.21.1
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 +162 -1
- package/dist/index.mjs +171 -2
- 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,9 +3797,13 @@ 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
|
-
gap: this.theme.blockGap
|
|
3805
|
+
gap: this.theme.blockGap,
|
|
3806
|
+
cullOffscreenChildren: true
|
|
3790
3807
|
});
|
|
3791
3808
|
this.add(this.content);
|
|
3792
3809
|
this.rawMarkdown = "";
|
|
@@ -3896,6 +3913,20 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3896
3913
|
const tokens = lexMarkdown(text, this._userTiming);
|
|
3897
3914
|
this.setTokens(tokens);
|
|
3898
3915
|
this.abbreviations = collectAbbreviations(tokens);
|
|
3916
|
+
if (this.virtualizeBlocks) {
|
|
3917
|
+
this.virtualTokens = tokens.filter((token) => this.producesEntity(token));
|
|
3918
|
+
const n = this.virtualTokens.length;
|
|
3919
|
+
this.virtualHeights = new import_ui4.RowHeights(n, 0);
|
|
3920
|
+
for (let i = 0; i < n; i++) {
|
|
3921
|
+
this.virtualHeights.set(
|
|
3922
|
+
i,
|
|
3923
|
+
this.estimateBlockHeight(this.virtualTokens[i]) + this.theme.blockGap
|
|
3924
|
+
);
|
|
3925
|
+
}
|
|
3926
|
+
this.virtualMounted.clear();
|
|
3927
|
+
this.reconcileVirtual();
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3899
3930
|
for (const token of tokens) {
|
|
3900
3931
|
const el = this.renderToken(token);
|
|
3901
3932
|
if (el) {
|
|
@@ -3905,8 +3936,123 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3905
3936
|
this.width = this.content.width;
|
|
3906
3937
|
this.height = this.content.height;
|
|
3907
3938
|
}
|
|
3939
|
+
/**
|
|
3940
|
+
* Drive the virtualized window from the host's scroll state.
|
|
3941
|
+
*
|
|
3942
|
+
* Only the blocks intersecting `[scrollY − overscan, scrollY + viewportHeight +
|
|
3943
|
+
* overscan]` stay materialized; every other block is a numeric offset in the
|
|
3944
|
+
* height tree, so the scrollbar still reports the full document height while
|
|
3945
|
+
* only a window of blocks exists as entities. A no-op when `virtualize` was
|
|
3946
|
+
* not enabled.
|
|
3947
|
+
*/
|
|
3948
|
+
setVisibleRange(scrollY, viewportHeight) {
|
|
3949
|
+
if (!this.virtualizeBlocks) return;
|
|
3950
|
+
this.virtualScrollY = Math.max(0, scrollY);
|
|
3951
|
+
this.virtualViewportH = Math.max(0, viewportHeight);
|
|
3952
|
+
this.reconcileVirtual();
|
|
3953
|
+
}
|
|
3954
|
+
/**
|
|
3955
|
+
* Cheap per-block height estimate from the token source, refined to the exact
|
|
3956
|
+
* measured height when the block first mounts. Coarse by design: the estimate
|
|
3957
|
+
* only has to keep the scrollbar plausible before a block is first seen; the
|
|
3958
|
+
* Fenwick tree corrects the rest.
|
|
3959
|
+
*/
|
|
3960
|
+
estimateBlockHeight(token) {
|
|
3961
|
+
const t = this.theme;
|
|
3962
|
+
switch (token.type) {
|
|
3963
|
+
case "heading": {
|
|
3964
|
+
const size = headingSize(t, token.depth);
|
|
3965
|
+
return Math.ceil(size * 1.5) + 8;
|
|
3966
|
+
}
|
|
3967
|
+
case "paragraph":
|
|
3968
|
+
return this.estimateTextHeight(token.text, t.bodyLineHeight);
|
|
3969
|
+
case "code": {
|
|
3970
|
+
const raw = token.text;
|
|
3971
|
+
const lines = Math.max(1, raw.split("\n").length - (raw.endsWith("\n") ? 1 : 0));
|
|
3972
|
+
let h = lines * t.codeLineHeight + t.codePadding * 2;
|
|
3973
|
+
if (this.showCodeLanguage) h += t.codeLangFontSize + 8;
|
|
3974
|
+
return h;
|
|
3975
|
+
}
|
|
3976
|
+
case "hr":
|
|
3977
|
+
return 24;
|
|
3978
|
+
case "blockquote":
|
|
3979
|
+
case "container": {
|
|
3980
|
+
const inner = token.tokens ?? [];
|
|
3981
|
+
let sum = 0;
|
|
3982
|
+
for (const tk of inner) sum += this.estimateBlockHeight(tk) + t.blockGap;
|
|
3983
|
+
return sum + t.quoteInnerGap * 2 + 8;
|
|
3984
|
+
}
|
|
3985
|
+
case "list": {
|
|
3986
|
+
const items = token.items ?? [];
|
|
3987
|
+
let sum = 0;
|
|
3988
|
+
for (const item of items) {
|
|
3989
|
+
for (const tk of item.tokens ?? []) sum += this.estimateBlockHeight(tk) + t.listItemGap;
|
|
3990
|
+
}
|
|
3991
|
+
return sum + t.listGap;
|
|
3992
|
+
}
|
|
3993
|
+
case "table": {
|
|
3994
|
+
const table = token;
|
|
3995
|
+
const rows = (table.rows?.length ?? 0) + (table.header?.length ?? 0);
|
|
3996
|
+
return rows * (t.tableFontSize + 16) + 16;
|
|
3997
|
+
}
|
|
3998
|
+
case "footnoteDef":
|
|
3999
|
+
return this.estimateTextHeight(token.text ?? "", t.bodyLineHeight);
|
|
4000
|
+
case "html":
|
|
4001
|
+
return 200;
|
|
4002
|
+
default:
|
|
4003
|
+
return t.bodyLineHeight;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
/** Estimate a wrapped line count from raw character length against the max width. */
|
|
4007
|
+
estimateTextHeight(text, lineHeight) {
|
|
4008
|
+
const charsPerLine = Math.max(20, Math.floor(this.maxWidth / (this.theme.fontSize * 0.5)));
|
|
4009
|
+
const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
|
|
4010
|
+
return lines * lineHeight;
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* Mount the blocks in the current window and unmount the rest. The `content`
|
|
4014
|
+
* Stack lays the mounted window contiguously from y = 0; `content.y` is then
|
|
4015
|
+
* set to the skipped prefix sum so the first mounted block lands at its true
|
|
4016
|
+
* y — a numeric spacer, not a wrapper entity.
|
|
4017
|
+
*/
|
|
4018
|
+
reconcileVirtual() {
|
|
4019
|
+
const tree = this.virtualHeights;
|
|
4020
|
+
const tokens = this.virtualTokens;
|
|
4021
|
+
if (!tree || !tokens) return;
|
|
4022
|
+
const first = tree.indexAt(Math.max(0, this.virtualScrollY - this.virtualOverscan));
|
|
4023
|
+
const last = tree.indexAt(
|
|
4024
|
+
Math.max(0, this.virtualScrollY + this.virtualViewportH + this.virtualOverscan)
|
|
4025
|
+
);
|
|
4026
|
+
let changed = false;
|
|
4027
|
+
for (const [idx, el] of this.virtualMounted) {
|
|
4028
|
+
if (idx < first || idx > last) {
|
|
4029
|
+
el.destroy();
|
|
4030
|
+
this.virtualMounted.delete(idx);
|
|
4031
|
+
changed = true;
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
for (let i = first; i <= last; i++) {
|
|
4035
|
+
if (this.virtualMounted.has(i)) continue;
|
|
4036
|
+
const el = this.renderToken(tokens[i]);
|
|
4037
|
+
if (!el) continue;
|
|
4038
|
+
this.content.add(el);
|
|
4039
|
+
tree.set(i, el.height + this.theme.blockGap);
|
|
4040
|
+
this.virtualMounted.set(i, el);
|
|
4041
|
+
changed = true;
|
|
4042
|
+
}
|
|
4043
|
+
if (changed) this.content.layout();
|
|
4044
|
+
this.content.y = tree.prefix(first);
|
|
4045
|
+
const nextHeight = Math.max(0, tree.total() - this.theme.blockGap);
|
|
4046
|
+
const heightChanged = nextHeight !== this.height;
|
|
4047
|
+
this.width = this.content.width;
|
|
4048
|
+
this.height = nextHeight;
|
|
4049
|
+
if (heightChanged) this.notifyLayoutUpdated();
|
|
4050
|
+
}
|
|
3908
4051
|
/** Create a frame-coalesced stream bound to this Markdown instance. */
|
|
3909
4052
|
createStream(options = {}) {
|
|
4053
|
+
if (this.virtualizeBlocks) {
|
|
4054
|
+
throw new Error("Markdown.createStream is not supported when virtualize is enabled");
|
|
4055
|
+
}
|
|
3910
4056
|
if (this.streamController) {
|
|
3911
4057
|
throw new Error("Markdown already has an active StreamController");
|
|
3912
4058
|
}
|
|
@@ -3979,6 +4125,18 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
3979
4125
|
const next = Math.max(0, maxWidth);
|
|
3980
4126
|
if (next === this.maxWidth) return this;
|
|
3981
4127
|
this.maxWidth = next;
|
|
4128
|
+
if (this.virtualizeBlocks && this.virtualTokens && this.virtualHeights) {
|
|
4129
|
+
const tokens = this.virtualTokens;
|
|
4130
|
+
const tree = this.virtualHeights;
|
|
4131
|
+
for (const el of this.virtualMounted.values()) el.destroy();
|
|
4132
|
+
this.virtualMounted.clear();
|
|
4133
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
4134
|
+
tree.set(i, this.estimateBlockHeight(tokens[i]) + this.theme.blockGap);
|
|
4135
|
+
}
|
|
4136
|
+
this.reconcileVirtual();
|
|
4137
|
+
this.scene?.markDirty();
|
|
4138
|
+
return this;
|
|
4139
|
+
}
|
|
3982
4140
|
let childIndex = 0;
|
|
3983
4141
|
const children = this.content.children;
|
|
3984
4142
|
for (const token of this.tokens) {
|
|
@@ -4513,6 +4671,9 @@ var Markdown = class _Markdown extends import_ui4.UIComponent {
|
|
|
4513
4671
|
return this.appendMarkdownCore(chunk);
|
|
4514
4672
|
}
|
|
4515
4673
|
appendMarkdownCore(chunk) {
|
|
4674
|
+
if (this.virtualizeBlocks) {
|
|
4675
|
+
throw new Error("Markdown.appendMarkdown is not supported when virtualize is enabled");
|
|
4676
|
+
}
|
|
4516
4677
|
const before = this.rawMarkdown.length;
|
|
4517
4678
|
this.consumeFrontMatter(chunk);
|
|
4518
4679
|
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,9 +3751,13 @@ 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
|
-
gap: this.theme.blockGap
|
|
3759
|
+
gap: this.theme.blockGap,
|
|
3760
|
+
cullOffscreenChildren: true
|
|
3736
3761
|
});
|
|
3737
3762
|
this.add(this.content);
|
|
3738
3763
|
this.rawMarkdown = "";
|
|
@@ -3842,6 +3867,20 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3842
3867
|
const tokens = lexMarkdown(text, this._userTiming);
|
|
3843
3868
|
this.setTokens(tokens);
|
|
3844
3869
|
this.abbreviations = collectAbbreviations(tokens);
|
|
3870
|
+
if (this.virtualizeBlocks) {
|
|
3871
|
+
this.virtualTokens = tokens.filter((token) => this.producesEntity(token));
|
|
3872
|
+
const n = this.virtualTokens.length;
|
|
3873
|
+
this.virtualHeights = new RowHeights(n, 0);
|
|
3874
|
+
for (let i = 0; i < n; i++) {
|
|
3875
|
+
this.virtualHeights.set(
|
|
3876
|
+
i,
|
|
3877
|
+
this.estimateBlockHeight(this.virtualTokens[i]) + this.theme.blockGap
|
|
3878
|
+
);
|
|
3879
|
+
}
|
|
3880
|
+
this.virtualMounted.clear();
|
|
3881
|
+
this.reconcileVirtual();
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3845
3884
|
for (const token of tokens) {
|
|
3846
3885
|
const el = this.renderToken(token);
|
|
3847
3886
|
if (el) {
|
|
@@ -3851,8 +3890,123 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3851
3890
|
this.width = this.content.width;
|
|
3852
3891
|
this.height = this.content.height;
|
|
3853
3892
|
}
|
|
3893
|
+
/**
|
|
3894
|
+
* Drive the virtualized window from the host's scroll state.
|
|
3895
|
+
*
|
|
3896
|
+
* Only the blocks intersecting `[scrollY − overscan, scrollY + viewportHeight +
|
|
3897
|
+
* overscan]` stay materialized; every other block is a numeric offset in the
|
|
3898
|
+
* height tree, so the scrollbar still reports the full document height while
|
|
3899
|
+
* only a window of blocks exists as entities. A no-op when `virtualize` was
|
|
3900
|
+
* not enabled.
|
|
3901
|
+
*/
|
|
3902
|
+
setVisibleRange(scrollY, viewportHeight) {
|
|
3903
|
+
if (!this.virtualizeBlocks) return;
|
|
3904
|
+
this.virtualScrollY = Math.max(0, scrollY);
|
|
3905
|
+
this.virtualViewportH = Math.max(0, viewportHeight);
|
|
3906
|
+
this.reconcileVirtual();
|
|
3907
|
+
}
|
|
3908
|
+
/**
|
|
3909
|
+
* Cheap per-block height estimate from the token source, refined to the exact
|
|
3910
|
+
* measured height when the block first mounts. Coarse by design: the estimate
|
|
3911
|
+
* only has to keep the scrollbar plausible before a block is first seen; the
|
|
3912
|
+
* Fenwick tree corrects the rest.
|
|
3913
|
+
*/
|
|
3914
|
+
estimateBlockHeight(token) {
|
|
3915
|
+
const t = this.theme;
|
|
3916
|
+
switch (token.type) {
|
|
3917
|
+
case "heading": {
|
|
3918
|
+
const size = headingSize(t, token.depth);
|
|
3919
|
+
return Math.ceil(size * 1.5) + 8;
|
|
3920
|
+
}
|
|
3921
|
+
case "paragraph":
|
|
3922
|
+
return this.estimateTextHeight(token.text, t.bodyLineHeight);
|
|
3923
|
+
case "code": {
|
|
3924
|
+
const raw = token.text;
|
|
3925
|
+
const lines = Math.max(1, raw.split("\n").length - (raw.endsWith("\n") ? 1 : 0));
|
|
3926
|
+
let h = lines * t.codeLineHeight + t.codePadding * 2;
|
|
3927
|
+
if (this.showCodeLanguage) h += t.codeLangFontSize + 8;
|
|
3928
|
+
return h;
|
|
3929
|
+
}
|
|
3930
|
+
case "hr":
|
|
3931
|
+
return 24;
|
|
3932
|
+
case "blockquote":
|
|
3933
|
+
case "container": {
|
|
3934
|
+
const inner = token.tokens ?? [];
|
|
3935
|
+
let sum = 0;
|
|
3936
|
+
for (const tk of inner) sum += this.estimateBlockHeight(tk) + t.blockGap;
|
|
3937
|
+
return sum + t.quoteInnerGap * 2 + 8;
|
|
3938
|
+
}
|
|
3939
|
+
case "list": {
|
|
3940
|
+
const items = token.items ?? [];
|
|
3941
|
+
let sum = 0;
|
|
3942
|
+
for (const item of items) {
|
|
3943
|
+
for (const tk of item.tokens ?? []) sum += this.estimateBlockHeight(tk) + t.listItemGap;
|
|
3944
|
+
}
|
|
3945
|
+
return sum + t.listGap;
|
|
3946
|
+
}
|
|
3947
|
+
case "table": {
|
|
3948
|
+
const table = token;
|
|
3949
|
+
const rows = (table.rows?.length ?? 0) + (table.header?.length ?? 0);
|
|
3950
|
+
return rows * (t.tableFontSize + 16) + 16;
|
|
3951
|
+
}
|
|
3952
|
+
case "footnoteDef":
|
|
3953
|
+
return this.estimateTextHeight(token.text ?? "", t.bodyLineHeight);
|
|
3954
|
+
case "html":
|
|
3955
|
+
return 200;
|
|
3956
|
+
default:
|
|
3957
|
+
return t.bodyLineHeight;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
/** Estimate a wrapped line count from raw character length against the max width. */
|
|
3961
|
+
estimateTextHeight(text, lineHeight) {
|
|
3962
|
+
const charsPerLine = Math.max(20, Math.floor(this.maxWidth / (this.theme.fontSize * 0.5)));
|
|
3963
|
+
const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
|
|
3964
|
+
return lines * lineHeight;
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Mount the blocks in the current window and unmount the rest. The `content`
|
|
3968
|
+
* Stack lays the mounted window contiguously from y = 0; `content.y` is then
|
|
3969
|
+
* set to the skipped prefix sum so the first mounted block lands at its true
|
|
3970
|
+
* y — a numeric spacer, not a wrapper entity.
|
|
3971
|
+
*/
|
|
3972
|
+
reconcileVirtual() {
|
|
3973
|
+
const tree = this.virtualHeights;
|
|
3974
|
+
const tokens = this.virtualTokens;
|
|
3975
|
+
if (!tree || !tokens) return;
|
|
3976
|
+
const first = tree.indexAt(Math.max(0, this.virtualScrollY - this.virtualOverscan));
|
|
3977
|
+
const last = tree.indexAt(
|
|
3978
|
+
Math.max(0, this.virtualScrollY + this.virtualViewportH + this.virtualOverscan)
|
|
3979
|
+
);
|
|
3980
|
+
let changed = false;
|
|
3981
|
+
for (const [idx, el] of this.virtualMounted) {
|
|
3982
|
+
if (idx < first || idx > last) {
|
|
3983
|
+
el.destroy();
|
|
3984
|
+
this.virtualMounted.delete(idx);
|
|
3985
|
+
changed = true;
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
for (let i = first; i <= last; i++) {
|
|
3989
|
+
if (this.virtualMounted.has(i)) continue;
|
|
3990
|
+
const el = this.renderToken(tokens[i]);
|
|
3991
|
+
if (!el) continue;
|
|
3992
|
+
this.content.add(el);
|
|
3993
|
+
tree.set(i, el.height + this.theme.blockGap);
|
|
3994
|
+
this.virtualMounted.set(i, el);
|
|
3995
|
+
changed = true;
|
|
3996
|
+
}
|
|
3997
|
+
if (changed) this.content.layout();
|
|
3998
|
+
this.content.y = tree.prefix(first);
|
|
3999
|
+
const nextHeight = Math.max(0, tree.total() - this.theme.blockGap);
|
|
4000
|
+
const heightChanged = nextHeight !== this.height;
|
|
4001
|
+
this.width = this.content.width;
|
|
4002
|
+
this.height = nextHeight;
|
|
4003
|
+
if (heightChanged) this.notifyLayoutUpdated();
|
|
4004
|
+
}
|
|
3854
4005
|
/** Create a frame-coalesced stream bound to this Markdown instance. */
|
|
3855
4006
|
createStream(options = {}) {
|
|
4007
|
+
if (this.virtualizeBlocks) {
|
|
4008
|
+
throw new Error("Markdown.createStream is not supported when virtualize is enabled");
|
|
4009
|
+
}
|
|
3856
4010
|
if (this.streamController) {
|
|
3857
4011
|
throw new Error("Markdown already has an active StreamController");
|
|
3858
4012
|
}
|
|
@@ -3925,6 +4079,18 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
3925
4079
|
const next = Math.max(0, maxWidth);
|
|
3926
4080
|
if (next === this.maxWidth) return this;
|
|
3927
4081
|
this.maxWidth = next;
|
|
4082
|
+
if (this.virtualizeBlocks && this.virtualTokens && this.virtualHeights) {
|
|
4083
|
+
const tokens = this.virtualTokens;
|
|
4084
|
+
const tree = this.virtualHeights;
|
|
4085
|
+
for (const el of this.virtualMounted.values()) el.destroy();
|
|
4086
|
+
this.virtualMounted.clear();
|
|
4087
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
4088
|
+
tree.set(i, this.estimateBlockHeight(tokens[i]) + this.theme.blockGap);
|
|
4089
|
+
}
|
|
4090
|
+
this.reconcileVirtual();
|
|
4091
|
+
this.scene?.markDirty();
|
|
4092
|
+
return this;
|
|
4093
|
+
}
|
|
3928
4094
|
let childIndex = 0;
|
|
3929
4095
|
const children = this.content.children;
|
|
3930
4096
|
for (const token of this.tokens) {
|
|
@@ -4459,6 +4625,9 @@ var Markdown = class _Markdown extends UIComponent3 {
|
|
|
4459
4625
|
return this.appendMarkdownCore(chunk);
|
|
4460
4626
|
}
|
|
4461
4627
|
appendMarkdownCore(chunk) {
|
|
4628
|
+
if (this.virtualizeBlocks) {
|
|
4629
|
+
throw new Error("Markdown.appendMarkdown is not supported when virtualize is enabled");
|
|
4630
|
+
}
|
|
4462
4631
|
const before = this.rawMarkdown.length;
|
|
4463
4632
|
this.consumeFrontMatter(chunk);
|
|
4464
4633
|
this.streamStats.appends++;
|