@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.
- package/package.json +2 -2
- package/src/__tests__/footnote.test.ts +0 -214
- package/src/benchmark/index.ts +0 -443
- package/src/benchmark/run.ts +0 -93
- package/src/detector/index.test.ts +0 -150
- package/src/detector/index.ts +0 -330
- package/src/extensions/html-extension/index.test.ts +0 -409
- package/src/extensions/html-extension/index.ts +0 -792
- package/src/extensions/micromark-gfm-footnote-incremental.ts +0 -275
- package/src/extensions/micromark-reference-extension.ts +0 -724
- package/src/index.ts +0 -128
- package/src/parser/IncremarkParser.comprehensive.test.ts +0 -418
- package/src/parser/IncremarkParser.footnote.test.ts +0 -334
- package/src/parser/IncremarkParser.robustness.test.ts +0 -428
- package/src/parser/IncremarkParser.test.ts +0 -110
- package/src/parser/IncremarkParser.ts +0 -839
- package/src/parser/index.ts +0 -2
- package/src/transformer/BlockTransformer.ts +0 -640
- package/src/transformer/index.ts +0 -36
- package/src/transformer/plugins.ts +0 -113
- package/src/transformer/types.ts +0 -115
- package/src/transformer/utils.ts +0 -364
- package/src/types/index.ts +0 -183
- package/src/utils/index.ts +0 -53
package/src/parser/index.ts
DELETED
|
@@ -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
|
-
|
package/src/transformer/index.ts
DELETED
|
@@ -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
|
-
|