@incremark/core 0.2.1 → 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,839 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 增量 Markdown 解析器
|
|
3
|
-
*
|
|
4
|
-
* 设计思路:
|
|
5
|
-
* 1. 维护一个文本缓冲区,接收流式输入
|
|
6
|
-
* 2. 识别"稳定边界"(如空行、标题等),将已完成的块标记为 completed
|
|
7
|
-
* 3. 对于正在接收的块,每次重新解析,但只解析该块的内容
|
|
8
|
-
* 4. 复杂嵌套节点(如列表、引用)作为整体处理,直到确认完成
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
12
|
-
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
|
13
|
-
import { gfm } from 'micromark-extension-gfm'
|
|
14
|
-
import { gfmFootnoteFromMarkdown } from 'mdast-util-gfm-footnote'
|
|
15
|
-
import type { Extension as MicromarkExtension } from 'micromark-util-types'
|
|
16
|
-
import type { Extension as MdastExtension } from 'mdast-util-from-markdown'
|
|
17
|
-
|
|
18
|
-
import type {
|
|
19
|
-
Root,
|
|
20
|
-
RootContent,
|
|
21
|
-
ParsedBlock,
|
|
22
|
-
IncrementalUpdate,
|
|
23
|
-
ParserOptions,
|
|
24
|
-
BlockStatus,
|
|
25
|
-
BlockContext,
|
|
26
|
-
ContainerConfig,
|
|
27
|
-
ParserState
|
|
28
|
-
} from '../types'
|
|
29
|
-
|
|
30
|
-
import { transformHtmlNodes, type HtmlTreeExtensionOptions } from '../extensions/html-extension'
|
|
31
|
-
import { micromarkReferenceExtension } from '../extensions/micromark-reference-extension'
|
|
32
|
-
import { gfmFootnoteIncremental } from '../extensions/micromark-gfm-footnote-incremental'
|
|
33
|
-
import type { HTML, Paragraph, Text, Parent as MdastParent, Definition, FootnoteDefinition } from 'mdast'
|
|
34
|
-
import type { DefinitionMap, FootnoteDefinitionMap } from '../types'
|
|
35
|
-
|
|
36
|
-
import {
|
|
37
|
-
createInitialContext,
|
|
38
|
-
updateContext,
|
|
39
|
-
isEmptyLine,
|
|
40
|
-
detectFenceStart,
|
|
41
|
-
isHeading,
|
|
42
|
-
isThematicBreak,
|
|
43
|
-
isBlockquoteStart,
|
|
44
|
-
isListItemStart,
|
|
45
|
-
detectContainer,
|
|
46
|
-
isFootnoteDefinitionStart,
|
|
47
|
-
isFootnoteContinuation
|
|
48
|
-
} from '../detector'
|
|
49
|
-
import { isDefinitionNode, isFootnoteDefinitionNode } from '../utils'
|
|
50
|
-
|
|
51
|
-
// ============ 解析器类 ============
|
|
52
|
-
|
|
53
|
-
export class IncremarkParser {
|
|
54
|
-
private buffer = ''
|
|
55
|
-
private lines: string[] = []
|
|
56
|
-
/** 行偏移量前缀和:lineOffsets[i] = 第i行起始位置的偏移量 */
|
|
57
|
-
private lineOffsets: number[] = [0]
|
|
58
|
-
private completedBlocks: ParsedBlock[] = []
|
|
59
|
-
private pendingStartLine = 0
|
|
60
|
-
private blockIdCounter = 0
|
|
61
|
-
private context: BlockContext
|
|
62
|
-
private options: ParserOptions
|
|
63
|
-
/** 缓存的容器配置,避免重复计算 */
|
|
64
|
-
private readonly containerConfig: ContainerConfig | undefined
|
|
65
|
-
/** 缓存的 HTML 树配置,避免重复计算 */
|
|
66
|
-
private readonly htmlTreeConfig: HtmlTreeExtensionOptions | undefined
|
|
67
|
-
/** 上次 append 返回的 pending blocks,用于 getAst 复用 */
|
|
68
|
-
private lastPendingBlocks: ParsedBlock[] = []
|
|
69
|
-
/** Definition 映射表(用于引用式图片和链接) */
|
|
70
|
-
private definitionMap: DefinitionMap = {}
|
|
71
|
-
/** Footnote Definition 映射表 */
|
|
72
|
-
private footnoteDefinitionMap: FootnoteDefinitionMap = {}
|
|
73
|
-
/** Footnote Reference 出现顺序(按引用在文档中的顺序) */
|
|
74
|
-
private footnoteReferenceOrder: string[] = []
|
|
75
|
-
|
|
76
|
-
constructor(options: ParserOptions = {}) {
|
|
77
|
-
this.options = {
|
|
78
|
-
gfm: true,
|
|
79
|
-
...options
|
|
80
|
-
}
|
|
81
|
-
this.context = createInitialContext()
|
|
82
|
-
// 初始化容器配置(构造时计算一次)
|
|
83
|
-
this.containerConfig = this.computeContainerConfig()
|
|
84
|
-
// 初始化 HTML 树配置
|
|
85
|
-
this.htmlTreeConfig = this.computeHtmlTreeConfig()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private generateBlockId(): string {
|
|
89
|
-
return `block-${++this.blockIdCounter}`
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private computeContainerConfig(): ContainerConfig | undefined {
|
|
93
|
-
const containers = this.options.containers
|
|
94
|
-
if (!containers) return undefined
|
|
95
|
-
return containers === true ? {} : containers
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private computeHtmlTreeConfig(): HtmlTreeExtensionOptions | undefined {
|
|
99
|
-
const htmlTree = this.options.htmlTree
|
|
100
|
-
if (!htmlTree) return undefined
|
|
101
|
-
return htmlTree === true ? {} : htmlTree
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 将 HTML 节点转换为纯文本
|
|
106
|
-
* 递归处理 AST 中所有 html 类型的节点
|
|
107
|
-
* - 块级 HTML 节点 → 转换为 paragraph 包含 text
|
|
108
|
-
* - 内联 HTML 节点(在段落内部)→ 转换为 text 节点
|
|
109
|
-
*/
|
|
110
|
-
private convertHtmlToText(ast: Root): Root {
|
|
111
|
-
// 处理内联节点(段落内部的 children)
|
|
112
|
-
const processInlineChildren = (children: unknown[]): unknown[] => {
|
|
113
|
-
return children.map(node => {
|
|
114
|
-
const n = node as RootContent
|
|
115
|
-
// 内联 html 节点转换为纯文本节点
|
|
116
|
-
if (n.type === 'html') {
|
|
117
|
-
const htmlNode = n as HTML
|
|
118
|
-
const textNode: Text = {
|
|
119
|
-
type: 'text',
|
|
120
|
-
value: htmlNode.value,
|
|
121
|
-
position: htmlNode.position
|
|
122
|
-
}
|
|
123
|
-
return textNode
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 递归处理有 children 的内联节点(如 strong, emphasis 等)
|
|
127
|
-
if ('children' in n && Array.isArray(n.children)) {
|
|
128
|
-
const parent = n as MdastParent
|
|
129
|
-
return {
|
|
130
|
-
...parent,
|
|
131
|
-
children: processInlineChildren(parent.children)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return n
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 处理块级节点
|
|
140
|
-
const processBlockChildren = (children: RootContent[]): RootContent[] => {
|
|
141
|
-
return children.map(node => {
|
|
142
|
-
// 块级 html 节点转换为段落包含纯文本
|
|
143
|
-
if (node.type === 'html') {
|
|
144
|
-
const htmlNode = node as HTML
|
|
145
|
-
const textNode: Text = {
|
|
146
|
-
type: 'text',
|
|
147
|
-
value: htmlNode.value
|
|
148
|
-
}
|
|
149
|
-
const paragraphNode: Paragraph = {
|
|
150
|
-
type: 'paragraph',
|
|
151
|
-
children: [textNode],
|
|
152
|
-
position: htmlNode.position
|
|
153
|
-
}
|
|
154
|
-
return paragraphNode as RootContent
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 递归处理有 children 的块级节点
|
|
158
|
-
if ('children' in node && Array.isArray(node.children)) {
|
|
159
|
-
const parent = node as MdastParent
|
|
160
|
-
// 对于段落等内联容器,使用 processInlineChildren
|
|
161
|
-
if (node.type === 'paragraph' || node.type === 'heading' ||
|
|
162
|
-
node.type === 'tableCell' || node.type === 'delete' ||
|
|
163
|
-
node.type === 'emphasis' || node.type === 'strong' ||
|
|
164
|
-
node.type === 'link' || node.type === 'linkReference') {
|
|
165
|
-
return {
|
|
166
|
-
...parent,
|
|
167
|
-
children: processInlineChildren(parent.children)
|
|
168
|
-
} as RootContent
|
|
169
|
-
}
|
|
170
|
-
// 对于其他块级容器,递归处理
|
|
171
|
-
return {
|
|
172
|
-
...parent,
|
|
173
|
-
children: processBlockChildren(parent.children as RootContent[])
|
|
174
|
-
} as RootContent
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return node
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
...ast,
|
|
183
|
-
children: processBlockChildren(ast.children)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private parse(text: string): Root {
|
|
188
|
-
const extensions: MicromarkExtension[] = []
|
|
189
|
-
const mdastExtensions: MdastExtension[] = []
|
|
190
|
-
|
|
191
|
-
// 先添加 GFM(包含原始的脚注扩展)
|
|
192
|
-
if (this.options.gfm) {
|
|
193
|
-
extensions.push(gfm())
|
|
194
|
-
mdastExtensions.push(...gfmFromMarkdown(), gfmFootnoteFromMarkdown())
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 如果用户传入了自定义扩展,添加它们
|
|
198
|
-
if (this.options.extensions) {
|
|
199
|
-
extensions.push(...this.options.extensions)
|
|
200
|
-
}
|
|
201
|
-
if (this.options.mdastExtensions) {
|
|
202
|
-
mdastExtensions.push(...this.options.mdastExtensions)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 添加增量脚注扩展,覆盖 GFM 脚注的定义检查
|
|
206
|
-
// ⚠️ 必须在 micromarkReferenceExtension 之前添加
|
|
207
|
-
// 因为 micromarkReferenceExtension 会拦截 `]`,并将 `[^1]` 交给脚注扩展处理
|
|
208
|
-
if (this.options.gfm) {
|
|
209
|
-
extensions.push(gfmFootnoteIncremental())
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// 添加 reference 扩展(支持增量解析),覆盖 commonmark 的 labelEnd
|
|
213
|
-
// ⚠️ 必须最后添加,确保它能拦截 `]` 并正确处理脚注
|
|
214
|
-
extensions.push(micromarkReferenceExtension())
|
|
215
|
-
|
|
216
|
-
// 生成 AST
|
|
217
|
-
let ast = fromMarkdown(text, { extensions, mdastExtensions })
|
|
218
|
-
|
|
219
|
-
// 如果启用了 HTML 树转换,应用转换
|
|
220
|
-
if (this.htmlTreeConfig) {
|
|
221
|
-
ast = transformHtmlNodes(ast, this.htmlTreeConfig)
|
|
222
|
-
} else {
|
|
223
|
-
// 如果未启用 HTML 树,将 HTML 节点转换为纯文本
|
|
224
|
-
ast = this.convertHtmlToText(ast)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return ast
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private updateDefinationsFromComplatedBlocks(blocks: ParsedBlock[]): void{
|
|
231
|
-
for (const block of blocks) {
|
|
232
|
-
this.definitionMap = {
|
|
233
|
-
...this.definitionMap,
|
|
234
|
-
...this.findDefinition(block)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
this.footnoteDefinitionMap = {
|
|
238
|
-
...this.footnoteDefinitionMap,
|
|
239
|
-
...this.findFootnoteDefinition(block)
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private findDefinition(block: ParsedBlock): DefinitionMap {
|
|
245
|
-
const definitions: Definition[] = [];
|
|
246
|
-
|
|
247
|
-
function findDefination(node: RootContent) {
|
|
248
|
-
if (isDefinitionNode(node)) {
|
|
249
|
-
definitions.push(node as Definition);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if ('children' in node && Array.isArray(node.children)) {
|
|
253
|
-
for (const child of node.children) {
|
|
254
|
-
findDefination(child as RootContent);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
findDefination(block.node);
|
|
260
|
-
|
|
261
|
-
return definitions.reduce<DefinitionMap>((acc, node) => {
|
|
262
|
-
acc[node.identifier] = node;
|
|
263
|
-
return acc;
|
|
264
|
-
}, {});
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private findFootnoteDefinition(block: ParsedBlock): FootnoteDefinitionMap {
|
|
269
|
-
const footnoteDefinitions: FootnoteDefinition[] = [];
|
|
270
|
-
|
|
271
|
-
function findFootnoteDefinition(node: RootContent) {
|
|
272
|
-
if (isFootnoteDefinitionNode(node)) {
|
|
273
|
-
footnoteDefinitions.push(node as FootnoteDefinition);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
findFootnoteDefinition(block.node);
|
|
278
|
-
|
|
279
|
-
return footnoteDefinitions.reduce<FootnoteDefinitionMap>((acc, node) => {
|
|
280
|
-
acc[node.identifier] = node;
|
|
281
|
-
return acc;
|
|
282
|
-
}, {});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* 收集 AST 中的脚注引用(按出现顺序)
|
|
287
|
-
* 用于确定脚注的显示顺序
|
|
288
|
-
*/
|
|
289
|
-
private collectFootnoteReferences(nodes: RootContent[]): void {
|
|
290
|
-
const visitNode = (node: any): void => {
|
|
291
|
-
if (!node) return
|
|
292
|
-
|
|
293
|
-
// 检查是否是脚注引用
|
|
294
|
-
if (node.type === 'footnoteReference') {
|
|
295
|
-
const identifier = node.identifier
|
|
296
|
-
// 去重:只记录第一次出现的位置
|
|
297
|
-
if (!this.footnoteReferenceOrder.includes(identifier)) {
|
|
298
|
-
this.footnoteReferenceOrder.push(identifier)
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// 递归遍历子节点
|
|
303
|
-
if (node.children && Array.isArray(node.children)) {
|
|
304
|
-
node.children.forEach(visitNode)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
nodes.forEach(visitNode)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* 增量更新 lines 和 lineOffsets
|
|
313
|
-
* 只处理新增的内容,避免全量 split
|
|
314
|
-
*/
|
|
315
|
-
private updateLines(): void {
|
|
316
|
-
const prevLineCount = this.lines.length
|
|
317
|
-
|
|
318
|
-
if (prevLineCount === 0) {
|
|
319
|
-
// 首次输入,直接 split
|
|
320
|
-
this.lines = this.buffer.split('\n')
|
|
321
|
-
this.lineOffsets = [0]
|
|
322
|
-
for (let i = 0; i < this.lines.length; i++) {
|
|
323
|
-
this.lineOffsets.push(this.lineOffsets[i] + this.lines[i].length + 1)
|
|
324
|
-
}
|
|
325
|
-
return
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// 找到最后一个不完整的行(可能被新 chunk 续上)
|
|
329
|
-
const lastLineStart = this.lineOffsets[prevLineCount - 1]
|
|
330
|
-
const textFromLastLine = this.buffer.slice(lastLineStart)
|
|
331
|
-
|
|
332
|
-
// 重新 split 最后一行及之后的内容
|
|
333
|
-
const newLines = textFromLastLine.split('\n')
|
|
334
|
-
|
|
335
|
-
// 替换最后一行并追加新行
|
|
336
|
-
this.lines.length = prevLineCount - 1
|
|
337
|
-
this.lineOffsets.length = prevLineCount
|
|
338
|
-
|
|
339
|
-
for (let i = 0; i < newLines.length; i++) {
|
|
340
|
-
this.lines.push(newLines[i])
|
|
341
|
-
const prevOffset = this.lineOffsets[this.lineOffsets.length - 1]
|
|
342
|
-
this.lineOffsets.push(prevOffset + newLines[i].length + 1)
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* O(1) 获取行偏移量
|
|
348
|
-
*/
|
|
349
|
-
private getLineOffset(lineIndex: number): number {
|
|
350
|
-
return this.lineOffsets[lineIndex] ?? 0
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* 查找稳定边界
|
|
355
|
-
* 返回稳定边界行号和该行对应的上下文(用于后续更新,避免重复计算)
|
|
356
|
-
*/
|
|
357
|
-
private findStableBoundary(): { line: number; contextAtLine: BlockContext } {
|
|
358
|
-
let stableLine = -1
|
|
359
|
-
let stableContext: BlockContext = this.context
|
|
360
|
-
let tempContext = { ...this.context }
|
|
361
|
-
|
|
362
|
-
for (let i = this.pendingStartLine; i < this.lines.length; i++) {
|
|
363
|
-
const line = this.lines[i]
|
|
364
|
-
const wasInFencedCode = tempContext.inFencedCode
|
|
365
|
-
const wasInContainer = tempContext.inContainer
|
|
366
|
-
const wasContainerDepth = tempContext.containerDepth
|
|
367
|
-
|
|
368
|
-
tempContext = updateContext(line, tempContext, this.containerConfig)
|
|
369
|
-
|
|
370
|
-
if (wasInFencedCode && !tempContext.inFencedCode) {
|
|
371
|
-
if (i < this.lines.length - 1) {
|
|
372
|
-
stableLine = i
|
|
373
|
-
stableContext = { ...tempContext }
|
|
374
|
-
}
|
|
375
|
-
continue
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (tempContext.inFencedCode) {
|
|
379
|
-
continue
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (wasInContainer && wasContainerDepth === 1 && !tempContext.inContainer) {
|
|
383
|
-
if (i < this.lines.length - 1) {
|
|
384
|
-
stableLine = i
|
|
385
|
-
stableContext = { ...tempContext }
|
|
386
|
-
}
|
|
387
|
-
continue
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (tempContext.inContainer) {
|
|
391
|
-
continue
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const stablePoint = this.checkStability(i)
|
|
395
|
-
if (stablePoint >= 0) {
|
|
396
|
-
stableLine = stablePoint
|
|
397
|
-
stableContext = { ...tempContext }
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return { line: stableLine, contextAtLine: stableContext }
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
private checkStability(lineIndex: number): number {
|
|
405
|
-
// 第一行永远不稳定
|
|
406
|
-
if (lineIndex === 0) {
|
|
407
|
-
return -1
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const line = this.lines[lineIndex]
|
|
411
|
-
const prevLine = this.lines[lineIndex - 1]
|
|
412
|
-
|
|
413
|
-
// 前一行是独立块(标题、分割线),该块已完成
|
|
414
|
-
if (isHeading(prevLine) || isThematicBreak(prevLine)) {
|
|
415
|
-
return lineIndex - 1
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// 最后一行不稳定(可能还有更多内容)
|
|
419
|
-
if (lineIndex >= this.lines.length - 1) {
|
|
420
|
-
return -1
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ============ 脚注定义的特殊处理 ============
|
|
424
|
-
|
|
425
|
-
// 情况 1: 前一行是脚注定义开始
|
|
426
|
-
if (isFootnoteDefinitionStart(prevLine)) {
|
|
427
|
-
// 当前行是空行或缩进行,脚注可能继续(不稳定)
|
|
428
|
-
if (isEmptyLine(line) || isFootnoteContinuation(line)) {
|
|
429
|
-
return -1
|
|
430
|
-
}
|
|
431
|
-
// 当前行是新脚注定义,前一个脚注完成
|
|
432
|
-
if (isFootnoteDefinitionStart(line)) {
|
|
433
|
-
return lineIndex - 1
|
|
434
|
-
}
|
|
435
|
-
// 当前行是非缩进的新块,前一个脚注完成
|
|
436
|
-
// 这种情况会在后续的判断中处理
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// 情况 2: 前一行是缩进行,可能是脚注延续
|
|
440
|
-
if (!isEmptyLine(prevLine) && isFootnoteContinuation(prevLine)) {
|
|
441
|
-
// 向上查找最近的脚注定义
|
|
442
|
-
const footnoteStartLine = this.findFootnoteStart(lineIndex - 1)
|
|
443
|
-
if (footnoteStartLine >= 0) {
|
|
444
|
-
// 确认属于脚注定义
|
|
445
|
-
// 当前行仍然是缩进或空行,脚注继续(不稳定)
|
|
446
|
-
if (isEmptyLine(line) || isFootnoteContinuation(line)) {
|
|
447
|
-
return -1
|
|
448
|
-
}
|
|
449
|
-
// 当前行是新脚注定义,前一个脚注完成
|
|
450
|
-
if (isFootnoteDefinitionStart(line)) {
|
|
451
|
-
return lineIndex - 1
|
|
452
|
-
}
|
|
453
|
-
// 当前行是非缩进的新块,前一个脚注完成
|
|
454
|
-
return lineIndex - 1
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// 前一行非空时,如果当前行是新块开始,则前一块已完成
|
|
459
|
-
if (!isEmptyLine(prevLine)) {
|
|
460
|
-
// 新脚注定义开始(排除连续脚注定义)
|
|
461
|
-
if (isFootnoteDefinitionStart(line) && !isFootnoteDefinitionStart(prevLine)) {
|
|
462
|
-
return lineIndex - 1
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// 新标题开始
|
|
466
|
-
if (isHeading(line)) {
|
|
467
|
-
return lineIndex - 1
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// 新代码块开始
|
|
471
|
-
if (detectFenceStart(line)) {
|
|
472
|
-
return lineIndex - 1
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// 新引用块开始(排除连续引用)
|
|
476
|
-
if (isBlockquoteStart(line) && !isBlockquoteStart(prevLine)) {
|
|
477
|
-
return lineIndex - 1
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// 新列表开始(排除连续列表项)
|
|
481
|
-
if (isListItemStart(line) && !isListItemStart(prevLine)) {
|
|
482
|
-
return lineIndex - 1
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// 新容器开始
|
|
486
|
-
if (this.containerConfig !== undefined) {
|
|
487
|
-
const container = detectContainer(line, this.containerConfig)
|
|
488
|
-
if (container && !container.isEnd) {
|
|
489
|
-
const prevContainer = detectContainer(prevLine, this.containerConfig)
|
|
490
|
-
if (!prevContainer || prevContainer.isEnd) {
|
|
491
|
-
return lineIndex - 1
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// 空行标志段落结束
|
|
498
|
-
if (isEmptyLine(line) && !isEmptyLine(prevLine)) {
|
|
499
|
-
return lineIndex
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
return -1
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* 从指定行向上查找脚注定义的起始行
|
|
507
|
-
*
|
|
508
|
-
* @param fromLine 开始查找的行索引
|
|
509
|
-
* @returns 脚注起始行索引,如果不属于脚注返回 -1
|
|
510
|
-
*
|
|
511
|
-
* @example
|
|
512
|
-
* // 假设 lines 为:
|
|
513
|
-
* // 0: "[^1]: 第一行"
|
|
514
|
-
* // 1: " 第二行"
|
|
515
|
-
* // 2: " 第三行"
|
|
516
|
-
* findFootnoteStart(2) // 返回 0
|
|
517
|
-
* findFootnoteStart(1) // 返回 0
|
|
518
|
-
*/
|
|
519
|
-
private findFootnoteStart(fromLine: number): number {
|
|
520
|
-
// 限制向上查找的最大行数,避免性能问题
|
|
521
|
-
const maxLookback = 20
|
|
522
|
-
const startLine = Math.max(0, fromLine - maxLookback)
|
|
523
|
-
|
|
524
|
-
for (let i = fromLine; i >= startLine; i--) {
|
|
525
|
-
const line = this.lines[i]
|
|
526
|
-
|
|
527
|
-
// 遇到脚注定义起始行
|
|
528
|
-
if (isFootnoteDefinitionStart(line)) {
|
|
529
|
-
return i
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// 遇到空行,继续向上查找(可能是脚注内部的段落分隔)
|
|
533
|
-
if (isEmptyLine(line)) {
|
|
534
|
-
continue
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// 遇到非缩进的普通行,说明不属于脚注
|
|
538
|
-
if (!isFootnoteContinuation(line)) {
|
|
539
|
-
return -1
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
return -1
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
private nodesToBlocks(
|
|
547
|
-
nodes: RootContent[],
|
|
548
|
-
startOffset: number,
|
|
549
|
-
rawText: string,
|
|
550
|
-
status: BlockStatus
|
|
551
|
-
): ParsedBlock[] {
|
|
552
|
-
const blocks: ParsedBlock[] = []
|
|
553
|
-
let currentOffset = startOffset
|
|
554
|
-
|
|
555
|
-
for (const node of nodes) {
|
|
556
|
-
const nodeStart = node.position?.start?.offset ?? currentOffset
|
|
557
|
-
const nodeEnd = node.position?.end?.offset ?? currentOffset + 1
|
|
558
|
-
const nodeText = rawText.substring(nodeStart - startOffset, nodeEnd - startOffset)
|
|
559
|
-
|
|
560
|
-
blocks.push({
|
|
561
|
-
id: this.generateBlockId(),
|
|
562
|
-
status,
|
|
563
|
-
node,
|
|
564
|
-
startOffset: nodeStart,
|
|
565
|
-
endOffset: nodeEnd,
|
|
566
|
-
rawText: nodeText
|
|
567
|
-
})
|
|
568
|
-
|
|
569
|
-
currentOffset = nodeEnd
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return blocks
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* 追加新的 chunk 并返回增量更新
|
|
577
|
-
*/
|
|
578
|
-
append(chunk: string): IncrementalUpdate {
|
|
579
|
-
this.buffer += chunk
|
|
580
|
-
this.updateLines()
|
|
581
|
-
|
|
582
|
-
const { line: stableBoundary, contextAtLine } = this.findStableBoundary()
|
|
583
|
-
|
|
584
|
-
const update: IncrementalUpdate = {
|
|
585
|
-
completed: [],
|
|
586
|
-
updated: [],
|
|
587
|
-
pending: [],
|
|
588
|
-
ast: { type: 'root', children: [] },
|
|
589
|
-
definitions: {},
|
|
590
|
-
footnoteDefinitions: {},
|
|
591
|
-
footnoteReferenceOrder: []
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (stableBoundary >= this.pendingStartLine && stableBoundary >= 0) {
|
|
595
|
-
const stableText = this.lines.slice(this.pendingStartLine, stableBoundary + 1).join('\n')
|
|
596
|
-
const stableOffset = this.getLineOffset(this.pendingStartLine)
|
|
597
|
-
|
|
598
|
-
const ast = this.parse(stableText)
|
|
599
|
-
const newBlocks = this.nodesToBlocks(ast.children, stableOffset, stableText, 'completed')
|
|
600
|
-
|
|
601
|
-
this.completedBlocks.push(...newBlocks)
|
|
602
|
-
update.completed = newBlocks
|
|
603
|
-
|
|
604
|
-
// 更新 definitions 从新完成的 blocks
|
|
605
|
-
this.updateDefinationsFromComplatedBlocks(newBlocks)
|
|
606
|
-
|
|
607
|
-
// 直接使用 findStableBoundary 计算好的上下文,避免重复遍历
|
|
608
|
-
this.context = contextAtLine
|
|
609
|
-
this.pendingStartLine = stableBoundary + 1
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (this.pendingStartLine < this.lines.length) {
|
|
613
|
-
const pendingText = this.lines.slice(this.pendingStartLine).join('\n')
|
|
614
|
-
|
|
615
|
-
if (pendingText.trim()) {
|
|
616
|
-
const pendingOffset = this.getLineOffset(this.pendingStartLine)
|
|
617
|
-
const ast = this.parse(pendingText)
|
|
618
|
-
|
|
619
|
-
update.pending = this.nodesToBlocks(ast.children, pendingOffset, pendingText, 'pending')
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// 缓存 pending blocks 供 getAst 使用
|
|
624
|
-
this.lastPendingBlocks = update.pending
|
|
625
|
-
|
|
626
|
-
update.ast = {
|
|
627
|
-
type: 'root',
|
|
628
|
-
children: [...this.completedBlocks.map((b) => b.node), ...update.pending.map((b) => b.node)]
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// 收集脚注引用顺序
|
|
632
|
-
this.collectFootnoteReferences(update.ast.children)
|
|
633
|
-
|
|
634
|
-
// 填充 definitions 和 footnote 相关数据
|
|
635
|
-
update.definitions = this.getDefinitionMap()
|
|
636
|
-
update.footnoteDefinitions = this.getFootnoteDefinitionMap()
|
|
637
|
-
update.footnoteReferenceOrder = this.getFootnoteReferenceOrder()
|
|
638
|
-
|
|
639
|
-
// 触发状态变化回调
|
|
640
|
-
this.emitChange(update.pending)
|
|
641
|
-
|
|
642
|
-
return update
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* 触发状态变化回调
|
|
647
|
-
*/
|
|
648
|
-
private emitChange(pendingBlocks: ParsedBlock[] = []): void {
|
|
649
|
-
if (this.options.onChange) {
|
|
650
|
-
const state: ParserState = {
|
|
651
|
-
completedBlocks: this.completedBlocks,
|
|
652
|
-
pendingBlocks,
|
|
653
|
-
markdown: this.buffer,
|
|
654
|
-
ast: {
|
|
655
|
-
type: 'root',
|
|
656
|
-
children: [
|
|
657
|
-
...this.completedBlocks.map((b) => b.node),
|
|
658
|
-
...pendingBlocks.map((b) => b.node)
|
|
659
|
-
]
|
|
660
|
-
},
|
|
661
|
-
definitions: { ...this.definitionMap },
|
|
662
|
-
footnoteDefinitions: { ...this.footnoteDefinitionMap }
|
|
663
|
-
}
|
|
664
|
-
this.options.onChange(state)
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* 标记解析完成,处理剩余内容
|
|
670
|
-
* 也可用于强制中断时(如用户点击停止),将 pending 内容标记为 completed
|
|
671
|
-
*/
|
|
672
|
-
finalize(): IncrementalUpdate {
|
|
673
|
-
const update: IncrementalUpdate = {
|
|
674
|
-
completed: [],
|
|
675
|
-
updated: [],
|
|
676
|
-
pending: [],
|
|
677
|
-
ast: { type: 'root', children: [] },
|
|
678
|
-
definitions: {},
|
|
679
|
-
footnoteDefinitions: {},
|
|
680
|
-
footnoteReferenceOrder: []
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
if (this.pendingStartLine < this.lines.length) {
|
|
684
|
-
const remainingText = this.lines.slice(this.pendingStartLine).join('\n')
|
|
685
|
-
|
|
686
|
-
if (remainingText.trim()) {
|
|
687
|
-
const remainingOffset = this.getLineOffset(this.pendingStartLine)
|
|
688
|
-
const ast = this.parse(remainingText)
|
|
689
|
-
|
|
690
|
-
const finalBlocks = this.nodesToBlocks(
|
|
691
|
-
ast.children,
|
|
692
|
-
remainingOffset,
|
|
693
|
-
remainingText,
|
|
694
|
-
'completed'
|
|
695
|
-
)
|
|
696
|
-
|
|
697
|
-
this.completedBlocks.push(...finalBlocks)
|
|
698
|
-
update.completed = finalBlocks
|
|
699
|
-
|
|
700
|
-
// 更新 definitions 从最终完成的 blocks
|
|
701
|
-
this.updateDefinationsFromComplatedBlocks(finalBlocks)
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// 清空 pending 缓存
|
|
706
|
-
this.lastPendingBlocks = []
|
|
707
|
-
this.pendingStartLine = this.lines.length
|
|
708
|
-
|
|
709
|
-
update.ast = {
|
|
710
|
-
type: 'root',
|
|
711
|
-
children: this.completedBlocks.map((b) => b.node)
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// 收集脚注引用顺序
|
|
715
|
-
this.collectFootnoteReferences(update.ast.children)
|
|
716
|
-
|
|
717
|
-
// 填充 definitions 和 footnote 相关数据
|
|
718
|
-
update.definitions = this.getDefinitionMap()
|
|
719
|
-
update.footnoteDefinitions = this.getFootnoteDefinitionMap()
|
|
720
|
-
update.footnoteReferenceOrder = this.getFootnoteReferenceOrder()
|
|
721
|
-
|
|
722
|
-
// 触发状态变化回调
|
|
723
|
-
this.emitChange([])
|
|
724
|
-
|
|
725
|
-
return update
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
* 强制中断解析,将所有待处理内容标记为完成
|
|
730
|
-
* 语义上等同于 finalize(),但名称更清晰
|
|
731
|
-
*/
|
|
732
|
-
abort(): IncrementalUpdate {
|
|
733
|
-
return this.finalize()
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* 获取当前完整的 AST
|
|
738
|
-
* 复用上次 append 的 pending 结果,避免重复解析
|
|
739
|
-
*/
|
|
740
|
-
getAst(): Root {
|
|
741
|
-
const children = [
|
|
742
|
-
...this.completedBlocks.map((b) => b.node),
|
|
743
|
-
...this.lastPendingBlocks.map((b) => b.node)
|
|
744
|
-
]
|
|
745
|
-
|
|
746
|
-
// 收集脚注引用顺序
|
|
747
|
-
this.collectFootnoteReferences(children)
|
|
748
|
-
|
|
749
|
-
return {
|
|
750
|
-
type: 'root',
|
|
751
|
-
children
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
/**
|
|
756
|
-
* 获取所有已完成的块
|
|
757
|
-
*/
|
|
758
|
-
getCompletedBlocks(): ParsedBlock[] {
|
|
759
|
-
return [...this.completedBlocks]
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* 获取当前缓冲区内容
|
|
764
|
-
*/
|
|
765
|
-
getBuffer(): string {
|
|
766
|
-
return this.buffer
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
/**
|
|
770
|
-
* 获取 Definition 映射表(用于引用式图片和链接)
|
|
771
|
-
*/
|
|
772
|
-
getDefinitionMap(): DefinitionMap {
|
|
773
|
-
return { ...this.definitionMap }
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* 获取 Footnote Definition 映射表
|
|
778
|
-
*/
|
|
779
|
-
getFootnoteDefinitionMap(): FootnoteDefinitionMap {
|
|
780
|
-
return { ...this.footnoteDefinitionMap }
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* 获取脚注引用的出现顺序
|
|
785
|
-
*/
|
|
786
|
-
getFootnoteReferenceOrder(): string[] {
|
|
787
|
-
return [...this.footnoteReferenceOrder]
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* 设置状态变化回调(用于 DevTools 等)
|
|
792
|
-
*/
|
|
793
|
-
setOnChange(callback: ((state: import('../types').ParserState) => void) | undefined): void {
|
|
794
|
-
const originalOnChange = this.options.onChange;
|
|
795
|
-
this.options.onChange = (state: ParserState) => {
|
|
796
|
-
originalOnChange?.(state);
|
|
797
|
-
callback?.(state);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* 重置解析器状态
|
|
803
|
-
*/
|
|
804
|
-
reset(): void {
|
|
805
|
-
this.buffer = ''
|
|
806
|
-
this.lines = []
|
|
807
|
-
this.lineOffsets = [0]
|
|
808
|
-
this.completedBlocks = []
|
|
809
|
-
this.pendingStartLine = 0
|
|
810
|
-
this.blockIdCounter = 0
|
|
811
|
-
this.context = createInitialContext()
|
|
812
|
-
this.lastPendingBlocks = []
|
|
813
|
-
// 清空 definition 映射
|
|
814
|
-
this.definitionMap = {}
|
|
815
|
-
this.footnoteDefinitionMap = {}
|
|
816
|
-
this.footnoteReferenceOrder = []
|
|
817
|
-
|
|
818
|
-
// 触发状态变化回调
|
|
819
|
-
this.emitChange([])
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* 一次性渲染完整 Markdown(reset + append + finalize)
|
|
824
|
-
* @param content 完整的 Markdown 内容
|
|
825
|
-
* @returns 解析结果
|
|
826
|
-
*/
|
|
827
|
-
render(content: string): IncrementalUpdate {
|
|
828
|
-
this.reset()
|
|
829
|
-
this.append(content)
|
|
830
|
-
return this.finalize()
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* 创建 Incremark 解析器实例
|
|
836
|
-
*/
|
|
837
|
-
export function createIncremarkParser(options?: ParserOptions): IncremarkParser {
|
|
838
|
-
return new IncremarkParser(options)
|
|
839
|
-
}
|