@incremark/core 0.1.1 → 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.
@@ -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
- return fromMarkdown(text, { extensions, mdastExtensions })
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
- this.options.onChange({
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 = callback
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([])
@@ -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) */
@@ -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
+ }