@incremark/core 0.1.0 → 0.1.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/detector/index.d.ts +1 -1
- package/dist/detector/index.js +37 -14
- package/dist/detector/index.js.map +1 -1
- package/dist/{index-i_qABRHQ.d.ts → index-ChNeZ1wr.d.ts} +11 -1
- package/dist/index.d.ts +34 -7
- package/dist/index.js +247 -61
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/detector/index.ts +46 -17
- package/src/index.ts +1 -0
- package/src/parser/IncremarkParser.ts +9 -17
- package/src/transformer/BlockTransformer.ts +111 -23
- package/src/transformer/types.ts +2 -1
- package/src/transformer/utils.ts +209 -36
- package/src/types/index.ts +11 -0
package/dist/index.js
CHANGED
|
@@ -5,8 +5,21 @@ import { gfm } from 'micromark-extension-gfm';
|
|
|
5
5
|
// src/parser/IncremarkParser.ts
|
|
6
6
|
|
|
7
7
|
// src/detector/index.ts
|
|
8
|
+
var RE_FENCE_START = /^(\s*)((`{3,})|(~{3,}))/;
|
|
9
|
+
var RE_EMPTY_LINE = /^\s*$/;
|
|
10
|
+
var RE_HEADING = /^#{1,6}\s/;
|
|
11
|
+
var RE_THEMATIC_BREAK = /^(\*{3,}|-{3,}|_{3,})\s*$/;
|
|
12
|
+
var RE_UNORDERED_LIST = /^(\s*)([-*+])\s/;
|
|
13
|
+
var RE_ORDERED_LIST = /^(\s*)(\d{1,9})[.)]\s/;
|
|
14
|
+
var RE_BLOCKQUOTE = /^\s{0,3}>/;
|
|
15
|
+
var RE_HTML_BLOCK_1 = /^\s{0,3}<(script|pre|style|textarea|!--|!DOCTYPE|\?|!\[CDATA\[)/i;
|
|
16
|
+
var RE_HTML_BLOCK_2 = /^\s{0,3}<\/?[a-zA-Z][a-zA-Z0-9-]*(\s|>|$)/;
|
|
17
|
+
var RE_TABLE_DELIMITER = /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?$/;
|
|
18
|
+
var RE_ESCAPE_SPECIAL = /[.*+?^${}()|[\]\\]/g;
|
|
19
|
+
var fenceEndPatternCache = /* @__PURE__ */ new Map();
|
|
20
|
+
var containerPatternCache = /* @__PURE__ */ new Map();
|
|
8
21
|
function detectFenceStart(line) {
|
|
9
|
-
const match = line.match(
|
|
22
|
+
const match = line.match(RE_FENCE_START);
|
|
10
23
|
if (match) {
|
|
11
24
|
const fence = match[2];
|
|
12
25
|
const char = fence[0];
|
|
@@ -18,45 +31,55 @@ function detectFenceEnd(line, context) {
|
|
|
18
31
|
if (!context.inFencedCode || !context.fenceChar || !context.fenceLength) {
|
|
19
32
|
return false;
|
|
20
33
|
}
|
|
21
|
-
const
|
|
34
|
+
const cacheKey = `${context.fenceChar}-${context.fenceLength}`;
|
|
35
|
+
let pattern = fenceEndPatternCache.get(cacheKey);
|
|
36
|
+
if (!pattern) {
|
|
37
|
+
pattern = new RegExp(`^\\s{0,3}${context.fenceChar}{${context.fenceLength},}\\s*$`);
|
|
38
|
+
fenceEndPatternCache.set(cacheKey, pattern);
|
|
39
|
+
}
|
|
22
40
|
return pattern.test(line);
|
|
23
41
|
}
|
|
24
42
|
function isEmptyLine(line) {
|
|
25
|
-
return
|
|
43
|
+
return RE_EMPTY_LINE.test(line);
|
|
26
44
|
}
|
|
27
45
|
function isHeading(line) {
|
|
28
|
-
return
|
|
46
|
+
return RE_HEADING.test(line);
|
|
29
47
|
}
|
|
30
48
|
function isThematicBreak(line) {
|
|
31
|
-
return
|
|
49
|
+
return RE_THEMATIC_BREAK.test(line.trim());
|
|
32
50
|
}
|
|
33
51
|
function isListItemStart(line) {
|
|
34
|
-
const unordered = line.match(
|
|
52
|
+
const unordered = line.match(RE_UNORDERED_LIST);
|
|
35
53
|
if (unordered) {
|
|
36
54
|
return { ordered: false, indent: unordered[1].length };
|
|
37
55
|
}
|
|
38
|
-
const ordered = line.match(
|
|
56
|
+
const ordered = line.match(RE_ORDERED_LIST);
|
|
39
57
|
if (ordered) {
|
|
40
58
|
return { ordered: true, indent: ordered[1].length };
|
|
41
59
|
}
|
|
42
60
|
return null;
|
|
43
61
|
}
|
|
44
62
|
function isBlockquoteStart(line) {
|
|
45
|
-
return
|
|
63
|
+
return RE_BLOCKQUOTE.test(line);
|
|
46
64
|
}
|
|
47
65
|
function isHtmlBlock(line) {
|
|
48
|
-
return
|
|
66
|
+
return RE_HTML_BLOCK_1.test(line) || RE_HTML_BLOCK_2.test(line);
|
|
49
67
|
}
|
|
50
68
|
function isTableDelimiter(line) {
|
|
51
|
-
return
|
|
69
|
+
return RE_TABLE_DELIMITER.test(line.trim());
|
|
52
70
|
}
|
|
53
71
|
function detectContainer(line, config) {
|
|
54
72
|
const marker = config?.marker || ":";
|
|
55
73
|
const minLength = config?.minMarkerLength || 3;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
const cacheKey = `${marker}-${minLength}`;
|
|
75
|
+
let pattern = containerPatternCache.get(cacheKey);
|
|
76
|
+
if (!pattern) {
|
|
77
|
+
const escapedMarker = marker.replace(RE_ESCAPE_SPECIAL, "\\$&");
|
|
78
|
+
pattern = new RegExp(
|
|
79
|
+
`^(\\s*)(${escapedMarker}{${minLength},})(?:\\s+(\\w[\\w-]*))?(?:\\s+(.*))?\\s*$`
|
|
80
|
+
);
|
|
81
|
+
containerPatternCache.set(cacheKey, pattern);
|
|
82
|
+
}
|
|
60
83
|
const match = line.match(pattern);
|
|
61
84
|
if (!match) {
|
|
62
85
|
return null;
|
|
@@ -168,7 +191,7 @@ var IncremarkParser = class {
|
|
|
168
191
|
context;
|
|
169
192
|
options;
|
|
170
193
|
/** 缓存的容器配置,避免重复计算 */
|
|
171
|
-
|
|
194
|
+
containerConfig;
|
|
172
195
|
/** 上次 append 返回的 pending blocks,用于 getAst 复用 */
|
|
173
196
|
lastPendingBlocks = [];
|
|
174
197
|
constructor(options = {}) {
|
|
@@ -177,7 +200,7 @@ var IncremarkParser = class {
|
|
|
177
200
|
...options
|
|
178
201
|
};
|
|
179
202
|
this.context = createInitialContext();
|
|
180
|
-
this.
|
|
203
|
+
this.containerConfig = this.computeContainerConfig();
|
|
181
204
|
}
|
|
182
205
|
generateBlockId() {
|
|
183
206
|
return `block-${++this.blockIdCounter}`;
|
|
@@ -187,9 +210,6 @@ var IncremarkParser = class {
|
|
|
187
210
|
if (!containers) return void 0;
|
|
188
211
|
return containers === true ? {} : containers;
|
|
189
212
|
}
|
|
190
|
-
getContainerConfig() {
|
|
191
|
-
return this.cachedContainerConfig ?? void 0;
|
|
192
|
-
}
|
|
193
213
|
parse(text) {
|
|
194
214
|
const extensions = [];
|
|
195
215
|
const mdastExtensions = [];
|
|
@@ -244,13 +264,12 @@ var IncremarkParser = class {
|
|
|
244
264
|
let stableLine = -1;
|
|
245
265
|
let stableContext = this.context;
|
|
246
266
|
let tempContext = { ...this.context };
|
|
247
|
-
const containerConfig = this.getContainerConfig();
|
|
248
267
|
for (let i = this.pendingStartLine; i < this.lines.length; i++) {
|
|
249
268
|
const line = this.lines[i];
|
|
250
269
|
const wasInFencedCode = tempContext.inFencedCode;
|
|
251
270
|
const wasInContainer = tempContext.inContainer;
|
|
252
271
|
const wasContainerDepth = tempContext.containerDepth;
|
|
253
|
-
tempContext = updateContext(line, tempContext, containerConfig);
|
|
272
|
+
tempContext = updateContext(line, tempContext, this.containerConfig);
|
|
254
273
|
if (wasInFencedCode && !tempContext.inFencedCode) {
|
|
255
274
|
if (i < this.lines.length - 1) {
|
|
256
275
|
stableLine = i;
|
|
@@ -271,7 +290,7 @@ var IncremarkParser = class {
|
|
|
271
290
|
if (tempContext.inContainer) {
|
|
272
291
|
continue;
|
|
273
292
|
}
|
|
274
|
-
const stablePoint = this.checkStability(i
|
|
293
|
+
const stablePoint = this.checkStability(i);
|
|
275
294
|
if (stablePoint >= 0) {
|
|
276
295
|
stableLine = stablePoint;
|
|
277
296
|
stableContext = { ...tempContext };
|
|
@@ -279,7 +298,7 @@ var IncremarkParser = class {
|
|
|
279
298
|
}
|
|
280
299
|
return { line: stableLine, contextAtLine: stableContext };
|
|
281
300
|
}
|
|
282
|
-
checkStability(lineIndex
|
|
301
|
+
checkStability(lineIndex) {
|
|
283
302
|
if (lineIndex === 0) {
|
|
284
303
|
return -1;
|
|
285
304
|
}
|
|
@@ -304,10 +323,10 @@ var IncremarkParser = class {
|
|
|
304
323
|
if (isListItemStart(line) && !isListItemStart(prevLine)) {
|
|
305
324
|
return lineIndex - 1;
|
|
306
325
|
}
|
|
307
|
-
if (containerConfig !== void 0) {
|
|
308
|
-
const container = detectContainer(line, containerConfig);
|
|
326
|
+
if (this.containerConfig !== void 0) {
|
|
327
|
+
const container = detectContainer(line, this.containerConfig);
|
|
309
328
|
if (container && !container.isEnd) {
|
|
310
|
-
const prevContainer = detectContainer(prevLine, containerConfig);
|
|
329
|
+
const prevContainer = detectContainer(prevLine, this.containerConfig);
|
|
311
330
|
if (!prevContainer || prevContainer.isEnd) {
|
|
312
331
|
return lineIndex - 1;
|
|
313
332
|
}
|
|
@@ -522,24 +541,25 @@ function joinLines(lines, start, end) {
|
|
|
522
541
|
|
|
523
542
|
// src/transformer/utils.ts
|
|
524
543
|
function countChars(node) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
544
|
+
return countCharsInNode(node);
|
|
545
|
+
}
|
|
546
|
+
function countCharsInNode(n) {
|
|
547
|
+
if (n.value && typeof n.value === "string") {
|
|
548
|
+
return n.value.length;
|
|
549
|
+
}
|
|
550
|
+
if (n.children && Array.isArray(n.children)) {
|
|
551
|
+
let count = 0;
|
|
552
|
+
for (const child of n.children) {
|
|
553
|
+
count += countCharsInNode(child);
|
|
535
554
|
}
|
|
555
|
+
return count;
|
|
536
556
|
}
|
|
537
|
-
|
|
538
|
-
return count;
|
|
557
|
+
return 1;
|
|
539
558
|
}
|
|
540
|
-
function sliceAst(node, maxChars, accumulatedChunks) {
|
|
559
|
+
function sliceAst(node, maxChars, accumulatedChunks, skipChars = 0) {
|
|
541
560
|
if (maxChars <= 0) return null;
|
|
542
|
-
|
|
561
|
+
if (skipChars >= maxChars) return null;
|
|
562
|
+
let remaining = maxChars - skipChars;
|
|
543
563
|
let charIndex = 0;
|
|
544
564
|
const chunkRanges = [];
|
|
545
565
|
if (accumulatedChunks && accumulatedChunks.chunks.length > 0) {
|
|
@@ -556,13 +576,18 @@ function sliceAst(node, maxChars, accumulatedChunks) {
|
|
|
556
576
|
function process(n) {
|
|
557
577
|
if (remaining <= 0) return null;
|
|
558
578
|
if (n.value && typeof n.value === "string") {
|
|
559
|
-
const
|
|
579
|
+
const nodeStart = charIndex;
|
|
580
|
+
const nodeEnd = charIndex + n.value.length;
|
|
581
|
+
if (nodeEnd <= skipChars) {
|
|
582
|
+
charIndex = nodeEnd;
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
const skipInNode = Math.max(0, skipChars - nodeStart);
|
|
586
|
+
const take = Math.min(n.value.length - skipInNode, remaining);
|
|
560
587
|
remaining -= take;
|
|
561
588
|
if (take === 0) return null;
|
|
562
|
-
const slicedValue = n.value.slice(
|
|
563
|
-
|
|
564
|
-
const nodeEnd = charIndex + take;
|
|
565
|
-
charIndex += take;
|
|
589
|
+
const slicedValue = n.value.slice(skipInNode, skipInNode + take);
|
|
590
|
+
charIndex = nodeEnd;
|
|
566
591
|
const result = {
|
|
567
592
|
...n,
|
|
568
593
|
value: slicedValue
|
|
@@ -571,11 +596,11 @@ function sliceAst(node, maxChars, accumulatedChunks) {
|
|
|
571
596
|
const nodeChunks = [];
|
|
572
597
|
let firstChunkLocalStart = take;
|
|
573
598
|
for (const range of chunkRanges) {
|
|
574
|
-
const overlapStart = Math.max(range.start, nodeStart);
|
|
575
|
-
const overlapEnd = Math.min(range.end,
|
|
599
|
+
const overlapStart = Math.max(range.start, nodeStart + skipInNode);
|
|
600
|
+
const overlapEnd = Math.min(range.end, nodeStart + skipInNode + take);
|
|
576
601
|
if (overlapStart < overlapEnd) {
|
|
577
|
-
const localStart = overlapStart - nodeStart;
|
|
578
|
-
const localEnd = overlapEnd - nodeStart;
|
|
602
|
+
const localStart = overlapStart - (nodeStart + skipInNode);
|
|
603
|
+
const localEnd = overlapEnd - (nodeStart + skipInNode);
|
|
579
604
|
const chunkText = slicedValue.slice(localStart, localEnd);
|
|
580
605
|
if (chunkText.length > 0) {
|
|
581
606
|
if (nodeChunks.length === 0) {
|
|
@@ -597,12 +622,24 @@ function sliceAst(node, maxChars, accumulatedChunks) {
|
|
|
597
622
|
}
|
|
598
623
|
if (n.children && Array.isArray(n.children)) {
|
|
599
624
|
const newChildren = [];
|
|
625
|
+
let childCharIndex = charIndex;
|
|
600
626
|
for (const child of n.children) {
|
|
601
627
|
if (remaining <= 0) break;
|
|
628
|
+
const childChars = countCharsInNode(child);
|
|
629
|
+
const childStart = childCharIndex;
|
|
630
|
+
const childEnd = childCharIndex + childChars;
|
|
631
|
+
if (childEnd <= skipChars) {
|
|
632
|
+
childCharIndex = childEnd;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const savedCharIndex = charIndex;
|
|
636
|
+
charIndex = childStart;
|
|
602
637
|
const processed = process(child);
|
|
638
|
+
charIndex = savedCharIndex;
|
|
603
639
|
if (processed) {
|
|
604
640
|
newChildren.push(processed);
|
|
605
641
|
}
|
|
642
|
+
childCharIndex = childEnd;
|
|
606
643
|
}
|
|
607
644
|
if (newChildren.length === 0) {
|
|
608
645
|
return null;
|
|
@@ -615,8 +652,79 @@ function sliceAst(node, maxChars, accumulatedChunks) {
|
|
|
615
652
|
}
|
|
616
653
|
return process(node);
|
|
617
654
|
}
|
|
655
|
+
function appendToAst(baseNode, sourceNode, startChars, endChars, accumulatedChunks) {
|
|
656
|
+
if (endChars <= startChars) {
|
|
657
|
+
return baseNode;
|
|
658
|
+
}
|
|
659
|
+
const newPart = sliceAst(sourceNode, endChars, accumulatedChunks, startChars);
|
|
660
|
+
if (!newPart) {
|
|
661
|
+
return baseNode;
|
|
662
|
+
}
|
|
663
|
+
return mergeAstNodes(baseNode, newPart);
|
|
664
|
+
}
|
|
665
|
+
function mergeAstNodes(baseNode, newPart) {
|
|
666
|
+
if (baseNode.type !== newPart.type) {
|
|
667
|
+
return baseNode;
|
|
668
|
+
}
|
|
669
|
+
const base = baseNode;
|
|
670
|
+
const part = newPart;
|
|
671
|
+
if (base.value && typeof base.value === "string" && part.value && typeof part.value === "string") {
|
|
672
|
+
const baseChunks = base.chunks || [];
|
|
673
|
+
const partChunks = part.chunks || [];
|
|
674
|
+
const mergedChunks = [...baseChunks, ...partChunks];
|
|
675
|
+
const mergedValue = base.value + part.value;
|
|
676
|
+
const baseStableLength = base.stableLength ?? 0;
|
|
677
|
+
const result = {
|
|
678
|
+
...base,
|
|
679
|
+
value: mergedValue,
|
|
680
|
+
stableLength: mergedChunks.length > 0 ? baseStableLength : void 0,
|
|
681
|
+
chunks: mergedChunks.length > 0 ? mergedChunks : void 0
|
|
682
|
+
};
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
if (base.children && Array.isArray(base.children) && part.children && Array.isArray(part.children)) {
|
|
686
|
+
if (base.children.length > 0 && part.children.length > 0) {
|
|
687
|
+
const lastBaseChild = base.children[base.children.length - 1];
|
|
688
|
+
const firstPartChild = part.children[0];
|
|
689
|
+
if (lastBaseChild.type === firstPartChild.type) {
|
|
690
|
+
const merged = mergeAstNodes(lastBaseChild, firstPartChild);
|
|
691
|
+
return {
|
|
692
|
+
...base,
|
|
693
|
+
children: [
|
|
694
|
+
...base.children.slice(0, -1),
|
|
695
|
+
merged,
|
|
696
|
+
...part.children.slice(1)
|
|
697
|
+
]
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
...base,
|
|
703
|
+
children: [...base.children, ...part.children]
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
return baseNode;
|
|
707
|
+
}
|
|
618
708
|
function cloneNode(node) {
|
|
619
|
-
|
|
709
|
+
if (typeof structuredClone === "function") {
|
|
710
|
+
return structuredClone(node);
|
|
711
|
+
}
|
|
712
|
+
return deepClone(node);
|
|
713
|
+
}
|
|
714
|
+
function deepClone(obj) {
|
|
715
|
+
if (obj === null || typeof obj !== "object") {
|
|
716
|
+
return obj;
|
|
717
|
+
}
|
|
718
|
+
if (Array.isArray(obj)) {
|
|
719
|
+
return obj.map((item) => deepClone(item));
|
|
720
|
+
}
|
|
721
|
+
const cloned = {};
|
|
722
|
+
for (const key in obj) {
|
|
723
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
724
|
+
cloned[key] = deepClone(obj[key]);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return cloned;
|
|
620
728
|
}
|
|
621
729
|
|
|
622
730
|
// src/transformer/BlockTransformer.ts
|
|
@@ -630,6 +738,13 @@ var BlockTransformer = class {
|
|
|
630
738
|
chunks = [];
|
|
631
739
|
// 累积的 chunks(用于 fade-in 动画)
|
|
632
740
|
visibilityHandler = null;
|
|
741
|
+
// ============ 性能优化:缓存机制 ============
|
|
742
|
+
/** 缓存的已截断 displayNode(稳定的部分,避免重复遍历) */
|
|
743
|
+
cachedDisplayNode = null;
|
|
744
|
+
/** 缓存的字符数(避免重复计算) */
|
|
745
|
+
cachedTotalChars = null;
|
|
746
|
+
/** 当前缓存的进度(对应 cachedDisplayNode) */
|
|
747
|
+
cachedProgress = 0;
|
|
633
748
|
constructor(options = {}) {
|
|
634
749
|
this.options = {
|
|
635
750
|
charsPerTick: options.charsPerTick ?? 1,
|
|
@@ -664,7 +779,8 @@ var BlockTransformer = class {
|
|
|
664
779
|
if (this.state.currentBlock) {
|
|
665
780
|
const updated = blocks.find((b) => b.id === this.state.currentBlock.id);
|
|
666
781
|
if (updated && updated.node !== this.state.currentBlock.node) {
|
|
667
|
-
|
|
782
|
+
this.clearCache();
|
|
783
|
+
const oldTotal = this.cachedTotalChars ?? this.countChars(this.state.currentBlock.node);
|
|
668
784
|
const newTotal = this.countChars(updated.node);
|
|
669
785
|
if (newTotal < oldTotal || newTotal < this.state.currentProgress) {
|
|
670
786
|
this.state.currentProgress = Math.min(this.state.currentProgress, newTotal);
|
|
@@ -683,10 +799,11 @@ var BlockTransformer = class {
|
|
|
683
799
|
*/
|
|
684
800
|
update(block) {
|
|
685
801
|
if (this.state.currentBlock?.id === block.id) {
|
|
686
|
-
const oldTotal = this.countChars(this.state.currentBlock.node);
|
|
802
|
+
const oldTotal = this.cachedTotalChars ?? this.countChars(this.state.currentBlock.node);
|
|
687
803
|
const newTotal = this.countChars(block.node);
|
|
688
804
|
this.state.currentBlock = block;
|
|
689
805
|
if (newTotal > oldTotal && !this.rafId && !this.isPaused && this.state.currentProgress >= oldTotal) {
|
|
806
|
+
this.clearCache();
|
|
690
807
|
this.startIfNeeded();
|
|
691
808
|
}
|
|
692
809
|
}
|
|
@@ -708,6 +825,7 @@ var BlockTransformer = class {
|
|
|
708
825
|
pendingBlocks: []
|
|
709
826
|
};
|
|
710
827
|
this.chunks = [];
|
|
828
|
+
this.clearCache();
|
|
711
829
|
this.emit();
|
|
712
830
|
}
|
|
713
831
|
/**
|
|
@@ -722,6 +840,7 @@ var BlockTransformer = class {
|
|
|
722
840
|
pendingBlocks: []
|
|
723
841
|
};
|
|
724
842
|
this.chunks = [];
|
|
843
|
+
this.clearCache();
|
|
725
844
|
this.emit();
|
|
726
845
|
}
|
|
727
846
|
/**
|
|
@@ -742,6 +861,7 @@ var BlockTransformer = class {
|
|
|
742
861
|
}
|
|
743
862
|
/**
|
|
744
863
|
* 获取用于渲染的 display blocks
|
|
864
|
+
* 优化:使用缓存的 displayNode,避免重复遍历已稳定的节点
|
|
745
865
|
*/
|
|
746
866
|
getDisplayBlocks() {
|
|
747
867
|
const result = [];
|
|
@@ -754,16 +874,13 @@ var BlockTransformer = class {
|
|
|
754
874
|
});
|
|
755
875
|
}
|
|
756
876
|
if (this.state.currentBlock) {
|
|
757
|
-
const total = this.
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
this.state.currentProgress,
|
|
762
|
-
accumulatedChunks
|
|
763
|
-
);
|
|
877
|
+
const total = this.getTotalChars();
|
|
878
|
+
if (this.state.currentProgress !== this.cachedProgress || !this.cachedDisplayNode) {
|
|
879
|
+
this.updateCachedDisplayNode();
|
|
880
|
+
}
|
|
764
881
|
result.push({
|
|
765
882
|
...this.state.currentBlock,
|
|
766
|
-
displayNode:
|
|
883
|
+
displayNode: this.cachedDisplayNode || { type: "paragraph", children: [] },
|
|
767
884
|
progress: total > 0 ? this.state.currentProgress / total : 1,
|
|
768
885
|
isDisplayComplete: false
|
|
769
886
|
});
|
|
@@ -863,6 +980,7 @@ var BlockTransformer = class {
|
|
|
863
980
|
if (!this.state.currentBlock && this.state.pendingBlocks.length > 0) {
|
|
864
981
|
this.state.currentBlock = this.state.pendingBlocks.shift();
|
|
865
982
|
this.state.currentProgress = 0;
|
|
983
|
+
this.clearCache();
|
|
866
984
|
}
|
|
867
985
|
if (this.state.currentBlock) {
|
|
868
986
|
this.isRunning = true;
|
|
@@ -893,7 +1011,7 @@ var BlockTransformer = class {
|
|
|
893
1011
|
this.processNext();
|
|
894
1012
|
return;
|
|
895
1013
|
}
|
|
896
|
-
const total = this.
|
|
1014
|
+
const total = this.getTotalChars();
|
|
897
1015
|
const step = this.getStep();
|
|
898
1016
|
const prevProgress = this.state.currentProgress;
|
|
899
1017
|
this.state.currentProgress = Math.min(prevProgress + step, total);
|
|
@@ -913,6 +1031,7 @@ var BlockTransformer = class {
|
|
|
913
1031
|
this.state.currentBlock = null;
|
|
914
1032
|
this.state.currentProgress = 0;
|
|
915
1033
|
this.chunks = [];
|
|
1034
|
+
this.clearCache();
|
|
916
1035
|
this.processNext();
|
|
917
1036
|
}
|
|
918
1037
|
}
|
|
@@ -958,6 +1077,7 @@ var BlockTransformer = class {
|
|
|
958
1077
|
this.state.currentBlock = this.state.pendingBlocks.shift();
|
|
959
1078
|
this.state.currentProgress = 0;
|
|
960
1079
|
this.chunks = [];
|
|
1080
|
+
this.clearCache();
|
|
961
1081
|
this.emit();
|
|
962
1082
|
} else {
|
|
963
1083
|
this.isRunning = false;
|
|
@@ -1006,6 +1126,72 @@ var BlockTransformer = class {
|
|
|
1006
1126
|
}
|
|
1007
1127
|
}
|
|
1008
1128
|
}
|
|
1129
|
+
// ============ 缓存管理方法 ============
|
|
1130
|
+
/**
|
|
1131
|
+
* 更新缓存的 displayNode
|
|
1132
|
+
* 使用真正的增量追加模式:只处理新增部分,不重复遍历已稳定的节点
|
|
1133
|
+
*/
|
|
1134
|
+
updateCachedDisplayNode() {
|
|
1135
|
+
const block = this.state.currentBlock;
|
|
1136
|
+
if (!block) {
|
|
1137
|
+
this.cachedDisplayNode = null;
|
|
1138
|
+
this.cachedProgress = 0;
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const currentProgress = this.state.currentProgress;
|
|
1142
|
+
if (currentProgress < this.cachedProgress) {
|
|
1143
|
+
this.cachedDisplayNode = this.sliceNode(
|
|
1144
|
+
block.node,
|
|
1145
|
+
currentProgress,
|
|
1146
|
+
this.getAccumulatedChunks()
|
|
1147
|
+
);
|
|
1148
|
+
this.cachedProgress = currentProgress;
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (currentProgress > this.cachedProgress && this.cachedDisplayNode) {
|
|
1152
|
+
this.cachedDisplayNode = appendToAst(
|
|
1153
|
+
this.cachedDisplayNode,
|
|
1154
|
+
block.node,
|
|
1155
|
+
this.cachedProgress,
|
|
1156
|
+
currentProgress,
|
|
1157
|
+
this.getAccumulatedChunks()
|
|
1158
|
+
);
|
|
1159
|
+
this.cachedProgress = currentProgress;
|
|
1160
|
+
} else if (!this.cachedDisplayNode) {
|
|
1161
|
+
this.cachedDisplayNode = this.sliceNode(
|
|
1162
|
+
block.node,
|
|
1163
|
+
currentProgress,
|
|
1164
|
+
this.getAccumulatedChunks()
|
|
1165
|
+
);
|
|
1166
|
+
this.cachedProgress = currentProgress;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* 获取总字符数(带缓存)
|
|
1171
|
+
*/
|
|
1172
|
+
getTotalChars() {
|
|
1173
|
+
if (this.cachedTotalChars === null && this.state.currentBlock) {
|
|
1174
|
+
this.cachedTotalChars = this.countChars(this.state.currentBlock.node);
|
|
1175
|
+
}
|
|
1176
|
+
return this.cachedTotalChars ?? 0;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* 清除缓存(当 block 切换或内容更新时)
|
|
1180
|
+
*/
|
|
1181
|
+
clearCache() {
|
|
1182
|
+
this.cachedDisplayNode = null;
|
|
1183
|
+
this.cachedTotalChars = null;
|
|
1184
|
+
this.cachedProgress = 0;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* 获取累积的 chunks(用于 fade-in 效果)
|
|
1188
|
+
*/
|
|
1189
|
+
getAccumulatedChunks() {
|
|
1190
|
+
if (this.options.effect === "fade-in" && this.chunks.length > 0) {
|
|
1191
|
+
return { stableChars: 0, chunks: this.chunks };
|
|
1192
|
+
}
|
|
1193
|
+
return void 0;
|
|
1194
|
+
}
|
|
1009
1195
|
};
|
|
1010
1196
|
function createBlockTransformer(options) {
|
|
1011
1197
|
return new BlockTransformer(options);
|