@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
|
@@ -50,7 +50,7 @@ export class IncremarkParser {
|
|
|
50
50
|
private context: BlockContext
|
|
51
51
|
private options: ParserOptions
|
|
52
52
|
/** 缓存的容器配置,避免重复计算 */
|
|
53
|
-
private
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
const
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
/**
|
package/src/transformer/index.ts
CHANGED
package/src/transformer/types.ts
CHANGED
|
@@ -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:
|
|
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
|
-
* - '
|
|
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 插件
|