@incremark/core 0.2.2 → 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
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { RootContent, Code } from 'mdast'
|
|
2
|
-
import type { TransformerPlugin } from './types'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 代码块插件:整体出现,不逐字符显示
|
|
6
|
-
*
|
|
7
|
-
* 注意:默认不启用,代码块默认参与打字机效果
|
|
8
|
-
* 如需整体显示代码块,可手动添加此插件
|
|
9
|
-
*/
|
|
10
|
-
export const codeBlockPlugin: TransformerPlugin = {
|
|
11
|
-
name: 'code-block',
|
|
12
|
-
match: (node: RootContent) => node.type === 'code',
|
|
13
|
-
countChars: () => 1, // 算作 1 个字符,整体出现
|
|
14
|
-
sliceNode: (node, displayedChars, totalChars) => {
|
|
15
|
-
// 要么全部显示,要么不显示
|
|
16
|
-
return displayedChars >= totalChars ? node : null
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Mermaid 图表插件:整体出现
|
|
22
|
-
*
|
|
23
|
-
* 注意:默认不启用,mermaid 默认参与打字机效果
|
|
24
|
-
* 如需整体显示 mermaid,可手动添加此插件
|
|
25
|
-
*/
|
|
26
|
-
export const mermaidPlugin: TransformerPlugin = {
|
|
27
|
-
name: 'mermaid',
|
|
28
|
-
match: (node: RootContent) => {
|
|
29
|
-
if (node.type !== 'code') return false
|
|
30
|
-
const codeNode = node as Code
|
|
31
|
-
return codeNode.lang === 'mermaid'
|
|
32
|
-
},
|
|
33
|
-
countChars: () => 1,
|
|
34
|
-
sliceNode: (node, displayedChars) => (displayedChars > 0 ? node : null)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 图片插件:立即显示(不参与打字机效果)
|
|
39
|
-
* 图片没有文本内容,应立即显示
|
|
40
|
-
*/
|
|
41
|
-
export const imagePlugin: TransformerPlugin = {
|
|
42
|
-
name: 'image',
|
|
43
|
-
match: (node: RootContent) => node.type === 'image',
|
|
44
|
-
countChars: () => 0 // 0 字符,立即显示
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 数学公式插件:整体出现
|
|
49
|
-
*
|
|
50
|
-
* 注意:默认不启用,数学公式默认参与打字机效果
|
|
51
|
-
* 如需整体显示公式,可手动添加此插件
|
|
52
|
-
*/
|
|
53
|
-
export const mathPlugin: TransformerPlugin = {
|
|
54
|
-
name: 'math',
|
|
55
|
-
match: (node: RootContent) => {
|
|
56
|
-
const type = node.type as string
|
|
57
|
-
return type === 'math' || type === 'inlineMath'
|
|
58
|
-
},
|
|
59
|
-
countChars: () => 1,
|
|
60
|
-
sliceNode: (node, displayedChars) => (displayedChars > 0 ? node : null)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 分割线插件:立即显示
|
|
65
|
-
* 分隔线没有文本内容,应立即显示
|
|
66
|
-
*/
|
|
67
|
-
export const thematicBreakPlugin: TransformerPlugin = {
|
|
68
|
-
name: 'thematic-break',
|
|
69
|
-
match: (node: RootContent) => node.type === 'thematicBreak',
|
|
70
|
-
countChars: () => 0
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 默认插件集合
|
|
75
|
-
*
|
|
76
|
-
* 只包含确实需要特殊处理的节点:
|
|
77
|
-
* - 图片:无文本内容,立即显示
|
|
78
|
-
* - 分隔线:无文本内容,立即显示
|
|
79
|
-
*
|
|
80
|
-
* 代码块、mermaid、数学公式默认参与打字机效果
|
|
81
|
-
* 如需整体显示,可手动添加对应插件
|
|
82
|
-
*/
|
|
83
|
-
export const defaultPlugins: TransformerPlugin[] = [
|
|
84
|
-
imagePlugin,
|
|
85
|
-
thematicBreakPlugin
|
|
86
|
-
]
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 完整插件集合(所有特殊节点整体显示)
|
|
90
|
-
* 包含代码块、mermaid、数学公式等的整体显示
|
|
91
|
-
*/
|
|
92
|
-
export const allPlugins: TransformerPlugin[] = [
|
|
93
|
-
mermaidPlugin, // mermaid 优先于普通 code block
|
|
94
|
-
codeBlockPlugin,
|
|
95
|
-
imagePlugin,
|
|
96
|
-
mathPlugin,
|
|
97
|
-
thematicBreakPlugin
|
|
98
|
-
]
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* 创建自定义插件的辅助函数
|
|
102
|
-
*/
|
|
103
|
-
export function createPlugin(
|
|
104
|
-
name: string,
|
|
105
|
-
matcher: (node: RootContent) => boolean,
|
|
106
|
-
options: Partial<Omit<TransformerPlugin, 'name' | 'match'>> = {}
|
|
107
|
-
): TransformerPlugin {
|
|
108
|
-
return {
|
|
109
|
-
name,
|
|
110
|
-
match: matcher,
|
|
111
|
-
...options
|
|
112
|
-
}
|
|
113
|
-
}
|
package/src/transformer/types.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import type { RootContent } from 'mdast'
|
|
2
|
-
import type { BlockStatus } from '../types'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 源 Block 类型(来自解析器)
|
|
6
|
-
*/
|
|
7
|
-
export interface SourceBlock<T = unknown> {
|
|
8
|
-
/** 唯一标识 */
|
|
9
|
-
id: string
|
|
10
|
-
/** AST 节点 */
|
|
11
|
-
node: RootContent
|
|
12
|
-
/** 块状态 */
|
|
13
|
-
status: BlockStatus
|
|
14
|
-
/** 用户自定义元数据 */
|
|
15
|
-
meta?: T
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 显示用的 Block(转换后)
|
|
20
|
-
*/
|
|
21
|
-
export interface DisplayBlock<T = unknown> extends SourceBlock<T> {
|
|
22
|
-
/** 用于显示的 AST 节点(可能是截断的) */
|
|
23
|
-
displayNode: RootContent
|
|
24
|
-
/** 显示进度 0-1 */
|
|
25
|
-
progress: number
|
|
26
|
-
/** 是否已完成显示 */
|
|
27
|
-
isDisplayComplete: boolean
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 动画效果类型
|
|
32
|
-
* - 'none': 无动画效果
|
|
33
|
-
* - 'fade-in': 新增字符渐入效果
|
|
34
|
-
* - 'typing': 打字机光标效果
|
|
35
|
-
*/
|
|
36
|
-
export type AnimationEffect = 'none' | 'fade-in' | 'typing'
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Transformer 插件
|
|
40
|
-
*/
|
|
41
|
-
export interface TransformerPlugin {
|
|
42
|
-
/** 插件名称 */
|
|
43
|
-
name: string
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 判断是否处理此节点
|
|
47
|
-
* 返回 true 表示这个插件要处理此节点
|
|
48
|
-
*/
|
|
49
|
-
match?: (node: RootContent) => boolean
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 自定义字符数计算
|
|
53
|
-
* 返回 undefined 则使用默认逻辑
|
|
54
|
-
* 返回 0 表示立即显示(不参与逐字符效果)
|
|
55
|
-
*/
|
|
56
|
-
countChars?: (node: RootContent) => number | undefined
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 自定义截断逻辑
|
|
60
|
-
* @param node 原始节点
|
|
61
|
-
* @param displayedChars 当前应显示的字符数
|
|
62
|
-
* @param totalChars 该节点的总字符数
|
|
63
|
-
* @returns 截断后的节点,null 表示不显示
|
|
64
|
-
*/
|
|
65
|
-
sliceNode?: (
|
|
66
|
-
node: RootContent,
|
|
67
|
-
displayedChars: number,
|
|
68
|
-
totalChars: number
|
|
69
|
-
) => RootContent | null
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 节点显示完成时的回调
|
|
73
|
-
*/
|
|
74
|
-
onComplete?: (node: RootContent) => void
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Transformer 配置选项
|
|
79
|
-
*/
|
|
80
|
-
export interface TransformerOptions {
|
|
81
|
-
/**
|
|
82
|
-
* 每 tick 增加的字符数
|
|
83
|
-
* - number: 固定步长(默认 1)
|
|
84
|
-
* - [min, max]: 随机步长区间(更自然的打字效果)
|
|
85
|
-
*/
|
|
86
|
-
charsPerTick?: number | [number, number]
|
|
87
|
-
/** tick 间隔 (ms),默认 20 */
|
|
88
|
-
tickInterval?: number
|
|
89
|
-
/** 动画效果,默认 'none' */
|
|
90
|
-
effect?: AnimationEffect
|
|
91
|
-
/** 插件列表 */
|
|
92
|
-
plugins?: TransformerPlugin[]
|
|
93
|
-
/** 状态变化回调 */
|
|
94
|
-
onChange?: (displayBlocks: DisplayBlock[]) => void
|
|
95
|
-
/**
|
|
96
|
-
* 是否在页面不可见时自动暂停
|
|
97
|
-
* 默认 true,节省资源
|
|
98
|
-
*/
|
|
99
|
-
pauseOnHidden?: boolean
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Transformer 内部状态
|
|
104
|
-
*/
|
|
105
|
-
export interface TransformerState<T = unknown> {
|
|
106
|
-
/** 已完成显示的 blocks */
|
|
107
|
-
completedBlocks: SourceBlock<T>[]
|
|
108
|
-
/** 当前正在显示的 block */
|
|
109
|
-
currentBlock: SourceBlock<T> | null
|
|
110
|
-
/** 当前 block 已显示的字符数 */
|
|
111
|
-
currentProgress: number
|
|
112
|
-
/** 等待显示的 blocks */
|
|
113
|
-
pendingBlocks: SourceBlock<T>[]
|
|
114
|
-
}
|
|
115
|
-
|
package/src/transformer/utils.ts
DELETED
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
import type { RootContent, Text } from 'mdast'
|
|
2
|
-
import type { AstNode } from '../types'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 文本块片段(用于渐入动画)
|
|
6
|
-
*/
|
|
7
|
-
export interface TextChunk {
|
|
8
|
-
/** 文本内容 */
|
|
9
|
-
text: string
|
|
10
|
-
/** 创建时间戳 */
|
|
11
|
-
createdAt: number
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 扩展的文本节点(支持 chunks)
|
|
16
|
-
*/
|
|
17
|
-
export interface TextNodeWithChunks extends Text {
|
|
18
|
-
/** 稳定部分的长度(不需要动画) */
|
|
19
|
-
stableLength?: number
|
|
20
|
-
/** 临时的文本片段,用于渐入动画 */
|
|
21
|
-
chunks?: TextChunk[]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 计算 AST 节点的总字符数
|
|
26
|
-
*/
|
|
27
|
-
export function countChars(node: RootContent): number {
|
|
28
|
-
return countCharsInNode(node as AstNode)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 计算单个 AST 节点的字符数(内部辅助函数)
|
|
33
|
-
*/
|
|
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)
|
|
42
|
-
}
|
|
43
|
-
return count
|
|
44
|
-
}
|
|
45
|
-
// 其他节点(如 thematicBreak, image)算作 1 个字符
|
|
46
|
-
return 1
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 累积的 chunks 信息
|
|
51
|
-
*/
|
|
52
|
-
export interface AccumulatedChunks {
|
|
53
|
-
/** 已经稳定显示的字符数(不需要动画) */
|
|
54
|
-
stableChars: number
|
|
55
|
-
/** 累积的 chunk 列表 */
|
|
56
|
-
chunks: TextChunk[]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** chunk 范围信息 */
|
|
60
|
-
interface ChunkRange {
|
|
61
|
-
start: number
|
|
62
|
-
end: number
|
|
63
|
-
chunk: TextChunk
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 截断 AST 节点,只保留前 maxChars 个字符
|
|
68
|
-
* 支持 chunks(用于渐入动画)
|
|
69
|
-
* 支持增量模式:跳过已处理的字符,只处理新增部分
|
|
70
|
-
*
|
|
71
|
-
* @param node 原始节点
|
|
72
|
-
* @param maxChars 最大字符数
|
|
73
|
-
* @param accumulatedChunks 累积的 chunks 信息(用于渐入动画)
|
|
74
|
-
* @param skipChars 跳过前 N 个字符(已处理的部分,用于增量追加)
|
|
75
|
-
* @returns 截断后的节点,如果 maxChars <= 0 返回 null
|
|
76
|
-
*/
|
|
77
|
-
export function sliceAst(
|
|
78
|
-
node: RootContent,
|
|
79
|
-
maxChars: number,
|
|
80
|
-
accumulatedChunks?: AccumulatedChunks,
|
|
81
|
-
skipChars: number = 0
|
|
82
|
-
): RootContent | null {
|
|
83
|
-
if (maxChars <= 0) return null
|
|
84
|
-
if (skipChars >= maxChars) return null
|
|
85
|
-
|
|
86
|
-
let remaining = maxChars - skipChars // 只处理新增部分
|
|
87
|
-
let charIndex = 0
|
|
88
|
-
|
|
89
|
-
// 计算 chunks 在文本中的范围
|
|
90
|
-
const chunkRanges: ChunkRange[] = []
|
|
91
|
-
if (accumulatedChunks && accumulatedChunks.chunks.length > 0) {
|
|
92
|
-
let chunkStart = accumulatedChunks.stableChars
|
|
93
|
-
for (const chunk of accumulatedChunks.chunks) {
|
|
94
|
-
chunkRanges.push({
|
|
95
|
-
start: chunkStart,
|
|
96
|
-
end: chunkStart + chunk.text.length,
|
|
97
|
-
chunk
|
|
98
|
-
})
|
|
99
|
-
chunkStart += chunk.text.length
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function process(n: AstNode): AstNode | null {
|
|
104
|
-
if (remaining <= 0) return null
|
|
105
|
-
|
|
106
|
-
// 文本类节点:截断 value,可能添加 chunks
|
|
107
|
-
if (n.value && typeof n.value === 'string') {
|
|
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)
|
|
120
|
-
remaining -= take
|
|
121
|
-
if (take === 0) return null
|
|
122
|
-
|
|
123
|
-
const slicedValue = n.value.slice(skipInNode, skipInNode + take)
|
|
124
|
-
charIndex = nodeEnd
|
|
125
|
-
|
|
126
|
-
const result: AstNode & { stableLength?: number; chunks?: TextChunk[] } = {
|
|
127
|
-
...n,
|
|
128
|
-
value: slicedValue
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 检查是否有 chunks 落在这个节点范围内
|
|
132
|
-
if (chunkRanges.length > 0 && accumulatedChunks) {
|
|
133
|
-
const nodeChunks: TextChunk[] = []
|
|
134
|
-
let firstChunkLocalStart = take // 第一个 chunk 在节点中的起始位置
|
|
135
|
-
|
|
136
|
-
for (const range of chunkRanges) {
|
|
137
|
-
// 计算 chunk 与当前节点的交集(考虑 skipChars)
|
|
138
|
-
const overlapStart = Math.max(range.start, nodeStart + skipInNode)
|
|
139
|
-
const overlapEnd = Math.min(range.end, nodeStart + skipInNode + take)
|
|
140
|
-
|
|
141
|
-
if (overlapStart < overlapEnd) {
|
|
142
|
-
// 有交集,提取对应的文本(相对于 slicedValue)
|
|
143
|
-
const localStart = overlapStart - (nodeStart + skipInNode)
|
|
144
|
-
const localEnd = overlapEnd - (nodeStart + skipInNode)
|
|
145
|
-
const chunkText = slicedValue.slice(localStart, localEnd)
|
|
146
|
-
|
|
147
|
-
if (chunkText.length > 0) {
|
|
148
|
-
// 记录第一个 chunk 的起始位置
|
|
149
|
-
if (nodeChunks.length === 0) {
|
|
150
|
-
firstChunkLocalStart = localStart
|
|
151
|
-
}
|
|
152
|
-
nodeChunks.push({
|
|
153
|
-
text: chunkText,
|
|
154
|
-
createdAt: range.chunk.createdAt
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (nodeChunks.length > 0) {
|
|
161
|
-
result.stableLength = firstChunkLocalStart
|
|
162
|
-
result.chunks = nodeChunks
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return result
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 容器节点:递归处理 children
|
|
170
|
-
if (n.children && Array.isArray(n.children)) {
|
|
171
|
-
const newChildren: AstNode[] = []
|
|
172
|
-
let childCharIndex = charIndex
|
|
173
|
-
|
|
174
|
-
for (const child of n.children) {
|
|
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
|
|
192
|
-
const processed = process(child)
|
|
193
|
-
charIndex = savedCharIndex
|
|
194
|
-
|
|
195
|
-
if (processed) {
|
|
196
|
-
newChildren.push(processed)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
childCharIndex = childEnd
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (newChildren.length === 0) {
|
|
203
|
-
return null
|
|
204
|
-
}
|
|
205
|
-
return { ...n, children: newChildren }
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// 其他节点(如 thematicBreak, image)
|
|
209
|
-
remaining -= 1
|
|
210
|
-
charIndex += 1
|
|
211
|
-
return { ...n }
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return process(node as AstNode) as RootContent | null
|
|
215
|
-
}
|
|
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
|
-
|
|
330
|
-
/**
|
|
331
|
-
* 深拷贝 AST 节点
|
|
332
|
-
* 使用递归浅拷贝实现,比 JSON.parse/stringify 更高效
|
|
333
|
-
* 且保持对象结构完整性
|
|
334
|
-
*/
|
|
335
|
-
export function cloneNode<T extends RootContent>(node: T): T {
|
|
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
|
|
364
|
-
}
|