@markuplint/mdx-parser 5.2.0 → 5.3.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/CHANGELOG.md +14 -0
- package/lib/parser.d.ts +22 -2
- package/lib/parser.js +38 -3
- package/package.json +7 -7
- package/src/component-name.spec.ts +28 -0
- package/src/index.spec.ts +216 -125
- package/src/parser.ts +41 -3
- package/src/tag-name-start.spec.ts +25 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/index.spec.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { describe, test, expect } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import type {
|
|
4
|
+
MLASTElement,
|
|
5
|
+
MLASTHTMLAttr,
|
|
6
|
+
MLASTPreprocessorSpecificBlock,
|
|
7
|
+
MLASTTag,
|
|
8
|
+
MLASTText,
|
|
9
|
+
} from '@markuplint/ml-ast';
|
|
3
10
|
import { nodeListToDebugMaps } from '@markuplint/parser-utils';
|
|
4
11
|
|
|
5
12
|
import { parser } from './parser.js';
|
|
@@ -10,6 +17,15 @@ function parse(code: string) {
|
|
|
10
17
|
|
|
11
18
|
describe('MDXParser', () => {
|
|
12
19
|
describe('JSX elements', () => {
|
|
20
|
+
test('treats tag and attribute names as case-sensitive', () => {
|
|
21
|
+
expect(parser.tagNameCaseSensitive).toBe(true);
|
|
22
|
+
expect(parser.attrNameCaseSensitive).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('says that every attribute name reaches the DOM through the DOM API', () => {
|
|
26
|
+
expect(parser.attrNameRoute).toBe('dom-api');
|
|
27
|
+
});
|
|
28
|
+
|
|
13
29
|
test('self-closing component', () => {
|
|
14
30
|
const doc = parse('<MyComponent prop="value" />');
|
|
15
31
|
const maps = nodeListToDebugMaps(doc.nodeList);
|
|
@@ -18,8 +34,12 @@ describe('MDXParser', () => {
|
|
|
18
34
|
|
|
19
35
|
test('component named like svg or math does not enter the namespace', () => {
|
|
20
36
|
const doc = parse('<div><Svg><g /></Svg></div>');
|
|
21
|
-
const svg = doc.nodeList.find(
|
|
22
|
-
|
|
37
|
+
const svg = doc.nodeList.find(
|
|
38
|
+
(node): node is MLASTElement => node.type === 'starttag' && node.nodeName === 'Svg',
|
|
39
|
+
);
|
|
40
|
+
const g = doc.nodeList.find(
|
|
41
|
+
(node): node is MLASTElement => node.type === 'starttag' && node.nodeName === 'g',
|
|
42
|
+
);
|
|
23
43
|
expect(svg?.namespace).toBe('http://www.w3.org/1999/xhtml');
|
|
24
44
|
expect(g?.namespace).toBe('http://www.w3.org/1999/xhtml');
|
|
25
45
|
});
|
|
@@ -45,13 +65,13 @@ describe('MDXParser', () => {
|
|
|
45
65
|
describe('Component detection', () => {
|
|
46
66
|
test('uppercase name is authored element', () => {
|
|
47
67
|
const doc = parse('<MyComponent />');
|
|
48
|
-
const startTag = doc.nodeList.find(n => n?.type === 'starttag');
|
|
68
|
+
const startTag = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag');
|
|
49
69
|
expect(startTag?.elementType).toBe('authored');
|
|
50
70
|
});
|
|
51
71
|
|
|
52
72
|
test('lowercase name is HTML element', () => {
|
|
53
73
|
const doc = parse('<div>x</div>');
|
|
54
|
-
const startTag = doc.nodeList.find(n => n?.type === 'starttag');
|
|
74
|
+
const startTag = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag');
|
|
55
75
|
expect(startTag?.elementType).toBe('html');
|
|
56
76
|
});
|
|
57
77
|
});
|
|
@@ -79,7 +99,7 @@ describe('MDXParser', () => {
|
|
|
79
99
|
describe('Markdown content', () => {
|
|
80
100
|
test('heading becomes h1 element', () => {
|
|
81
101
|
const doc = parse('# Hello');
|
|
82
|
-
const h1 = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'h1');
|
|
102
|
+
const h1 = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'h1');
|
|
83
103
|
expect(h1).toBeDefined();
|
|
84
104
|
expect(h1!.elementType).toBe('html');
|
|
85
105
|
});
|
|
@@ -92,20 +112,20 @@ describe('MDXParser', () => {
|
|
|
92
112
|
['###### H6', 'h6'],
|
|
93
113
|
] as const)('heading %s becomes %s element', (md, tag) => {
|
|
94
114
|
const doc = parse(md);
|
|
95
|
-
const el = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === tag);
|
|
115
|
+
const el = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === tag);
|
|
96
116
|
expect(el).toBeDefined();
|
|
97
117
|
expect(el!.elementType).toBe('html');
|
|
98
118
|
});
|
|
99
119
|
|
|
100
120
|
test('paragraph becomes p element', () => {
|
|
101
121
|
const doc = parse('Some text here.');
|
|
102
|
-
const p = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'p');
|
|
122
|
+
const p = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'p');
|
|
103
123
|
expect(p).toBeDefined();
|
|
104
124
|
});
|
|
105
125
|
|
|
106
126
|
test('emphasis becomes em element', () => {
|
|
107
127
|
const doc = parse('*emphasized*');
|
|
108
|
-
const em = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'em');
|
|
128
|
+
const em = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'em');
|
|
109
129
|
expect(em).toBeDefined();
|
|
110
130
|
const text = em!.childNodes.find(c => c.type === 'text');
|
|
111
131
|
expect(text).toBeDefined();
|
|
@@ -114,7 +134,9 @@ describe('MDXParser', () => {
|
|
|
114
134
|
|
|
115
135
|
test('strong becomes strong element', () => {
|
|
116
136
|
const doc = parse('**bold**');
|
|
117
|
-
const strong = doc.nodeList.find(
|
|
137
|
+
const strong = doc.nodeList.find(
|
|
138
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'strong',
|
|
139
|
+
);
|
|
118
140
|
expect(strong).toBeDefined();
|
|
119
141
|
const text = strong!.childNodes.find(c => c.type === 'text');
|
|
120
142
|
expect(text).toBeDefined();
|
|
@@ -123,20 +145,26 @@ describe('MDXParser', () => {
|
|
|
123
145
|
|
|
124
146
|
test('link becomes <a> with href', () => {
|
|
125
147
|
const doc = parse('[link](https://example.com)');
|
|
126
|
-
const a = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'a');
|
|
148
|
+
const a = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'a');
|
|
127
149
|
expect(a).toBeDefined();
|
|
128
|
-
const href = a!.attributes.find(
|
|
150
|
+
const href = a!.attributes.find(
|
|
151
|
+
(attr): attr is MLASTHTMLAttr => attr.type === 'attr' && attr.name.raw === 'href',
|
|
152
|
+
);
|
|
129
153
|
expect(href).toBeDefined();
|
|
130
154
|
expect(href!.value.raw).toBe('https://example.com');
|
|
131
155
|
});
|
|
132
156
|
|
|
133
157
|
test('image becomes <img> with src and alt', () => {
|
|
134
158
|
const doc = parse('');
|
|
135
|
-
const img = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'img');
|
|
159
|
+
const img = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'img');
|
|
136
160
|
expect(img).toBeDefined();
|
|
137
|
-
const src = img!.attributes.find(
|
|
161
|
+
const src = img!.attributes.find(
|
|
162
|
+
(attr): attr is MLASTHTMLAttr => attr.type === 'attr' && attr.name.raw === 'src',
|
|
163
|
+
);
|
|
138
164
|
expect(src!.value.raw).toBe('image.png');
|
|
139
|
-
const alt = img!.attributes.find(
|
|
165
|
+
const alt = img!.attributes.find(
|
|
166
|
+
(attr): attr is MLASTHTMLAttr => attr.type === 'attr' && attr.name.raw === 'alt',
|
|
167
|
+
);
|
|
140
168
|
expect(alt!.value.raw).toBe('alt text');
|
|
141
169
|
});
|
|
142
170
|
});
|
|
@@ -196,7 +224,7 @@ describe('MDXParser', () => {
|
|
|
196
224
|
|
|
197
225
|
test('fragment starttag has nodeName #jsx-fragment', () => {
|
|
198
226
|
const doc = parse('<>\n <div>inside fragment</div>\n</>');
|
|
199
|
-
const startTag = doc.nodeList.find(n => n?.type === 'starttag');
|
|
227
|
+
const startTag = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag');
|
|
200
228
|
expect(startTag?.nodeName).toBe('#jsx-fragment');
|
|
201
229
|
});
|
|
202
230
|
});
|
|
@@ -214,7 +242,7 @@ describe('MDXParser', () => {
|
|
|
214
242
|
|
|
215
243
|
test('dot notation elementType is authored', () => {
|
|
216
244
|
const doc = parse('<Layout.Header>content</Layout.Header>');
|
|
217
|
-
const startTag = doc.nodeList.find(n => n?.type === 'starttag');
|
|
245
|
+
const startTag = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag');
|
|
218
246
|
expect(startTag?.elementType).toBe('authored');
|
|
219
247
|
});
|
|
220
248
|
});
|
|
@@ -222,17 +250,17 @@ describe('MDXParser', () => {
|
|
|
222
250
|
describe('Expression attributes', () => {
|
|
223
251
|
test('curly-brace attribute values are marked as dynamic', () => {
|
|
224
252
|
const doc = parse('<Component data={value} style={{ color: "red" }} />');
|
|
225
|
-
const startTag = doc.nodeList.find(n => n?.type === 'starttag');
|
|
253
|
+
const startTag = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag');
|
|
226
254
|
expect(startTag).toBeDefined();
|
|
227
255
|
const attrs = startTag!.attributes;
|
|
228
256
|
expect(attrs).toHaveLength(2);
|
|
229
257
|
|
|
230
|
-
const dataAttr = attrs.find(a => a.type === 'attr' && a.name.raw === 'data');
|
|
258
|
+
const dataAttr = attrs.find((a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'data');
|
|
231
259
|
expect(dataAttr).toBeDefined();
|
|
232
260
|
expect(dataAttr!.isDynamicValue).toBe(true);
|
|
233
261
|
expect(dataAttr!.value.raw).toBe('value');
|
|
234
262
|
|
|
235
|
-
const styleAttr = attrs.find(a => a.type === 'attr' && a.name.raw === 'style');
|
|
263
|
+
const styleAttr = attrs.find((a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'style');
|
|
236
264
|
expect(styleAttr).toBeDefined();
|
|
237
265
|
expect(styleAttr!.isDynamicValue).toBe(true);
|
|
238
266
|
expect(styleAttr!.value.raw).toBe('{ color: "red" }');
|
|
@@ -250,12 +278,12 @@ describe('MDXParser', () => {
|
|
|
250
278
|
describe('Spread attributes', () => {
|
|
251
279
|
test('spread attribute is detected as spread type', () => {
|
|
252
280
|
const doc = parse('<Component {...props} />');
|
|
253
|
-
const startTag = doc.nodeList.find(n => n?.type === 'starttag');
|
|
281
|
+
const startTag = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag');
|
|
254
282
|
expect(startTag).toBeDefined();
|
|
255
283
|
const attrs = startTag!.attributes;
|
|
256
284
|
expect(attrs).toHaveLength(1);
|
|
257
|
-
expect(attrs[0]
|
|
258
|
-
expect(attrs[0]
|
|
285
|
+
expect(attrs[0]!.type).toBe('spread');
|
|
286
|
+
expect(attrs[0]!.raw).toBe('{...props}');
|
|
259
287
|
});
|
|
260
288
|
|
|
261
289
|
test('debug map for spread attribute element', () => {
|
|
@@ -282,7 +310,9 @@ describe('MDXParser', () => {
|
|
|
282
310
|
|
|
283
311
|
test('outer element types are authored', () => {
|
|
284
312
|
const doc = parse('<Outer>\n <Inner>content</Inner>\n</Outer>');
|
|
285
|
-
const outerStart = doc.nodeList.find(
|
|
313
|
+
const outerStart = doc.nodeList.find(
|
|
314
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Outer',
|
|
315
|
+
);
|
|
286
316
|
expect(outerStart?.elementType).toBe('authored');
|
|
287
317
|
});
|
|
288
318
|
});
|
|
@@ -302,7 +332,7 @@ describe('MDXParser', () => {
|
|
|
302
332
|
|
|
303
333
|
test('inline JSX component is authored element', () => {
|
|
304
334
|
const doc = parse('Text with <Badge color="blue">inline</Badge> component.');
|
|
305
|
-
const badge = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Badge');
|
|
335
|
+
const badge = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Badge');
|
|
306
336
|
expect(badge?.elementType).toBe('authored');
|
|
307
337
|
});
|
|
308
338
|
});
|
|
@@ -338,7 +368,7 @@ describe('MDXParser', () => {
|
|
|
338
368
|
const doc = parse(
|
|
339
369
|
'import { Alert } from "./Alert"\nimport { Badge } from "./Badge"\n\n<Alert type="warning">\n <Badge>NEW</Badge>\n</Alert>',
|
|
340
370
|
);
|
|
341
|
-
const alert = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Alert');
|
|
371
|
+
const alert = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Alert');
|
|
342
372
|
expect(alert?.elementType).toBe('authored');
|
|
343
373
|
});
|
|
344
374
|
});
|
|
@@ -362,14 +392,14 @@ describe('MDXParser', () => {
|
|
|
362
392
|
|
|
363
393
|
test('heading is an h1 element', () => {
|
|
364
394
|
const doc = parse('# Heading\n\nSome paragraph text.\n\n<Card>content</Card>\n\nMore paragraph text.');
|
|
365
|
-
const heading = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'h1');
|
|
395
|
+
const heading = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'h1');
|
|
366
396
|
expect(heading).toBeDefined();
|
|
367
397
|
expect(heading!.type).toBe('starttag');
|
|
368
398
|
});
|
|
369
399
|
|
|
370
400
|
test('Card element is authored', () => {
|
|
371
401
|
const doc = parse('# Heading\n\nSome paragraph text.\n\n<Card>content</Card>\n\nMore paragraph text.');
|
|
372
|
-
const card = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Card');
|
|
402
|
+
const card = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Card');
|
|
373
403
|
expect(card?.elementType).toBe('authored');
|
|
374
404
|
});
|
|
375
405
|
});
|
|
@@ -377,20 +407,20 @@ describe('MDXParser', () => {
|
|
|
377
407
|
describe('Boolean attributes in JSX', () => {
|
|
378
408
|
test('boolean attributes have empty value', () => {
|
|
379
409
|
const doc = parse('<Input disabled required name="email" />');
|
|
380
|
-
const startTag = doc.nodeList.find(n => n?.type === 'starttag');
|
|
410
|
+
const startTag = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag');
|
|
381
411
|
expect(startTag).toBeDefined();
|
|
382
412
|
const attrs = startTag!.attributes;
|
|
383
413
|
expect(attrs).toHaveLength(3);
|
|
384
414
|
|
|
385
|
-
const disabled = attrs.find(a => a.type === 'attr' && a.name.raw === 'disabled');
|
|
415
|
+
const disabled = attrs.find((a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'disabled');
|
|
386
416
|
expect(disabled).toBeDefined();
|
|
387
417
|
expect(disabled!.value.raw).toBe('');
|
|
388
418
|
|
|
389
|
-
const required = attrs.find(a => a.type === 'attr' && a.name.raw === 'required');
|
|
419
|
+
const required = attrs.find((a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'required');
|
|
390
420
|
expect(required).toBeDefined();
|
|
391
421
|
expect(required!.value.raw).toBe('');
|
|
392
422
|
|
|
393
|
-
const name = attrs.find(a => a.type === 'attr' && a.name.raw === 'name');
|
|
423
|
+
const name = attrs.find((a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'name');
|
|
394
424
|
expect(name).toBeDefined();
|
|
395
425
|
expect(name!.value.raw).toBe('email');
|
|
396
426
|
});
|
|
@@ -415,9 +445,9 @@ describe('MDXParser', () => {
|
|
|
415
445
|
|
|
416
446
|
test('void elements are html type', () => {
|
|
417
447
|
const doc = parse('<br />\n<hr />\n<img src="test.png" alt="test" />');
|
|
418
|
-
const br = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'br');
|
|
419
|
-
const hr = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'hr');
|
|
420
|
-
const img = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'img');
|
|
448
|
+
const br = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'br');
|
|
449
|
+
const hr = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'hr');
|
|
450
|
+
const img = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'img');
|
|
421
451
|
expect(br?.elementType).toBe('html');
|
|
422
452
|
expect(hr?.elementType).toBe('html');
|
|
423
453
|
expect(img?.elementType).toBe('html');
|
|
@@ -425,15 +455,15 @@ describe('MDXParser', () => {
|
|
|
425
455
|
|
|
426
456
|
test('img element has attributes', () => {
|
|
427
457
|
const doc = parse('<br />\n<hr />\n<img src="test.png" alt="test" />');
|
|
428
|
-
const img = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'img');
|
|
458
|
+
const img = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'img');
|
|
429
459
|
expect(img).toBeDefined();
|
|
430
460
|
expect(img!.attributes).toHaveLength(2);
|
|
431
461
|
|
|
432
|
-
const src = img!.attributes.find(a => a.type === 'attr' && a.name.raw === 'src');
|
|
462
|
+
const src = img!.attributes.find((a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'src');
|
|
433
463
|
expect(src).toBeDefined();
|
|
434
464
|
expect(src!.value.raw).toBe('test.png');
|
|
435
465
|
|
|
436
|
-
const alt = img!.attributes.find(a => a.type === 'attr' && a.name.raw === 'alt');
|
|
466
|
+
const alt = img!.attributes.find((a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'alt');
|
|
437
467
|
expect(alt).toBeDefined();
|
|
438
468
|
expect(alt!.value.raw).toBe('test');
|
|
439
469
|
});
|
|
@@ -448,14 +478,14 @@ describe('MDXParser', () => {
|
|
|
448
478
|
|
|
449
479
|
test('empty component is authored element', () => {
|
|
450
480
|
const doc = parse('<Spacer />');
|
|
451
|
-
const startTag = doc.nodeList.find(n => n?.type === 'starttag');
|
|
481
|
+
const startTag = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag');
|
|
452
482
|
expect(startTag?.elementType).toBe('authored');
|
|
453
483
|
expect(startTag?.nodeName).toBe('Spacer');
|
|
454
484
|
});
|
|
455
485
|
|
|
456
486
|
test('empty component has no attributes', () => {
|
|
457
487
|
const doc = parse('<Spacer />');
|
|
458
|
-
const startTag = doc.nodeList.find(n => n?.type === 'starttag');
|
|
488
|
+
const startTag = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag');
|
|
459
489
|
expect(startTag).toBeDefined();
|
|
460
490
|
expect(startTag!.attributes).toHaveLength(0);
|
|
461
491
|
});
|
|
@@ -464,51 +494,61 @@ describe('MDXParser', () => {
|
|
|
464
494
|
describe('Link and image references', () => {
|
|
465
495
|
test('linkReference resolves to <a> element', () => {
|
|
466
496
|
const doc = parse('[link text][ref]\n\n[ref]: https://example.com "Example"\n');
|
|
467
|
-
const a = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'a');
|
|
497
|
+
const a = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'a');
|
|
468
498
|
expect(a).toBeDefined();
|
|
469
|
-
const href = a!.attributes.find(
|
|
499
|
+
const href = a!.attributes.find(
|
|
500
|
+
(attr): attr is MLASTHTMLAttr => attr.type === 'attr' && attr.name.raw === 'href',
|
|
501
|
+
);
|
|
470
502
|
expect(href).toBeDefined();
|
|
471
503
|
expect(href!.value.raw).toBe('https://example.com');
|
|
472
|
-
const title = a!.attributes.find(
|
|
504
|
+
const title = a!.attributes.find(
|
|
505
|
+
(attr): attr is MLASTHTMLAttr => attr.type === 'attr' && attr.name.raw === 'title',
|
|
506
|
+
);
|
|
473
507
|
expect(title).toBeDefined();
|
|
474
508
|
expect(title!.value.raw).toBe('Example');
|
|
475
509
|
});
|
|
476
510
|
|
|
477
511
|
test('linkReference without title does NOT have title attribute', () => {
|
|
478
512
|
const doc = parse('[link text][ref]\n\n[ref]: https://example.com\n');
|
|
479
|
-
const a = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'a');
|
|
513
|
+
const a = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'a');
|
|
480
514
|
expect(a).toBeDefined();
|
|
481
|
-
const title = a!.attributes.find(
|
|
515
|
+
const title = a!.attributes.find(
|
|
516
|
+
(attr): attr is MLASTHTMLAttr => attr.type === 'attr' && attr.name.raw === 'title',
|
|
517
|
+
);
|
|
482
518
|
expect(title).toBeUndefined();
|
|
483
519
|
expect(a!.attributes.length).toBe(1);
|
|
484
520
|
});
|
|
485
521
|
|
|
486
522
|
test('imageReference resolves to <img> element with alt', () => {
|
|
487
523
|
const doc = parse('![alt text][img]\n\n[img]: image.png\n');
|
|
488
|
-
const img = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'img');
|
|
524
|
+
const img = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'img');
|
|
489
525
|
expect(img).toBeDefined();
|
|
490
|
-
const src = img!.attributes.find(
|
|
526
|
+
const src = img!.attributes.find(
|
|
527
|
+
(attr): attr is MLASTHTMLAttr => attr.type === 'attr' && attr.name.raw === 'src',
|
|
528
|
+
);
|
|
491
529
|
expect(src).toBeDefined();
|
|
492
530
|
expect(src!.value.raw).toBe('image.png');
|
|
493
|
-
const alt = img!.attributes.find(
|
|
531
|
+
const alt = img!.attributes.find(
|
|
532
|
+
(attr): attr is MLASTHTMLAttr => attr.type === 'attr' && attr.name.raw === 'alt',
|
|
533
|
+
);
|
|
494
534
|
expect(alt).toBeDefined();
|
|
495
535
|
expect(alt!.value.raw).toBe('alt text');
|
|
496
536
|
});
|
|
497
537
|
|
|
498
538
|
test('unresolved linkReference is treated as plain text', () => {
|
|
499
539
|
const doc = parse('[text][missing]\n');
|
|
500
|
-
const a = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'a');
|
|
540
|
+
const a = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'a');
|
|
501
541
|
expect(a).toBeUndefined();
|
|
502
|
-
const text = doc.nodeList.find(n => n?.type === 'text');
|
|
542
|
+
const text = doc.nodeList.find((n): n is MLASTText => n?.type === 'text');
|
|
503
543
|
expect(text).toBeDefined();
|
|
504
544
|
expect(text!.raw).toContain('[text][missing]');
|
|
505
545
|
});
|
|
506
546
|
|
|
507
547
|
test('unresolved imageReference is treated as plain text', () => {
|
|
508
548
|
const doc = parse('![alt][missing]\n');
|
|
509
|
-
const img = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'img');
|
|
549
|
+
const img = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'img');
|
|
510
550
|
expect(img).toBeUndefined();
|
|
511
|
-
const text = doc.nodeList.find(n => n?.type === 'text');
|
|
551
|
+
const text = doc.nodeList.find((n): n is MLASTText => n?.type === 'text');
|
|
512
552
|
expect(text).toBeDefined();
|
|
513
553
|
expect(text!.raw).toContain('![alt][missing]');
|
|
514
554
|
});
|
|
@@ -517,9 +557,11 @@ describe('MDXParser', () => {
|
|
|
517
557
|
describe('Blockquote with JSX', () => {
|
|
518
558
|
test('JSX inside blockquote is parsed correctly', () => {
|
|
519
559
|
const doc = parse('> <Badge>test</Badge>\n');
|
|
520
|
-
const blockquote = doc.nodeList.find(
|
|
560
|
+
const blockquote = doc.nodeList.find(
|
|
561
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'blockquote',
|
|
562
|
+
);
|
|
521
563
|
expect(blockquote).toBeDefined();
|
|
522
|
-
const badge = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Badge');
|
|
564
|
+
const badge = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Badge');
|
|
523
565
|
expect(badge).toBeDefined();
|
|
524
566
|
expect(badge!.elementType).toBe('authored');
|
|
525
567
|
const text = badge!.childNodes.find(c => c.type === 'text');
|
|
@@ -536,7 +578,7 @@ describe('MDXParser', () => {
|
|
|
536
578
|
|
|
537
579
|
test('JSX comment is valid alternative to HTML comment', () => {
|
|
538
580
|
const doc = parse('{/* comment */}\n');
|
|
539
|
-
const psblock = doc.nodeList.find(n => n?.type === 'psblock');
|
|
581
|
+
const psblock = doc.nodeList.find((n): n is MLASTPreprocessorSpecificBlock => n?.type === 'psblock');
|
|
540
582
|
expect(psblock).toBeDefined();
|
|
541
583
|
expect(psblock!.raw).toContain('/* comment */');
|
|
542
584
|
});
|
|
@@ -545,7 +587,10 @@ describe('MDXParser', () => {
|
|
|
545
587
|
describe('Footnotes', () => {
|
|
546
588
|
test('footnoteReference becomes psblock with correct raw', () => {
|
|
547
589
|
const doc = parse('Text with a note[^1]\n\n[^1]: Footnote content\n');
|
|
548
|
-
const fnRef = doc.nodeList.find(
|
|
590
|
+
const fnRef = doc.nodeList.find(
|
|
591
|
+
(n): n is MLASTPreprocessorSpecificBlock =>
|
|
592
|
+
n?.type === 'psblock' && n.nodeName === '#ps:footnoteReference',
|
|
593
|
+
);
|
|
549
594
|
expect(fnRef).toBeDefined();
|
|
550
595
|
expect(fnRef!.nodeName).toBe('#ps:footnoteReference');
|
|
551
596
|
expect(fnRef!.raw).toBe('[^1]');
|
|
@@ -553,7 +598,10 @@ describe('MDXParser', () => {
|
|
|
553
598
|
|
|
554
599
|
test('footnoteDefinition becomes psblock with correct raw', () => {
|
|
555
600
|
const doc = parse('Text with a note[^1]\n\n[^1]: Footnote content\n');
|
|
556
|
-
const fnDef = doc.nodeList.find(
|
|
601
|
+
const fnDef = doc.nodeList.find(
|
|
602
|
+
(n): n is MLASTPreprocessorSpecificBlock =>
|
|
603
|
+
n?.type === 'psblock' && n.nodeName === '#ps:footnoteDefinition',
|
|
604
|
+
);
|
|
557
605
|
expect(fnDef).toBeDefined();
|
|
558
606
|
expect(fnDef!.nodeName).toBe('#ps:footnoteDefinition');
|
|
559
607
|
expect(fnDef!.raw).toContain('Footnote content');
|
|
@@ -564,27 +612,27 @@ describe('MDXParser', () => {
|
|
|
564
612
|
test('definitions do not leak across MDX parse() calls', () => {
|
|
565
613
|
// First parse: define [ref]
|
|
566
614
|
const doc1 = parse('[link][ref]\n\n[ref]: https://example.com\n');
|
|
567
|
-
const a1 = doc1.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'a');
|
|
615
|
+
const a1 = doc1.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'a');
|
|
568
616
|
expect(a1).toBeDefined();
|
|
569
617
|
|
|
570
618
|
// Second parse: [ref] without definition — should NOT resolve
|
|
571
619
|
const doc2 = parse('[link][ref]\n');
|
|
572
|
-
const a2 = doc2.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'a');
|
|
620
|
+
const a2 = doc2.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'a');
|
|
573
621
|
expect(a2).toBeUndefined();
|
|
574
|
-
const text = doc2.nodeList.find(n => n?.type === 'text');
|
|
622
|
+
const text = doc2.nodeList.find((n): n is MLASTText => n?.type === 'text');
|
|
575
623
|
expect(text).toBeDefined();
|
|
576
624
|
expect(text!.raw).toContain('[link][ref]');
|
|
577
625
|
});
|
|
578
626
|
|
|
579
627
|
test('table header state does not leak across MDX parse() calls', () => {
|
|
580
628
|
const doc1 = parse('| A |\n| - |\n| 1 |\n');
|
|
581
|
-
const ths1 = doc1.nodeList.filter(n => n?.type === 'starttag' && n.nodeName === 'th');
|
|
629
|
+
const ths1 = doc1.nodeList.filter((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'th');
|
|
582
630
|
expect(ths1.length).toBe(1);
|
|
583
631
|
|
|
584
632
|
const doc2 = parse('| B |\n| - |\n| 2 |\n');
|
|
585
|
-
const ths2 = doc2.nodeList.filter(n => n?.type === 'starttag' && n.nodeName === 'th');
|
|
633
|
+
const ths2 = doc2.nodeList.filter((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'th');
|
|
586
634
|
expect(ths2.length).toBe(1);
|
|
587
|
-
const tds2 = doc2.nodeList.filter(n => n?.type === 'starttag' && n.nodeName === 'td');
|
|
635
|
+
const tds2 = doc2.nodeList.filter((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'td');
|
|
588
636
|
expect(tds2.length).toBe(1);
|
|
589
637
|
});
|
|
590
638
|
});
|
|
@@ -592,14 +640,17 @@ describe('MDXParser', () => {
|
|
|
592
640
|
describe('Inline expression in paragraph (flattenMdastChildren)', () => {
|
|
593
641
|
test('paragraph with inline expression is unwrapped', () => {
|
|
594
642
|
const doc = parse('Text {variable} more text');
|
|
595
|
-
const expr = doc.nodeList.find(
|
|
643
|
+
const expr = doc.nodeList.find(
|
|
644
|
+
(n): n is MLASTPreprocessorSpecificBlock =>
|
|
645
|
+
n?.type === 'psblock' && n.nodeName === '#ps:mdxTextExpression',
|
|
646
|
+
);
|
|
596
647
|
expect(expr).toBeDefined();
|
|
597
648
|
expect(expr!.raw).toBe('{variable}');
|
|
598
649
|
});
|
|
599
650
|
|
|
600
651
|
test('paragraph without JSX or expressions is preserved as <p>', () => {
|
|
601
652
|
const doc = parse('Just plain paragraph text.');
|
|
602
|
-
const p = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'p');
|
|
653
|
+
const p = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'p');
|
|
603
654
|
expect(p).toBeDefined();
|
|
604
655
|
// No unwrapping happened — text is inside <p>
|
|
605
656
|
const text = p!.childNodes.find(c => c.type === 'text');
|
|
@@ -611,37 +662,43 @@ describe('MDXParser', () => {
|
|
|
611
662
|
describe('MDX inline code and code blocks', () => {
|
|
612
663
|
test('inline code in MDX produces <code> element', () => {
|
|
613
664
|
const doc = parse('Use `const x = 1` here');
|
|
614
|
-
const code = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'code');
|
|
665
|
+
const code = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'code');
|
|
615
666
|
expect(code).toBeDefined();
|
|
616
667
|
expect(code!.childNodes.length).toBe(1);
|
|
617
|
-
expect(code!.childNodes[0]
|
|
668
|
+
expect(code!.childNodes[0]!.raw).toBe('const x = 1');
|
|
618
669
|
});
|
|
619
670
|
|
|
620
671
|
test('fenced code block in MDX produces pre>code elements', () => {
|
|
621
672
|
const doc = parse('```typescript\nconst x: number = 1;\n```\n');
|
|
622
|
-
const pre = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'pre');
|
|
673
|
+
const pre = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'pre');
|
|
623
674
|
expect(pre).toBeDefined();
|
|
624
|
-
const code = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'code');
|
|
675
|
+
const code = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'code');
|
|
625
676
|
expect(code).toBeDefined();
|
|
626
|
-
const langAttr = code!.attributes.find(
|
|
677
|
+
const langAttr = code!.attributes.find(
|
|
678
|
+
(a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'class',
|
|
679
|
+
);
|
|
627
680
|
expect(langAttr).toBeDefined();
|
|
628
681
|
expect(langAttr!.value.raw).toBe('language-typescript');
|
|
629
682
|
expect(code!.childNodes.length).toBe(1);
|
|
630
|
-
expect(code!.childNodes[0]
|
|
683
|
+
expect(code!.childNodes[0]!.raw).toBe('const x: number = 1;');
|
|
631
684
|
});
|
|
632
685
|
});
|
|
633
686
|
|
|
634
687
|
describe('Export default', () => {
|
|
635
688
|
test('export default function becomes mdxjsEsm psblock', () => {
|
|
636
689
|
const doc = parse('export default function Layout() {}\n');
|
|
637
|
-
const esm = doc.nodeList.find(
|
|
690
|
+
const esm = doc.nodeList.find(
|
|
691
|
+
(n): n is MLASTPreprocessorSpecificBlock => n?.type === 'psblock' && n.nodeName === '#ps:mdxjsEsm',
|
|
692
|
+
);
|
|
638
693
|
expect(esm).toBeDefined();
|
|
639
694
|
expect(esm!.raw).toContain('export default function Layout');
|
|
640
695
|
});
|
|
641
696
|
|
|
642
697
|
test('export default object expression', () => {
|
|
643
698
|
const doc = parse("export default { title: 'My Page' }\n");
|
|
644
|
-
const esm = doc.nodeList.find(
|
|
699
|
+
const esm = doc.nodeList.find(
|
|
700
|
+
(n): n is MLASTPreprocessorSpecificBlock => n?.type === 'psblock' && n.nodeName === '#ps:mdxjsEsm',
|
|
701
|
+
);
|
|
645
702
|
expect(esm).toBeDefined();
|
|
646
703
|
expect(esm!.raw).toContain('export default');
|
|
647
704
|
});
|
|
@@ -662,17 +719,25 @@ describe('MDXParser', () => {
|
|
|
662
719
|
describe('Multi-line JSX attributes', () => {
|
|
663
720
|
test('attributes spanning multiple lines are parsed correctly', () => {
|
|
664
721
|
const doc = parse('<Component\n name="test"\n value={42}\n disabled\n/>\n');
|
|
665
|
-
const startTag = doc.nodeList.find(
|
|
722
|
+
const startTag = doc.nodeList.find(
|
|
723
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Component',
|
|
724
|
+
);
|
|
666
725
|
expect(startTag).toBeDefined();
|
|
667
726
|
expect(startTag!.attributes.length).toBe(3);
|
|
668
|
-
const name = startTag!.attributes.find(
|
|
727
|
+
const name = startTag!.attributes.find(
|
|
728
|
+
(a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'name',
|
|
729
|
+
);
|
|
669
730
|
expect(name).toBeDefined();
|
|
670
731
|
expect(name!.value.raw).toBe('test');
|
|
671
|
-
const value = startTag!.attributes.find(
|
|
732
|
+
const value = startTag!.attributes.find(
|
|
733
|
+
(a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'value',
|
|
734
|
+
);
|
|
672
735
|
expect(value).toBeDefined();
|
|
673
736
|
expect(value!.isDynamicValue).toBe(true);
|
|
674
737
|
expect(value!.value.raw).toBe('42');
|
|
675
|
-
const disabled = startTag!.attributes.find(
|
|
738
|
+
const disabled = startTag!.attributes.find(
|
|
739
|
+
(a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'disabled',
|
|
740
|
+
);
|
|
676
741
|
expect(disabled).toBeDefined();
|
|
677
742
|
expect(disabled!.value.raw).toBe('');
|
|
678
743
|
});
|
|
@@ -681,13 +746,17 @@ describe('MDXParser', () => {
|
|
|
681
746
|
describe('JSX children templates', () => {
|
|
682
747
|
test('nested component structure (Card > CardHeader + CardBody)', () => {
|
|
683
748
|
const doc = parse('<Card>\n <CardHeader>Title</CardHeader>\n <CardBody>Content</CardBody>\n</Card>\n');
|
|
684
|
-
const card = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Card');
|
|
749
|
+
const card = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Card');
|
|
685
750
|
expect(card).toBeDefined();
|
|
686
751
|
expect(card!.elementType).toBe('authored');
|
|
687
|
-
const header = doc.nodeList.find(
|
|
752
|
+
const header = doc.nodeList.find(
|
|
753
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'CardHeader',
|
|
754
|
+
);
|
|
688
755
|
expect(header).toBeDefined();
|
|
689
756
|
expect(header!.elementType).toBe('authored');
|
|
690
|
-
const body = doc.nodeList.find(
|
|
757
|
+
const body = doc.nodeList.find(
|
|
758
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'CardBody',
|
|
759
|
+
);
|
|
691
760
|
expect(body).toBeDefined();
|
|
692
761
|
expect(body!.elementType).toBe('authored');
|
|
693
762
|
});
|
|
@@ -696,11 +765,13 @@ describe('MDXParser', () => {
|
|
|
696
765
|
describe('Markdown inside JSX', () => {
|
|
697
766
|
test('Markdown heading inside JSX component', () => {
|
|
698
767
|
const doc = parse('<Callout>\n\n## Warning\n\nBe careful\n\n</Callout>\n');
|
|
699
|
-
const callout = doc.nodeList.find(
|
|
768
|
+
const callout = doc.nodeList.find(
|
|
769
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Callout',
|
|
770
|
+
);
|
|
700
771
|
expect(callout).toBeDefined();
|
|
701
|
-
const h2 = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'h2');
|
|
772
|
+
const h2 = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'h2');
|
|
702
773
|
expect(h2).toBeDefined();
|
|
703
|
-
const p = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'p');
|
|
774
|
+
const p = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'p');
|
|
704
775
|
expect(p).toBeDefined();
|
|
705
776
|
});
|
|
706
777
|
});
|
|
@@ -708,7 +779,9 @@ describe('MDXParser', () => {
|
|
|
708
779
|
describe('Multiple inline JSX in paragraph', () => {
|
|
709
780
|
test('multiple JSX elements in same paragraph', () => {
|
|
710
781
|
const doc = parse('<Badge>A</Badge> and <Badge>B</Badge>\n');
|
|
711
|
-
const badges = doc.nodeList.filter(
|
|
782
|
+
const badges = doc.nodeList.filter(
|
|
783
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Badge',
|
|
784
|
+
);
|
|
712
785
|
expect(badges.length).toBe(2);
|
|
713
786
|
const texts = badges.map(b => b.childNodes.find(c => c.type === 'text')?.raw);
|
|
714
787
|
expect(texts).toStrictEqual(['A', 'B']);
|
|
@@ -718,14 +791,16 @@ describe('MDXParser', () => {
|
|
|
718
791
|
describe('Empty open/close tags (no children)', () => {
|
|
719
792
|
test('element with explicit closing tag but no children', () => {
|
|
720
793
|
const doc = parse('<div></div>\n');
|
|
721
|
-
const div = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'div');
|
|
794
|
+
const div = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'div');
|
|
722
795
|
expect(div).toBeDefined();
|
|
723
796
|
expect(div!.elementType).toBe('html');
|
|
724
797
|
});
|
|
725
798
|
|
|
726
799
|
test('authored component with explicit closing tag but no children', () => {
|
|
727
800
|
const doc = parse('<Container></Container>\n');
|
|
728
|
-
const el = doc.nodeList.find(
|
|
801
|
+
const el = doc.nodeList.find(
|
|
802
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Container',
|
|
803
|
+
);
|
|
729
804
|
expect(el).toBeDefined();
|
|
730
805
|
expect(el!.elementType).toBe('authored');
|
|
731
806
|
});
|
|
@@ -734,9 +809,13 @@ describe('MDXParser', () => {
|
|
|
734
809
|
describe('Expression attribute with complex value', () => {
|
|
735
810
|
test('ternary expression in attribute value', () => {
|
|
736
811
|
const doc = parse('<Component value={isActive ? "yes" : "no"} />\n');
|
|
737
|
-
const startTag = doc.nodeList.find(
|
|
812
|
+
const startTag = doc.nodeList.find(
|
|
813
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Component',
|
|
814
|
+
);
|
|
738
815
|
expect(startTag).toBeDefined();
|
|
739
|
-
const attr = startTag!.attributes.find(
|
|
816
|
+
const attr = startTag!.attributes.find(
|
|
817
|
+
(a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'value',
|
|
818
|
+
);
|
|
740
819
|
expect(attr).toBeDefined();
|
|
741
820
|
expect(attr!.isDynamicValue).toBe(true);
|
|
742
821
|
expect(attr!.value.raw).toBe('isActive ? "yes" : "no"');
|
|
@@ -744,9 +823,13 @@ describe('MDXParser', () => {
|
|
|
744
823
|
|
|
745
824
|
test('array expression in attribute value', () => {
|
|
746
825
|
const doc = parse('<Component items={[1, 2, 3]} />\n');
|
|
747
|
-
const startTag = doc.nodeList.find(
|
|
826
|
+
const startTag = doc.nodeList.find(
|
|
827
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Component',
|
|
828
|
+
);
|
|
748
829
|
expect(startTag).toBeDefined();
|
|
749
|
-
const attr = startTag!.attributes.find(
|
|
830
|
+
const attr = startTag!.attributes.find(
|
|
831
|
+
(a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'items',
|
|
832
|
+
);
|
|
750
833
|
expect(attr).toBeDefined();
|
|
751
834
|
expect(attr!.isDynamicValue).toBe(true);
|
|
752
835
|
expect(attr!.value.raw).toBe('[1, 2, 3]');
|
|
@@ -756,9 +839,11 @@ describe('MDXParser', () => {
|
|
|
756
839
|
describe('IDL attribute name conversion', () => {
|
|
757
840
|
test('className is kept as-is (IDL resolution handled by ml-core)', () => {
|
|
758
841
|
const doc = parse('<div className="test">text</div>\n');
|
|
759
|
-
const div = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'div');
|
|
842
|
+
const div = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'div');
|
|
760
843
|
expect(div).toBeDefined();
|
|
761
|
-
const attr = div!.attributes.find(
|
|
844
|
+
const attr = div!.attributes.find(
|
|
845
|
+
(a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'className',
|
|
846
|
+
);
|
|
762
847
|
expect(attr).toBeDefined();
|
|
763
848
|
// potentialName is not set by the parser; IDL resolution is handled by ml-core's acceptedAttrNames
|
|
764
849
|
expect(attr!.potentialName).toBeUndefined();
|
|
@@ -766,9 +851,11 @@ describe('MDXParser', () => {
|
|
|
766
851
|
|
|
767
852
|
test('htmlFor is kept as-is (IDL resolution handled by ml-core)', () => {
|
|
768
853
|
const doc = parse('<label htmlFor="input-id">Label</label>\n');
|
|
769
|
-
const label = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'label');
|
|
854
|
+
const label = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'label');
|
|
770
855
|
expect(label).toBeDefined();
|
|
771
|
-
const attr = label!.attributes.find(
|
|
856
|
+
const attr = label!.attributes.find(
|
|
857
|
+
(a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'htmlFor',
|
|
858
|
+
);
|
|
772
859
|
expect(attr).toBeDefined();
|
|
773
860
|
// potentialName is not set by the parser; IDL resolution is handled by ml-core's acceptedAttrNames
|
|
774
861
|
expect(attr!.potentialName).toBeUndefined();
|
|
@@ -776,9 +863,9 @@ describe('MDXParser', () => {
|
|
|
776
863
|
|
|
777
864
|
test('class in JSX is kept as-is (IDL resolution handled by ml-core)', () => {
|
|
778
865
|
const doc = parse('<div class="test">text</div>\n');
|
|
779
|
-
const div = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'div');
|
|
866
|
+
const div = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'div');
|
|
780
867
|
expect(div).toBeDefined();
|
|
781
|
-
const attr = div!.attributes.find(a => a.type === 'attr' && a.name.raw === 'class');
|
|
868
|
+
const attr = div!.attributes.find((a): a is MLASTHTMLAttr => a.type === 'attr' && a.name.raw === 'class');
|
|
782
869
|
expect(attr).toBeDefined();
|
|
783
870
|
// candidate is not set by the parser; IDL resolution is handled by ml-core's acceptedAttrNames
|
|
784
871
|
expect(attr!.candidate).toBeUndefined();
|
|
@@ -788,21 +875,21 @@ describe('MDXParser', () => {
|
|
|
788
875
|
describe('MDX Markdown elements', () => {
|
|
789
876
|
test('unordered list in MDX', () => {
|
|
790
877
|
const doc = parse('- item 1\n- item 2\n');
|
|
791
|
-
const ul = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'ul');
|
|
878
|
+
const ul = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'ul');
|
|
792
879
|
expect(ul).toBeDefined();
|
|
793
|
-
const lis = doc.nodeList.filter(n => n?.type === 'starttag' && n.nodeName === 'li');
|
|
880
|
+
const lis = doc.nodeList.filter((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'li');
|
|
794
881
|
expect(lis.length).toBe(2);
|
|
795
882
|
});
|
|
796
883
|
|
|
797
884
|
test('thematicBreak in MDX (with content around it to avoid frontmatter)', () => {
|
|
798
885
|
const doc = parse('Text above\n\n---\n\nText below\n');
|
|
799
|
-
const hr = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'hr');
|
|
886
|
+
const hr = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'hr');
|
|
800
887
|
expect(hr).toBeDefined();
|
|
801
888
|
});
|
|
802
889
|
|
|
803
890
|
test('hard line break in MDX (two trailing spaces)', () => {
|
|
804
891
|
const doc = parse('line one \nline two\n');
|
|
805
|
-
const br = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'br');
|
|
892
|
+
const br = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'br');
|
|
806
893
|
expect(br).toBeDefined();
|
|
807
894
|
});
|
|
808
895
|
});
|
|
@@ -810,9 +897,11 @@ describe('MDXParser', () => {
|
|
|
810
897
|
describe('List with JSX content', () => {
|
|
811
898
|
test('list items containing JSX components', () => {
|
|
812
899
|
const doc = parse('- <Badge>item 1</Badge>\n- <Badge>item 2</Badge>\n');
|
|
813
|
-
const ul = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'ul');
|
|
900
|
+
const ul = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'ul');
|
|
814
901
|
expect(ul).toBeDefined();
|
|
815
|
-
const badges = doc.nodeList.filter(
|
|
902
|
+
const badges = doc.nodeList.filter(
|
|
903
|
+
(n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Badge',
|
|
904
|
+
);
|
|
816
905
|
expect(badges.length).toBe(2);
|
|
817
906
|
});
|
|
818
907
|
});
|
|
@@ -820,9 +909,9 @@ describe('MDXParser', () => {
|
|
|
820
909
|
describe('Heading with JSX', () => {
|
|
821
910
|
test('heading containing inline JSX component', () => {
|
|
822
911
|
const doc = parse('# Hello <Badge>New</Badge>\n');
|
|
823
|
-
const h1 = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'h1');
|
|
912
|
+
const h1 = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'h1');
|
|
824
913
|
expect(h1).toBeDefined();
|
|
825
|
-
const badge = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Badge');
|
|
914
|
+
const badge = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Badge');
|
|
826
915
|
expect(badge).toBeDefined();
|
|
827
916
|
expect(badge!.elementType).toBe('authored');
|
|
828
917
|
});
|
|
@@ -831,9 +920,9 @@ describe('MDXParser', () => {
|
|
|
831
920
|
describe('Deep nesting JSX', () => {
|
|
832
921
|
test('three levels of nested JSX components', () => {
|
|
833
922
|
const doc = parse('<Level1>\n <Level2>\n <Level3>deep</Level3>\n </Level2>\n</Level1>\n');
|
|
834
|
-
const l1 = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Level1');
|
|
835
|
-
const l2 = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Level2');
|
|
836
|
-
const l3 = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Level3');
|
|
923
|
+
const l1 = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Level1');
|
|
924
|
+
const l2 = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Level2');
|
|
925
|
+
const l3 = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Level3');
|
|
837
926
|
expect(l1).toBeDefined();
|
|
838
927
|
expect(l2).toBeDefined();
|
|
839
928
|
expect(l3).toBeDefined();
|
|
@@ -848,16 +937,16 @@ describe('MDXParser', () => {
|
|
|
848
937
|
const doc = parse('{a}\n\n{b}\n\n{c}\n');
|
|
849
938
|
const exprs = doc.nodeList.filter(n => n?.type === 'psblock' && n.raw.startsWith('{'));
|
|
850
939
|
expect(exprs.length).toBe(3);
|
|
851
|
-
expect(exprs[0]
|
|
852
|
-
expect(exprs[1]
|
|
853
|
-
expect(exprs[2]
|
|
940
|
+
expect(exprs[0]!.raw).toBe('{a}');
|
|
941
|
+
expect(exprs[1]!.raw).toBe('{b}');
|
|
942
|
+
expect(exprs[2]!.raw).toBe('{c}');
|
|
854
943
|
});
|
|
855
944
|
});
|
|
856
945
|
|
|
857
946
|
describe('Empty expression', () => {
|
|
858
947
|
test('expression with only a comment', () => {
|
|
859
948
|
const doc = parse('{/* empty */}\n');
|
|
860
|
-
const expr = doc.nodeList.find(n => n?.type === 'psblock');
|
|
949
|
+
const expr = doc.nodeList.find((n): n is MLASTPreprocessorSpecificBlock => n?.type === 'psblock');
|
|
861
950
|
expect(expr).toBeDefined();
|
|
862
951
|
expect(expr!.raw).toBe('{/* empty */}');
|
|
863
952
|
});
|
|
@@ -866,11 +955,13 @@ describe('MDXParser', () => {
|
|
|
866
955
|
describe('GFM autolink with JSX', () => {
|
|
867
956
|
test('autolink URL and JSX component in same paragraph', () => {
|
|
868
957
|
const doc = parse('Visit https://example.com and <Badge>click</Badge>\n');
|
|
869
|
-
const badge = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'Badge');
|
|
958
|
+
const badge = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'Badge');
|
|
870
959
|
expect(badge).toBeDefined();
|
|
871
|
-
const a = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'a');
|
|
960
|
+
const a = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'a');
|
|
872
961
|
expect(a).toBeDefined();
|
|
873
|
-
const href = a!.attributes.find(
|
|
962
|
+
const href = a!.attributes.find(
|
|
963
|
+
(attr): attr is MLASTHTMLAttr => attr.type === 'attr' && attr.name.raw === 'href',
|
|
964
|
+
);
|
|
874
965
|
expect(href).toBeDefined();
|
|
875
966
|
expect(href!.value.raw).toBe('https://example.com');
|
|
876
967
|
});
|
|
@@ -879,9 +970,9 @@ describe('MDXParser', () => {
|
|
|
879
970
|
describe('flattenMdastChildren: expression-only paragraph', () => {
|
|
880
971
|
test('paragraph containing only an expression is unwrapped (no <p>)', () => {
|
|
881
972
|
const doc = parse('{variable}\n');
|
|
882
|
-
const expr = doc.nodeList.find(n => n?.type === 'psblock');
|
|
973
|
+
const expr = doc.nodeList.find((n): n is MLASTPreprocessorSpecificBlock => n?.type === 'psblock');
|
|
883
974
|
expect(expr).toBeDefined();
|
|
884
|
-
const p = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'p');
|
|
975
|
+
const p = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'p');
|
|
885
976
|
expect(p).toBeUndefined();
|
|
886
977
|
});
|
|
887
978
|
});
|
|
@@ -889,17 +980,17 @@ describe('MDXParser', () => {
|
|
|
889
980
|
describe('GFM extensions', () => {
|
|
890
981
|
test('GFM table produces table>tr>th/td elements', () => {
|
|
891
982
|
const doc = parse('| A | B |\n| - | - |\n| 1 | 2 |\n');
|
|
892
|
-
const table = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'table');
|
|
983
|
+
const table = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'table');
|
|
893
984
|
expect(table).toBeDefined();
|
|
894
|
-
const ths = doc.nodeList.filter(n => n?.type === 'starttag' && n.nodeName === 'th');
|
|
985
|
+
const ths = doc.nodeList.filter((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'th');
|
|
895
986
|
expect(ths.length).toBe(2);
|
|
896
|
-
const tds = doc.nodeList.filter(n => n?.type === 'starttag' && n.nodeName === 'td');
|
|
987
|
+
const tds = doc.nodeList.filter((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'td');
|
|
897
988
|
expect(tds.length).toBe(2);
|
|
898
989
|
});
|
|
899
990
|
|
|
900
991
|
test('GFM table header cells contain correct text', () => {
|
|
901
992
|
const doc = parse('| Name | Age |\n| - | - |\n| Alice | 30 |\n');
|
|
902
|
-
const ths = doc.nodeList.filter(n => n?.type === 'starttag' && n.nodeName === 'th');
|
|
993
|
+
const ths = doc.nodeList.filter((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'th');
|
|
903
994
|
expect(ths.length).toBe(2);
|
|
904
995
|
const thTexts = ths.map(th => th.childNodes.find(c => c.type === 'text')?.raw);
|
|
905
996
|
expect(thTexts).toStrictEqual(['Name', 'Age']);
|
|
@@ -907,21 +998,21 @@ describe('MDXParser', () => {
|
|
|
907
998
|
|
|
908
999
|
test('GFM table with only header row has 0 td cells', () => {
|
|
909
1000
|
const doc = parse('| A | B |\n| - | - |\n');
|
|
910
|
-
const ths = doc.nodeList.filter(n => n?.type === 'starttag' && n.nodeName === 'th');
|
|
1001
|
+
const ths = doc.nodeList.filter((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'th');
|
|
911
1002
|
expect(ths.length).toBe(2);
|
|
912
|
-
const tds = doc.nodeList.filter(n => n?.type === 'starttag' && n.nodeName === 'td');
|
|
1003
|
+
const tds = doc.nodeList.filter((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'td');
|
|
913
1004
|
expect(tds.length).toBe(0);
|
|
914
1005
|
});
|
|
915
1006
|
|
|
916
1007
|
test('GFM strikethrough becomes <del> element', () => {
|
|
917
1008
|
const doc = parse('~~deleted~~\n');
|
|
918
|
-
const del = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'del');
|
|
1009
|
+
const del = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'del');
|
|
919
1010
|
expect(del).toBeDefined();
|
|
920
1011
|
});
|
|
921
1012
|
|
|
922
1013
|
test('GFM strikethrough contains correct text content', () => {
|
|
923
1014
|
const doc = parse('~~deleted text~~\n');
|
|
924
|
-
const del = doc.nodeList.find(n => n?.type === 'starttag' && n.nodeName === 'del');
|
|
1015
|
+
const del = doc.nodeList.find((n): n is MLASTElement => n?.type === 'starttag' && n.nodeName === 'del');
|
|
925
1016
|
expect(del).toBeDefined();
|
|
926
1017
|
const text = del!.childNodes.find(c => c.type === 'text');
|
|
927
1018
|
expect(text).toBeDefined();
|
|
@@ -939,7 +1030,7 @@ describe('MDXParser', () => {
|
|
|
939
1030
|
|
|
940
1031
|
test('script body wrapped in template literal expression child', () => {
|
|
941
1032
|
const doc = parse('# title\n\n<div><script>{`const t = s.replace(/<br\\s*\\/?>/gi, " ");`}</script></div>');
|
|
942
|
-
const tags = doc.nodeList.filter(n => n?.type === 'starttag' || n?.type === 'endtag');
|
|
1033
|
+
const tags = doc.nodeList.filter((n): n is MLASTTag => n?.type === 'starttag' || n?.type === 'endtag');
|
|
943
1034
|
const tagSig = tags.map(t => `${t!.type}:${t!.nodeName}`);
|
|
944
1035
|
expect(tagSig).toContain('starttag:script');
|
|
945
1036
|
expect(tagSig).toContain('endtag:script');
|
|
@@ -947,7 +1038,7 @@ describe('MDXParser', () => {
|
|
|
947
1038
|
|
|
948
1039
|
test('style body wrapped in template literal expression child', () => {
|
|
949
1040
|
const doc = parse('# title\n\n<div><style>{`/* <br = */ a{color:red}`}</style></div>');
|
|
950
|
-
const tags = doc.nodeList.filter(n => n?.type === 'starttag' || n?.type === 'endtag');
|
|
1041
|
+
const tags = doc.nodeList.filter((n): n is MLASTTag => n?.type === 'starttag' || n?.type === 'endtag');
|
|
951
1042
|
const tagSig = tags.map(t => `${t!.type}:${t!.nodeName}`);
|
|
952
1043
|
expect(tagSig).toContain('starttag:style');
|
|
953
1044
|
expect(tagSig).toContain('endtag:style');
|