@incremark/core 0.0.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/LICENSE +22 -0
- package/README.md +99 -0
- package/dist/detector/index.d.ts +4 -0
- package/dist/detector/index.js +155 -0
- package/dist/detector/index.js.map +1 -0
- package/dist/index-i_qABRHQ.d.ts +207 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +515 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.d.ts +22 -0
- package/dist/utils/index.js +25 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +59 -0
- package/src/detector/index.test.ts +150 -0
- package/src/detector/index.ts +271 -0
- package/src/index.ts +51 -0
- package/src/parser/IncremarkParser.comprehensive.test.ts +418 -0
- package/src/parser/IncremarkParser.robustness.test.ts +428 -0
- package/src/parser/IncremarkParser.test.ts +110 -0
- package/src/parser/IncremarkParser.ts +476 -0
- package/src/parser/index.ts +2 -0
- package/src/types/index.ts +144 -0
- package/src/utils/index.ts +44 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全面的 Markdown 语法测试
|
|
3
|
+
* 覆盖所有常见的 Markdown 语法,确保增量解析器正确处理
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest'
|
|
7
|
+
import { IncremarkParser } from './IncremarkParser'
|
|
8
|
+
|
|
9
|
+
// 包含所有 Markdown 语法的完整文档
|
|
10
|
+
const COMPREHENSIVE_MARKDOWN = `# 一级标题
|
|
11
|
+
|
|
12
|
+
## 二级标题
|
|
13
|
+
|
|
14
|
+
### 三级标题
|
|
15
|
+
|
|
16
|
+
#### 四级标题
|
|
17
|
+
|
|
18
|
+
##### 五级标题
|
|
19
|
+
|
|
20
|
+
###### 六级标题
|
|
21
|
+
|
|
22
|
+
这是一个普通段落,包含**粗体**、*斜体*、***粗斜体***、~~删除线~~和\`行内代码\`。
|
|
23
|
+
|
|
24
|
+
这是第二个段落,包含[链接](https://example.com)和。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
分割线上方的段落。
|
|
29
|
+
|
|
30
|
+
***
|
|
31
|
+
|
|
32
|
+
另一种分割线。
|
|
33
|
+
|
|
34
|
+
___
|
|
35
|
+
|
|
36
|
+
第三种分割线。
|
|
37
|
+
|
|
38
|
+
> 这是一个引用块
|
|
39
|
+
> 包含多行内容
|
|
40
|
+
>
|
|
41
|
+
> 引用中的第二段
|
|
42
|
+
|
|
43
|
+
> 嵌套引用
|
|
44
|
+
>> 第二层引用
|
|
45
|
+
>>> 第三层引用
|
|
46
|
+
|
|
47
|
+
- 无序列表项 1
|
|
48
|
+
- 无序列表项 2
|
|
49
|
+
- 嵌套列表项 2.1
|
|
50
|
+
- 嵌套列表项 2.2
|
|
51
|
+
- 更深的嵌套
|
|
52
|
+
- 无序列表项 3
|
|
53
|
+
|
|
54
|
+
* 另一种无序列表
|
|
55
|
+
* 使用星号
|
|
56
|
+
|
|
57
|
+
+ 还有一种无序列表
|
|
58
|
+
+ 使用加号
|
|
59
|
+
|
|
60
|
+
1. 有序列表项 1
|
|
61
|
+
2. 有序列表项 2
|
|
62
|
+
1. 嵌套有序列表 2.1
|
|
63
|
+
2. 嵌套有序列表 2.2
|
|
64
|
+
3. 有序列表项 3
|
|
65
|
+
|
|
66
|
+
- [x] 已完成的任务
|
|
67
|
+
- [ ] 未完成的任务
|
|
68
|
+
- [x] 另一个已完成的任务
|
|
69
|
+
|
|
70
|
+
\`\`\`javascript
|
|
71
|
+
// JavaScript 代码块
|
|
72
|
+
function hello(name) {
|
|
73
|
+
console.log(\`Hello, \${name}!\`);
|
|
74
|
+
}
|
|
75
|
+
hello('World');
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
\`\`\`python
|
|
79
|
+
# Python 代码块
|
|
80
|
+
def hello(name):
|
|
81
|
+
print(f"Hello, {name}!")
|
|
82
|
+
|
|
83
|
+
hello("World")
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
\`\`\`
|
|
87
|
+
无语言标识的代码块
|
|
88
|
+
just plain text
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
~~~
|
|
92
|
+
使用波浪线的代码块
|
|
93
|
+
也是有效的
|
|
94
|
+
~~~
|
|
95
|
+
|
|
96
|
+
缩进代码块
|
|
97
|
+
四个空格开头
|
|
98
|
+
这也是代码
|
|
99
|
+
|
|
100
|
+
| 表头 1 | 表头 2 | 表头 3 |
|
|
101
|
+
|--------|:------:|-------:|
|
|
102
|
+
| 左对齐 | 居中 | 右对齐 |
|
|
103
|
+
| 单元格 | 单元格 | 单元格 |
|
|
104
|
+
| 更多内容 | **粗体** | *斜体* |
|
|
105
|
+
|
|
106
|
+
| 简单表格 | 第二列 |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| A | B |
|
|
109
|
+
| C | D |
|
|
110
|
+
|
|
111
|
+
这是一个包含脚注的段落[^1]。
|
|
112
|
+
|
|
113
|
+
[^1]: 这是脚注的内容。
|
|
114
|
+
|
|
115
|
+
术语
|
|
116
|
+
: 定义列表的定义内容
|
|
117
|
+
|
|
118
|
+
另一个术语
|
|
119
|
+
: 另一个定义
|
|
120
|
+
|
|
121
|
+
段落中的换行
|
|
122
|
+
使用两个空格后换行。
|
|
123
|
+
|
|
124
|
+
HTML 实体:© & < > "
|
|
125
|
+
|
|
126
|
+
自动链接:<https://example.com>
|
|
127
|
+
|
|
128
|
+
邮箱链接:<email@example.com>
|
|
129
|
+
|
|
130
|
+
转义字符:\\* \\# \\[ \\] \\( \\) \\! \\\` \\- \\_
|
|
131
|
+
|
|
132
|
+
行内 HTML:<span style="color: red;">红色文字</span>
|
|
133
|
+
|
|
134
|
+
<div>
|
|
135
|
+
块级 HTML 元素
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
段落后的结尾。
|
|
139
|
+
`
|
|
140
|
+
|
|
141
|
+
describe('全面的 Markdown 语法测试', () => {
|
|
142
|
+
it('一次性解析完整文档', () => {
|
|
143
|
+
const parser = new IncremarkParser()
|
|
144
|
+
parser.append(COMPREHENSIVE_MARKDOWN)
|
|
145
|
+
const result = parser.finalize()
|
|
146
|
+
|
|
147
|
+
// 验证 AST 存在
|
|
148
|
+
expect(result.ast).toBeDefined()
|
|
149
|
+
expect(result.ast.children.length).toBeGreaterThan(0)
|
|
150
|
+
|
|
151
|
+
// 统计节点类型
|
|
152
|
+
const nodeTypes = new Set<string>()
|
|
153
|
+
function collectTypes(node: any) {
|
|
154
|
+
nodeTypes.add(node.type)
|
|
155
|
+
if (node.children) {
|
|
156
|
+
node.children.forEach(collectTypes)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
result.ast.children.forEach(collectTypes)
|
|
160
|
+
|
|
161
|
+
// 验证关键节点类型都被解析
|
|
162
|
+
expect(nodeTypes.has('heading')).toBe(true)
|
|
163
|
+
expect(nodeTypes.has('paragraph')).toBe(true)
|
|
164
|
+
expect(nodeTypes.has('blockquote')).toBe(true)
|
|
165
|
+
expect(nodeTypes.has('list')).toBe(true)
|
|
166
|
+
expect(nodeTypes.has('code')).toBe(true)
|
|
167
|
+
expect(nodeTypes.has('table')).toBe(true)
|
|
168
|
+
expect(nodeTypes.has('thematicBreak')).toBe(true)
|
|
169
|
+
|
|
170
|
+
console.log('解析出的节点类型:', [...nodeTypes].sort())
|
|
171
|
+
console.log('顶级节点数量:', result.ast.children.length)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('逐行解析完整文档', () => {
|
|
175
|
+
const parser = new IncremarkParser()
|
|
176
|
+
const lines = COMPREHENSIVE_MARKDOWN.split('\n')
|
|
177
|
+
|
|
178
|
+
let totalCompleted = 0
|
|
179
|
+
for (let i = 0; i < lines.length; i++) {
|
|
180
|
+
const chunk = lines[i] + (i < lines.length - 1 ? '\n' : '')
|
|
181
|
+
const update = parser.append(chunk)
|
|
182
|
+
|
|
183
|
+
totalCompleted += update.completed.length
|
|
184
|
+
|
|
185
|
+
// 验证每次 append 都返回有效 AST
|
|
186
|
+
expect(update.ast).toBeDefined()
|
|
187
|
+
expect(update.ast.type).toBe('root')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = parser.finalize()
|
|
191
|
+
totalCompleted += result.completed.length
|
|
192
|
+
|
|
193
|
+
console.log('逐行解析 - 总完成块数:', totalCompleted)
|
|
194
|
+
console.log('逐行解析 - 最终节点数:', result.ast.children.length)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('随机分块解析完整文档', () => {
|
|
198
|
+
const parser = new IncremarkParser()
|
|
199
|
+
|
|
200
|
+
// 使用随机长度的分块(5-50 字符)
|
|
201
|
+
let remaining = COMPREHENSIVE_MARKDOWN
|
|
202
|
+
let chunkCount = 0
|
|
203
|
+
|
|
204
|
+
while (remaining.length > 0) {
|
|
205
|
+
const chunkSize = Math.min(
|
|
206
|
+
Math.floor(Math.random() * 45) + 5,
|
|
207
|
+
remaining.length
|
|
208
|
+
)
|
|
209
|
+
const chunk = remaining.slice(0, chunkSize)
|
|
210
|
+
remaining = remaining.slice(chunkSize)
|
|
211
|
+
|
|
212
|
+
const update = parser.append(chunk)
|
|
213
|
+
chunkCount++
|
|
214
|
+
|
|
215
|
+
// 验证 AST 始终有效
|
|
216
|
+
expect(update.ast).toBeDefined()
|
|
217
|
+
expect(update.ast.type).toBe('root')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result = parser.finalize()
|
|
221
|
+
|
|
222
|
+
console.log('随机分块 - 总块数:', chunkCount)
|
|
223
|
+
console.log('随机分块 - 最终节点数:', result.ast.children.length)
|
|
224
|
+
|
|
225
|
+
// 对比一次性解析的结果
|
|
226
|
+
const onePassParser = new IncremarkParser()
|
|
227
|
+
onePassParser.append(COMPREHENSIVE_MARKDOWN)
|
|
228
|
+
const onePassResult = onePassParser.finalize()
|
|
229
|
+
|
|
230
|
+
// 节点数量应该相同
|
|
231
|
+
expect(result.ast.children.length).toBe(onePassResult.ast.children.length)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('逐字符解析完整文档', () => {
|
|
235
|
+
const parser = new IncremarkParser()
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < COMPREHENSIVE_MARKDOWN.length; i++) {
|
|
238
|
+
const update = parser.append(COMPREHENSIVE_MARKDOWN[i])
|
|
239
|
+
|
|
240
|
+
// 验证 AST 始终有效
|
|
241
|
+
expect(update.ast).toBeDefined()
|
|
242
|
+
expect(update.ast.type).toBe('root')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const result = parser.finalize()
|
|
246
|
+
|
|
247
|
+
console.log('逐字符解析 - 最终节点数:', result.ast.children.length)
|
|
248
|
+
|
|
249
|
+
// 对比一次性解析的结果
|
|
250
|
+
const onePassParser = new IncremarkParser()
|
|
251
|
+
onePassParser.append(COMPREHENSIVE_MARKDOWN)
|
|
252
|
+
const onePassResult = onePassParser.finalize()
|
|
253
|
+
|
|
254
|
+
// 节点数量应该相同
|
|
255
|
+
expect(result.ast.children.length).toBe(onePassResult.ast.children.length)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('验证所有标题级别', () => {
|
|
259
|
+
const parser = new IncremarkParser()
|
|
260
|
+
parser.append(COMPREHENSIVE_MARKDOWN)
|
|
261
|
+
const result = parser.finalize()
|
|
262
|
+
|
|
263
|
+
const headings = result.ast.children.filter((n: any) => n.type === 'heading')
|
|
264
|
+
const depths = headings.map((h: any) => h.depth)
|
|
265
|
+
|
|
266
|
+
expect(depths).toContain(1)
|
|
267
|
+
expect(depths).toContain(2)
|
|
268
|
+
expect(depths).toContain(3)
|
|
269
|
+
expect(depths).toContain(4)
|
|
270
|
+
expect(depths).toContain(5)
|
|
271
|
+
expect(depths).toContain(6)
|
|
272
|
+
|
|
273
|
+
console.log('标题级别:', depths)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('验证代码块语言', () => {
|
|
277
|
+
const parser = new IncremarkParser()
|
|
278
|
+
parser.append(COMPREHENSIVE_MARKDOWN)
|
|
279
|
+
const result = parser.finalize()
|
|
280
|
+
|
|
281
|
+
const codeBlocks = result.ast.children.filter((n: any) => n.type === 'code')
|
|
282
|
+
const languages = codeBlocks.map((c: any) => c.lang || '(none)')
|
|
283
|
+
|
|
284
|
+
expect(languages).toContain('javascript')
|
|
285
|
+
expect(languages).toContain('python')
|
|
286
|
+
|
|
287
|
+
console.log('代码块语言:', languages)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('验证表格结构', () => {
|
|
291
|
+
const parser = new IncremarkParser()
|
|
292
|
+
parser.append(COMPREHENSIVE_MARKDOWN)
|
|
293
|
+
const result = parser.finalize()
|
|
294
|
+
|
|
295
|
+
const tables = result.ast.children.filter((n: any) => n.type === 'table')
|
|
296
|
+
|
|
297
|
+
expect(tables.length).toBeGreaterThanOrEqual(2)
|
|
298
|
+
|
|
299
|
+
for (const table of tables) {
|
|
300
|
+
expect((table as any).children.length).toBeGreaterThan(0)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log('表格数量:', tables.length)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('验证列表类型', () => {
|
|
307
|
+
const parser = new IncremarkParser()
|
|
308
|
+
parser.append(COMPREHENSIVE_MARKDOWN)
|
|
309
|
+
const result = parser.finalize()
|
|
310
|
+
|
|
311
|
+
const lists = result.ast.children.filter((n: any) => n.type === 'list')
|
|
312
|
+
|
|
313
|
+
const orderedCount = lists.filter((l: any) => l.ordered).length
|
|
314
|
+
const unorderedCount = lists.filter((l: any) => !l.ordered).length
|
|
315
|
+
|
|
316
|
+
expect(orderedCount).toBeGreaterThan(0)
|
|
317
|
+
expect(unorderedCount).toBeGreaterThan(0)
|
|
318
|
+
|
|
319
|
+
console.log('有序列表:', orderedCount, '无序列表:', unorderedCount)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('验证分割线', () => {
|
|
323
|
+
const parser = new IncremarkParser()
|
|
324
|
+
parser.append(COMPREHENSIVE_MARKDOWN)
|
|
325
|
+
const result = parser.finalize()
|
|
326
|
+
|
|
327
|
+
const thematicBreaks = result.ast.children.filter(
|
|
328
|
+
(n: any) => n.type === 'thematicBreak'
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
expect(thematicBreaks.length).toBe(3)
|
|
332
|
+
|
|
333
|
+
console.log('分割线数量:', thematicBreaks.length)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('验证 abort 中断功能', () => {
|
|
337
|
+
const parser = new IncremarkParser()
|
|
338
|
+
|
|
339
|
+
// 只解析一部分
|
|
340
|
+
const partial = COMPREHENSIVE_MARKDOWN.slice(0, 500)
|
|
341
|
+
parser.append(partial)
|
|
342
|
+
|
|
343
|
+
// 中断解析
|
|
344
|
+
const result = parser.abort()
|
|
345
|
+
|
|
346
|
+
expect(result.ast).toBeDefined()
|
|
347
|
+
expect(result.ast.children.length).toBeGreaterThan(0)
|
|
348
|
+
expect(result.pending.length).toBe(0) // abort 后没有 pending
|
|
349
|
+
|
|
350
|
+
console.log('中断后节点数:', result.ast.children.length)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('验证 reset 重置功能', () => {
|
|
354
|
+
const parser = new IncremarkParser()
|
|
355
|
+
|
|
356
|
+
parser.append('# Hello\n\nWorld')
|
|
357
|
+
parser.finalize()
|
|
358
|
+
|
|
359
|
+
// 重置
|
|
360
|
+
parser.reset()
|
|
361
|
+
|
|
362
|
+
// 重新解析
|
|
363
|
+
parser.append('# New Content\n\nNew paragraph')
|
|
364
|
+
const result = parser.finalize()
|
|
365
|
+
|
|
366
|
+
expect(result.ast.children.length).toBe(2)
|
|
367
|
+
|
|
368
|
+
const heading = result.ast.children[0] as any
|
|
369
|
+
expect(heading.type).toBe('heading')
|
|
370
|
+
expect(heading.children[0].value).toBe('New Content')
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('验证 GFM 扩展功能', () => {
|
|
374
|
+
const parser = new IncremarkParser({ gfm: true })
|
|
375
|
+
|
|
376
|
+
const gfmContent = `
|
|
377
|
+
| A | B |
|
|
378
|
+
|---|---|
|
|
379
|
+
| 1 | 2 |
|
|
380
|
+
|
|
381
|
+
- [x] Task 1
|
|
382
|
+
- [ ] Task 2
|
|
383
|
+
|
|
384
|
+
~~strikethrough~~
|
|
385
|
+
|
|
386
|
+
https://auto.link
|
|
387
|
+
`
|
|
388
|
+
|
|
389
|
+
parser.append(gfmContent)
|
|
390
|
+
const result = parser.finalize()
|
|
391
|
+
|
|
392
|
+
// 应该有表格
|
|
393
|
+
const tables = result.ast.children.filter((n: any) => n.type === 'table')
|
|
394
|
+
expect(tables.length).toBe(1)
|
|
395
|
+
|
|
396
|
+
// 应该有任务列表
|
|
397
|
+
const lists = result.ast.children.filter((n: any) => n.type === 'list')
|
|
398
|
+
expect(lists.length).toBeGreaterThan(0)
|
|
399
|
+
|
|
400
|
+
console.log('GFM 功能验证通过')
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('验证禁用 GFM', () => {
|
|
404
|
+
const parser = new IncremarkParser({ gfm: false })
|
|
405
|
+
|
|
406
|
+
const content = `| A | B |
|
|
407
|
+
|---|---|
|
|
408
|
+
| 1 | 2 |`
|
|
409
|
+
|
|
410
|
+
parser.append(content)
|
|
411
|
+
const result = parser.finalize()
|
|
412
|
+
|
|
413
|
+
// 没有 GFM,表格语法不会被解析为 table 节点
|
|
414
|
+
const tables = result.ast.children.filter((n: any) => n.type === 'table')
|
|
415
|
+
expect(tables.length).toBe(0)
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
|