@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/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(/^(\s*)((`{3,})|(~{3,}))/);
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 pattern = new RegExp(`^\\s{0,3}${context.fenceChar}{${context.fenceLength},}\\s*$`);
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 /^\s*$/.test(line);
43
+ return RE_EMPTY_LINE.test(line);
26
44
  }
27
45
  function isHeading(line) {
28
- return /^#{1,6}\s/.test(line);
46
+ return RE_HEADING.test(line);
29
47
  }
30
48
  function isThematicBreak(line) {
31
- return /^(\*{3,}|-{3,}|_{3,})\s*$/.test(line.trim());
49
+ return RE_THEMATIC_BREAK.test(line.trim());
32
50
  }
33
51
  function isListItemStart(line) {
34
- const unordered = line.match(/^(\s*)([-*+])\s/);
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(/^(\s*)(\d{1,9})[.)]\s/);
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 /^\s{0,3}>/.test(line);
63
+ return RE_BLOCKQUOTE.test(line);
46
64
  }
47
65
  function isHtmlBlock(line) {
48
- return /^\s{0,3}<(script|pre|style|textarea|!--|!DOCTYPE|\?|!\[CDATA\[)/i.test(line) || /^\s{0,3}<\/?[a-zA-Z][a-zA-Z0-9-]*(\s|>|$)/.test(line);
66
+ return RE_HTML_BLOCK_1.test(line) || RE_HTML_BLOCK_2.test(line);
49
67
  }
50
68
  function isTableDelimiter(line) {
51
- return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?$/.test(line.trim());
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 escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
57
- const pattern = new RegExp(
58
- `^(\\s*)(${escapedMarker}{${minLength},})(?:\\s+(\\w[\\w-]*))?(?:\\s+(.*))?\\s*$`
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
- cachedContainerConfig = null;
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.cachedContainerConfig = this.computeContainerConfig();
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, containerConfig);
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, containerConfig) {
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
- let count = 0;
526
- function traverse(n) {
527
- if (n.value && typeof n.value === "string") {
528
- count += n.value.length;
529
- return;
530
- }
531
- if (n.children && Array.isArray(n.children)) {
532
- for (const child of n.children) {
533
- traverse(child);
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
- traverse(node);
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
- let remaining = maxChars;
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 take = Math.min(n.value.length, remaining);
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
- return { ...n, value: n.value.slice(0, take) };
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
- return JSON.parse(JSON.stringify(node));
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
- const total = this.countChars(updated.node);
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.countChars(this.state.currentBlock.node);
702
- const displayNode = this.sliceNode(this.state.currentBlock.node, this.state.currentProgress);
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: displayNode || { type: "paragraph", children: [] },
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.countChars(block.node);
1014
+ const total = this.getTotalChars();
836
1015
  const step = this.getStep();
837
- this.state.currentProgress = Math.min(this.state.currentProgress + step, total);
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);