@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.
@@ -1,6 +1,9 @@
1
+ import { RootContent, Definition, FootnoteDefinition } from 'mdast';
2
+
1
3
  /**
2
4
  * 工具函数
3
5
  */
6
+
4
7
  declare function generateId(prefix?: string): string;
5
8
  /**
6
9
  * 重置 ID 计数器(用于测试)
@@ -18,5 +21,7 @@ declare function splitLines(text: string): string[];
18
21
  * 合并行为文本
19
22
  */
20
23
  declare function joinLines(lines: string[], start: number, end: number): string;
24
+ declare function isDefinitionNode(node: RootContent): node is Definition;
25
+ declare function isFootnoteDefinitionNode(node: RootContent): node is FootnoteDefinition;
21
26
 
22
- export { calculateLineOffset, generateId, joinLines, resetIdCounter, splitLines };
27
+ export { calculateLineOffset, generateId, isDefinitionNode, isFootnoteDefinitionNode, joinLines, resetIdCounter, splitLines };
@@ -19,7 +19,13 @@ function splitLines(text) {
19
19
  function joinLines(lines, start, end) {
20
20
  return lines.slice(start, end + 1).join("\n");
21
21
  }
22
+ function isDefinitionNode(node) {
23
+ return node.type === "definition";
24
+ }
25
+ function isFootnoteDefinitionNode(node) {
26
+ return node.type === "footnoteDefinition";
27
+ }
22
28
 
23
- export { calculateLineOffset, generateId, joinLines, resetIdCounter, splitLines };
29
+ export { calculateLineOffset, generateId, isDefinitionNode, isFootnoteDefinitionNode, joinLines, resetIdCounter, splitLines };
24
30
  //# sourceMappingURL=index.js.map
25
31
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/index.ts"],"names":[],"mappings":";AAOA,IAAI,SAAA,GAAY,CAAA;AACT,SAAS,UAAA,CAAW,SAAS,OAAA,EAAiB;AACnD,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,EAAE,SAAS,CAAA,CAAA;AACjC;AAKO,SAAS,cAAA,GAAuB;AACrC,EAAA,SAAA,GAAY,CAAA;AACd;AAKO,SAAS,mBAAA,CAAoB,OAAiB,SAAA,EAA2B;AAC9E,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,aAAa,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACtD,IAAA,MAAA,IAAU,KAAA,CAAM,CAAC,CAAA,CAAE,MAAA,GAAS,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,MAAA;AACT;AAKO,SAAS,WAAW,IAAA,EAAwB;AACjD,EAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AACxB;AAKO,SAAS,SAAA,CAAU,KAAA,EAAiB,KAAA,EAAe,GAAA,EAAqB;AAC7E,EAAA,OAAO,MAAM,KAAA,CAAM,KAAA,EAAO,MAAM,CAAC,CAAA,CAAE,KAAK,IAAI,CAAA;AAC9C","file":"index.js","sourcesContent":["/**\n * 工具函数\n */\n\n/**\n * 生成唯一 ID\n */\nlet idCounter = 0\nexport function generateId(prefix = 'block'): string {\n return `${prefix}-${++idCounter}`\n}\n\n/**\n * 重置 ID 计数器(用于测试)\n */\nexport function resetIdCounter(): void {\n idCounter = 0\n}\n\n/**\n * 计算行的偏移量\n */\nexport function calculateLineOffset(lines: string[], lineIndex: number): number {\n let offset = 0\n for (let i = 0; i < lineIndex && i < lines.length; i++) {\n offset += lines[i].length + 1 // +1 for newline\n }\n return offset\n}\n\n/**\n * 将文本按行分割\n */\nexport function splitLines(text: string): string[] {\n return text.split('\\n')\n}\n\n/**\n * 合并行为文本\n */\nexport function joinLines(lines: string[], start: number, end: number): string {\n return lines.slice(start, end + 1).join('\\n')\n}\n\n"]}
1
+ {"version":3,"sources":["../../src/utils/index.ts"],"names":[],"mappings":";AASA,IAAI,SAAA,GAAY,CAAA;AACT,SAAS,UAAA,CAAW,SAAS,OAAA,EAAiB;AACnD,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,EAAE,SAAS,CAAA,CAAA;AACjC;AAKO,SAAS,cAAA,GAAuB;AACrC,EAAA,SAAA,GAAY,CAAA;AACd;AAKO,SAAS,mBAAA,CAAoB,OAAiB,SAAA,EAA2B;AAC9E,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,aAAa,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACtD,IAAA,MAAA,IAAU,KAAA,CAAM,CAAC,CAAA,CAAE,MAAA,GAAS,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,MAAA;AACT;AAKO,SAAS,WAAW,IAAA,EAAwB;AACjD,EAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AACxB;AAKO,SAAS,SAAA,CAAU,KAAA,EAAiB,KAAA,EAAe,GAAA,EAAqB;AAC7E,EAAA,OAAO,MAAM,KAAA,CAAM,KAAA,EAAO,MAAM,CAAC,CAAA,CAAE,KAAK,IAAI,CAAA;AAC9C;AAEO,SAAS,iBAAiB,IAAA,EAAuC;AACtE,EAAA,OAAO,KAAK,IAAA,KAAS,YAAA;AACvB;AAEO,SAAS,yBAAyB,IAAA,EAA+C;AACtF,EAAA,OAAO,KAAK,IAAA,KAAS,oBAAA;AACvB","file":"index.js","sourcesContent":["/**\n * 工具函数\n */\n\nimport type { Definition, FootnoteDefinition, RootContent } from \"mdast\"\n\n/**\n * 生成唯一 ID\n */\nlet idCounter = 0\nexport function generateId(prefix = 'block'): string {\n return `${prefix}-${++idCounter}`\n}\n\n/**\n * 重置 ID 计数器(用于测试)\n */\nexport function resetIdCounter(): void {\n idCounter = 0\n}\n\n/**\n * 计算行的偏移量\n */\nexport function calculateLineOffset(lines: string[], lineIndex: number): number {\n let offset = 0\n for (let i = 0; i < lineIndex && i < lines.length; i++) {\n offset += lines[i].length + 1 // +1 for newline\n }\n return offset\n}\n\n/**\n * 将文本按行分割\n */\nexport function splitLines(text: string): string[] {\n return text.split('\\n')\n}\n\n/**\n * 合并行为文本\n */\nexport function joinLines(lines: string[], start: number, end: number): string {\n return lines.slice(start, end + 1).join('\\n')\n}\n\nexport function isDefinitionNode(node: RootContent): node is Definition {\n return node.type === 'definition'\n}\n\nexport function isFootnoteDefinitionNode(node: RootContent): node is FootnoteDefinition {\n return node.type === 'footnoteDefinition'\n}"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@incremark/core",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "增量式 Markdown 解析器核心库",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,6 +30,12 @@
30
30
  "mdast-util-from-markdown": "^2.0.0",
31
31
  "mdast-util-gfm": "^3.0.0",
32
32
  "micromark-extension-gfm": "^3.0.0",
33
+ "micromark-factory-destination": "^2.0.0",
34
+ "micromark-factory-label": "^2.0.0",
35
+ "micromark-factory-title": "^2.0.0",
36
+ "micromark-factory-whitespace": "^2.0.0",
37
+ "micromark-util-character": "^2.0.0",
38
+ "micromark-util-symbol": "^2.0.0",
33
39
  "micromark-util-types": "^2.0.0"
34
40
  },
35
41
  "devDependencies": {
@@ -0,0 +1,214 @@
1
+ /**
2
+ * 脚注解析测试
3
+ *
4
+ * 测试增量解析场景下的脚注引用和定义
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest'
8
+ import { createIncremarkParser } from '../parser/IncremarkParser'
9
+
10
+ describe('Footnote Parsing', () => {
11
+ describe('Basic Footnote', () => {
12
+ it('should parse footnote reference before definition', () => {
13
+ const markdown = `这是一个简单的脚注[^1]。
14
+
15
+ [^1]: 这是第一个脚注的内容。`
16
+
17
+ const parser = createIncremarkParser({ gfm: true })
18
+ const result = parser.render(markdown)
19
+ const ast = result.ast
20
+
21
+ // 检查是否有脚注引用
22
+ const paragraph = ast.children[0]
23
+ expect(paragraph.type).toBe('paragraph')
24
+
25
+ const hasFootnoteRef = paragraph.children?.some(
26
+ (node: any) => node.type === 'footnoteReference' && node.identifier === '1'
27
+ )
28
+ expect(hasFootnoteRef).toBe(true)
29
+
30
+ // 检查是否有脚注定义
31
+ const hasFootnoteDef = ast.children.some(
32
+ (node: any) => node.type === 'footnoteDefinition' && node.identifier === '1'
33
+ )
34
+ expect(hasFootnoteDef).toBe(true)
35
+ })
36
+
37
+ it('should parse multiple footnotes', () => {
38
+ const markdown = `第一个脚注[^1],第二个脚注[^2]。
39
+
40
+ [^1]: 第一个内容。
41
+ [^2]: 第二个内容。`
42
+
43
+ const parser = createIncremarkParser({ gfm: true })
44
+ const result = parser.render(markdown)
45
+ const ast = result.ast
46
+
47
+ // 检查两个脚注引用
48
+ const paragraph = ast.children[0]
49
+ const footnoteRefs = paragraph.children?.filter(
50
+ (node: any) => node.type === 'footnoteReference'
51
+ )
52
+ expect(footnoteRefs).toHaveLength(2)
53
+ expect(footnoteRefs?.[0].identifier).toBe('1')
54
+ expect(footnoteRefs?.[1].identifier).toBe('2')
55
+
56
+ // 检查两个脚注定义
57
+ const footnoteDefs = ast.children.filter(
58
+ (node: any) => node.type === 'footnoteDefinition'
59
+ )
60
+ expect(footnoteDefs).toHaveLength(2)
61
+ expect(footnoteDefs[0].identifier).toBe('1')
62
+ expect(footnoteDefs[1].identifier).toBe('2')
63
+ })
64
+ })
65
+
66
+ describe('Multiline Footnote', () => {
67
+ it('should parse multiline footnote content', () => {
68
+ const markdown = `多行脚注[^long]。
69
+
70
+ [^long]: 第一段内容。
71
+
72
+ 第二段内容(缩进)。`
73
+
74
+ const parser = createIncremarkParser({ gfm: true })
75
+ const result = parser.render(markdown)
76
+ const ast = result.ast
77
+
78
+ // 检查脚注引用
79
+ const paragraph = ast.children[0]
80
+ const hasFootnoteRef = paragraph.children?.some(
81
+ (node: any) => node.type === 'footnoteReference' && node.identifier === 'long'
82
+ )
83
+ expect(hasFootnoteRef).toBe(true)
84
+
85
+ // 检查脚注定义
86
+ const footnoteDef = ast.children.find(
87
+ (node: any) => node.type === 'footnoteDefinition' && node.identifier === 'long'
88
+ ) as any
89
+ expect(footnoteDef).toBeDefined()
90
+
91
+ // 检查脚注内容是否包含多个段落
92
+ expect(footnoteDef.children.length).toBeGreaterThan(1)
93
+ })
94
+ })
95
+
96
+ describe('Incremental Parsing', () => {
97
+ it('should handle footnote reference in pending block', () => {
98
+ const parser = createIncremarkParser({ gfm: true })
99
+
100
+ // 第一次追加:只有引用
101
+ const update1 = parser.append('这是一个脚注[^1]。\n\n')
102
+
103
+ // 检查 pending blocks 中是否有脚注引用
104
+ const pendingParagraph = update1.pending[0]?.node
105
+ const hasPendingRef = pendingParagraph?.children?.some(
106
+ (node: any) => node.type === 'footnoteReference'
107
+ )
108
+ expect(hasPendingRef).toBe(true)
109
+
110
+ // 第二次追加:添加定义
111
+ const update2 = parser.append('[^1]: 脚注内容。')
112
+ parser.finalize()
113
+
114
+ // 检查最终 AST
115
+ const ast = parser.getAst()
116
+ const hasFootnoteDef = ast.children.some(
117
+ (node: any) => node.type === 'footnoteDefinition' && node.identifier === '1'
118
+ )
119
+ expect(hasFootnoteDef).toBe(true)
120
+ })
121
+
122
+ it('should handle definition before reference', () => {
123
+ const markdown = `[^1]: 脚注内容。
124
+
125
+ 这是一个脚注[^1]。`
126
+
127
+ const parser = createIncremarkParser({ gfm: true })
128
+ const result = parser.render(markdown)
129
+ const ast = result.ast
130
+
131
+ // 即使定义在前,引用也应该被正确解析
132
+ const paragraph = ast.children.find((node: any) => node.type === 'paragraph')
133
+ const hasFootnoteRef = paragraph?.children?.some(
134
+ (node: any) => node.type === 'footnoteReference' && node.identifier === '1'
135
+ )
136
+ expect(hasFootnoteRef).toBe(true)
137
+
138
+ const hasFootnoteDef = ast.children.some(
139
+ (node: any) => node.type === 'footnoteDefinition' && node.identifier === '1'
140
+ )
141
+ expect(hasFootnoteDef).toBe(true)
142
+ })
143
+ })
144
+
145
+ describe('Edge Cases', () => {
146
+ it('should handle footnote with special characters in identifier', () => {
147
+ const markdown = `脚注[^note-1]。
148
+
149
+ [^note-1]: 内容。`
150
+
151
+ const parser = createIncremarkParser({ gfm: true })
152
+ const result = parser.render(markdown)
153
+ const ast = result.ast
154
+
155
+ const paragraph = ast.children[0]
156
+ const hasFootnoteRef = paragraph.children?.some(
157
+ (node: any) => node.type === 'footnoteReference' && node.identifier === 'note-1'
158
+ )
159
+ expect(hasFootnoteRef).toBe(true)
160
+ })
161
+
162
+ it('should handle footnote with markdown in content', () => {
163
+ const markdown = `脚注[^complex]。
164
+
165
+ [^complex]: 包含 **粗体** 和 *斜体*。`
166
+
167
+ const parser = createIncremarkParser({ gfm: true })
168
+ const result = parser.render(markdown)
169
+ const ast = result.ast
170
+
171
+ const footnoteDef = ast.children.find(
172
+ (node: any) => node.type === 'footnoteDefinition' && node.identifier === 'complex'
173
+ ) as any
174
+ expect(footnoteDef).toBeDefined()
175
+
176
+ // 检查脚注内容是否包含格式化文本
177
+ const paragraph = footnoteDef.children[0]
178
+ const hasStrong = paragraph.children?.some((node: any) => node.type === 'strong')
179
+ const hasEmphasis = paragraph.children?.some((node: any) => node.type === 'emphasis')
180
+ expect(hasStrong || hasEmphasis).toBe(true)
181
+ })
182
+
183
+ it('should not parse invalid footnote syntax', () => {
184
+ const markdown = `这不是脚注[^ 1]。`
185
+
186
+ const parser = createIncremarkParser({ gfm: true })
187
+ const result = parser.render(markdown)
188
+ const ast = result.ast
189
+
190
+ // 空格会导致解析失败,应该被当作普通文本
191
+ const paragraph = ast.children[0]
192
+ const hasFootnoteRef = paragraph.children?.some(
193
+ (node: any) => node.type === 'footnoteReference'
194
+ )
195
+ expect(hasFootnoteRef).toBe(false)
196
+ })
197
+ })
198
+
199
+ describe('Footnote Reference Order', () => {
200
+ it('should track footnote reference order', () => {
201
+ const markdown = `第二个[^2]出现在第一个[^1]之前。
202
+
203
+ [^1]: 第一个定义。
204
+ [^2]: 第二个定义。`
205
+
206
+ const parser = createIncremarkParser({ gfm: true })
207
+ const result = parser.render(markdown)
208
+
209
+ // 检查引用顺序
210
+ expect(result.footnoteReferenceOrder).toEqual(['2', '1'])
211
+ })
212
+ })
213
+ })
214
+
@@ -19,6 +19,8 @@ const RE_HTML_BLOCK_1 = /^\s{0,3}<(script|pre|style|textarea|!--|!DOCTYPE|\?|!\[
19
19
  const RE_HTML_BLOCK_2 = /^\s{0,3}<\/?[a-zA-Z][a-zA-Z0-9-]*(\s|>|$)/
20
20
  const RE_TABLE_DELIMITER = /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?$/
21
21
  const RE_ESCAPE_SPECIAL = /[.*+?^${}()|[\]\\]/g
22
+ const RE_FOOTNOTE_DEFINITION = /^\[\^[^\]]+\]:\s/
23
+ const RE_FOOTNOTE_CONTINUATION = /^(?: |\t)/
22
24
 
23
25
  /** fence 结束模式缓存 */
24
26
  const fenceEndPatternCache = new Map<string, RegExp>()
@@ -122,6 +124,34 @@ export function isTableDelimiter(line: string): boolean {
122
124
  return RE_TABLE_DELIMITER.test(line.trim())
123
125
  }
124
126
 
127
+ // ============ 脚注检测 ============
128
+
129
+ /**
130
+ * 检测是否是脚注定义的起始行
131
+ * 格式: [^id]: content
132
+ *
133
+ * @example
134
+ * isFootnoteDefinitionStart('[^1]: 脚注内容') // true
135
+ * isFootnoteDefinitionStart('[^note]: 内容') // true
136
+ * isFootnoteDefinitionStart(' 缩进内容') // false
137
+ */
138
+ export function isFootnoteDefinitionStart(line: string): boolean {
139
+ return RE_FOOTNOTE_DEFINITION.test(line)
140
+ }
141
+
142
+ /**
143
+ * 检测是否是脚注定义的延续行(缩进行)
144
+ * 至少4个空格或1个tab
145
+ *
146
+ * @example
147
+ * isFootnoteContinuation(' 第二行') // true
148
+ * isFootnoteContinuation('\t第二行') // true
149
+ * isFootnoteContinuation(' 两个空格') // false
150
+ */
151
+ export function isFootnoteContinuation(line: string): boolean {
152
+ return RE_FOOTNOTE_CONTINUATION.test(line)
153
+ }
154
+
125
155
  // ============ 容器检测 ============
126
156
 
127
157
  /**