@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/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(n => n?.type === 'starttag' && n.nodeName === 'strong');
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(attr => attr.type === 'attr' && attr.name.raw === 'href');
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('![alt text](image.png)');
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(attr => attr.type === 'attr' && attr.name.raw === 'src');
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(attr => attr.type === 'attr' && attr.name.raw === 'alt');
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].type).toBe('spread');
250
- expect(attrs[0].raw).toBe('{...props}');
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(n => n?.type === 'starttag' && n.nodeName === 'Outer');
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(attr => attr.type === 'attr' && attr.name.raw === 'href');
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(attr => attr.type === 'attr' && attr.name.raw === 'title');
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(attr => attr.type === 'attr' && attr.name.raw === 'title');
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(attr => attr.type === 'attr' && attr.name.raw === 'src');
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(attr => attr.type === 'attr' && attr.name.raw === 'alt');
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(n => n?.type === 'starttag' && n.nodeName === 'blockquote');
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(n => n?.type === 'psblock' && n.nodeName === '#ps:footnoteReference');
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(n => n?.type === 'psblock' && n.nodeName === '#ps:footnoteDefinition');
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(n => n?.type === 'psblock' && n.nodeName === '#ps:mdxTextExpression');
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].raw).toBe('const x = 1');
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(a => a.type === 'attr' && a.name.raw === 'class');
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].raw).toBe('const x: number = 1;');
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(n => n?.type === 'psblock' && n.nodeName === '#ps:mdxjsEsm');
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(n => n?.type === 'psblock' && n.nodeName === '#ps:mdxjsEsm');
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(n => n?.type === 'starttag' && n.nodeName === 'Component');
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(a => a.type === 'attr' && a.name.raw === 'name');
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(a => a.type === 'attr' && a.name.raw === 'value');
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(a => a.type === 'attr' && a.name.raw === 'disabled');
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(n => n?.type === 'starttag' && n.nodeName === 'CardHeader');
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(n => n?.type === 'starttag' && n.nodeName === 'CardBody');
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(n => n?.type === 'starttag' && n.nodeName === 'Callout');
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(n => n?.type === 'starttag' && n.nodeName === 'Badge');
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(n => n?.type === 'starttag' && n.nodeName === 'Container');
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(n => n?.type === 'starttag' && n.nodeName === 'Component');
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(a => a.type === 'attr' && a.name.raw === 'value');
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(n => n?.type === 'starttag' && n.nodeName === 'Component');
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(a => a.type === 'attr' && a.name.raw === 'items');
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(a => a.type === 'attr' && a.name.raw === 'className');
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(a => a.type === 'attr' && a.name.raw === 'htmlFor');
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(n => n?.type === 'starttag' && n.nodeName === 'Badge');
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].raw).toBe('{a}');
844
- expect(exprs[1].raw).toBe('{b}');
845
- expect(exprs[2].raw).toBe('{c}');
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(attr => attr.type === 'attr' && attr.name.raw === 'href');
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');