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