@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
package/src/transformer/utils.ts
CHANGED
|
@@ -1,85 +1,364 @@
|
|
|
1
|
-
import type { RootContent } from 'mdast'
|
|
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
|
+
}
|
|
2
23
|
|
|
3
24
|
/**
|
|
4
25
|
* 计算 AST 节点的总字符数
|
|
5
26
|
*/
|
|
6
27
|
export function countChars(node: RootContent): number {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
function traverse(n: any): void {
|
|
10
|
-
// 文本类节点
|
|
11
|
-
if (n.value && typeof n.value === 'string') {
|
|
12
|
-
count += n.value.length
|
|
13
|
-
return
|
|
14
|
-
}
|
|
28
|
+
return countCharsInNode(node as AstNode)
|
|
29
|
+
}
|
|
15
30
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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)
|
|
21
42
|
}
|
|
43
|
+
return count
|
|
22
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
|
+
}
|
|
23
58
|
|
|
24
|
-
|
|
25
|
-
|
|
59
|
+
/** chunk 范围信息 */
|
|
60
|
+
interface ChunkRange {
|
|
61
|
+
start: number
|
|
62
|
+
end: number
|
|
63
|
+
chunk: TextChunk
|
|
26
64
|
}
|
|
27
65
|
|
|
28
66
|
/**
|
|
29
67
|
* 截断 AST 节点,只保留前 maxChars 个字符
|
|
68
|
+
* 支持 chunks(用于渐入动画)
|
|
69
|
+
* 支持增量模式:跳过已处理的字符,只处理新增部分
|
|
30
70
|
*
|
|
31
71
|
* @param node 原始节点
|
|
32
72
|
* @param maxChars 最大字符数
|
|
73
|
+
* @param accumulatedChunks 累积的 chunks 信息(用于渐入动画)
|
|
74
|
+
* @param skipChars 跳过前 N 个字符(已处理的部分,用于增量追加)
|
|
33
75
|
* @returns 截断后的节点,如果 maxChars <= 0 返回 null
|
|
34
76
|
*/
|
|
35
|
-
export function sliceAst(
|
|
77
|
+
export function sliceAst(
|
|
78
|
+
node: RootContent,
|
|
79
|
+
maxChars: number,
|
|
80
|
+
accumulatedChunks?: AccumulatedChunks,
|
|
81
|
+
skipChars: number = 0
|
|
82
|
+
): RootContent | null {
|
|
36
83
|
if (maxChars <= 0) return null
|
|
84
|
+
if (skipChars >= maxChars) return null
|
|
37
85
|
|
|
38
|
-
let remaining = maxChars
|
|
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
|
+
}
|
|
39
102
|
|
|
40
|
-
function process(n:
|
|
103
|
+
function process(n: AstNode): AstNode | null {
|
|
41
104
|
if (remaining <= 0) return null
|
|
42
105
|
|
|
43
|
-
// 文本类节点:截断 value
|
|
106
|
+
// 文本类节点:截断 value,可能添加 chunks
|
|
44
107
|
if (n.value && typeof n.value === 'string') {
|
|
45
|
-
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)
|
|
46
120
|
remaining -= take
|
|
47
121
|
if (take === 0) return null
|
|
48
|
-
|
|
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
|
|
49
167
|
}
|
|
50
168
|
|
|
51
169
|
// 容器节点:递归处理 children
|
|
52
170
|
if (n.children && Array.isArray(n.children)) {
|
|
53
|
-
const newChildren:
|
|
171
|
+
const newChildren: AstNode[] = []
|
|
172
|
+
let childCharIndex = charIndex
|
|
173
|
+
|
|
54
174
|
for (const child of n.children) {
|
|
55
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
|
|
56
192
|
const processed = process(child)
|
|
193
|
+
charIndex = savedCharIndex
|
|
194
|
+
|
|
57
195
|
if (processed) {
|
|
58
196
|
newChildren.push(processed)
|
|
59
197
|
}
|
|
198
|
+
|
|
199
|
+
childCharIndex = childEnd
|
|
60
200
|
}
|
|
61
|
-
|
|
201
|
+
|
|
62
202
|
if (newChildren.length === 0) {
|
|
63
|
-
// 对于某些容器节点,即使没有内容也应该保留结构
|
|
64
|
-
// 例如 list 节点如果没有 children 就不应该渲染
|
|
65
203
|
return null
|
|
66
204
|
}
|
|
67
205
|
return { ...n, children: newChildren }
|
|
68
206
|
}
|
|
69
207
|
|
|
70
|
-
// 其他节点(如 thematicBreak, image
|
|
71
|
-
// 算作 1 个字符的消耗
|
|
208
|
+
// 其他节点(如 thematicBreak, image)
|
|
72
209
|
remaining -= 1
|
|
210
|
+
charIndex += 1
|
|
73
211
|
return { ...n }
|
|
74
212
|
}
|
|
75
213
|
|
|
76
|
-
return process(node)
|
|
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
|
|
77
328
|
}
|
|
78
329
|
|
|
79
330
|
/**
|
|
80
331
|
* 深拷贝 AST 节点
|
|
332
|
+
* 使用递归浅拷贝实现,比 JSON.parse/stringify 更高效
|
|
333
|
+
* 且保持对象结构完整性
|
|
81
334
|
*/
|
|
82
335
|
export function cloneNode<T extends RootContent>(node: T): T {
|
|
83
|
-
|
|
336
|
+
// 优先使用 structuredClone(Node 17+ / 现代浏览器)
|
|
337
|
+
if (typeof structuredClone === 'function') {
|
|
338
|
+
return structuredClone(node)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 回退到递归拷贝
|
|
342
|
+
return deepClone(node) as T
|
|
84
343
|
}
|
|
85
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
|
+
}
|
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
|
*/
|