@incremark/core 0.1.0 → 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 +34 -7
- package/dist/index.js +247 -61
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/detector/index.ts +46 -17
- package/src/index.ts +1 -0
- package/src/parser/IncremarkParser.ts +9 -17
- package/src/transformer/BlockTransformer.ts +111 -23
- package/src/transformer/types.ts +2 -1
- package/src/transformer/utils.ts +209 -36
- 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, type TextChunk, type AccumulatedChunks } 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
|
|
@@ -56,6 +57,14 @@ export class BlockTransformer<T = unknown> {
|
|
|
56
57
|
private isPaused = false
|
|
57
58
|
private chunks: TextChunk[] = [] // 累积的 chunks(用于 fade-in 动画)
|
|
58
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
|
|
59
68
|
|
|
60
69
|
constructor(options: TransformerOptions = {}) {
|
|
61
70
|
this.options = {
|
|
@@ -99,7 +108,10 @@ export class BlockTransformer<T = unknown> {
|
|
|
99
108
|
if (this.state.currentBlock) {
|
|
100
109
|
const updated = blocks.find((b) => b.id === this.state.currentBlock!.id)
|
|
101
110
|
if (updated && updated.node !== this.state.currentBlock.node) {
|
|
102
|
-
|
|
111
|
+
// 内容更新,清除缓存
|
|
112
|
+
this.clearCache()
|
|
113
|
+
|
|
114
|
+
const oldTotal = this.cachedTotalChars ?? this.countChars(this.state.currentBlock.node)
|
|
103
115
|
const newTotal = this.countChars(updated.node)
|
|
104
116
|
|
|
105
117
|
// 如果字符数减少了(AST 结构变化,如 **xxx 变成 **xxx**)
|
|
@@ -125,13 +137,15 @@ export class BlockTransformer<T = unknown> {
|
|
|
125
137
|
*/
|
|
126
138
|
update(block: SourceBlock<T>): void {
|
|
127
139
|
if (this.state.currentBlock?.id === block.id) {
|
|
128
|
-
const oldTotal = this.countChars(this.state.currentBlock.node)
|
|
140
|
+
const oldTotal = this.cachedTotalChars ?? this.countChars(this.state.currentBlock.node)
|
|
129
141
|
const newTotal = this.countChars(block.node)
|
|
130
142
|
|
|
131
143
|
this.state.currentBlock = block
|
|
132
144
|
|
|
133
145
|
// 如果内容增加了且之前暂停了,继续
|
|
134
146
|
if (newTotal > oldTotal && !this.rafId && !this.isPaused && this.state.currentProgress >= oldTotal) {
|
|
147
|
+
// 内容变化,清除缓存
|
|
148
|
+
this.clearCache()
|
|
135
149
|
this.startIfNeeded()
|
|
136
150
|
}
|
|
137
151
|
}
|
|
@@ -156,6 +170,7 @@ export class BlockTransformer<T = unknown> {
|
|
|
156
170
|
pendingBlocks: []
|
|
157
171
|
}
|
|
158
172
|
this.chunks = []
|
|
173
|
+
this.clearCache()
|
|
159
174
|
|
|
160
175
|
this.emit()
|
|
161
176
|
}
|
|
@@ -172,6 +187,7 @@ export class BlockTransformer<T = unknown> {
|
|
|
172
187
|
pendingBlocks: []
|
|
173
188
|
}
|
|
174
189
|
this.chunks = []
|
|
190
|
+
this.clearCache()
|
|
175
191
|
this.emit()
|
|
176
192
|
}
|
|
177
193
|
|
|
@@ -195,6 +211,7 @@ export class BlockTransformer<T = unknown> {
|
|
|
195
211
|
|
|
196
212
|
/**
|
|
197
213
|
* 获取用于渲染的 display blocks
|
|
214
|
+
* 优化:使用缓存的 displayNode,避免重复遍历已稳定的节点
|
|
198
215
|
*/
|
|
199
216
|
getDisplayBlocks(): DisplayBlock<T>[] {
|
|
200
217
|
const result: DisplayBlock<T>[] = []
|
|
@@ -211,22 +228,17 @@ export class BlockTransformer<T = unknown> {
|
|
|
211
228
|
|
|
212
229
|
// 当前正在显示的 block
|
|
213
230
|
if (this.state.currentBlock) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const accumulatedChunks: AccumulatedChunks | undefined =
|
|
217
|
-
this.options.effect === 'fade-in' && this.chunks.length > 0
|
|
218
|
-
? { stableChars: 0, chunks: this.chunks }
|
|
219
|
-
: undefined
|
|
231
|
+
// 使用缓存的字符数
|
|
232
|
+
const total = this.getTotalChars()
|
|
220
233
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.
|
|
224
|
-
|
|
225
|
-
)
|
|
234
|
+
// 如果进度变化了或缓存无效,更新缓存的 displayNode
|
|
235
|
+
if (this.state.currentProgress !== this.cachedProgress || !this.cachedDisplayNode) {
|
|
236
|
+
this.updateCachedDisplayNode()
|
|
237
|
+
}
|
|
226
238
|
|
|
227
239
|
result.push({
|
|
228
240
|
...this.state.currentBlock,
|
|
229
|
-
displayNode:
|
|
241
|
+
displayNode: this.cachedDisplayNode || { type: 'paragraph', children: [] },
|
|
230
242
|
progress: total > 0 ? this.state.currentProgress / total : 1,
|
|
231
243
|
isDisplayComplete: false
|
|
232
244
|
})
|
|
@@ -346,6 +358,7 @@ export class BlockTransformer<T = unknown> {
|
|
|
346
358
|
if (!this.state.currentBlock && this.state.pendingBlocks.length > 0) {
|
|
347
359
|
this.state.currentBlock = this.state.pendingBlocks.shift()!
|
|
348
360
|
this.state.currentProgress = 0
|
|
361
|
+
this.clearCache() // 新 block,清除缓存
|
|
349
362
|
}
|
|
350
363
|
|
|
351
364
|
if (this.state.currentBlock) {
|
|
@@ -387,7 +400,8 @@ export class BlockTransformer<T = unknown> {
|
|
|
387
400
|
return
|
|
388
401
|
}
|
|
389
402
|
|
|
390
|
-
|
|
403
|
+
// 使用缓存的字符数,避免重复计算
|
|
404
|
+
const total = this.getTotalChars()
|
|
391
405
|
const step = this.getStep()
|
|
392
406
|
const prevProgress = this.state.currentProgress
|
|
393
407
|
|
|
@@ -408,12 +422,13 @@ export class BlockTransformer<T = unknown> {
|
|
|
408
422
|
this.emit()
|
|
409
423
|
|
|
410
424
|
if (this.state.currentProgress >= total) {
|
|
411
|
-
// 当前 block 完成,清空 chunks
|
|
425
|
+
// 当前 block 完成,清空 chunks 和缓存
|
|
412
426
|
this.notifyComplete(block.node)
|
|
413
427
|
this.state.completedBlocks.push(block)
|
|
414
428
|
this.state.currentBlock = null
|
|
415
429
|
this.state.currentProgress = 0
|
|
416
430
|
this.chunks = []
|
|
431
|
+
this.clearCache()
|
|
417
432
|
this.processNext()
|
|
418
433
|
}
|
|
419
434
|
}
|
|
@@ -425,12 +440,6 @@ export class BlockTransformer<T = unknown> {
|
|
|
425
440
|
let result = ''
|
|
426
441
|
let charIndex = 0
|
|
427
442
|
|
|
428
|
-
interface AstNode {
|
|
429
|
-
type: string
|
|
430
|
-
value?: string
|
|
431
|
-
children?: AstNode[]
|
|
432
|
-
}
|
|
433
|
-
|
|
434
443
|
function traverse(n: AstNode): boolean {
|
|
435
444
|
if (charIndex >= end) return false
|
|
436
445
|
|
|
@@ -477,6 +486,7 @@ export class BlockTransformer<T = unknown> {
|
|
|
477
486
|
this.state.currentBlock = this.state.pendingBlocks.shift()!
|
|
478
487
|
this.state.currentProgress = 0
|
|
479
488
|
this.chunks = []
|
|
489
|
+
this.clearCache() // 新 block,清除缓存
|
|
480
490
|
this.emit()
|
|
481
491
|
// 继续运行(rAF 已经在调度中)
|
|
482
492
|
} else {
|
|
@@ -537,6 +547,84 @@ export class BlockTransformer<T = unknown> {
|
|
|
537
547
|
}
|
|
538
548
|
}
|
|
539
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
|
+
}
|
|
540
628
|
}
|
|
541
629
|
|
|
542
630
|
/**
|
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
|
}
|
package/src/transformer/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { RootContent, Text
|
|
1
|
+
import type { RootContent, Text } from 'mdast'
|
|
2
|
+
import type { AstNode } from '../types'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 文本块片段(用于渐入动画)
|
|
@@ -21,34 +22,28 @@ export interface TextNodeWithChunks extends Text {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
* AST
|
|
25
|
+
* 计算 AST 节点的总字符数
|
|
25
26
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
value?: string
|
|
29
|
-
children?: AstNode[]
|
|
27
|
+
export function countChars(node: RootContent): number {
|
|
28
|
+
return countCharsInNode(node as AstNode)
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
|
-
*
|
|
32
|
+
* 计算单个 AST 节点的字符数(内部辅助函数)
|
|
34
33
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (n.children && Array.isArray(n.children)) {
|
|
44
|
-
for (const child of n.children) {
|
|
45
|
-
traverse(child)
|
|
46
|
-
}
|
|
34
|
+
function countCharsInNode(n: AstNode): number {
|
|
35
|
+
if (n.value && typeof n.value === 'string') {
|
|
36
|
+
return n.value.length
|
|
37
|
+
}
|
|
38
|
+
if (n.children && Array.isArray(n.children)) {
|
|
39
|
+
let count = 0
|
|
40
|
+
for (const child of n.children) {
|
|
41
|
+
count += countCharsInNode(child)
|
|
47
42
|
}
|
|
43
|
+
return count
|
|
48
44
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return count
|
|
45
|
+
// 其他节点(如 thematicBreak, image)算作 1 个字符
|
|
46
|
+
return 1
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
/**
|
|
@@ -71,20 +66,24 @@ interface ChunkRange {
|
|
|
71
66
|
/**
|
|
72
67
|
* 截断 AST 节点,只保留前 maxChars 个字符
|
|
73
68
|
* 支持 chunks(用于渐入动画)
|
|
69
|
+
* 支持增量模式:跳过已处理的字符,只处理新增部分
|
|
74
70
|
*
|
|
75
71
|
* @param node 原始节点
|
|
76
72
|
* @param maxChars 最大字符数
|
|
77
73
|
* @param accumulatedChunks 累积的 chunks 信息(用于渐入动画)
|
|
74
|
+
* @param skipChars 跳过前 N 个字符(已处理的部分,用于增量追加)
|
|
78
75
|
* @returns 截断后的节点,如果 maxChars <= 0 返回 null
|
|
79
76
|
*/
|
|
80
77
|
export function sliceAst(
|
|
81
78
|
node: RootContent,
|
|
82
79
|
maxChars: number,
|
|
83
|
-
accumulatedChunks?: AccumulatedChunks
|
|
80
|
+
accumulatedChunks?: AccumulatedChunks,
|
|
81
|
+
skipChars: number = 0
|
|
84
82
|
): RootContent | null {
|
|
85
83
|
if (maxChars <= 0) return null
|
|
84
|
+
if (skipChars >= maxChars) return null
|
|
86
85
|
|
|
87
|
-
let remaining = maxChars
|
|
86
|
+
let remaining = maxChars - skipChars // 只处理新增部分
|
|
88
87
|
let charIndex = 0
|
|
89
88
|
|
|
90
89
|
// 计算 chunks 在文本中的范围
|
|
@@ -106,14 +105,23 @@ export function sliceAst(
|
|
|
106
105
|
|
|
107
106
|
// 文本类节点:截断 value,可能添加 chunks
|
|
108
107
|
if (n.value && typeof n.value === 'string') {
|
|
109
|
-
const
|
|
108
|
+
const nodeStart = charIndex
|
|
109
|
+
const nodeEnd = charIndex + n.value.length
|
|
110
|
+
|
|
111
|
+
// 如果整个节点都在 skipChars 之前,跳过
|
|
112
|
+
if (nodeEnd <= skipChars) {
|
|
113
|
+
charIndex = nodeEnd
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 如果节点跨越 skipChars,需要从 skipChars 位置开始取
|
|
118
|
+
const skipInNode = Math.max(0, skipChars - nodeStart)
|
|
119
|
+
const take = Math.min(n.value.length - skipInNode, remaining)
|
|
110
120
|
remaining -= take
|
|
111
121
|
if (take === 0) return null
|
|
112
122
|
|
|
113
|
-
const slicedValue = n.value.slice(
|
|
114
|
-
|
|
115
|
-
const nodeEnd = charIndex + take
|
|
116
|
-
charIndex += take
|
|
123
|
+
const slicedValue = n.value.slice(skipInNode, skipInNode + take)
|
|
124
|
+
charIndex = nodeEnd
|
|
117
125
|
|
|
118
126
|
const result: AstNode & { stableLength?: number; chunks?: TextChunk[] } = {
|
|
119
127
|
...n,
|
|
@@ -126,14 +134,14 @@ export function sliceAst(
|
|
|
126
134
|
let firstChunkLocalStart = take // 第一个 chunk 在节点中的起始位置
|
|
127
135
|
|
|
128
136
|
for (const range of chunkRanges) {
|
|
129
|
-
// 计算 chunk
|
|
130
|
-
const overlapStart = Math.max(range.start, nodeStart)
|
|
131
|
-
const overlapEnd = Math.min(range.end,
|
|
137
|
+
// 计算 chunk 与当前节点的交集(考虑 skipChars)
|
|
138
|
+
const overlapStart = Math.max(range.start, nodeStart + skipInNode)
|
|
139
|
+
const overlapEnd = Math.min(range.end, nodeStart + skipInNode + take)
|
|
132
140
|
|
|
133
141
|
if (overlapStart < overlapEnd) {
|
|
134
|
-
//
|
|
135
|
-
const localStart = overlapStart - nodeStart
|
|
136
|
-
const localEnd = overlapEnd - nodeStart
|
|
142
|
+
// 有交集,提取对应的文本(相对于 slicedValue)
|
|
143
|
+
const localStart = overlapStart - (nodeStart + skipInNode)
|
|
144
|
+
const localEnd = overlapEnd - (nodeStart + skipInNode)
|
|
137
145
|
const chunkText = slicedValue.slice(localStart, localEnd)
|
|
138
146
|
|
|
139
147
|
if (chunkText.length > 0) {
|
|
@@ -161,13 +169,36 @@ export function sliceAst(
|
|
|
161
169
|
// 容器节点:递归处理 children
|
|
162
170
|
if (n.children && Array.isArray(n.children)) {
|
|
163
171
|
const newChildren: AstNode[] = []
|
|
172
|
+
let childCharIndex = charIndex
|
|
173
|
+
|
|
164
174
|
for (const child of n.children) {
|
|
165
175
|
if (remaining <= 0) break
|
|
176
|
+
|
|
177
|
+
// 计算子节点的字符范围
|
|
178
|
+
const childChars = countCharsInNode(child as AstNode)
|
|
179
|
+
const childStart = childCharIndex
|
|
180
|
+
const childEnd = childCharIndex + childChars
|
|
181
|
+
|
|
182
|
+
// 如果子节点完全在 skipChars 之前,跳过
|
|
183
|
+
if (childEnd <= skipChars) {
|
|
184
|
+
childCharIndex = childEnd
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 如果子节点跨越 skipChars,需要处理
|
|
189
|
+
// 临时更新 charIndex 以便子节点正确处理 skipChars
|
|
190
|
+
const savedCharIndex = charIndex
|
|
191
|
+
charIndex = childStart
|
|
166
192
|
const processed = process(child)
|
|
193
|
+
charIndex = savedCharIndex
|
|
194
|
+
|
|
167
195
|
if (processed) {
|
|
168
196
|
newChildren.push(processed)
|
|
169
197
|
}
|
|
198
|
+
|
|
199
|
+
childCharIndex = childEnd
|
|
170
200
|
}
|
|
201
|
+
|
|
171
202
|
if (newChildren.length === 0) {
|
|
172
203
|
return null
|
|
173
204
|
}
|
|
@@ -183,9 +214,151 @@ export function sliceAst(
|
|
|
183
214
|
return process(node as AstNode) as RootContent | null
|
|
184
215
|
}
|
|
185
216
|
|
|
217
|
+
/**
|
|
218
|
+
* 增量追加:将新增的字符范围追加到现有的 displayNode
|
|
219
|
+
* 这是真正的增量追加实现,只处理新增部分,不重复遍历已稳定的节点
|
|
220
|
+
*
|
|
221
|
+
* @param baseNode 已截断的基础节点(稳定的部分)
|
|
222
|
+
* @param sourceNode 原始完整节点
|
|
223
|
+
* @param startChars 起始字符位置(已处理的字符数)
|
|
224
|
+
* @param endChars 结束字符位置(新的进度)
|
|
225
|
+
* @param accumulatedChunks 累积的 chunks 信息(用于渐入动画)
|
|
226
|
+
* @returns 追加后的完整节点
|
|
227
|
+
*/
|
|
228
|
+
export function appendToAst(
|
|
229
|
+
baseNode: RootContent,
|
|
230
|
+
sourceNode: RootContent,
|
|
231
|
+
startChars: number,
|
|
232
|
+
endChars: number,
|
|
233
|
+
accumulatedChunks?: AccumulatedChunks
|
|
234
|
+
): RootContent {
|
|
235
|
+
// 如果新增字符数为 0,直接返回 baseNode
|
|
236
|
+
if (endChars <= startChars) {
|
|
237
|
+
return baseNode
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 从 sourceNode 中提取新增的字符范围(跳过已处理的部分)
|
|
241
|
+
const newChars = endChars - startChars
|
|
242
|
+
const newPart = sliceAst(sourceNode, endChars, accumulatedChunks, startChars)
|
|
243
|
+
|
|
244
|
+
// 如果提取失败,返回 baseNode
|
|
245
|
+
if (!newPart) {
|
|
246
|
+
return baseNode
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 将新增部分合并到 baseNode
|
|
250
|
+
return mergeAstNodes(baseNode, newPart)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 合并两个 AST 节点
|
|
255
|
+
* 将 newPart 追加到 baseNode 的最后一个可追加节点中
|
|
256
|
+
*/
|
|
257
|
+
function mergeAstNodes(baseNode: RootContent, newPart: RootContent): RootContent {
|
|
258
|
+
// 如果两个节点类型不同,无法合并,返回 baseNode
|
|
259
|
+
if (baseNode.type !== newPart.type) {
|
|
260
|
+
return baseNode
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const base = baseNode as AstNode
|
|
264
|
+
const part = newPart as AstNode
|
|
265
|
+
|
|
266
|
+
// 如果是文本节点,合并文本和 chunks
|
|
267
|
+
if (base.value && typeof base.value === 'string' && part.value && typeof part.value === 'string') {
|
|
268
|
+
const baseChunks = (base as TextNodeWithChunks).chunks || []
|
|
269
|
+
const partChunks = (part as TextNodeWithChunks).chunks || []
|
|
270
|
+
|
|
271
|
+
// 合并所有 chunks:累积所有读取的 chunks
|
|
272
|
+
// chunks 数组包含每次读取的新文本片段,它们 join 到一起就是 value
|
|
273
|
+
const mergedChunks = [...baseChunks, ...partChunks]
|
|
274
|
+
|
|
275
|
+
// 根据设计:value = stableText + chunks[0].text + chunks[1].text + ... + chunks[n].text
|
|
276
|
+
// base.value = baseStableText + baseChunks[0].text + ... + baseChunks[n].text
|
|
277
|
+
// part.value = partStableText + partChunks[0].text + ... + partChunks[m].text
|
|
278
|
+
// 合并后:value = base.value + part.value(完整文本)
|
|
279
|
+
const mergedValue = base.value + part.value
|
|
280
|
+
|
|
281
|
+
// stableLength 是稳定部分的长度(不需要动画的部分)
|
|
282
|
+
// base 的稳定部分保持不变,base 的 chunks 和 part 的 chunks 都需要动画
|
|
283
|
+
const baseStableLength = (base as TextNodeWithChunks).stableLength ?? 0
|
|
284
|
+
|
|
285
|
+
// 验证:mergedValue 应该等于 baseStableText + 所有 chunks 的文本
|
|
286
|
+
// baseStableText = base.value.slice(0, baseStableLength)
|
|
287
|
+
// 所有 chunks 的文本 = baseChunks + partChunks 的文本
|
|
288
|
+
const result = {
|
|
289
|
+
...base,
|
|
290
|
+
value: mergedValue,
|
|
291
|
+
stableLength: mergedChunks.length > 0 ? baseStableLength : undefined,
|
|
292
|
+
chunks: mergedChunks.length > 0 ? mergedChunks : undefined
|
|
293
|
+
} as TextNodeWithChunks
|
|
294
|
+
|
|
295
|
+
return result as RootContent
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 如果是容器节点,合并 children
|
|
299
|
+
if (base.children && Array.isArray(base.children) && part.children && Array.isArray(part.children)) {
|
|
300
|
+
// 如果 base 的最后一个子节点和 part 的第一个子节点类型相同,尝试合并
|
|
301
|
+
if (base.children.length > 0 && part.children.length > 0) {
|
|
302
|
+
const lastBaseChild = base.children[base.children.length - 1]
|
|
303
|
+
const firstPartChild = part.children[0]
|
|
304
|
+
|
|
305
|
+
if (lastBaseChild.type === firstPartChild.type) {
|
|
306
|
+
// 尝试合并最后一个和第一个子节点
|
|
307
|
+
const merged = mergeAstNodes(lastBaseChild as RootContent, firstPartChild as RootContent)
|
|
308
|
+
return {
|
|
309
|
+
...base,
|
|
310
|
+
children: [
|
|
311
|
+
...base.children.slice(0, -1),
|
|
312
|
+
merged as AstNode,
|
|
313
|
+
...part.children.slice(1)
|
|
314
|
+
]
|
|
315
|
+
} as RootContent
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 否则直接追加所有子节点
|
|
320
|
+
return {
|
|
321
|
+
...base,
|
|
322
|
+
children: [...base.children, ...part.children]
|
|
323
|
+
} as RootContent
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 其他情况,返回 baseNode(无法合并)
|
|
327
|
+
return baseNode
|
|
328
|
+
}
|
|
329
|
+
|
|
186
330
|
/**
|
|
187
331
|
* 深拷贝 AST 节点
|
|
332
|
+
* 使用递归浅拷贝实现,比 JSON.parse/stringify 更高效
|
|
333
|
+
* 且保持对象结构完整性
|
|
188
334
|
*/
|
|
189
335
|
export function cloneNode<T extends RootContent>(node: T): T {
|
|
190
|
-
|
|
336
|
+
// 优先使用 structuredClone(Node 17+ / 现代浏览器)
|
|
337
|
+
if (typeof structuredClone === 'function') {
|
|
338
|
+
return structuredClone(node)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 回退到递归拷贝
|
|
342
|
+
return deepClone(node) as T
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 递归深拷贝对象
|
|
347
|
+
*/
|
|
348
|
+
function deepClone<T>(obj: T): T {
|
|
349
|
+
if (obj === null || typeof obj !== 'object') {
|
|
350
|
+
return obj
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (Array.isArray(obj)) {
|
|
354
|
+
return obj.map(item => deepClone(item)) as T
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const cloned = {} as T
|
|
358
|
+
for (const key in obj) {
|
|
359
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
360
|
+
cloned[key] = deepClone(obj[key])
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return cloned
|
|
191
364
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -10,6 +10,17 @@ export type BlockStatus =
|
|
|
10
10
|
| 'stable' // 可能完整,但下一个 chunk 可能会改变它
|
|
11
11
|
| 'completed' // 确认完成,不会再改变
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* AST 节点的通用接口(用于遍历)
|
|
15
|
+
* 统一定义,避免各模块重复声明
|
|
16
|
+
*/
|
|
17
|
+
export interface AstNode {
|
|
18
|
+
type: string
|
|
19
|
+
value?: string
|
|
20
|
+
children?: AstNode[]
|
|
21
|
+
[key: string]: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
/**
|
|
14
25
|
* 解析出的块
|
|
15
26
|
*/
|