@incremark/core 0.1.2 → 0.2.0
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 +9 -1
- package/dist/detector/index.js.map +1 -1
- package/dist/index-3rgnFbip.d.ts +396 -0
- package/dist/index.d.ts +53 -2
- package/dist/index.js +1564 -35
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +6 -1
- package/dist/utils/index.js +7 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +7 -1
- package/src/__tests__/footnote.test.ts +214 -0
- package/src/detector/index.ts +30 -0
- package/src/extensions/html-extension/index.test.ts +409 -0
- package/src/extensions/html-extension/index.ts +792 -0
- package/src/extensions/micromark-gfm-footnote-incremental.ts +275 -0
- package/src/extensions/micromark-reference-extension.ts +724 -0
- package/src/index.ts +33 -0
- package/src/parser/IncremarkParser.footnote.test.ts +334 -0
- package/src/parser/IncremarkParser.ts +374 -14
- package/src/types/index.ts +29 -1
- package/src/utils/index.ts +9 -0
- package/dist/index-ChNeZ1wr.d.ts +0 -217
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
12
12
|
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
|
13
13
|
import { gfm } from 'micromark-extension-gfm'
|
|
14
|
+
import { gfmFootnoteFromMarkdown } from 'mdast-util-gfm-footnote'
|
|
14
15
|
import type { Extension as MicromarkExtension } from 'micromark-util-types'
|
|
15
16
|
import type { Extension as MdastExtension } from 'mdast-util-from-markdown'
|
|
16
17
|
|
|
@@ -22,9 +23,16 @@ import type {
|
|
|
22
23
|
ParserOptions,
|
|
23
24
|
BlockStatus,
|
|
24
25
|
BlockContext,
|
|
25
|
-
ContainerConfig
|
|
26
|
+
ContainerConfig,
|
|
27
|
+
ParserState
|
|
26
28
|
} from '../types'
|
|
27
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
|
+
|
|
28
36
|
import {
|
|
29
37
|
createInitialContext,
|
|
30
38
|
updateContext,
|
|
@@ -34,8 +42,11 @@ import {
|
|
|
34
42
|
isThematicBreak,
|
|
35
43
|
isBlockquoteStart,
|
|
36
44
|
isListItemStart,
|
|
37
|
-
detectContainer
|
|
45
|
+
detectContainer,
|
|
46
|
+
isFootnoteDefinitionStart,
|
|
47
|
+
isFootnoteContinuation
|
|
38
48
|
} from '../detector'
|
|
49
|
+
import { isDefinitionNode, isFootnoteDefinitionNode } from '../utils'
|
|
39
50
|
|
|
40
51
|
// ============ 解析器类 ============
|
|
41
52
|
|
|
@@ -51,8 +62,16 @@ export class IncremarkParser {
|
|
|
51
62
|
private options: ParserOptions
|
|
52
63
|
/** 缓存的容器配置,避免重复计算 */
|
|
53
64
|
private readonly containerConfig: ContainerConfig | undefined
|
|
65
|
+
/** 缓存的 HTML 树配置,避免重复计算 */
|
|
66
|
+
private readonly htmlTreeConfig: HtmlTreeExtensionOptions | undefined
|
|
54
67
|
/** 上次 append 返回的 pending blocks,用于 getAst 复用 */
|
|
55
68
|
private lastPendingBlocks: ParsedBlock[] = []
|
|
69
|
+
/** Definition 映射表(用于引用式图片和链接) */
|
|
70
|
+
private definitionMap: DefinitionMap = {}
|
|
71
|
+
/** Footnote Definition 映射表 */
|
|
72
|
+
private footnoteDefinitionMap: FootnoteDefinitionMap = {}
|
|
73
|
+
/** Footnote Reference 出现顺序(按引用在文档中的顺序) */
|
|
74
|
+
private footnoteReferenceOrder: string[] = []
|
|
56
75
|
|
|
57
76
|
constructor(options: ParserOptions = {}) {
|
|
58
77
|
this.options = {
|
|
@@ -62,6 +81,8 @@ export class IncremarkParser {
|
|
|
62
81
|
this.context = createInitialContext()
|
|
63
82
|
// 初始化容器配置(构造时计算一次)
|
|
64
83
|
this.containerConfig = this.computeContainerConfig()
|
|
84
|
+
// 初始化 HTML 树配置
|
|
85
|
+
this.htmlTreeConfig = this.computeHtmlTreeConfig()
|
|
65
86
|
}
|
|
66
87
|
|
|
67
88
|
private generateBlockId(): string {
|
|
@@ -74,13 +95,103 @@ export class IncremarkParser {
|
|
|
74
95
|
return containers === true ? {} : containers
|
|
75
96
|
}
|
|
76
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
|
+
|
|
77
187
|
private parse(text: string): Root {
|
|
78
188
|
const extensions: MicromarkExtension[] = []
|
|
79
189
|
const mdastExtensions: MdastExtension[] = []
|
|
80
190
|
|
|
191
|
+
// 先添加 GFM(包含原始的脚注扩展)
|
|
81
192
|
if (this.options.gfm) {
|
|
82
193
|
extensions.push(gfm())
|
|
83
|
-
mdastExtensions.push(...gfmFromMarkdown())
|
|
194
|
+
mdastExtensions.push(...gfmFromMarkdown(), gfmFootnoteFromMarkdown())
|
|
84
195
|
}
|
|
85
196
|
|
|
86
197
|
// 如果用户传入了自定义扩展,添加它们
|
|
@@ -91,7 +202,110 @@ export class IncremarkParser {
|
|
|
91
202
|
mdastExtensions.push(...this.options.mdastExtensions)
|
|
92
203
|
}
|
|
93
204
|
|
|
94
|
-
|
|
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)
|
|
95
309
|
}
|
|
96
310
|
|
|
97
311
|
/**
|
|
@@ -206,8 +420,48 @@ export class IncremarkParser {
|
|
|
206
420
|
return -1
|
|
207
421
|
}
|
|
208
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
|
+
|
|
209
458
|
// 前一行非空时,如果当前行是新块开始,则前一块已完成
|
|
210
459
|
if (!isEmptyLine(prevLine)) {
|
|
460
|
+
// 新脚注定义开始(排除连续脚注定义)
|
|
461
|
+
if (isFootnoteDefinitionStart(line) && !isFootnoteDefinitionStart(prevLine)) {
|
|
462
|
+
return lineIndex - 1
|
|
463
|
+
}
|
|
464
|
+
|
|
211
465
|
// 新标题开始
|
|
212
466
|
if (isHeading(line)) {
|
|
213
467
|
return lineIndex - 1
|
|
@@ -248,6 +502,47 @@ export class IncremarkParser {
|
|
|
248
502
|
return -1
|
|
249
503
|
}
|
|
250
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
|
+
|
|
251
546
|
private nodesToBlocks(
|
|
252
547
|
nodes: RootContent[],
|
|
253
548
|
startOffset: number,
|
|
@@ -290,7 +585,10 @@ export class IncremarkParser {
|
|
|
290
585
|
completed: [],
|
|
291
586
|
updated: [],
|
|
292
587
|
pending: [],
|
|
293
|
-
ast: { type: 'root', children: [] }
|
|
588
|
+
ast: { type: 'root', children: [] },
|
|
589
|
+
definitions: {},
|
|
590
|
+
footnoteDefinitions: {},
|
|
591
|
+
footnoteReferenceOrder: []
|
|
294
592
|
}
|
|
295
593
|
|
|
296
594
|
if (stableBoundary >= this.pendingStartLine && stableBoundary >= 0) {
|
|
@@ -303,6 +601,9 @@ export class IncremarkParser {
|
|
|
303
601
|
this.completedBlocks.push(...newBlocks)
|
|
304
602
|
update.completed = newBlocks
|
|
305
603
|
|
|
604
|
+
// 更新 definitions 从新完成的 blocks
|
|
605
|
+
this.updateDefinationsFromComplatedBlocks(newBlocks)
|
|
606
|
+
|
|
306
607
|
// 直接使用 findStableBoundary 计算好的上下文,避免重复遍历
|
|
307
608
|
this.context = contextAtLine
|
|
308
609
|
this.pendingStartLine = stableBoundary + 1
|
|
@@ -327,6 +628,14 @@ export class IncremarkParser {
|
|
|
327
628
|
children: [...this.completedBlocks.map((b) => b.node), ...update.pending.map((b) => b.node)]
|
|
328
629
|
}
|
|
329
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
|
+
|
|
330
639
|
// 触发状态变化回调
|
|
331
640
|
this.emitChange(update.pending)
|
|
332
641
|
|
|
@@ -338,7 +647,7 @@ export class IncremarkParser {
|
|
|
338
647
|
*/
|
|
339
648
|
private emitChange(pendingBlocks: ParsedBlock[] = []): void {
|
|
340
649
|
if (this.options.onChange) {
|
|
341
|
-
|
|
650
|
+
const state: ParserState = {
|
|
342
651
|
completedBlocks: this.completedBlocks,
|
|
343
652
|
pendingBlocks,
|
|
344
653
|
markdown: this.buffer,
|
|
@@ -348,8 +657,11 @@ export class IncremarkParser {
|
|
|
348
657
|
...this.completedBlocks.map((b) => b.node),
|
|
349
658
|
...pendingBlocks.map((b) => b.node)
|
|
350
659
|
]
|
|
351
|
-
}
|
|
352
|
-
|
|
660
|
+
},
|
|
661
|
+
definitions: { ...this.definitionMap },
|
|
662
|
+
footnoteDefinitions: { ...this.footnoteDefinitionMap }
|
|
663
|
+
}
|
|
664
|
+
this.options.onChange(state)
|
|
353
665
|
}
|
|
354
666
|
}
|
|
355
667
|
|
|
@@ -362,7 +674,10 @@ export class IncremarkParser {
|
|
|
362
674
|
completed: [],
|
|
363
675
|
updated: [],
|
|
364
676
|
pending: [],
|
|
365
|
-
ast: { type: 'root', children: [] }
|
|
677
|
+
ast: { type: 'root', children: [] },
|
|
678
|
+
definitions: {},
|
|
679
|
+
footnoteDefinitions: {},
|
|
680
|
+
footnoteReferenceOrder: []
|
|
366
681
|
}
|
|
367
682
|
|
|
368
683
|
if (this.pendingStartLine < this.lines.length) {
|
|
@@ -381,6 +696,9 @@ export class IncremarkParser {
|
|
|
381
696
|
|
|
382
697
|
this.completedBlocks.push(...finalBlocks)
|
|
383
698
|
update.completed = finalBlocks
|
|
699
|
+
|
|
700
|
+
// 更新 definitions 从最终完成的 blocks
|
|
701
|
+
this.updateDefinationsFromComplatedBlocks(finalBlocks)
|
|
384
702
|
}
|
|
385
703
|
}
|
|
386
704
|
|
|
@@ -393,6 +711,14 @@ export class IncremarkParser {
|
|
|
393
711
|
children: this.completedBlocks.map((b) => b.node)
|
|
394
712
|
}
|
|
395
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
|
+
|
|
396
722
|
// 触发状态变化回调
|
|
397
723
|
this.emitChange([])
|
|
398
724
|
|
|
@@ -412,12 +738,17 @@ export class IncremarkParser {
|
|
|
412
738
|
* 复用上次 append 的 pending 结果,避免重复解析
|
|
413
739
|
*/
|
|
414
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
|
+
|
|
415
749
|
return {
|
|
416
750
|
type: 'root',
|
|
417
|
-
children
|
|
418
|
-
...this.completedBlocks.map((b) => b.node),
|
|
419
|
-
...this.lastPendingBlocks.map((b) => b.node)
|
|
420
|
-
]
|
|
751
|
+
children
|
|
421
752
|
}
|
|
422
753
|
}
|
|
423
754
|
|
|
@@ -435,11 +766,36 @@ export class IncremarkParser {
|
|
|
435
766
|
return this.buffer
|
|
436
767
|
}
|
|
437
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
|
+
|
|
438
790
|
/**
|
|
439
791
|
* 设置状态变化回调(用于 DevTools 等)
|
|
440
792
|
*/
|
|
441
793
|
setOnChange(callback: ((state: import('../types').ParserState) => void) | undefined): void {
|
|
442
|
-
this.options.onChange
|
|
794
|
+
const originalOnChange = this.options.onChange;
|
|
795
|
+
this.options.onChange = (state: ParserState) => {
|
|
796
|
+
originalOnChange?.(state);
|
|
797
|
+
callback?.(state);
|
|
798
|
+
}
|
|
443
799
|
}
|
|
444
800
|
|
|
445
801
|
/**
|
|
@@ -454,6 +810,10 @@ export class IncremarkParser {
|
|
|
454
810
|
this.blockIdCounter = 0
|
|
455
811
|
this.context = createInitialContext()
|
|
456
812
|
this.lastPendingBlocks = []
|
|
813
|
+
// 清空 definition 映射
|
|
814
|
+
this.definitionMap = {}
|
|
815
|
+
this.footnoteDefinitionMap = {}
|
|
816
|
+
this.footnoteReferenceOrder = []
|
|
457
817
|
|
|
458
818
|
// 触发状态变化回调
|
|
459
819
|
this.emitChange([])
|
package/src/types/index.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { Root, RootContent } from 'mdast'
|
|
2
2
|
import type { Extension as MicromarkExtension } from 'micromark-util-types'
|
|
3
3
|
import type { Extension as MdastExtension } from 'mdast-util-from-markdown'
|
|
4
|
+
import type { HtmlTreeExtensionOptions } from '../extensions/html-extension'
|
|
5
|
+
import type { Definition, FootnoteDefinition } from 'mdast'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Definition 映射类型
|
|
9
|
+
*/
|
|
10
|
+
export interface DefinitionMap {
|
|
11
|
+
[identifier: string]: Definition
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FootnoteDefinitionMap {
|
|
15
|
+
[identifier: string]: FootnoteDefinition
|
|
16
|
+
}
|
|
4
17
|
|
|
5
18
|
/**
|
|
6
19
|
* 解析块的状态
|
|
@@ -51,6 +64,12 @@ export interface IncrementalUpdate {
|
|
|
51
64
|
pending: ParsedBlock[]
|
|
52
65
|
/** 完整的 AST(包含所有已解析的内容) */
|
|
53
66
|
ast: Root
|
|
67
|
+
/** Definition 映射表(用于引用式图片和链接) */
|
|
68
|
+
definitions: DefinitionMap
|
|
69
|
+
/** Footnote Definition 映射表 */
|
|
70
|
+
footnoteDefinitions: FootnoteDefinitionMap
|
|
71
|
+
/** 脚注引用的出现顺序(用于渲染时排序) */
|
|
72
|
+
footnoteReferenceOrder: string[]
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
/**
|
|
@@ -76,7 +95,9 @@ export interface ParserState {
|
|
|
76
95
|
/** 完整的 Markdown 内容 */
|
|
77
96
|
markdown: string
|
|
78
97
|
/** 完整的 AST */
|
|
79
|
-
ast: Root
|
|
98
|
+
ast: Root,
|
|
99
|
+
definitions: DefinitionMap,
|
|
100
|
+
footnoteDefinitions: FootnoteDefinitionMap
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
/**
|
|
@@ -92,6 +113,13 @@ export interface ParserOptions {
|
|
|
92
113
|
* - ContainerConfig: 使用自定义配置启用
|
|
93
114
|
*/
|
|
94
115
|
containers?: boolean | ContainerConfig
|
|
116
|
+
/**
|
|
117
|
+
* 启用 HTML 树转换
|
|
118
|
+
* - false/undefined: 禁用(默认),HTML 节点保持原始 type: 'html' 格式
|
|
119
|
+
* - true: 使用默认配置启用,将 HTML 节点转换为结构化的 htmlElement 节点
|
|
120
|
+
* - HtmlTreeExtensionOptions: 使用自定义配置启用(可配置黑名单等)
|
|
121
|
+
*/
|
|
122
|
+
htmlTree?: boolean | HtmlTreeExtensionOptions
|
|
95
123
|
/** 自定义块边界检测函数 */
|
|
96
124
|
blockBoundaryDetector?: (content: string, position: number) => boolean
|
|
97
125
|
/** 自定义 micromark 扩展(如 directive) */
|
package/src/utils/index.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* 工具函数
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { Definition, FootnoteDefinition, RootContent } from "mdast"
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* 生成唯一 ID
|
|
7
9
|
*/
|
|
@@ -42,3 +44,10 @@ export function joinLines(lines: string[], start: number, end: number): string {
|
|
|
42
44
|
return lines.slice(start, end + 1).join('\n')
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
export function isDefinitionNode(node: RootContent): node is Definition {
|
|
48
|
+
return node.type === 'definition'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isFootnoteDefinitionNode(node: RootContent): node is FootnoteDefinition {
|
|
52
|
+
return node.type === 'footnoteDefinition'
|
|
53
|
+
}
|