@incremark/core 0.0.5 → 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 +72 -10
- package/dist/index.js +341 -53
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/detector/index.ts +46 -17
- package/src/index.ts +4 -1
- package/src/parser/IncremarkParser.ts +9 -17
- package/src/transformer/BlockTransformer.ts +182 -14
- package/src/transformer/index.ts +1 -0
- package/src/transformer/types.ts +5 -3
- package/src/transformer/utils.ts +309 -30
- 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,40 +541,105 @@ 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) {
|
|
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;
|
|
563
|
+
let charIndex = 0;
|
|
564
|
+
const chunkRanges = [];
|
|
565
|
+
if (accumulatedChunks && accumulatedChunks.chunks.length > 0) {
|
|
566
|
+
let chunkStart = accumulatedChunks.stableChars;
|
|
567
|
+
for (const chunk of accumulatedChunks.chunks) {
|
|
568
|
+
chunkRanges.push({
|
|
569
|
+
start: chunkStart,
|
|
570
|
+
end: chunkStart + chunk.text.length,
|
|
571
|
+
chunk
|
|
572
|
+
});
|
|
573
|
+
chunkStart += chunk.text.length;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
543
576
|
function process(n) {
|
|
544
577
|
if (remaining <= 0) return null;
|
|
545
578
|
if (n.value && typeof n.value === "string") {
|
|
546
|
-
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);
|
|
547
587
|
remaining -= take;
|
|
548
588
|
if (take === 0) return null;
|
|
549
|
-
|
|
589
|
+
const slicedValue = n.value.slice(skipInNode, skipInNode + take);
|
|
590
|
+
charIndex = nodeEnd;
|
|
591
|
+
const result = {
|
|
592
|
+
...n,
|
|
593
|
+
value: slicedValue
|
|
594
|
+
};
|
|
595
|
+
if (chunkRanges.length > 0 && accumulatedChunks) {
|
|
596
|
+
const nodeChunks = [];
|
|
597
|
+
let firstChunkLocalStart = take;
|
|
598
|
+
for (const range of chunkRanges) {
|
|
599
|
+
const overlapStart = Math.max(range.start, nodeStart + skipInNode);
|
|
600
|
+
const overlapEnd = Math.min(range.end, nodeStart + skipInNode + take);
|
|
601
|
+
if (overlapStart < overlapEnd) {
|
|
602
|
+
const localStart = overlapStart - (nodeStart + skipInNode);
|
|
603
|
+
const localEnd = overlapEnd - (nodeStart + skipInNode);
|
|
604
|
+
const chunkText = slicedValue.slice(localStart, localEnd);
|
|
605
|
+
if (chunkText.length > 0) {
|
|
606
|
+
if (nodeChunks.length === 0) {
|
|
607
|
+
firstChunkLocalStart = localStart;
|
|
608
|
+
}
|
|
609
|
+
nodeChunks.push({
|
|
610
|
+
text: chunkText,
|
|
611
|
+
createdAt: range.chunk.createdAt
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (nodeChunks.length > 0) {
|
|
617
|
+
result.stableLength = firstChunkLocalStart;
|
|
618
|
+
result.chunks = nodeChunks;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return result;
|
|
550
622
|
}
|
|
551
623
|
if (n.children && Array.isArray(n.children)) {
|
|
552
624
|
const newChildren = [];
|
|
625
|
+
let childCharIndex = charIndex;
|
|
553
626
|
for (const child of n.children) {
|
|
554
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;
|
|
555
637
|
const processed = process(child);
|
|
638
|
+
charIndex = savedCharIndex;
|
|
556
639
|
if (processed) {
|
|
557
640
|
newChildren.push(processed);
|
|
558
641
|
}
|
|
642
|
+
childCharIndex = childEnd;
|
|
559
643
|
}
|
|
560
644
|
if (newChildren.length === 0) {
|
|
561
645
|
return null;
|
|
@@ -563,12 +647,84 @@ function sliceAst(node, maxChars) {
|
|
|
563
647
|
return { ...n, children: newChildren };
|
|
564
648
|
}
|
|
565
649
|
remaining -= 1;
|
|
650
|
+
charIndex += 1;
|
|
566
651
|
return { ...n };
|
|
567
652
|
}
|
|
568
653
|
return process(node);
|
|
569
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
|
+
}
|
|
570
708
|
function cloneNode(node) {
|
|
571
|
-
|
|
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;
|
|
572
728
|
}
|
|
573
729
|
|
|
574
730
|
// src/transformer/BlockTransformer.ts
|
|
@@ -579,7 +735,16 @@ var BlockTransformer = class {
|
|
|
579
735
|
lastTickTime = 0;
|
|
580
736
|
isRunning = false;
|
|
581
737
|
isPaused = false;
|
|
738
|
+
chunks = [];
|
|
739
|
+
// 累积的 chunks(用于 fade-in 动画)
|
|
582
740
|
visibilityHandler = null;
|
|
741
|
+
// ============ 性能优化:缓存机制 ============
|
|
742
|
+
/** 缓存的已截断 displayNode(稳定的部分,避免重复遍历) */
|
|
743
|
+
cachedDisplayNode = null;
|
|
744
|
+
/** 缓存的字符数(避免重复计算) */
|
|
745
|
+
cachedTotalChars = null;
|
|
746
|
+
/** 当前缓存的进度(对应 cachedDisplayNode) */
|
|
747
|
+
cachedProgress = 0;
|
|
583
748
|
constructor(options = {}) {
|
|
584
749
|
this.options = {
|
|
585
750
|
charsPerTick: options.charsPerTick ?? 1,
|
|
@@ -614,10 +779,15 @@ var BlockTransformer = class {
|
|
|
614
779
|
if (this.state.currentBlock) {
|
|
615
780
|
const updated = blocks.find((b) => b.id === this.state.currentBlock.id);
|
|
616
781
|
if (updated && updated.node !== this.state.currentBlock.node) {
|
|
782
|
+
this.clearCache();
|
|
783
|
+
const oldTotal = this.cachedTotalChars ?? this.countChars(this.state.currentBlock.node);
|
|
784
|
+
const newTotal = this.countChars(updated.node);
|
|
785
|
+
if (newTotal < oldTotal || newTotal < this.state.currentProgress) {
|
|
786
|
+
this.state.currentProgress = Math.min(this.state.currentProgress, newTotal);
|
|
787
|
+
}
|
|
617
788
|
this.state.currentBlock = updated;
|
|
618
789
|
if (!this.rafId && !this.isPaused) {
|
|
619
|
-
|
|
620
|
-
if (this.state.currentProgress < total) {
|
|
790
|
+
if (this.state.currentProgress < newTotal) {
|
|
621
791
|
this.startIfNeeded();
|
|
622
792
|
}
|
|
623
793
|
}
|
|
@@ -629,10 +799,11 @@ var BlockTransformer = class {
|
|
|
629
799
|
*/
|
|
630
800
|
update(block) {
|
|
631
801
|
if (this.state.currentBlock?.id === block.id) {
|
|
632
|
-
const oldTotal = this.countChars(this.state.currentBlock.node);
|
|
802
|
+
const oldTotal = this.cachedTotalChars ?? this.countChars(this.state.currentBlock.node);
|
|
633
803
|
const newTotal = this.countChars(block.node);
|
|
634
804
|
this.state.currentBlock = block;
|
|
635
805
|
if (newTotal > oldTotal && !this.rafId && !this.isPaused && this.state.currentProgress >= oldTotal) {
|
|
806
|
+
this.clearCache();
|
|
636
807
|
this.startIfNeeded();
|
|
637
808
|
}
|
|
638
809
|
}
|
|
@@ -653,6 +824,8 @@ var BlockTransformer = class {
|
|
|
653
824
|
currentProgress: 0,
|
|
654
825
|
pendingBlocks: []
|
|
655
826
|
};
|
|
827
|
+
this.chunks = [];
|
|
828
|
+
this.clearCache();
|
|
656
829
|
this.emit();
|
|
657
830
|
}
|
|
658
831
|
/**
|
|
@@ -666,6 +839,8 @@ var BlockTransformer = class {
|
|
|
666
839
|
currentProgress: 0,
|
|
667
840
|
pendingBlocks: []
|
|
668
841
|
};
|
|
842
|
+
this.chunks = [];
|
|
843
|
+
this.clearCache();
|
|
669
844
|
this.emit();
|
|
670
845
|
}
|
|
671
846
|
/**
|
|
@@ -686,6 +861,7 @@ var BlockTransformer = class {
|
|
|
686
861
|
}
|
|
687
862
|
/**
|
|
688
863
|
* 获取用于渲染的 display blocks
|
|
864
|
+
* 优化:使用缓存的 displayNode,避免重复遍历已稳定的节点
|
|
689
865
|
*/
|
|
690
866
|
getDisplayBlocks() {
|
|
691
867
|
const result = [];
|
|
@@ -698,11 +874,13 @@ var BlockTransformer = class {
|
|
|
698
874
|
});
|
|
699
875
|
}
|
|
700
876
|
if (this.state.currentBlock) {
|
|
701
|
-
const total = this.
|
|
702
|
-
|
|
877
|
+
const total = this.getTotalChars();
|
|
878
|
+
if (this.state.currentProgress !== this.cachedProgress || !this.cachedDisplayNode) {
|
|
879
|
+
this.updateCachedDisplayNode();
|
|
880
|
+
}
|
|
703
881
|
result.push({
|
|
704
882
|
...this.state.currentBlock,
|
|
705
|
-
displayNode:
|
|
883
|
+
displayNode: this.cachedDisplayNode || { type: "paragraph", children: [] },
|
|
706
884
|
progress: total > 0 ? this.state.currentProgress / total : 1,
|
|
707
885
|
isDisplayComplete: false
|
|
708
886
|
});
|
|
@@ -802,6 +980,7 @@ var BlockTransformer = class {
|
|
|
802
980
|
if (!this.state.currentBlock && this.state.pendingBlocks.length > 0) {
|
|
803
981
|
this.state.currentBlock = this.state.pendingBlocks.shift();
|
|
804
982
|
this.state.currentProgress = 0;
|
|
983
|
+
this.clearCache();
|
|
805
984
|
}
|
|
806
985
|
if (this.state.currentBlock) {
|
|
807
986
|
this.isRunning = true;
|
|
@@ -832,18 +1011,59 @@ var BlockTransformer = class {
|
|
|
832
1011
|
this.processNext();
|
|
833
1012
|
return;
|
|
834
1013
|
}
|
|
835
|
-
const total = this.
|
|
1014
|
+
const total = this.getTotalChars();
|
|
836
1015
|
const step = this.getStep();
|
|
837
|
-
|
|
1016
|
+
const prevProgress = this.state.currentProgress;
|
|
1017
|
+
this.state.currentProgress = Math.min(prevProgress + step, total);
|
|
1018
|
+
if (this.options.effect === "fade-in" && this.state.currentProgress > prevProgress) {
|
|
1019
|
+
const newText = this.extractText(block.node, prevProgress, this.state.currentProgress);
|
|
1020
|
+
if (newText.length > 0) {
|
|
1021
|
+
this.chunks.push({
|
|
1022
|
+
text: newText,
|
|
1023
|
+
createdAt: Date.now()
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
838
1027
|
this.emit();
|
|
839
1028
|
if (this.state.currentProgress >= total) {
|
|
840
1029
|
this.notifyComplete(block.node);
|
|
841
1030
|
this.state.completedBlocks.push(block);
|
|
842
1031
|
this.state.currentBlock = null;
|
|
843
1032
|
this.state.currentProgress = 0;
|
|
1033
|
+
this.chunks = [];
|
|
1034
|
+
this.clearCache();
|
|
844
1035
|
this.processNext();
|
|
845
1036
|
}
|
|
846
1037
|
}
|
|
1038
|
+
/**
|
|
1039
|
+
* 从 AST 节点中提取指定范围的文本
|
|
1040
|
+
*/
|
|
1041
|
+
extractText(node, start, end) {
|
|
1042
|
+
let result = "";
|
|
1043
|
+
let charIndex = 0;
|
|
1044
|
+
function traverse(n) {
|
|
1045
|
+
if (charIndex >= end) return false;
|
|
1046
|
+
if (n.value && typeof n.value === "string") {
|
|
1047
|
+
const nodeStart = charIndex;
|
|
1048
|
+
const nodeEnd = charIndex + n.value.length;
|
|
1049
|
+
charIndex = nodeEnd;
|
|
1050
|
+
const overlapStart = Math.max(start, nodeStart);
|
|
1051
|
+
const overlapEnd = Math.min(end, nodeEnd);
|
|
1052
|
+
if (overlapStart < overlapEnd) {
|
|
1053
|
+
result += n.value.slice(overlapStart - nodeStart, overlapEnd - nodeStart);
|
|
1054
|
+
}
|
|
1055
|
+
return charIndex < end;
|
|
1056
|
+
}
|
|
1057
|
+
if (n.children && Array.isArray(n.children)) {
|
|
1058
|
+
for (const child of n.children) {
|
|
1059
|
+
if (!traverse(child)) return false;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
traverse(node);
|
|
1065
|
+
return result;
|
|
1066
|
+
}
|
|
847
1067
|
getStep() {
|
|
848
1068
|
const { charsPerTick } = this.options;
|
|
849
1069
|
if (typeof charsPerTick === "number") {
|
|
@@ -856,6 +1076,8 @@ var BlockTransformer = class {
|
|
|
856
1076
|
if (this.state.pendingBlocks.length > 0) {
|
|
857
1077
|
this.state.currentBlock = this.state.pendingBlocks.shift();
|
|
858
1078
|
this.state.currentProgress = 0;
|
|
1079
|
+
this.chunks = [];
|
|
1080
|
+
this.clearCache();
|
|
859
1081
|
this.emit();
|
|
860
1082
|
} else {
|
|
861
1083
|
this.isRunning = false;
|
|
@@ -887,7 +1109,7 @@ var BlockTransformer = class {
|
|
|
887
1109
|
}
|
|
888
1110
|
return countChars(node);
|
|
889
1111
|
}
|
|
890
|
-
sliceNode(node, chars) {
|
|
1112
|
+
sliceNode(node, chars, accumulatedChunks) {
|
|
891
1113
|
for (const plugin of this.options.plugins) {
|
|
892
1114
|
if (plugin.match?.(node) && plugin.sliceNode) {
|
|
893
1115
|
const total = this.countChars(node);
|
|
@@ -895,7 +1117,7 @@ var BlockTransformer = class {
|
|
|
895
1117
|
if (result !== null) return result;
|
|
896
1118
|
}
|
|
897
1119
|
}
|
|
898
|
-
return sliceAst(node, chars);
|
|
1120
|
+
return sliceAst(node, chars, accumulatedChunks);
|
|
899
1121
|
}
|
|
900
1122
|
notifyComplete(node) {
|
|
901
1123
|
for (const plugin of this.options.plugins) {
|
|
@@ -904,6 +1126,72 @@ var BlockTransformer = class {
|
|
|
904
1126
|
}
|
|
905
1127
|
}
|
|
906
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
|
+
}
|
|
907
1195
|
};
|
|
908
1196
|
function createBlockTransformer(options) {
|
|
909
1197
|
return new BlockTransformer(options);
|