@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.
@@ -50,7 +50,7 @@ export class IncremarkParser {
50
50
  private context: BlockContext
51
51
  private options: ParserOptions
52
52
  /** 缓存的容器配置,避免重复计算 */
53
- private cachedContainerConfig: ContainerConfig | undefined | null = null
53
+ private readonly containerConfig: ContainerConfig | undefined
54
54
  /** 上次 append 返回的 pending blocks,用于 getAst 复用 */
55
55
  private lastPendingBlocks: ParsedBlock[] = []
56
56
 
@@ -60,8 +60,8 @@ export class IncremarkParser {
60
60
  ...options
61
61
  }
62
62
  this.context = createInitialContext()
63
- // 初始化容器配置缓存
64
- this.cachedContainerConfig = this.computeContainerConfig()
63
+ // 初始化容器配置(构造时计算一次)
64
+ this.containerConfig = this.computeContainerConfig()
65
65
  }
66
66
 
67
67
  private generateBlockId(): string {
@@ -74,10 +74,6 @@ export class IncremarkParser {
74
74
  return containers === true ? {} : containers
75
75
  }
76
76
 
77
- private getContainerConfig(): ContainerConfig | undefined {
78
- return this.cachedContainerConfig ?? undefined
79
- }
80
-
81
77
  private parse(text: string): Root {
82
78
  const extensions: MicromarkExtension[] = []
83
79
  const mdastExtensions: MdastExtension[] = []
@@ -148,7 +144,6 @@ export class IncremarkParser {
148
144
  let stableLine = -1
149
145
  let stableContext: BlockContext = this.context
150
146
  let tempContext = { ...this.context }
151
- const containerConfig = this.getContainerConfig()
152
147
 
153
148
  for (let i = this.pendingStartLine; i < this.lines.length; i++) {
154
149
  const line = this.lines[i]
@@ -156,7 +151,7 @@ export class IncremarkParser {
156
151
  const wasInContainer = tempContext.inContainer
157
152
  const wasContainerDepth = tempContext.containerDepth
158
153
 
159
- tempContext = updateContext(line, tempContext, containerConfig)
154
+ tempContext = updateContext(line, tempContext, this.containerConfig)
160
155
 
161
156
  if (wasInFencedCode && !tempContext.inFencedCode) {
162
157
  if (i < this.lines.length - 1) {
@@ -182,7 +177,7 @@ export class IncremarkParser {
182
177
  continue
183
178
  }
184
179
 
185
- const stablePoint = this.checkStability(i, containerConfig)
180
+ const stablePoint = this.checkStability(i)
186
181
  if (stablePoint >= 0) {
187
182
  stableLine = stablePoint
188
183
  stableContext = { ...tempContext }
@@ -192,10 +187,7 @@ export class IncremarkParser {
192
187
  return { line: stableLine, contextAtLine: stableContext }
193
188
  }
194
189
 
195
- private checkStability(
196
- lineIndex: number,
197
- containerConfig: ContainerConfig | undefined
198
- ): number {
190
+ private checkStability(lineIndex: number): number {
199
191
  // 第一行永远不稳定
200
192
  if (lineIndex === 0) {
201
193
  return -1
@@ -237,10 +229,10 @@ export class IncremarkParser {
237
229
  }
238
230
 
239
231
  // 新容器开始
240
- if (containerConfig !== undefined) {
241
- const container = detectContainer(line, containerConfig)
232
+ if (this.containerConfig !== undefined) {
233
+ const container = detectContainer(line, this.containerConfig)
242
234
  if (container && !container.isEnd) {
243
- const prevContainer = detectContainer(prevLine, containerConfig)
235
+ const prevContainer = detectContainer(prevLine, this.containerConfig)
244
236
  if (!prevContainer || prevContainer.isEnd) {
245
237
  return lineIndex - 1
246
238
  }
@@ -1,4 +1,5 @@
1
1
  import type { RootContent } from 'mdast'
2
+ import type { AstNode } from '../types'
2
3
  import type {
3
4
  SourceBlock,
4
5
  DisplayBlock,
@@ -7,7 +8,7 @@ import type {
7
8
  TransformerPlugin,
8
9
  AnimationEffect
9
10
  } from './types'
10
- import { countChars as defaultCountChars, sliceAst as defaultSliceAst } from './utils'
11
+ import { countChars as defaultCountChars, sliceAst as defaultSliceAst, appendToAst, type TextChunk, type AccumulatedChunks } from './utils'
11
12
 
12
13
  /**
13
14
  * Block Transformer
@@ -54,7 +55,16 @@ export class BlockTransformer<T = unknown> {
54
55
  private lastTickTime = 0
55
56
  private isRunning = false
56
57
  private isPaused = false
58
+ private chunks: TextChunk[] = [] // 累积的 chunks(用于 fade-in 动画)
57
59
  private visibilityHandler: (() => void) | null = null
60
+
61
+ // ============ 性能优化:缓存机制 ============
62
+ /** 缓存的已截断 displayNode(稳定的部分,避免重复遍历) */
63
+ private cachedDisplayNode: RootContent | null = null
64
+ /** 缓存的字符数(避免重复计算) */
65
+ private cachedTotalChars: number | null = null
66
+ /** 当前缓存的进度(对应 cachedDisplayNode) */
67
+ private cachedProgress: number = 0
58
68
 
59
69
  constructor(options: TransformerOptions = {}) {
60
70
  this.options = {
@@ -98,12 +108,23 @@ export class BlockTransformer<T = unknown> {
98
108
  if (this.state.currentBlock) {
99
109
  const updated = blocks.find((b) => b.id === this.state.currentBlock!.id)
100
110
  if (updated && updated.node !== this.state.currentBlock.node) {
101
- // 内容增加了,更新引用
111
+ // 内容更新,清除缓存
112
+ this.clearCache()
113
+
114
+ const oldTotal = this.cachedTotalChars ?? this.countChars(this.state.currentBlock.node)
115
+ const newTotal = this.countChars(updated.node)
116
+
117
+ // 如果字符数减少了(AST 结构变化,如 **xxx 变成 **xxx**)
118
+ // 重新计算进度,保持相对位置
119
+ if (newTotal < oldTotal || newTotal < this.state.currentProgress) {
120
+ this.state.currentProgress = Math.min(this.state.currentProgress, newTotal)
121
+ }
122
+
123
+ // 内容更新,更新引用
102
124
  this.state.currentBlock = updated
103
125
  // 如果之前暂停了(因为到达末尾),重新开始
104
126
  if (!this.rafId && !this.isPaused) {
105
- const total = this.countChars(updated.node)
106
- if (this.state.currentProgress < total) {
127
+ if (this.state.currentProgress < newTotal) {
107
128
  this.startIfNeeded()
108
129
  }
109
130
  }
@@ -116,13 +137,15 @@ export class BlockTransformer<T = unknown> {
116
137
  */
117
138
  update(block: SourceBlock<T>): void {
118
139
  if (this.state.currentBlock?.id === block.id) {
119
- const oldTotal = this.countChars(this.state.currentBlock.node)
140
+ const oldTotal = this.cachedTotalChars ?? this.countChars(this.state.currentBlock.node)
120
141
  const newTotal = this.countChars(block.node)
121
142
 
122
143
  this.state.currentBlock = block
123
144
 
124
145
  // 如果内容增加了且之前暂停了,继续
125
146
  if (newTotal > oldTotal && !this.rafId && !this.isPaused && this.state.currentProgress >= oldTotal) {
147
+ // 内容变化,清除缓存
148
+ this.clearCache()
126
149
  this.startIfNeeded()
127
150
  }
128
151
  }
@@ -146,6 +169,8 @@ export class BlockTransformer<T = unknown> {
146
169
  currentProgress: 0,
147
170
  pendingBlocks: []
148
171
  }
172
+ this.chunks = []
173
+ this.clearCache()
149
174
 
150
175
  this.emit()
151
176
  }
@@ -161,6 +186,8 @@ export class BlockTransformer<T = unknown> {
161
186
  currentProgress: 0,
162
187
  pendingBlocks: []
163
188
  }
189
+ this.chunks = []
190
+ this.clearCache()
164
191
  this.emit()
165
192
  }
166
193
 
@@ -184,6 +211,7 @@ export class BlockTransformer<T = unknown> {
184
211
 
185
212
  /**
186
213
  * 获取用于渲染的 display blocks
214
+ * 优化:使用缓存的 displayNode,避免重复遍历已稳定的节点
187
215
  */
188
216
  getDisplayBlocks(): DisplayBlock<T>[] {
189
217
  const result: DisplayBlock<T>[] = []
@@ -200,12 +228,17 @@ export class BlockTransformer<T = unknown> {
200
228
 
201
229
  // 当前正在显示的 block
202
230
  if (this.state.currentBlock) {
203
- const total = this.countChars(this.state.currentBlock.node)
204
- const displayNode = this.sliceNode(this.state.currentBlock.node, this.state.currentProgress)
231
+ // 使用缓存的字符数
232
+ const total = this.getTotalChars()
233
+
234
+ // 如果进度变化了或缓存无效,更新缓存的 displayNode
235
+ if (this.state.currentProgress !== this.cachedProgress || !this.cachedDisplayNode) {
236
+ this.updateCachedDisplayNode()
237
+ }
205
238
 
206
239
  result.push({
207
240
  ...this.state.currentBlock,
208
- displayNode: displayNode || { type: 'paragraph', children: [] },
241
+ displayNode: this.cachedDisplayNode || { type: 'paragraph', children: [] },
209
242
  progress: total > 0 ? this.state.currentProgress / total : 1,
210
243
  isDisplayComplete: false
211
244
  })
@@ -325,6 +358,7 @@ export class BlockTransformer<T = unknown> {
325
358
  if (!this.state.currentBlock && this.state.pendingBlocks.length > 0) {
326
359
  this.state.currentBlock = this.state.pendingBlocks.shift()!
327
360
  this.state.currentProgress = 0
361
+ this.clearCache() // 新 block,清除缓存
328
362
  }
329
363
 
330
364
  if (this.state.currentBlock) {
@@ -366,23 +400,77 @@ export class BlockTransformer<T = unknown> {
366
400
  return
367
401
  }
368
402
 
369
- const total = this.countChars(block.node)
403
+ // 使用缓存的字符数,避免重复计算
404
+ const total = this.getTotalChars()
370
405
  const step = this.getStep()
406
+ const prevProgress = this.state.currentProgress
371
407
 
372
- this.state.currentProgress = Math.min(this.state.currentProgress + step, total)
408
+ this.state.currentProgress = Math.min(prevProgress + step, total)
409
+
410
+ // 如果是 fade-in 效果,添加新的 chunk
411
+ if (this.options.effect === 'fade-in' && this.state.currentProgress > prevProgress) {
412
+ // 从 block.node 中提取新增的字符
413
+ const newText = this.extractText(block.node, prevProgress, this.state.currentProgress)
414
+ if (newText.length > 0) {
415
+ this.chunks.push({
416
+ text: newText,
417
+ createdAt: Date.now()
418
+ })
419
+ }
420
+ }
373
421
 
374
422
  this.emit()
375
423
 
376
424
  if (this.state.currentProgress >= total) {
377
- // 当前 block 完成
425
+ // 当前 block 完成,清空 chunks 和缓存
378
426
  this.notifyComplete(block.node)
379
427
  this.state.completedBlocks.push(block)
380
428
  this.state.currentBlock = null
381
429
  this.state.currentProgress = 0
430
+ this.chunks = []
431
+ this.clearCache()
382
432
  this.processNext()
383
433
  }
384
434
  }
385
435
 
436
+ /**
437
+ * 从 AST 节点中提取指定范围的文本
438
+ */
439
+ private extractText(node: RootContent, start: number, end: number): string {
440
+ let result = ''
441
+ let charIndex = 0
442
+
443
+ function traverse(n: AstNode): boolean {
444
+ if (charIndex >= end) return false
445
+
446
+ if (n.value && typeof n.value === 'string') {
447
+ const nodeStart = charIndex
448
+ const nodeEnd = charIndex + n.value.length
449
+ charIndex = nodeEnd
450
+
451
+ // 计算交集
452
+ const overlapStart = Math.max(start, nodeStart)
453
+ const overlapEnd = Math.min(end, nodeEnd)
454
+
455
+ if (overlapStart < overlapEnd) {
456
+ result += n.value.slice(overlapStart - nodeStart, overlapEnd - nodeStart)
457
+ }
458
+ return charIndex < end
459
+ }
460
+
461
+ if (n.children && Array.isArray(n.children)) {
462
+ for (const child of n.children) {
463
+ if (!traverse(child)) return false
464
+ }
465
+ }
466
+
467
+ return true
468
+ }
469
+
470
+ traverse(node as AstNode)
471
+ return result
472
+ }
473
+
386
474
  private getStep(): number {
387
475
  const { charsPerTick } = this.options
388
476
  if (typeof charsPerTick === 'number') {
@@ -397,6 +485,8 @@ export class BlockTransformer<T = unknown> {
397
485
  if (this.state.pendingBlocks.length > 0) {
398
486
  this.state.currentBlock = this.state.pendingBlocks.shift()!
399
487
  this.state.currentProgress = 0
488
+ this.chunks = []
489
+ this.clearCache() // 新 block,清除缓存
400
490
  this.emit()
401
491
  // 继续运行(rAF 已经在调度中)
402
492
  } else {
@@ -437,7 +527,7 @@ export class BlockTransformer<T = unknown> {
437
527
  return defaultCountChars(node)
438
528
  }
439
529
 
440
- private sliceNode(node: RootContent, chars: number): RootContent | null {
530
+ private sliceNode(node: RootContent, chars: number, accumulatedChunks?: AccumulatedChunks): RootContent | null {
441
531
  // 先找匹配的插件
442
532
  for (const plugin of this.options.plugins) {
443
533
  if (plugin.match?.(node) && plugin.sliceNode) {
@@ -446,8 +536,8 @@ export class BlockTransformer<T = unknown> {
446
536
  if (result !== null) return result
447
537
  }
448
538
  }
449
- // 默认截断
450
- return defaultSliceAst(node, chars)
539
+ // 默认截断,传入累积的 chunks
540
+ return defaultSliceAst(node, chars, accumulatedChunks)
451
541
  }
452
542
 
453
543
  private notifyComplete(node: RootContent): void {
@@ -457,6 +547,84 @@ export class BlockTransformer<T = unknown> {
457
547
  }
458
548
  }
459
549
  }
550
+
551
+ // ============ 缓存管理方法 ============
552
+
553
+ /**
554
+ * 更新缓存的 displayNode
555
+ * 使用真正的增量追加模式:只处理新增部分,不重复遍历已稳定的节点
556
+ */
557
+ private updateCachedDisplayNode(): void {
558
+ const block = this.state.currentBlock
559
+ if (!block) {
560
+ this.cachedDisplayNode = null
561
+ this.cachedProgress = 0
562
+ return
563
+ }
564
+
565
+ const currentProgress = this.state.currentProgress
566
+
567
+ // 如果进度减少了(内容更新导致),需要重新截断
568
+ if (currentProgress < this.cachedProgress) {
569
+ this.cachedDisplayNode = this.sliceNode(
570
+ block.node,
571
+ currentProgress,
572
+ this.getAccumulatedChunks()
573
+ )
574
+ this.cachedProgress = currentProgress
575
+ return
576
+ }
577
+
578
+ // 如果进度增加了,使用增量追加模式
579
+ if (currentProgress > this.cachedProgress && this.cachedDisplayNode) {
580
+ // 真正的增量追加:只处理新增部分,不重复遍历已稳定的节点
581
+ this.cachedDisplayNode = appendToAst(
582
+ this.cachedDisplayNode,
583
+ block.node,
584
+ this.cachedProgress,
585
+ currentProgress,
586
+ this.getAccumulatedChunks()
587
+ )
588
+ this.cachedProgress = currentProgress
589
+ } else if (!this.cachedDisplayNode) {
590
+ // 首次截断
591
+ this.cachedDisplayNode = this.sliceNode(
592
+ block.node,
593
+ currentProgress,
594
+ this.getAccumulatedChunks()
595
+ )
596
+ this.cachedProgress = currentProgress
597
+ }
598
+ }
599
+
600
+ /**
601
+ * 获取总字符数(带缓存)
602
+ */
603
+ private getTotalChars(): number {
604
+ if (this.cachedTotalChars === null && this.state.currentBlock) {
605
+ this.cachedTotalChars = this.countChars(this.state.currentBlock.node)
606
+ }
607
+ return this.cachedTotalChars ?? 0
608
+ }
609
+
610
+ /**
611
+ * 清除缓存(当 block 切换或内容更新时)
612
+ */
613
+ private clearCache(): void {
614
+ this.cachedDisplayNode = null
615
+ this.cachedTotalChars = null
616
+ this.cachedProgress = 0
617
+ }
618
+
619
+ /**
620
+ * 获取累积的 chunks(用于 fade-in 效果)
621
+ */
622
+ private getAccumulatedChunks(): AccumulatedChunks | undefined {
623
+ if (this.options.effect === 'fade-in' && this.chunks.length > 0) {
624
+ return { stableChars: 0, chunks: this.chunks }
625
+ }
626
+ return undefined
627
+ }
460
628
  }
461
629
 
462
630
  /**
@@ -20,6 +20,7 @@ export type {
20
20
 
21
21
  // 工具函数
22
22
  export { countChars, sliceAst, cloneNode } from './utils'
23
+ export type { TextChunk, TextNodeWithChunks, AccumulatedChunks } from './utils'
23
24
 
24
25
  // 内置插件
25
26
  export {
@@ -1,4 +1,5 @@
1
1
  import type { RootContent } from 'mdast'
2
+ import type { BlockStatus } from '../types'
2
3
 
3
4
  /**
4
5
  * 源 Block 类型(来自解析器)
@@ -9,7 +10,7 @@ export interface SourceBlock<T = unknown> {
9
10
  /** AST 节点 */
10
11
  node: RootContent
11
12
  /** 块状态 */
12
- status: 'pending' | 'stable' | 'completed'
13
+ status: BlockStatus
13
14
  /** 用户自定义元数据 */
14
15
  meta?: T
15
16
  }
@@ -29,9 +30,10 @@ export interface DisplayBlock<T = unknown> extends SourceBlock<T> {
29
30
  /**
30
31
  * 动画效果类型
31
32
  * - 'none': 无动画效果
32
- * - 'typing': 打字机光标效果(需配合 CSS)
33
+ * - 'fade-in': 新增字符渐入效果
34
+ * - 'typing': 打字机光标效果
33
35
  */
34
- export type AnimationEffect = 'none' | 'typing'
36
+ export type AnimationEffect = 'none' | 'fade-in' | 'typing'
35
37
 
36
38
  /**
37
39
  * Transformer 插件