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