@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.
@@ -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
- }