@incremark/core 0.2.1 → 0.2.3

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.
@@ -1,2 +0,0 @@
1
- export { IncremarkParser, createIncremarkParser } from './IncremarkParser'
2
-
@@ -1,640 +0,0 @@
1
- import type { RootContent } from 'mdast'
2
- import type { AstNode } from '../types'
3
- import type {
4
- SourceBlock,
5
- DisplayBlock,
6
- TransformerOptions,
7
- TransformerState,
8
- TransformerPlugin,
9
- AnimationEffect
10
- } from './types'
11
- import { countChars as defaultCountChars, sliceAst as defaultSliceAst, appendToAst, type TextChunk, type AccumulatedChunks } from './utils'
12
-
13
- /**
14
- * Block Transformer
15
- *
16
- * 用于控制 blocks 的逐步显示(打字机效果)
17
- * 作为解析器和渲染器之间的中间层
18
- *
19
- * 特性:
20
- * - 使用 requestAnimationFrame 实现流畅动画
21
- * - 支持随机步长,模拟真实打字效果
22
- * - 支持 typing 动画效果
23
- * - 页面不可见时自动暂停,节省资源
24
- * - 插件系统支持自定义节点处理
25
- *
26
- * @example
27
- * ```typescript
28
- * const transformer = new BlockTransformer({
29
- * charsPerTick: [1, 3], // 随机 1-3 个字符
30
- * tickInterval: 30,
31
- * effect: 'typing',
32
- * onChange: (displayBlocks) => {
33
- * // 更新 UI
34
- * }
35
- * })
36
- *
37
- * // 推入新 blocks
38
- * transformer.push(blocks)
39
- *
40
- * // 获取当前显示状态
41
- * const displayBlocks = transformer.getDisplayBlocks()
42
- * ```
43
- */
44
- export class BlockTransformer<T = unknown> {
45
- private state: TransformerState<T>
46
- private options: {
47
- charsPerTick: number | [number, number]
48
- tickInterval: number
49
- effect: AnimationEffect
50
- plugins: TransformerPlugin[]
51
- onChange: (displayBlocks: DisplayBlock<T>[]) => void
52
- pauseOnHidden: boolean
53
- }
54
- private rafId: number | null = null
55
- private lastTickTime = 0
56
- private isRunning = false
57
- private isPaused = false
58
- private chunks: TextChunk[] = [] // 累积的 chunks(用于 fade-in 动画)
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
68
-
69
- constructor(options: TransformerOptions = {}) {
70
- this.options = {
71
- charsPerTick: options.charsPerTick ?? 1,
72
- tickInterval: options.tickInterval ?? 20,
73
- effect: options.effect ?? 'none',
74
- plugins: options.plugins ?? [],
75
- onChange: options.onChange ?? (() => {}),
76
- pauseOnHidden: options.pauseOnHidden ?? true
77
- }
78
-
79
- this.state = {
80
- completedBlocks: [],
81
- currentBlock: null,
82
- currentProgress: 0,
83
- pendingBlocks: []
84
- }
85
-
86
- // 设置页面可见性监听
87
- if (this.options.pauseOnHidden && typeof document !== 'undefined') {
88
- this.setupVisibilityHandler()
89
- }
90
- }
91
-
92
- /**
93
- * 推入新的 blocks
94
- * 会自动过滤已存在的 blocks
95
- */
96
- push(blocks: SourceBlock<T>[]): void {
97
- const existingIds = this.getAllBlockIds()
98
-
99
- // 找出新增的 blocks
100
- const newBlocks = blocks.filter((b) => !existingIds.has(b.id))
101
-
102
- if (newBlocks.length > 0) {
103
- this.state.pendingBlocks.push(...newBlocks)
104
- this.startIfNeeded()
105
- }
106
-
107
- // 如果当前正在显示的 block 内容更新了(pending block 变化)
108
- if (this.state.currentBlock) {
109
- const updated = blocks.find((b) => b.id === this.state.currentBlock!.id)
110
- if (updated && updated.node !== this.state.currentBlock.node) {
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
- // 内容更新,更新引用
124
- this.state.currentBlock = updated
125
- // 如果之前暂停了(因为到达末尾),重新开始
126
- if (!this.rafId && !this.isPaused) {
127
- if (this.state.currentProgress < newTotal) {
128
- this.startIfNeeded()
129
- }
130
- }
131
- }
132
- }
133
- }
134
-
135
- /**
136
- * 更新指定 block(用于 pending block 内容增加时)
137
- */
138
- update(block: SourceBlock<T>): void {
139
- if (this.state.currentBlock?.id === block.id) {
140
- const oldTotal = this.cachedTotalChars ?? this.countChars(this.state.currentBlock.node)
141
- const newTotal = this.countChars(block.node)
142
-
143
- this.state.currentBlock = block
144
-
145
- // 如果内容增加了且之前暂停了,继续
146
- if (newTotal > oldTotal && !this.rafId && !this.isPaused && this.state.currentProgress >= oldTotal) {
147
- // 内容变化,清除缓存
148
- this.clearCache()
149
- this.startIfNeeded()
150
- }
151
- }
152
- }
153
-
154
- /**
155
- * 跳过所有动画,直接显示全部内容
156
- */
157
- skip(): void {
158
- this.stop()
159
-
160
- const allBlocks = [
161
- ...this.state.completedBlocks,
162
- ...(this.state.currentBlock ? [this.state.currentBlock] : []),
163
- ...this.state.pendingBlocks
164
- ]
165
-
166
- this.state = {
167
- completedBlocks: allBlocks,
168
- currentBlock: null,
169
- currentProgress: 0,
170
- pendingBlocks: []
171
- }
172
- this.chunks = []
173
- this.clearCache()
174
-
175
- this.emit()
176
- }
177
-
178
- /**
179
- * 重置状态
180
- */
181
- reset(): void {
182
- this.stop()
183
- this.state = {
184
- completedBlocks: [],
185
- currentBlock: null,
186
- currentProgress: 0,
187
- pendingBlocks: []
188
- }
189
- this.chunks = []
190
- this.clearCache()
191
- this.emit()
192
- }
193
-
194
- /**
195
- * 暂停动画
196
- */
197
- pause(): void {
198
- this.isPaused = true
199
- this.cancelRaf()
200
- }
201
-
202
- /**
203
- * 恢复动画
204
- */
205
- resume(): void {
206
- if (this.isPaused) {
207
- this.isPaused = false
208
- this.startIfNeeded()
209
- }
210
- }
211
-
212
- /**
213
- * 获取用于渲染的 display blocks
214
- * 优化:使用缓存的 displayNode,避免重复遍历已稳定的节点
215
- */
216
- getDisplayBlocks(): DisplayBlock<T>[] {
217
- const result: DisplayBlock<T>[] = []
218
-
219
- // 已完成的 blocks
220
- for (const block of this.state.completedBlocks) {
221
- result.push({
222
- ...block,
223
- displayNode: block.node,
224
- progress: 1,
225
- isDisplayComplete: true
226
- })
227
- }
228
-
229
- // 当前正在显示的 block
230
- if (this.state.currentBlock) {
231
- // 使用缓存的字符数
232
- const total = this.getTotalChars()
233
-
234
- // 如果进度变化了或缓存无效,更新缓存的 displayNode
235
- if (this.state.currentProgress !== this.cachedProgress || !this.cachedDisplayNode) {
236
- this.updateCachedDisplayNode()
237
- }
238
-
239
- result.push({
240
- ...this.state.currentBlock,
241
- displayNode: this.cachedDisplayNode || { type: 'paragraph', children: [] },
242
- progress: total > 0 ? this.state.currentProgress / total : 1,
243
- isDisplayComplete: false
244
- })
245
- }
246
-
247
- return result
248
- }
249
-
250
- /**
251
- * 是否正在处理中
252
- */
253
- isProcessing(): boolean {
254
- return this.isRunning || this.state.currentBlock !== null || this.state.pendingBlocks.length > 0
255
- }
256
-
257
- /**
258
- * 是否已暂停
259
- */
260
- isPausedState(): boolean {
261
- return this.isPaused
262
- }
263
-
264
- /**
265
- * 获取内部状态(用于调试)
266
- */
267
- getState(): Readonly<TransformerState<T>> {
268
- return { ...this.state }
269
- }
270
-
271
- /**
272
- * 动态更新配置
273
- */
274
- setOptions(options: Partial<Pick<TransformerOptions, 'charsPerTick' | 'tickInterval' | 'effect' | 'pauseOnHidden'>>): void {
275
- if (options.charsPerTick !== undefined) {
276
- this.options.charsPerTick = options.charsPerTick
277
- }
278
- if (options.tickInterval !== undefined) {
279
- this.options.tickInterval = options.tickInterval
280
- }
281
- if (options.effect !== undefined) {
282
- this.options.effect = options.effect
283
- }
284
- if (options.pauseOnHidden !== undefined) {
285
- this.options.pauseOnHidden = options.pauseOnHidden
286
- if (options.pauseOnHidden && typeof document !== 'undefined') {
287
- this.setupVisibilityHandler()
288
- } else {
289
- this.removeVisibilityHandler()
290
- }
291
- }
292
- }
293
-
294
- /**
295
- * 获取当前配置
296
- */
297
- getOptions(): {
298
- charsPerTick: number | [number, number]
299
- tickInterval: number
300
- effect: AnimationEffect
301
- } {
302
- return {
303
- charsPerTick: this.options.charsPerTick,
304
- tickInterval: this.options.tickInterval,
305
- effect: this.options.effect
306
- }
307
- }
308
-
309
- /**
310
- * 获取当前动画效果
311
- */
312
- getEffect(): AnimationEffect {
313
- return this.options.effect
314
- }
315
-
316
- /**
317
- * 销毁,清理资源
318
- */
319
- destroy(): void {
320
- this.stop()
321
- this.removeVisibilityHandler()
322
- }
323
-
324
- // ============ 私有方法 ============
325
-
326
- private getAllBlockIds(): Set<string> {
327
- return new Set([
328
- ...this.state.completedBlocks.map((b) => b.id),
329
- this.state.currentBlock?.id,
330
- ...this.state.pendingBlocks.map((b) => b.id)
331
- ].filter((id): id is string => id !== undefined))
332
- }
333
-
334
- private setupVisibilityHandler(): void {
335
- if (this.visibilityHandler) return
336
-
337
- this.visibilityHandler = () => {
338
- if (document.hidden) {
339
- this.pause()
340
- } else {
341
- this.resume()
342
- }
343
- }
344
-
345
- document.addEventListener('visibilitychange', this.visibilityHandler)
346
- }
347
-
348
- private removeVisibilityHandler(): void {
349
- if (this.visibilityHandler) {
350
- document.removeEventListener('visibilitychange', this.visibilityHandler)
351
- this.visibilityHandler = null
352
- }
353
- }
354
-
355
- private startIfNeeded(): void {
356
- if (this.rafId || this.isPaused) return
357
-
358
- if (!this.state.currentBlock && this.state.pendingBlocks.length > 0) {
359
- this.state.currentBlock = this.state.pendingBlocks.shift()!
360
- this.state.currentProgress = 0
361
- this.clearCache() // 新 block,清除缓存
362
- }
363
-
364
- if (this.state.currentBlock) {
365
- this.isRunning = true
366
- this.lastTickTime = 0
367
- this.scheduleNextFrame()
368
- }
369
- }
370
-
371
- private scheduleNextFrame(): void {
372
- this.rafId = requestAnimationFrame((time) => this.animationFrame(time))
373
- }
374
-
375
- private animationFrame(time: number): void {
376
- this.rafId = null
377
-
378
- // 计算是否应该执行 tick
379
- if (this.lastTickTime === 0) {
380
- this.lastTickTime = time
381
- }
382
-
383
- const elapsed = time - this.lastTickTime
384
-
385
- if (elapsed >= this.options.tickInterval) {
386
- this.lastTickTime = time
387
- this.tick()
388
- }
389
-
390
- // 如果还在运行,继续调度
391
- if (this.isRunning && !this.isPaused) {
392
- this.scheduleNextFrame()
393
- }
394
- }
395
-
396
- private tick(): void {
397
- const block = this.state.currentBlock
398
- if (!block) {
399
- this.processNext()
400
- return
401
- }
402
-
403
- // 使用缓存的字符数,避免重复计算
404
- const total = this.getTotalChars()
405
- const step = this.getStep()
406
- const prevProgress = this.state.currentProgress
407
-
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
- }
421
-
422
- this.emit()
423
-
424
- if (this.state.currentProgress >= total) {
425
- // 当前 block 完成,清空 chunks 和缓存
426
- this.notifyComplete(block.node)
427
- this.state.completedBlocks.push(block)
428
- this.state.currentBlock = null
429
- this.state.currentProgress = 0
430
- this.chunks = []
431
- this.clearCache()
432
- this.processNext()
433
- }
434
- }
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
-
474
- private getStep(): number {
475
- const { charsPerTick } = this.options
476
- if (typeof charsPerTick === 'number') {
477
- return charsPerTick
478
- }
479
- // 随机步长
480
- const [min, max] = charsPerTick
481
- return Math.floor(Math.random() * (max - min + 1)) + min
482
- }
483
-
484
- private processNext(): void {
485
- if (this.state.pendingBlocks.length > 0) {
486
- this.state.currentBlock = this.state.pendingBlocks.shift()!
487
- this.state.currentProgress = 0
488
- this.chunks = []
489
- this.clearCache() // 新 block,清除缓存
490
- this.emit()
491
- // 继续运行(rAF 已经在调度中)
492
- } else {
493
- this.isRunning = false
494
- this.cancelRaf()
495
- this.emit()
496
- }
497
- }
498
-
499
- private cancelRaf(): void {
500
- if (this.rafId) {
501
- cancelAnimationFrame(this.rafId)
502
- this.rafId = null
503
- }
504
- }
505
-
506
- private stop(): void {
507
- this.cancelRaf()
508
- this.isRunning = false
509
- this.isPaused = false
510
- }
511
-
512
- private emit(): void {
513
- this.options.onChange(this.getDisplayBlocks())
514
- }
515
-
516
- // ============ 插件调用 ============
517
-
518
- private countChars(node: RootContent): number {
519
- // 先找匹配的插件
520
- for (const plugin of this.options.plugins) {
521
- if (plugin.match?.(node) && plugin.countChars) {
522
- const result = plugin.countChars(node)
523
- if (result !== undefined) return result
524
- }
525
- }
526
- // 默认计算
527
- return defaultCountChars(node)
528
- }
529
-
530
- private sliceNode(node: RootContent, chars: number, accumulatedChunks?: AccumulatedChunks): RootContent | null {
531
- // 先找匹配的插件
532
- for (const plugin of this.options.plugins) {
533
- if (plugin.match?.(node) && plugin.sliceNode) {
534
- const total = this.countChars(node)
535
- const result = plugin.sliceNode(node, chars, total)
536
- if (result !== null) return result
537
- }
538
- }
539
- // 默认截断,传入累积的 chunks
540
- return defaultSliceAst(node, chars, accumulatedChunks)
541
- }
542
-
543
- private notifyComplete(node: RootContent): void {
544
- for (const plugin of this.options.plugins) {
545
- if (plugin.match?.(node) && plugin.onComplete) {
546
- plugin.onComplete(node)
547
- }
548
- }
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
- }
628
- }
629
-
630
- /**
631
- * 创建 BlockTransformer 实例的工厂函数
632
- */
633
- export function createBlockTransformer<T = unknown>(
634
- options?: TransformerOptions
635
- ): BlockTransformer<T> {
636
- return new BlockTransformer<T>(options)
637
- }
638
-
639
-
640
-
@@ -1,36 +0,0 @@
1
- /**
2
- * Block Transformer
3
- *
4
- * 用于控制 blocks 的逐步显示(打字机效果)
5
- * 作为解析器和渲染器之间的中间层
6
- */
7
-
8
- // 核心类
9
- export { BlockTransformer, createBlockTransformer } from './BlockTransformer'
10
-
11
- // 类型
12
- export type {
13
- SourceBlock,
14
- DisplayBlock,
15
- TransformerPlugin,
16
- TransformerOptions,
17
- TransformerState,
18
- AnimationEffect
19
- } from './types'
20
-
21
- // 工具函数
22
- export { countChars, sliceAst, cloneNode } from './utils'
23
- export type { TextChunk, TextNodeWithChunks, AccumulatedChunks } from './utils'
24
-
25
- // 内置插件
26
- export {
27
- codeBlockPlugin,
28
- mermaidPlugin,
29
- imagePlugin,
30
- mathPlugin,
31
- thematicBreakPlugin,
32
- defaultPlugins,
33
- allPlugins,
34
- createPlugin
35
- } from './plugins'
36
-