@bernierllc/markdown-parser 1.2.5 → 1.5.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/MarkdownParser.js +3 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/utils/cache.js +3 -0
- package/package.json +7 -3
- package/.eslintrc.js +0 -27
- package/CHANGELOG.md +0 -116
- package/__tests__/MarkdownParser.test.ts +0 -791
- package/__tests__/ast-utils.test.ts +0 -319
- package/__tests__/cache.test.ts +0 -243
- package/__tests__/plugins.test.ts +0 -282
- package/jest.config.cjs +0 -29
- package/src/MarkdownParser.ts +0 -517
- package/src/index.ts +0 -13
- package/src/plugins/commonmark.ts +0 -23
- package/src/plugins/gfm.ts +0 -23
- package/src/plugins/index.ts +0 -10
- package/src/types.ts +0 -114
- package/src/utils/ast-utils.ts +0 -81
- package/src/utils/cache.ts +0 -106
- package/tsconfig.json +0 -30
|
@@ -1,791 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2025 Bernier LLC
|
|
3
|
-
|
|
4
|
-
This file is licensed to the client under a limited-use license.
|
|
5
|
-
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
-
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { MarkdownParser } from '../src/MarkdownParser';
|
|
10
|
-
|
|
11
|
-
describe('MarkdownParser', () => {
|
|
12
|
-
describe('Constructor', () => {
|
|
13
|
-
it('should create parser with default config', () => {
|
|
14
|
-
const parser = new MarkdownParser();
|
|
15
|
-
expect(parser).toBeInstanceOf(MarkdownParser);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should create parser with custom config', () => {
|
|
19
|
-
const parser = new MarkdownParser({
|
|
20
|
-
gfm: false,
|
|
21
|
-
commonmark: false,
|
|
22
|
-
cache: { enabled: true, maxSize: 500, ttl: 1800 },
|
|
23
|
-
});
|
|
24
|
-
expect(parser).toBeInstanceOf(MarkdownParser);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('parse() - Headings', () => {
|
|
29
|
-
it('should parse heading levels correctly', async () => {
|
|
30
|
-
const parser = new MarkdownParser();
|
|
31
|
-
const result = await parser.parse('# Heading 1\n## Heading 2\n### Heading 3');
|
|
32
|
-
|
|
33
|
-
expect(result.success).toBe(true);
|
|
34
|
-
expect(result.ast!.children).toHaveLength(3);
|
|
35
|
-
expect(result.ast!.children![0].type).toBe('heading');
|
|
36
|
-
expect(result.ast!.children![0].attributes!.level).toBe(1);
|
|
37
|
-
expect(result.ast!.children![1].attributes!.level).toBe(2);
|
|
38
|
-
expect(result.ast!.children![2].attributes!.level).toBe(3);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should parse heading text correctly', async () => {
|
|
42
|
-
const parser = new MarkdownParser();
|
|
43
|
-
const result = await parser.parse('# Test Heading');
|
|
44
|
-
|
|
45
|
-
expect(result.success).toBe(true);
|
|
46
|
-
const heading = result.ast!.children![0];
|
|
47
|
-
expect(heading.children![0].type).toBe('text');
|
|
48
|
-
expect(heading.children![0].value).toBe('Test Heading');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should handle headings with inline formatting', async () => {
|
|
52
|
-
const parser = new MarkdownParser();
|
|
53
|
-
const result = await parser.parse('# This is **bold** text');
|
|
54
|
-
|
|
55
|
-
expect(result.success).toBe(true);
|
|
56
|
-
const heading = result.ast!.children![0];
|
|
57
|
-
expect(heading.children).toHaveLength(3);
|
|
58
|
-
expect(heading.children![1].type).toBe('strong');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should handle all heading levels (1-6)', async () => {
|
|
62
|
-
const parser = new MarkdownParser();
|
|
63
|
-
const markdown = ['# H1', '## H2', '### H3', '#### H4', '##### H5', '###### H6'].join('\n');
|
|
64
|
-
const result = await parser.parse(markdown);
|
|
65
|
-
|
|
66
|
-
expect(result.success).toBe(true);
|
|
67
|
-
expect(result.ast!.children).toHaveLength(6);
|
|
68
|
-
for (let i = 0; i < 6; i++) {
|
|
69
|
-
expect(result.ast!.children![i].attributes!.level).toBe(i + 1);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('parse() - Paragraphs', () => {
|
|
75
|
-
it('should parse simple paragraph', async () => {
|
|
76
|
-
const parser = new MarkdownParser();
|
|
77
|
-
const result = await parser.parse('This is a paragraph.');
|
|
78
|
-
|
|
79
|
-
expect(result.success).toBe(true);
|
|
80
|
-
expect(result.ast!.children).toHaveLength(1);
|
|
81
|
-
expect(result.ast!.children![0].type).toBe('paragraph');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should parse multi-line paragraph', async () => {
|
|
85
|
-
const parser = new MarkdownParser();
|
|
86
|
-
const result = await parser.parse('Line 1\nLine 2\nLine 3');
|
|
87
|
-
|
|
88
|
-
expect(result.success).toBe(true);
|
|
89
|
-
expect(result.ast!.children).toHaveLength(1);
|
|
90
|
-
expect(result.ast!.children![0].type).toBe('paragraph');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should split paragraphs on blank lines', async () => {
|
|
94
|
-
const parser = new MarkdownParser();
|
|
95
|
-
const result = await parser.parse('Paragraph 1\n\nParagraph 2');
|
|
96
|
-
|
|
97
|
-
expect(result.success).toBe(true);
|
|
98
|
-
expect(result.ast!.children).toHaveLength(2);
|
|
99
|
-
expect(result.ast!.children![0].type).toBe('paragraph');
|
|
100
|
-
expect(result.ast!.children![1].type).toBe('paragraph');
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe('parse() - Code Blocks', () => {
|
|
105
|
-
it('should parse fenced code block', async () => {
|
|
106
|
-
const parser = new MarkdownParser();
|
|
107
|
-
const markdown = '```javascript\nconst x = 1;\n```';
|
|
108
|
-
const result = await parser.parse(markdown);
|
|
109
|
-
|
|
110
|
-
expect(result.success).toBe(true);
|
|
111
|
-
expect(result.ast!.children![0].type).toBe('codeBlock');
|
|
112
|
-
expect(result.ast!.children![0].value).toBe('const x = 1;');
|
|
113
|
-
expect(result.ast!.children![0].attributes!.language).toBe('javascript');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should parse code block without language', async () => {
|
|
117
|
-
const parser = new MarkdownParser();
|
|
118
|
-
const markdown = '```\ncode here\n```';
|
|
119
|
-
const result = await parser.parse(markdown);
|
|
120
|
-
|
|
121
|
-
expect(result.success).toBe(true);
|
|
122
|
-
expect(result.ast!.children![0].type).toBe('codeBlock');
|
|
123
|
-
expect(result.ast!.children![0].attributes!.language).toBe('plaintext');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should parse multi-line code block', async () => {
|
|
127
|
-
const parser = new MarkdownParser();
|
|
128
|
-
const markdown = '```python\ndef hello():\n print("Hello")\n```';
|
|
129
|
-
const result = await parser.parse(markdown);
|
|
130
|
-
|
|
131
|
-
expect(result.success).toBe(true);
|
|
132
|
-
expect(result.ast!.children![0].value).toBe('def hello():\n print("Hello")');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('parse() - Lists', () => {
|
|
137
|
-
it('should parse unordered list', async () => {
|
|
138
|
-
const parser = new MarkdownParser();
|
|
139
|
-
const result = await parser.parse('- Item 1\n- Item 2\n- Item 3');
|
|
140
|
-
|
|
141
|
-
expect(result.success).toBe(true);
|
|
142
|
-
expect(result.ast!.children![0].type).toBe('list');
|
|
143
|
-
expect(result.ast!.children![0].attributes!.ordered).toBe(false);
|
|
144
|
-
expect(result.ast!.children![0].children).toHaveLength(3);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should parse ordered list', async () => {
|
|
148
|
-
const parser = new MarkdownParser();
|
|
149
|
-
const result = await parser.parse('1. Item 1\n2. Item 2\n3. Item 3');
|
|
150
|
-
|
|
151
|
-
expect(result.success).toBe(true);
|
|
152
|
-
expect(result.ast!.children![0].type).toBe('list');
|
|
153
|
-
expect(result.ast!.children![0].attributes!.ordered).toBe(true);
|
|
154
|
-
expect(result.ast!.children![0].children).toHaveLength(3);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should parse list with different markers', async () => {
|
|
158
|
-
const parser = new MarkdownParser();
|
|
159
|
-
const result1 = await parser.parse('* Item 1\n* Item 2');
|
|
160
|
-
const result2 = await parser.parse('+ Item 1\n+ Item 2');
|
|
161
|
-
|
|
162
|
-
expect(result1.success).toBe(true);
|
|
163
|
-
expect(result2.success).toBe(true);
|
|
164
|
-
expect(result1.ast!.children![0].type).toBe('list');
|
|
165
|
-
expect(result2.ast!.children![0].type).toBe('list');
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('parse() - GFM Task Lists', () => {
|
|
170
|
-
it('should parse unchecked task', async () => {
|
|
171
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
172
|
-
const result = await parser.parse('- [ ] Task 1');
|
|
173
|
-
|
|
174
|
-
expect(result.success).toBe(true);
|
|
175
|
-
expect(result.ast!.children![0].type).toBe('taskListItem');
|
|
176
|
-
expect(result.ast!.children![0].attributes!.checked).toBe(false);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should parse checked task', async () => {
|
|
180
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
181
|
-
const result = await parser.parse('- [x] Task 1');
|
|
182
|
-
|
|
183
|
-
expect(result.success).toBe(true);
|
|
184
|
-
expect(result.ast!.children![0].type).toBe('taskListItem');
|
|
185
|
-
expect(result.ast!.children![0].attributes!.checked).toBe(true);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should parse multiple tasks', async () => {
|
|
189
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
190
|
-
const result = await parser.parse('- [ ] Task 1\n- [x] Task 2\n- [ ] Task 3');
|
|
191
|
-
|
|
192
|
-
expect(result.success).toBe(true);
|
|
193
|
-
expect(result.ast!.children).toHaveLength(3);
|
|
194
|
-
expect(result.ast!.children![0].attributes!.checked).toBe(false);
|
|
195
|
-
expect(result.ast!.children![1].attributes!.checked).toBe(true);
|
|
196
|
-
expect(result.ast!.children![2].attributes!.checked).toBe(false);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should not parse tasks when GFM is disabled', async () => {
|
|
200
|
-
const parser = new MarkdownParser({ gfm: false });
|
|
201
|
-
const result = await parser.parse('- [ ] Task 1');
|
|
202
|
-
|
|
203
|
-
expect(result.success).toBe(true);
|
|
204
|
-
expect(result.ast!.children![0].type).toBe('list');
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe('parse() - Blockquotes', () => {
|
|
209
|
-
it('should parse simple blockquote', async () => {
|
|
210
|
-
const parser = new MarkdownParser();
|
|
211
|
-
const result = await parser.parse('> Quote text');
|
|
212
|
-
|
|
213
|
-
expect(result.success).toBe(true);
|
|
214
|
-
expect(result.ast!.children![0].type).toBe('blockquote');
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('should parse multi-line blockquote', async () => {
|
|
218
|
-
const parser = new MarkdownParser();
|
|
219
|
-
const result = await parser.parse('> Line 1\n> Line 2\n> Line 3');
|
|
220
|
-
|
|
221
|
-
expect(result.success).toBe(true);
|
|
222
|
-
expect(result.ast!.children![0].type).toBe('blockquote');
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
describe('parse() - Horizontal Rules', () => {
|
|
227
|
-
it('should parse horizontal rule with asterisks', async () => {
|
|
228
|
-
const parser = new MarkdownParser();
|
|
229
|
-
const result = await parser.parse('***');
|
|
230
|
-
|
|
231
|
-
expect(result.success).toBe(true);
|
|
232
|
-
expect(result.ast!.children![0].type).toBe('horizontalRule');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('should parse horizontal rule with hyphens', async () => {
|
|
236
|
-
const parser = new MarkdownParser();
|
|
237
|
-
const result = await parser.parse('---');
|
|
238
|
-
|
|
239
|
-
expect(result.success).toBe(true);
|
|
240
|
-
expect(result.ast!.children![0].type).toBe('horizontalRule');
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('should parse horizontal rule with underscores', async () => {
|
|
244
|
-
const parser = new MarkdownParser();
|
|
245
|
-
const result = await parser.parse('___');
|
|
246
|
-
|
|
247
|
-
expect(result.success).toBe(true);
|
|
248
|
-
expect(result.ast!.children![0].type).toBe('horizontalRule');
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe('parse() - Inline Formatting', () => {
|
|
253
|
-
it('should parse bold text with **', async () => {
|
|
254
|
-
const parser = new MarkdownParser();
|
|
255
|
-
const result = await parser.parse('This is **bold** text');
|
|
256
|
-
|
|
257
|
-
expect(result.success).toBe(true);
|
|
258
|
-
const paragraph = result.ast!.children![0];
|
|
259
|
-
expect(paragraph.children![1].type).toBe('strong');
|
|
260
|
-
expect(paragraph.children![1].children![0].value).toBe('bold');
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('should parse bold text with __', async () => {
|
|
264
|
-
const parser = new MarkdownParser();
|
|
265
|
-
const result = await parser.parse('This is __bold__ text');
|
|
266
|
-
|
|
267
|
-
expect(result.success).toBe(true);
|
|
268
|
-
const paragraph = result.ast!.children![0];
|
|
269
|
-
expect(paragraph.children![1].type).toBe('strong');
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('should parse italic text with *', async () => {
|
|
273
|
-
const parser = new MarkdownParser();
|
|
274
|
-
const result = await parser.parse('This is *italic* text');
|
|
275
|
-
|
|
276
|
-
expect(result.success).toBe(true);
|
|
277
|
-
const paragraph = result.ast!.children![0];
|
|
278
|
-
expect(paragraph.children![1].type).toBe('emphasis');
|
|
279
|
-
expect(paragraph.children![1].children![0].value).toBe('italic');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('should parse italic text with _', async () => {
|
|
283
|
-
const parser = new MarkdownParser();
|
|
284
|
-
const result = await parser.parse('This is _italic_ text');
|
|
285
|
-
|
|
286
|
-
expect(result.success).toBe(true);
|
|
287
|
-
const paragraph = result.ast!.children![0];
|
|
288
|
-
expect(paragraph.children![1].type).toBe('emphasis');
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('should parse inline code', async () => {
|
|
292
|
-
const parser = new MarkdownParser();
|
|
293
|
-
const result = await parser.parse('Use `console.log()` to debug');
|
|
294
|
-
|
|
295
|
-
expect(result.success).toBe(true);
|
|
296
|
-
const paragraph = result.ast!.children![0];
|
|
297
|
-
expect(paragraph.children![1].type).toBe('code');
|
|
298
|
-
expect(paragraph.children![1].value).toBe('console.log()');
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it('should parse strikethrough (GFM)', async () => {
|
|
302
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
303
|
-
const result = await parser.parse('This is ~~deleted~~ text');
|
|
304
|
-
|
|
305
|
-
expect(result.success).toBe(true);
|
|
306
|
-
const paragraph = result.ast!.children![0];
|
|
307
|
-
expect(paragraph.children![1].type).toBe('strikethrough');
|
|
308
|
-
expect(paragraph.children![1].children![0].value).toBe('deleted');
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it('should not parse strikethrough when GFM is disabled', async () => {
|
|
312
|
-
const parser = new MarkdownParser({ gfm: false });
|
|
313
|
-
const result = await parser.parse('This is ~~deleted~~ text');
|
|
314
|
-
|
|
315
|
-
expect(result.success).toBe(true);
|
|
316
|
-
const paragraph = result.ast!.children![0];
|
|
317
|
-
// Should be treated as plain text
|
|
318
|
-
expect(paragraph.children![0].type).toBe('text');
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
describe('parse() - Links and Images', () => {
|
|
323
|
-
it('should parse link', async () => {
|
|
324
|
-
const parser = new MarkdownParser();
|
|
325
|
-
const result = await parser.parse('[Link text](https://example.com)');
|
|
326
|
-
|
|
327
|
-
expect(result.success).toBe(true);
|
|
328
|
-
const paragraph = result.ast!.children![0];
|
|
329
|
-
expect(paragraph.children![0].type).toBe('link');
|
|
330
|
-
expect(paragraph.children![0].attributes!.href).toBe('https://example.com');
|
|
331
|
-
expect(paragraph.children![0].children![0].value).toBe('Link text');
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('should parse image', async () => {
|
|
335
|
-
const parser = new MarkdownParser();
|
|
336
|
-
const result = await parser.parse('');
|
|
337
|
-
|
|
338
|
-
expect(result.success).toBe(true);
|
|
339
|
-
const paragraph = result.ast!.children![0];
|
|
340
|
-
expect(paragraph.children![0].type).toBe('image');
|
|
341
|
-
expect(paragraph.children![0].attributes!.alt).toBe('Alt text');
|
|
342
|
-
expect(paragraph.children![0].attributes!.src).toBe('https://example.com/image.jpg');
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('should parse autolink (GFM)', async () => {
|
|
346
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
347
|
-
const result = await parser.parse('Visit https://example.com for more');
|
|
348
|
-
|
|
349
|
-
expect(result.success).toBe(true);
|
|
350
|
-
const paragraph = result.ast!.children![0];
|
|
351
|
-
expect(paragraph.children![1].type).toBe('autolink');
|
|
352
|
-
expect(paragraph.children![1].attributes!.href).toBe('https://example.com');
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
describe('parse() - Tables (GFM)', () => {
|
|
357
|
-
it('should parse simple table', async () => {
|
|
358
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
359
|
-
const markdown = '| Header 1 | Header 2 |\n| --- | --- |\n| Cell 1 | Cell 2 |';
|
|
360
|
-
const result = await parser.parse(markdown);
|
|
361
|
-
|
|
362
|
-
expect(result.success).toBe(true);
|
|
363
|
-
expect(result.ast!.children![0].type).toBe('table');
|
|
364
|
-
expect(result.ast!.children![0].children).toHaveLength(2); // Header + 1 body row
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it('should parse table with alignment', async () => {
|
|
368
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
369
|
-
const markdown = '| Left | Center | Right |\n| :--- | :---: | ---: |\n| L | C | R |';
|
|
370
|
-
const result = await parser.parse(markdown);
|
|
371
|
-
|
|
372
|
-
expect(result.success).toBe(true);
|
|
373
|
-
const table = result.ast!.children![0];
|
|
374
|
-
const bodyRow = table.children![1];
|
|
375
|
-
expect(bodyRow.children![0].attributes!.align).toBe('left');
|
|
376
|
-
expect(bodyRow.children![1].attributes!.align).toBe('center');
|
|
377
|
-
expect(bodyRow.children![2].attributes!.align).toBe('right');
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it('should not parse tables when GFM is disabled', async () => {
|
|
381
|
-
const parser = new MarkdownParser({ gfm: false });
|
|
382
|
-
const markdown = '| Header 1 | Header 2 |\n| --- | --- |\n| Cell 1 | Cell 2 |';
|
|
383
|
-
const result = await parser.parse(markdown);
|
|
384
|
-
|
|
385
|
-
expect(result.success).toBe(true);
|
|
386
|
-
// Should be treated as paragraphs
|
|
387
|
-
expect(result.ast!.children![0].type).toBe('paragraph');
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
describe('parse() - Edge Cases', () => {
|
|
392
|
-
it('should handle empty string', async () => {
|
|
393
|
-
const parser = new MarkdownParser();
|
|
394
|
-
const result = await parser.parse('');
|
|
395
|
-
|
|
396
|
-
expect(result.success).toBe(true);
|
|
397
|
-
expect(result.ast!.children).toHaveLength(0);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it('should handle only whitespace', async () => {
|
|
401
|
-
const parser = new MarkdownParser();
|
|
402
|
-
const result = await parser.parse(' \n \n ');
|
|
403
|
-
|
|
404
|
-
expect(result.success).toBe(true);
|
|
405
|
-
expect(result.ast!.children).toHaveLength(0);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it('should handle malformed markdown gracefully', async () => {
|
|
409
|
-
const parser = new MarkdownParser();
|
|
410
|
-
const result = await parser.parse('# Heading [broken link(');
|
|
411
|
-
|
|
412
|
-
expect(result.success).toBe(true);
|
|
413
|
-
// Should not crash
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it('should handle unicode characters', async () => {
|
|
417
|
-
const parser = new MarkdownParser();
|
|
418
|
-
const result = await parser.parse('# Hello 世界 🌍');
|
|
419
|
-
|
|
420
|
-
expect(result.success).toBe(true);
|
|
421
|
-
expect(result.ast!.children![0].children![0].value).toContain('世界');
|
|
422
|
-
expect(result.ast!.children![0].children![0].value).toContain('🌍');
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it('should handle very long lines', async () => {
|
|
426
|
-
const parser = new MarkdownParser();
|
|
427
|
-
const longLine = 'a'.repeat(10000);
|
|
428
|
-
const result = await parser.parse(longLine);
|
|
429
|
-
|
|
430
|
-
expect(result.success).toBe(true);
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
describe('parse() - Metadata', () => {
|
|
435
|
-
it('should include parse time in metadata', async () => {
|
|
436
|
-
const parser = new MarkdownParser();
|
|
437
|
-
const result = await parser.parse('# Test');
|
|
438
|
-
|
|
439
|
-
expect(result.metadata).toBeDefined();
|
|
440
|
-
expect(result.metadata!.parseTime).toBeGreaterThanOrEqual(0);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it('should include node count in metadata', async () => {
|
|
444
|
-
const parser = new MarkdownParser();
|
|
445
|
-
const result = await parser.parse('# Test\n\nParagraph');
|
|
446
|
-
|
|
447
|
-
expect(result.metadata!.nodeCount).toBeGreaterThan(0);
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it('should indicate cache hit in metadata', async () => {
|
|
451
|
-
const parser = new MarkdownParser({ cache: { enabled: true } });
|
|
452
|
-
const markdown = '# Test';
|
|
453
|
-
|
|
454
|
-
await parser.parse(markdown);
|
|
455
|
-
const result = await parser.parse(markdown);
|
|
456
|
-
|
|
457
|
-
expect(result.metadata!.cached).toBe(true);
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
describe('clearCache()', () => {
|
|
462
|
-
it('should clear the cache', async () => {
|
|
463
|
-
const parser = new MarkdownParser({ cache: { enabled: true } });
|
|
464
|
-
const markdown = '# Test';
|
|
465
|
-
|
|
466
|
-
await parser.parse(markdown);
|
|
467
|
-
parser.clearCache();
|
|
468
|
-
const result = await parser.parse(markdown);
|
|
469
|
-
|
|
470
|
-
expect(result.metadata!.cached).toBe(false);
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
describe('getCacheStats()', () => {
|
|
475
|
-
it('should return cache statistics', async () => {
|
|
476
|
-
const parser = new MarkdownParser({ cache: { enabled: true } });
|
|
477
|
-
|
|
478
|
-
const stats1 = parser.getCacheStats();
|
|
479
|
-
expect(stats1.enabled).toBe(true);
|
|
480
|
-
expect(stats1.size).toBe(0);
|
|
481
|
-
|
|
482
|
-
await parser.parse('# Test 1');
|
|
483
|
-
await parser.parse('# Test 2');
|
|
484
|
-
|
|
485
|
-
const stats2 = parser.getCacheStats();
|
|
486
|
-
expect(stats2.size).toBe(2);
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it('should return disabled cache stats', () => {
|
|
490
|
-
const parser = new MarkdownParser({ cache: { enabled: false } });
|
|
491
|
-
|
|
492
|
-
const stats = parser.getCacheStats();
|
|
493
|
-
expect(stats.enabled).toBe(false);
|
|
494
|
-
expect(stats.size).toBe(0);
|
|
495
|
-
});
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
describe('pruneCache()', () => {
|
|
499
|
-
it('should remove expired entries', async () => {
|
|
500
|
-
const parser = new MarkdownParser({
|
|
501
|
-
cache: { enabled: true, ttl: 1 }, // 1 second TTL
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
await parser.parse('# Test 1');
|
|
505
|
-
await parser.parse('# Test 2');
|
|
506
|
-
|
|
507
|
-
// Wait for expiration
|
|
508
|
-
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
509
|
-
|
|
510
|
-
const removed = parser.pruneCache();
|
|
511
|
-
expect(removed).toBe(2);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it('should return 0 when cache is disabled', () => {
|
|
515
|
-
const parser = new MarkdownParser({ cache: { enabled: false } });
|
|
516
|
-
const removed = parser.pruneCache();
|
|
517
|
-
expect(removed).toBe(0);
|
|
518
|
-
});
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
describe('parse() - Advanced Table Features (GFM)', () => {
|
|
522
|
-
it('should parse table with multiple body rows', async () => {
|
|
523
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
524
|
-
const markdown = '| H1 | H2 |\n| --- | --- |\n| R1C1 | R1C2 |\n| R2C1 | R2C2 |\n| R3C1 | R3C2 |';
|
|
525
|
-
const result = await parser.parse(markdown);
|
|
526
|
-
|
|
527
|
-
expect(result.success).toBe(true);
|
|
528
|
-
const table = result.ast!.children![0];
|
|
529
|
-
expect(table.type).toBe('table');
|
|
530
|
-
expect(table.children).toHaveLength(4); // 1 header + 3 body rows
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it('should parse table with left alignment', async () => {
|
|
534
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
535
|
-
const markdown = '| Left |\n| :--- |\n| L |';
|
|
536
|
-
const result = await parser.parse(markdown);
|
|
537
|
-
|
|
538
|
-
expect(result.success).toBe(true);
|
|
539
|
-
const table = result.ast!.children![0];
|
|
540
|
-
const bodyRow = table.children![1];
|
|
541
|
-
expect(bodyRow.children![0].attributes!.align).toBe('left');
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it('should parse table with default left alignment when no colon', async () => {
|
|
545
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
546
|
-
const markdown = '| Default |\n| --- |\n| D |';
|
|
547
|
-
const result = await parser.parse(markdown);
|
|
548
|
-
|
|
549
|
-
expect(result.success).toBe(true);
|
|
550
|
-
const table = result.ast!.children![0];
|
|
551
|
-
const bodyRow = table.children![1];
|
|
552
|
-
expect(bodyRow.children![0].attributes!.align).toBe('left');
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
it('should parse table with inline formatting in cells', async () => {
|
|
556
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
557
|
-
const markdown = '| Header |\n| --- |\n| **bold** and *italic* |';
|
|
558
|
-
const result = await parser.parse(markdown);
|
|
559
|
-
|
|
560
|
-
expect(result.success).toBe(true);
|
|
561
|
-
const table = result.ast!.children![0];
|
|
562
|
-
const bodyRow = table.children![1];
|
|
563
|
-
const cell = bodyRow.children![0];
|
|
564
|
-
expect(cell.children!.some(n => n.type === 'strong')).toBe(true);
|
|
565
|
-
expect(cell.children!.some(n => n.type === 'emphasis')).toBe(true);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
it('should stop parsing table when row does not contain pipe', async () => {
|
|
569
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
570
|
-
const markdown = '| H1 | H2 |\n| --- | --- |\n| R1 | R2 |\nNot a table row\n| R3 | R4 |';
|
|
571
|
-
const result = await parser.parse(markdown);
|
|
572
|
-
|
|
573
|
-
expect(result.success).toBe(true);
|
|
574
|
-
// Should parse first table with 2 rows (header + R1)
|
|
575
|
-
const table = result.ast!.children![0];
|
|
576
|
-
expect(table.type).toBe('table');
|
|
577
|
-
// Should also have a paragraph for "Not a table row"
|
|
578
|
-
expect(result.ast!.children![1].type).toBe('paragraph');
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
describe('parse() - Inline Parsing Edge Cases', () => {
|
|
583
|
-
it('should parse image with empty alt text', async () => {
|
|
584
|
-
const parser = new MarkdownParser();
|
|
585
|
-
const result = await parser.parse('');
|
|
586
|
-
|
|
587
|
-
expect(result.success).toBe(true);
|
|
588
|
-
const paragraph = result.ast!.children![0];
|
|
589
|
-
expect(paragraph.children![0].type).toBe('image');
|
|
590
|
-
expect(paragraph.children![0].attributes!.alt).toBe('');
|
|
591
|
-
expect(paragraph.children![0].attributes!.src).toBe('https://example.com/img.jpg');
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
it('should not parse autolink when GFM is disabled', async () => {
|
|
595
|
-
const parser = new MarkdownParser({ gfm: false });
|
|
596
|
-
const result = await parser.parse('Visit https://example.com');
|
|
597
|
-
|
|
598
|
-
expect(result.success).toBe(true);
|
|
599
|
-
// Should be plain text
|
|
600
|
-
const paragraph = result.ast!.children![0];
|
|
601
|
-
expect(paragraph.children![0].type).toBe('text');
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it('should handle single character fallback in inline parsing', async () => {
|
|
605
|
-
const parser = new MarkdownParser();
|
|
606
|
-
const result = await parser.parse('Text with * single asterisk');
|
|
607
|
-
|
|
608
|
-
expect(result.success).toBe(true);
|
|
609
|
-
const paragraph = result.ast!.children![0];
|
|
610
|
-
// Should include the asterisk as text
|
|
611
|
-
expect(paragraph.children!.some(n => n.type === 'text' && n.value === '*')).toBe(true);
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it('should handle special characters in text', async () => {
|
|
615
|
-
const parser = new MarkdownParser();
|
|
616
|
-
const result = await parser.parse('Text with ~ and [ characters');
|
|
617
|
-
|
|
618
|
-
expect(result.success).toBe(true);
|
|
619
|
-
const paragraph = result.ast!.children![0];
|
|
620
|
-
expect(paragraph.children!.some(n => n.type === 'text')).toBe(true);
|
|
621
|
-
});
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
describe('parse() - Multi-line Paragraph Edge Cases', () => {
|
|
625
|
-
it('should not continue paragraph when encountering heading', async () => {
|
|
626
|
-
const parser = new MarkdownParser();
|
|
627
|
-
const result = await parser.parse('Paragraph line 1\n# Heading');
|
|
628
|
-
|
|
629
|
-
expect(result.success).toBe(true);
|
|
630
|
-
expect(result.ast!.children).toHaveLength(2);
|
|
631
|
-
expect(result.ast!.children![0].type).toBe('paragraph');
|
|
632
|
-
expect(result.ast!.children![1].type).toBe('heading');
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
it('should not continue paragraph when encountering code block', async () => {
|
|
636
|
-
const parser = new MarkdownParser();
|
|
637
|
-
const result = await parser.parse('Paragraph line 1\n```\ncode\n```');
|
|
638
|
-
|
|
639
|
-
expect(result.success).toBe(true);
|
|
640
|
-
expect(result.ast!.children![0].type).toBe('paragraph');
|
|
641
|
-
expect(result.ast!.children![1].type).toBe('codeBlock');
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
it('should not continue paragraph when encountering blockquote', async () => {
|
|
645
|
-
const parser = new MarkdownParser();
|
|
646
|
-
const result = await parser.parse('Paragraph line 1\n> Quote');
|
|
647
|
-
|
|
648
|
-
expect(result.success).toBe(true);
|
|
649
|
-
expect(result.ast!.children![0].type).toBe('paragraph');
|
|
650
|
-
expect(result.ast!.children![1].type).toBe('blockquote');
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
it('should not continue paragraph when encountering list', async () => {
|
|
654
|
-
const parser = new MarkdownParser();
|
|
655
|
-
const result = await parser.parse('Paragraph line 1\n- List item');
|
|
656
|
-
|
|
657
|
-
expect(result.success).toBe(true);
|
|
658
|
-
expect(result.ast!.children![0].type).toBe('paragraph');
|
|
659
|
-
expect(result.ast!.children![1].type).toBe('list');
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
it('should not continue paragraph when encountering horizontal rule', async () => {
|
|
663
|
-
const parser = new MarkdownParser();
|
|
664
|
-
const result = await parser.parse('Paragraph line 1\n***');
|
|
665
|
-
|
|
666
|
-
expect(result.success).toBe(true);
|
|
667
|
-
expect(result.ast!.children![0].type).toBe('paragraph');
|
|
668
|
-
expect(result.ast!.children![1].type).toBe('horizontalRule');
|
|
669
|
-
});
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
describe('parse() - Error Handling', () => {
|
|
673
|
-
it('should handle errors gracefully and return error result', async () => {
|
|
674
|
-
const parser = new MarkdownParser();
|
|
675
|
-
|
|
676
|
-
// Mock parseToAST to throw error
|
|
677
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
678
|
-
const parserWithPrivate = parser as any;
|
|
679
|
-
const originalParseToAST = parserWithPrivate.parseToAST.bind(parser);
|
|
680
|
-
parserWithPrivate.parseToAST = jest.fn().mockRejectedValue(new Error('Parse failed'));
|
|
681
|
-
|
|
682
|
-
const result = await parser.parse('# Test');
|
|
683
|
-
|
|
684
|
-
expect(result.success).toBe(false);
|
|
685
|
-
expect(result.error).toBe('Parse failed');
|
|
686
|
-
expect(result.ast).toBeUndefined();
|
|
687
|
-
|
|
688
|
-
// Restore original method
|
|
689
|
-
parserWithPrivate.parseToAST = originalParseToAST;
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
it('should handle unknown errors', async () => {
|
|
693
|
-
const parser = new MarkdownParser();
|
|
694
|
-
|
|
695
|
-
// Mock parseToAST to throw non-Error
|
|
696
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
697
|
-
const parserWithPrivate = parser as any;
|
|
698
|
-
const originalParseToAST = parserWithPrivate.parseToAST.bind(parser);
|
|
699
|
-
parserWithPrivate.parseToAST = jest.fn().mockRejectedValue('String error');
|
|
700
|
-
|
|
701
|
-
const result = await parser.parse('# Test');
|
|
702
|
-
|
|
703
|
-
expect(result.success).toBe(false);
|
|
704
|
-
expect(result.error).toBe('Unknown parse error');
|
|
705
|
-
|
|
706
|
-
// Restore original method
|
|
707
|
-
parserWithPrivate.parseToAST = originalParseToAST;
|
|
708
|
-
});
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
describe('parse() - Position Tracking', () => {
|
|
712
|
-
it('should track position for headings', async () => {
|
|
713
|
-
const parser = new MarkdownParser();
|
|
714
|
-
const result = await parser.parse('# Heading');
|
|
715
|
-
|
|
716
|
-
expect(result.success).toBe(true);
|
|
717
|
-
const heading = result.ast!.children![0];
|
|
718
|
-
expect(heading.position).toBeDefined();
|
|
719
|
-
expect(heading.position!.start.line).toBe(1);
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
it('should track position for code blocks', async () => {
|
|
723
|
-
const parser = new MarkdownParser();
|
|
724
|
-
const result = await parser.parse('```\ncode\n```');
|
|
725
|
-
|
|
726
|
-
expect(result.success).toBe(true);
|
|
727
|
-
const codeBlock = result.ast!.children![0];
|
|
728
|
-
expect(codeBlock.position).toBeDefined();
|
|
729
|
-
expect(codeBlock.position!.start.line).toBe(1);
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
it('should track position for blockquotes', async () => {
|
|
733
|
-
const parser = new MarkdownParser();
|
|
734
|
-
const result = await parser.parse('> Quote');
|
|
735
|
-
|
|
736
|
-
expect(result.success).toBe(true);
|
|
737
|
-
const blockquote = result.ast!.children![0];
|
|
738
|
-
expect(blockquote.position).toBeDefined();
|
|
739
|
-
expect(blockquote.position!.start.line).toBe(1);
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
it('should track position for lists', async () => {
|
|
743
|
-
const parser = new MarkdownParser();
|
|
744
|
-
const result = await parser.parse('- Item 1\n- Item 2');
|
|
745
|
-
|
|
746
|
-
expect(result.success).toBe(true);
|
|
747
|
-
const list = result.ast!.children![0];
|
|
748
|
-
expect(list.position).toBeDefined();
|
|
749
|
-
expect(list.position!.start.line).toBe(1);
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
it('should track position for task list items', async () => {
|
|
753
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
754
|
-
const result = await parser.parse('- [ ] Task');
|
|
755
|
-
|
|
756
|
-
expect(result.success).toBe(true);
|
|
757
|
-
const task = result.ast!.children![0];
|
|
758
|
-
expect(task.position).toBeDefined();
|
|
759
|
-
expect(task.position!.start.line).toBe(1);
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
it('should track position for tables', async () => {
|
|
763
|
-
const parser = new MarkdownParser({ gfm: true });
|
|
764
|
-
const result = await parser.parse('| H |\n| --- |\n| C |');
|
|
765
|
-
|
|
766
|
-
expect(result.success).toBe(true);
|
|
767
|
-
const table = result.ast!.children![0];
|
|
768
|
-
expect(table.position).toBeDefined();
|
|
769
|
-
expect(table.position!.start.line).toBe(1);
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
it('should track position for paragraphs', async () => {
|
|
773
|
-
const parser = new MarkdownParser();
|
|
774
|
-
const result = await parser.parse('Paragraph text');
|
|
775
|
-
|
|
776
|
-
expect(result.success).toBe(true);
|
|
777
|
-
const paragraph = result.ast!.children![0];
|
|
778
|
-
expect(paragraph.position).toBeDefined();
|
|
779
|
-
expect(paragraph.position!.start.line).toBe(1);
|
|
780
|
-
});
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
describe('clearCache() - Cache disabled scenarios', () => {
|
|
785
|
-
it('should not throw when clearing disabled cache', () => {
|
|
786
|
-
const parser = new MarkdownParser({ cache: { enabled: false } });
|
|
787
|
-
|
|
788
|
-
expect(() => parser.clearCache()).not.toThrow();
|
|
789
|
-
});
|
|
790
|
-
});
|
|
791
|
-
});
|