safe-mdx 1.1.0 → 1.2.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/README.md +109 -3
- package/dist/dynamic-esm-component.d.ts +6 -0
- package/dist/dynamic-esm-component.d.ts.map +1 -0
- package/dist/dynamic-esm-component.js +58 -0
- package/dist/dynamic-esm-component.js.map +1 -0
- package/dist/esm-parser.d.ts +19 -0
- package/dist/esm-parser.d.ts.map +1 -0
- package/dist/esm-parser.js +69 -0
- package/dist/esm-parser.js.map +1 -0
- package/dist/esm-parser.test.d.ts +2 -0
- package/dist/esm-parser.test.d.ts.map +1 -0
- package/dist/esm-parser.test.js +124 -0
- package/dist/esm-parser.test.js.map +1 -0
- package/dist/safe-mdx.bench.js +6 -0
- package/dist/safe-mdx.bench.js.map +1 -1
- package/dist/safe-mdx.d.ts +8 -2
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +291 -83
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +593 -24
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +3 -1
- package/src/dynamic-esm-component.tsx +85 -0
- package/src/esm-parser.test.ts +141 -0
- package/src/esm-parser.ts +89 -0
- package/src/safe-mdx.bench.tsx +6 -0
- package/src/safe-mdx.test.tsx +648 -24
- package/src/safe-mdx.tsx +405 -120
package/dist/safe-mdx.test.js
CHANGED
|
@@ -15,9 +15,9 @@ const components = {
|
|
|
15
15
|
return _jsx("div", { children: children });
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
|
-
function render(code, componentPropsSchema) {
|
|
18
|
+
function render(code, componentPropsSchema, allowClientEsmImports) {
|
|
19
19
|
const mdast = mdxParse(code);
|
|
20
|
-
const visitor = new MdastToJsx({ markdown: code, mdast, components, componentPropsSchema });
|
|
20
|
+
const visitor = new MdastToJsx({ markdown: code, mdast, components, componentPropsSchema, allowClientEsmImports });
|
|
21
21
|
const result = visitor.run();
|
|
22
22
|
const html = renderToStaticMarkup(result);
|
|
23
23
|
// console.log(JSON.stringify(result, null, 2))
|
|
@@ -29,6 +29,18 @@ test('htmlToJsx', () => {
|
|
|
29
29
|
expect(htmlToJsx('before <p>text</p>')).toMatchInlineSnapshot(`"<>before <p>text</p></>"`);
|
|
30
30
|
expect(htmlToJsx('<nonexisting>text</nonexisting>')).toMatchInlineSnapshot(`"<nonexisting>text</nonexisting>"`);
|
|
31
31
|
});
|
|
32
|
+
test('reference links with titles', () => {
|
|
33
|
+
const code = dedent `
|
|
34
|
+
> **Heads-up:** Check the [API docs][1] for more info.
|
|
35
|
+
|
|
36
|
+
Visit [Slack developers][2] for details.
|
|
37
|
+
|
|
38
|
+
[1]: https://api.slack.com/methods/search.messages "search.messages method - Slack API"
|
|
39
|
+
[2]: https://slack.dev/secure-data-connectivity/ "Secure Data Connectivity - Slack Developers"
|
|
40
|
+
`;
|
|
41
|
+
const { html } = render(code);
|
|
42
|
+
expect(html).toMatchInlineSnapshot(`"<blockquote><p><strong>Heads-up:</strong> Check the <a href="https://api.slack.com/methods/search.messages" title="search.messages method - Slack API">API docs</a> for more info.</p></blockquote><p>Visit <a href="https://slack.dev/secure-data-connectivity/" title="Secure Data Connectivity - Slack Developers">Slack developers</a> for details.</p>"`);
|
|
43
|
+
});
|
|
32
44
|
test('markdown inside jsx', () => {
|
|
33
45
|
const code = dedent `
|
|
34
46
|
# Hello
|
|
@@ -476,36 +488,42 @@ test('props parsing', () => {
|
|
|
476
488
|
{
|
|
477
489
|
"errors": [
|
|
478
490
|
{
|
|
479
|
-
"line":
|
|
480
|
-
"message": "
|
|
491
|
+
"line": 8,
|
|
492
|
+
"message": "Failed to evaluate expression attribute: expression2={Boolean(1)}",
|
|
481
493
|
},
|
|
482
494
|
{
|
|
483
495
|
"line": 8,
|
|
484
|
-
"message": "Expressions in jsx
|
|
496
|
+
"message": "Expressions in jsx prop not evaluated: (expression2={Boolean(1)})",
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
"line": 9,
|
|
500
|
+
"message": "Unsupported jsx component SomeComponent in attribute",
|
|
485
501
|
},
|
|
486
502
|
{
|
|
487
503
|
"line": 9,
|
|
488
|
-
"message": "
|
|
504
|
+
"message": "Failed to evaluate expression attribute: jsx={<SomeComponent />}",
|
|
489
505
|
},
|
|
490
506
|
{
|
|
491
|
-
"line":
|
|
492
|
-
"message": "Expressions in jsx
|
|
507
|
+
"line": 9,
|
|
508
|
+
"message": "Expressions in jsx prop not evaluated: (jsx={<SomeComponent />})",
|
|
493
509
|
},
|
|
494
510
|
],
|
|
495
511
|
"html": "<h1><p>hi</p></h1>",
|
|
496
512
|
"result": <React.Fragment>
|
|
497
513
|
<Heading
|
|
498
|
-
backTick="some
|
|
514
|
+
backTick="some undefined value"
|
|
499
515
|
boolean={false}
|
|
500
516
|
doublequote="a " string"
|
|
517
|
+
expression1={4}
|
|
501
518
|
null={null}
|
|
502
519
|
num={2}
|
|
503
|
-
quote="a
|
|
520
|
+
quote="a ' string"
|
|
504
521
|
someJson={
|
|
505
522
|
{
|
|
506
523
|
"a": 1,
|
|
507
524
|
}
|
|
508
525
|
}
|
|
526
|
+
spread={true}
|
|
509
527
|
>
|
|
510
528
|
<p>
|
|
511
529
|
hi
|
|
@@ -515,6 +533,125 @@ test('props parsing', () => {
|
|
|
515
533
|
}
|
|
516
534
|
`);
|
|
517
535
|
});
|
|
536
|
+
test('jsx attributes with arithmetic expressions', () => {
|
|
537
|
+
expect(render(dedent `
|
|
538
|
+
<Heading
|
|
539
|
+
level={1 + 2}
|
|
540
|
+
width={100 * 2}
|
|
541
|
+
active={!false}
|
|
542
|
+
comparison={5 > 3}
|
|
543
|
+
concat={"hello " + "world"}
|
|
544
|
+
/>
|
|
545
|
+
`)).toMatchInlineSnapshot(`
|
|
546
|
+
{
|
|
547
|
+
"errors": [],
|
|
548
|
+
"html": "<h1></h1>",
|
|
549
|
+
"result": <React.Fragment>
|
|
550
|
+
<Heading
|
|
551
|
+
active={true}
|
|
552
|
+
comparison={true}
|
|
553
|
+
concat="hello world"
|
|
554
|
+
level={3}
|
|
555
|
+
width={200}
|
|
556
|
+
/>
|
|
557
|
+
</React.Fragment>,
|
|
558
|
+
}
|
|
559
|
+
`);
|
|
560
|
+
});
|
|
561
|
+
test('jsx attributes with complex objects and arrays', () => {
|
|
562
|
+
expect(render(dedent `
|
|
563
|
+
<Heading
|
|
564
|
+
simpleArray={[1, 2, 3]}
|
|
565
|
+
stringArray={["one", "two", "three"]}
|
|
566
|
+
mixedArray={[1, "two", true, null]}
|
|
567
|
+
simpleObject={{name: "John", age: 30}}
|
|
568
|
+
nestedObject={{
|
|
569
|
+
user: {
|
|
570
|
+
name: "Alice",
|
|
571
|
+
preferences: {
|
|
572
|
+
theme: "dark",
|
|
573
|
+
lang: "en"
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
settings: {
|
|
577
|
+
notifications: true,
|
|
578
|
+
emails: ["alice@example.com", "alice.work@example.com"]
|
|
579
|
+
}
|
|
580
|
+
}}
|
|
581
|
+
arrayOfObjects={[
|
|
582
|
+
{id: 1, name: "Item 1"},
|
|
583
|
+
{id: 2, name: "Item 2"}
|
|
584
|
+
]}
|
|
585
|
+
/>
|
|
586
|
+
`)).toMatchInlineSnapshot(`
|
|
587
|
+
{
|
|
588
|
+
"errors": [],
|
|
589
|
+
"html": "<h1></h1>",
|
|
590
|
+
"result": <React.Fragment>
|
|
591
|
+
<Heading
|
|
592
|
+
arrayOfObjects={
|
|
593
|
+
[
|
|
594
|
+
{
|
|
595
|
+
"id": 1,
|
|
596
|
+
"name": "Item 1",
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
"id": 2,
|
|
600
|
+
"name": "Item 2",
|
|
601
|
+
},
|
|
602
|
+
]
|
|
603
|
+
}
|
|
604
|
+
mixedArray={
|
|
605
|
+
[
|
|
606
|
+
1,
|
|
607
|
+
"two",
|
|
608
|
+
true,
|
|
609
|
+
null,
|
|
610
|
+
]
|
|
611
|
+
}
|
|
612
|
+
nestedObject={
|
|
613
|
+
{
|
|
614
|
+
"settings": {
|
|
615
|
+
"emails": [
|
|
616
|
+
"alice@example.com",
|
|
617
|
+
"alice.work@example.com",
|
|
618
|
+
],
|
|
619
|
+
"notifications": true,
|
|
620
|
+
},
|
|
621
|
+
"user": {
|
|
622
|
+
"name": "Alice",
|
|
623
|
+
"preferences": {
|
|
624
|
+
"lang": "en",
|
|
625
|
+
"theme": "dark",
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
simpleArray={
|
|
631
|
+
[
|
|
632
|
+
1,
|
|
633
|
+
2,
|
|
634
|
+
3,
|
|
635
|
+
]
|
|
636
|
+
}
|
|
637
|
+
simpleObject={
|
|
638
|
+
{
|
|
639
|
+
"age": 30,
|
|
640
|
+
"name": "John",
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
stringArray={
|
|
644
|
+
[
|
|
645
|
+
"one",
|
|
646
|
+
"two",
|
|
647
|
+
"three",
|
|
648
|
+
]
|
|
649
|
+
}
|
|
650
|
+
/>
|
|
651
|
+
</React.Fragment>,
|
|
652
|
+
}
|
|
653
|
+
`);
|
|
654
|
+
});
|
|
518
655
|
test('breaks', () => {
|
|
519
656
|
expect(render(dedent `
|
|
520
657
|
To have a line break without a paragraph, you will need to use two trailing spaces.
|
|
@@ -1006,7 +1143,7 @@ test('kitchen sink', () => {
|
|
|
1006
1143
|
|
|
1007
1144
|
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
|
1008
1145
|
[1]: http://slashdot.org
|
|
1009
|
-
[link text itself]: http://www.reddit.com</code></pre><p><a href="https://www.google.com" title="">I'm an inline-style link</a></p><p><a href="https://www.google.com" title="Google's Homepage">I'm an inline-style link with title</a></p><p><a href="https://www.mozilla.org">I'm a reference-style link</a></p><p><a href="../blob/master/LICENSE" title="">I'm a relative reference to a repository file</a></p><p><a href="http://slashdot.org">You can use numbers for reference-style link definitions</a></p><p>Or leave it empty and use the <a href="http://www.reddit.com">link text itself</a>.</p><p>URLs and URLs in angle brackets will automatically get turned into links.
|
|
1146
|
+
[link text itself]: http://www.reddit.com</code></pre><p><a href="https://www.google.com" title="">I'm an inline-style link</a></p><p><a href="https://www.google.com" title="Google's Homepage">I'm an inline-style link with title</a></p><p><a href="https://www.mozilla.org" title="">I'm a reference-style link</a></p><p><a href="../blob/master/LICENSE" title="">I'm a relative reference to a repository file</a></p><p><a href="http://slashdot.org" title="">You can use numbers for reference-style link definitions</a></p><p>Or leave it empty and use the <a href="http://www.reddit.com" title="">link text itself</a>.</p><p>URLs and URLs in angle brackets will automatically get turned into links.
|
|
1010
1147
|
<a href="http://www.example.com" title="">http://www.example.com</a> and sometimes
|
|
1011
1148
|
example.com (but not on Github, for example).</p><p>Some text to show that the reference links can follow later.</p><a name="images"></a><h2>Images</h2><pre><code class="language-no-highlight">Here's our logo (hover to see the title text):
|
|
1012
1149
|
|
|
@@ -1505,6 +1642,7 @@ test('kitchen sink', () => {
|
|
|
1505
1642
|
<p>
|
|
1506
1643
|
<a
|
|
1507
1644
|
href="https://www.mozilla.org"
|
|
1645
|
+
title=""
|
|
1508
1646
|
>
|
|
1509
1647
|
I'm a reference-style link
|
|
1510
1648
|
</a>
|
|
@@ -1520,6 +1658,7 @@ test('kitchen sink', () => {
|
|
|
1520
1658
|
<p>
|
|
1521
1659
|
<a
|
|
1522
1660
|
href="http://slashdot.org"
|
|
1661
|
+
title=""
|
|
1523
1662
|
>
|
|
1524
1663
|
You can use numbers for reference-style link definitions
|
|
1525
1664
|
</a>
|
|
@@ -1528,6 +1667,7 @@ test('kitchen sink', () => {
|
|
|
1528
1667
|
Or leave it empty and use the
|
|
1529
1668
|
<a
|
|
1530
1669
|
href="http://www.reddit.com"
|
|
1670
|
+
title=""
|
|
1531
1671
|
>
|
|
1532
1672
|
link text itself
|
|
1533
1673
|
</a>
|
|
@@ -2326,6 +2466,76 @@ test('component props schema validation with zod', () => {
|
|
|
2326
2466
|
}
|
|
2327
2467
|
`);
|
|
2328
2468
|
});
|
|
2469
|
+
test('mdx expressions evaluation', () => {
|
|
2470
|
+
expect(render(dedent `
|
|
2471
|
+
# Expression Test
|
|
2472
|
+
|
|
2473
|
+
Simple math: {1 + 2}
|
|
2474
|
+
|
|
2475
|
+
<Heading>
|
|
2476
|
+
Inside JSX: {3 * 4}
|
|
2477
|
+
</Heading>
|
|
2478
|
+
|
|
2479
|
+
Boolean: {true}
|
|
2480
|
+
String concat: {"hello" + " world"}
|
|
2481
|
+
`)).toMatchInlineSnapshot(`
|
|
2482
|
+
{
|
|
2483
|
+
"errors": [],
|
|
2484
|
+
"html": "<h1>Expression Test</h1><p>Simple math: 3</p><h1><p>Inside JSX: 12</p></h1><p>Boolean:
|
|
2485
|
+
String concat: hello world</p>",
|
|
2486
|
+
"result": <React.Fragment>
|
|
2487
|
+
<h1>
|
|
2488
|
+
Expression Test
|
|
2489
|
+
</h1>
|
|
2490
|
+
<p>
|
|
2491
|
+
Simple math:
|
|
2492
|
+
3
|
|
2493
|
+
</p>
|
|
2494
|
+
<Heading>
|
|
2495
|
+
<p>
|
|
2496
|
+
Inside JSX:
|
|
2497
|
+
12
|
|
2498
|
+
</p>
|
|
2499
|
+
</Heading>
|
|
2500
|
+
<p>
|
|
2501
|
+
Boolean:
|
|
2502
|
+
true
|
|
2503
|
+
|
|
2504
|
+
String concat:
|
|
2505
|
+
hello world
|
|
2506
|
+
</p>
|
|
2507
|
+
</React.Fragment>,
|
|
2508
|
+
}
|
|
2509
|
+
`);
|
|
2510
|
+
});
|
|
2511
|
+
test('mdx expressions with unsupported functions', () => {
|
|
2512
|
+
expect(render(dedent `
|
|
2513
|
+
Math function: {Math.max(5, 10)}
|
|
2514
|
+
Console: {console.log("test")}
|
|
2515
|
+
`)).toMatchInlineSnapshot(`
|
|
2516
|
+
{
|
|
2517
|
+
"errors": [
|
|
2518
|
+
{
|
|
2519
|
+
"line": 1,
|
|
2520
|
+
"message": "Failed to evaluate expression: Math.max(5, 10)",
|
|
2521
|
+
},
|
|
2522
|
+
{
|
|
2523
|
+
"line": 2,
|
|
2524
|
+
"message": "Failed to evaluate expression: console.log("test")",
|
|
2525
|
+
},
|
|
2526
|
+
],
|
|
2527
|
+
"html": "<p>Math function:
|
|
2528
|
+
Console: </p>",
|
|
2529
|
+
"result": <React.Fragment>
|
|
2530
|
+
<p>
|
|
2531
|
+
Math function:
|
|
2532
|
+
|
|
2533
|
+
Console:
|
|
2534
|
+
</p>
|
|
2535
|
+
</React.Fragment>,
|
|
2536
|
+
}
|
|
2537
|
+
`);
|
|
2538
|
+
});
|
|
2329
2539
|
test('schema validation without errors', () => {
|
|
2330
2540
|
const HeadingSchema = z.object({
|
|
2331
2541
|
level: z.number().min(1).max(6),
|
|
@@ -2409,30 +2619,389 @@ test('validation error includes schema path', () => {
|
|
|
2409
2619
|
"errors": [
|
|
2410
2620
|
{
|
|
2411
2621
|
"line": 1,
|
|
2412
|
-
"message": "
|
|
2413
|
-
|
|
2414
|
-
{
|
|
2415
|
-
"line": 1,
|
|
2416
|
-
"message": "Expressions in jsx props are not supported (settings={{ theme: "invalid" }})",
|
|
2622
|
+
"message": "Invalid props for component "Heading" at "user.age": Number must be greater than or equal to 0",
|
|
2623
|
+
"schemaPath": "user.age",
|
|
2417
2624
|
},
|
|
2418
2625
|
{
|
|
2419
2626
|
"line": 1,
|
|
2420
|
-
"message": "Invalid props for component "Heading" at "
|
|
2421
|
-
"schemaPath": "
|
|
2422
|
-
},
|
|
2423
|
-
{
|
|
2424
|
-
"line": 1,
|
|
2425
|
-
"message": "Invalid props for component "Heading" at "settings": Required",
|
|
2426
|
-
"schemaPath": "settings",
|
|
2627
|
+
"message": "Invalid props for component "Heading" at "settings.theme": Invalid enum value. Expected 'light' | 'dark', received 'invalid'",
|
|
2628
|
+
"schemaPath": "settings.theme",
|
|
2427
2629
|
},
|
|
2428
2630
|
],
|
|
2429
2631
|
"html": "<h1>Complex validation</h1>",
|
|
2430
2632
|
"result": <React.Fragment>
|
|
2431
|
-
<Heading
|
|
2633
|
+
<Heading
|
|
2634
|
+
settings={
|
|
2635
|
+
{
|
|
2636
|
+
"theme": "invalid",
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
user={
|
|
2640
|
+
{
|
|
2641
|
+
"age": -1,
|
|
2642
|
+
"name": "test",
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
>
|
|
2432
2646
|
Complex validation
|
|
2433
2647
|
</Heading>
|
|
2434
2648
|
</React.Fragment>,
|
|
2435
2649
|
}
|
|
2436
2650
|
`);
|
|
2437
2651
|
});
|
|
2652
|
+
test('mdxJsxExpressionAttribute spread syntax', () => {
|
|
2653
|
+
expect(render(dedent `
|
|
2654
|
+
<Heading
|
|
2655
|
+
{...{key: '1', level: 2}}
|
|
2656
|
+
title="test"
|
|
2657
|
+
>
|
|
2658
|
+
Content with spread
|
|
2659
|
+
</Heading>
|
|
2660
|
+
`)).toMatchInlineSnapshot(`
|
|
2661
|
+
{
|
|
2662
|
+
"errors": [],
|
|
2663
|
+
"html": "<h1><p>Content with spread</p></h1>",
|
|
2664
|
+
"result": <React.Fragment>
|
|
2665
|
+
<Heading
|
|
2666
|
+
level={2}
|
|
2667
|
+
title="test"
|
|
2668
|
+
>
|
|
2669
|
+
<p>
|
|
2670
|
+
Content with spread
|
|
2671
|
+
</p>
|
|
2672
|
+
</Heading>
|
|
2673
|
+
</React.Fragment>,
|
|
2674
|
+
}
|
|
2675
|
+
`);
|
|
2676
|
+
});
|
|
2677
|
+
test('mdxJsxExpressionAttribute complex spread cases', () => {
|
|
2678
|
+
expect(render(dedent `
|
|
2679
|
+
<Heading
|
|
2680
|
+
{...{
|
|
2681
|
+
level: 3,
|
|
2682
|
+
active: true,
|
|
2683
|
+
disabled: false,
|
|
2684
|
+
count: 42,
|
|
2685
|
+
title: "spread title",
|
|
2686
|
+
nested: {
|
|
2687
|
+
prop: "value"
|
|
2688
|
+
}
|
|
2689
|
+
}}
|
|
2690
|
+
>
|
|
2691
|
+
Complex spread test
|
|
2692
|
+
</Heading>
|
|
2693
|
+
|
|
2694
|
+
<Cards
|
|
2695
|
+
{...{style: {color: "red", fontSize: "16px"}}}
|
|
2696
|
+
{...{className: "test-class", id: "test-id"}}
|
|
2697
|
+
>
|
|
2698
|
+
Multiple spreads
|
|
2699
|
+
</Cards>
|
|
2700
|
+
`)).toMatchInlineSnapshot(`
|
|
2701
|
+
{
|
|
2702
|
+
"errors": [],
|
|
2703
|
+
"html": "<h1><p>Complex spread test</p></h1><div><p>Multiple spreads</p></div>",
|
|
2704
|
+
"result": <React.Fragment>
|
|
2705
|
+
<Heading
|
|
2706
|
+
active={true}
|
|
2707
|
+
count={42}
|
|
2708
|
+
disabled={false}
|
|
2709
|
+
level={3}
|
|
2710
|
+
nested={
|
|
2711
|
+
{
|
|
2712
|
+
"prop": "value",
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
title="spread title"
|
|
2716
|
+
>
|
|
2717
|
+
<p>
|
|
2718
|
+
Complex spread test
|
|
2719
|
+
</p>
|
|
2720
|
+
</Heading>
|
|
2721
|
+
<Cards
|
|
2722
|
+
className="test-class"
|
|
2723
|
+
id="test-id"
|
|
2724
|
+
style={
|
|
2725
|
+
{
|
|
2726
|
+
"color": "red",
|
|
2727
|
+
"fontSize": "16px",
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
>
|
|
2731
|
+
<p>
|
|
2732
|
+
Multiple spreads
|
|
2733
|
+
</p>
|
|
2734
|
+
</Cards>
|
|
2735
|
+
</React.Fragment>,
|
|
2736
|
+
}
|
|
2737
|
+
`);
|
|
2738
|
+
});
|
|
2739
|
+
test('mdxJsxExpressionAttribute edge cases', () => {
|
|
2740
|
+
expect(render(dedent `
|
|
2741
|
+
<Heading {...{}} title="empty spread">Empty spread</Heading>
|
|
2742
|
+
|
|
2743
|
+
<Heading {...{null: null, undefined: undefined}} title="null/undefined values">Null/undefined</Heading>
|
|
2744
|
+
|
|
2745
|
+
<Heading {...{array: [1, 2, 3], object: {nested: true}}} title="complex values">Complex types</Heading>
|
|
2746
|
+
`)).toMatchInlineSnapshot(`
|
|
2747
|
+
{
|
|
2748
|
+
"errors": [
|
|
2749
|
+
{
|
|
2750
|
+
"line": 3,
|
|
2751
|
+
"message": "Failed to evaluate expression attribute: ...{null: null, undefined: undefined}",
|
|
2752
|
+
},
|
|
2753
|
+
],
|
|
2754
|
+
"html": "<h1>Empty spread</h1><h1>Null/undefined</h1><h1>Complex types</h1>",
|
|
2755
|
+
"result": <React.Fragment>
|
|
2756
|
+
<Heading
|
|
2757
|
+
title="empty spread"
|
|
2758
|
+
>
|
|
2759
|
+
Empty spread
|
|
2760
|
+
</Heading>
|
|
2761
|
+
<Heading
|
|
2762
|
+
title="null/undefined values"
|
|
2763
|
+
>
|
|
2764
|
+
Null/undefined
|
|
2765
|
+
</Heading>
|
|
2766
|
+
<Heading
|
|
2767
|
+
array={
|
|
2768
|
+
[
|
|
2769
|
+
1,
|
|
2770
|
+
2,
|
|
2771
|
+
3,
|
|
2772
|
+
]
|
|
2773
|
+
}
|
|
2774
|
+
object={
|
|
2775
|
+
{
|
|
2776
|
+
"nested": true,
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
title="complex values"
|
|
2780
|
+
>
|
|
2781
|
+
Complex types
|
|
2782
|
+
</Heading>
|
|
2783
|
+
</React.Fragment>,
|
|
2784
|
+
}
|
|
2785
|
+
`);
|
|
2786
|
+
});
|
|
2787
|
+
test('ESM imports from https URLs', () => {
|
|
2788
|
+
const code = dedent `
|
|
2789
|
+
import Button from 'https://esm.sh/some-button-component'
|
|
2790
|
+
import { Card, Modal } from 'https://esm.sh/some-ui-library'
|
|
2791
|
+
|
|
2792
|
+
# Hello
|
|
2793
|
+
|
|
2794
|
+
<Button>Click me</Button>
|
|
2795
|
+
|
|
2796
|
+
<Card title="Test Card">
|
|
2797
|
+
Content inside card
|
|
2798
|
+
</Card>
|
|
2799
|
+
|
|
2800
|
+
<Modal open={true}>
|
|
2801
|
+
Modal content
|
|
2802
|
+
</Modal>
|
|
2803
|
+
`;
|
|
2804
|
+
const mdast = mdxParse(code);
|
|
2805
|
+
const visitor = new MdastToJsx({ markdown: code, mdast, components, allowClientEsmImports: true });
|
|
2806
|
+
const result = visitor.run();
|
|
2807
|
+
// Check that imports were parsed correctly
|
|
2808
|
+
expect(visitor.esmImports.size).toBe(3);
|
|
2809
|
+
expect(visitor.esmImports.get('Button')).toBe('https://esm.sh/some-button-component');
|
|
2810
|
+
expect(visitor.esmImports.get('Card')).toBe('https://esm.sh/some-ui-library#Card');
|
|
2811
|
+
expect(visitor.esmImports.get('Modal')).toBe('https://esm.sh/some-ui-library#Modal');
|
|
2812
|
+
// Since these are dynamic imports that only work on client, the server render should return null
|
|
2813
|
+
const html = renderToStaticMarkup(result);
|
|
2814
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Hello</h1>"`);
|
|
2815
|
+
expect(visitor.errors).toEqual([]);
|
|
2816
|
+
});
|
|
2817
|
+
test('ESM imports error handling', () => {
|
|
2818
|
+
const code = dedent `
|
|
2819
|
+
import Button from 'file:///local/path'
|
|
2820
|
+
import Component from './relative/path'
|
|
2821
|
+
|
|
2822
|
+
# Test
|
|
2823
|
+
|
|
2824
|
+
<Button>Local import should not work</Button>
|
|
2825
|
+
<Component>Relative import should not work</Component>
|
|
2826
|
+
`;
|
|
2827
|
+
const mdast = mdxParse(code);
|
|
2828
|
+
const visitor = new MdastToJsx({ markdown: code, mdast, components, allowClientEsmImports: true });
|
|
2829
|
+
const result = visitor.run();
|
|
2830
|
+
// Only https imports should be processed
|
|
2831
|
+
expect(visitor.esmImports.size).toBe(0);
|
|
2832
|
+
// Should have 4 errors: 2 for invalid imports, 2 for unsupported components
|
|
2833
|
+
expect(visitor.errors.length).toBe(4);
|
|
2834
|
+
// First two errors are for invalid imports
|
|
2835
|
+
expect(visitor.errors[0].message).toContain('Invalid import URL');
|
|
2836
|
+
expect(visitor.errors[1].message).toContain('Invalid import URL');
|
|
2837
|
+
// Last two errors are for unsupported components
|
|
2838
|
+
expect(visitor.errors[2].message).toContain('Unsupported jsx component Button');
|
|
2839
|
+
expect(visitor.errors[3].message).toContain('Unsupported jsx component Component');
|
|
2840
|
+
});
|
|
2841
|
+
test('jsx components in attributes', () => {
|
|
2842
|
+
const code = dedent `
|
|
2843
|
+
# JSX Components in Attributes
|
|
2844
|
+
|
|
2845
|
+
<Heading icon={<span>👋</span>} level={1}>
|
|
2846
|
+
Hello World
|
|
2847
|
+
</Heading>
|
|
2848
|
+
|
|
2849
|
+
<Cards items={<div>Item 1</div>}>
|
|
2850
|
+
Some content
|
|
2851
|
+
</Cards>
|
|
2852
|
+
`;
|
|
2853
|
+
const { result, errors, html } = render(code);
|
|
2854
|
+
// Should not have any errors
|
|
2855
|
+
expect(errors).toMatchInlineSnapshot(`[]`);
|
|
2856
|
+
// Should render correctly
|
|
2857
|
+
expect(html).toMatchInlineSnapshot(`"<h1>JSX Components in Attributes</h1><h1><p>Hello World</p></h1><div><p>Some content</p></div>"`);
|
|
2858
|
+
expect(result).toMatchInlineSnapshot(`
|
|
2859
|
+
<React.Fragment>
|
|
2860
|
+
<h1>
|
|
2861
|
+
JSX Components in Attributes
|
|
2862
|
+
</h1>
|
|
2863
|
+
<Heading
|
|
2864
|
+
icon={
|
|
2865
|
+
<span>
|
|
2866
|
+
👋
|
|
2867
|
+
</span>
|
|
2868
|
+
}
|
|
2869
|
+
level={1}
|
|
2870
|
+
>
|
|
2871
|
+
<p>
|
|
2872
|
+
Hello World
|
|
2873
|
+
</p>
|
|
2874
|
+
</Heading>
|
|
2875
|
+
<Cards
|
|
2876
|
+
items={
|
|
2877
|
+
<div>
|
|
2878
|
+
Item 1
|
|
2879
|
+
</div>
|
|
2880
|
+
}
|
|
2881
|
+
>
|
|
2882
|
+
<p>
|
|
2883
|
+
Some content
|
|
2884
|
+
</p>
|
|
2885
|
+
</Cards>
|
|
2886
|
+
</React.Fragment>
|
|
2887
|
+
`);
|
|
2888
|
+
});
|
|
2889
|
+
test('jsx components in attributes with ESM imports', () => {
|
|
2890
|
+
const code = dedent `
|
|
2891
|
+
import Button from 'https://esm.sh/some-button-component'
|
|
2892
|
+
import { Icon } from 'https://esm.sh/some-icon-library'
|
|
2893
|
+
|
|
2894
|
+
# ESM Components in Attributes
|
|
2895
|
+
|
|
2896
|
+
<Heading icon={<Icon name="star" />} level={1}>
|
|
2897
|
+
Hello World
|
|
2898
|
+
</Heading>
|
|
2899
|
+
|
|
2900
|
+
<Cards actionButton={<Button>Click me</Button>}>
|
|
2901
|
+
Some content
|
|
2902
|
+
</Cards>
|
|
2903
|
+
`;
|
|
2904
|
+
const { result, errors, html } = render(code, undefined, true);
|
|
2905
|
+
// Should not have any errors
|
|
2906
|
+
expect(errors).toMatchInlineSnapshot(`[]`);
|
|
2907
|
+
// Should render correctly - ESM components should be wrapped in DynamicEsmComponent
|
|
2908
|
+
expect(html).toMatchInlineSnapshot(`"<h1>ESM Components in Attributes</h1><h1><p>Hello World</p></h1><div><p>Some content</p></div>"`);
|
|
2909
|
+
expect(result).toMatchInlineSnapshot(`
|
|
2910
|
+
<React.Fragment>
|
|
2911
|
+
<h1>
|
|
2912
|
+
ESM Components in Attributes
|
|
2913
|
+
</h1>
|
|
2914
|
+
<Heading
|
|
2915
|
+
icon={
|
|
2916
|
+
<DynamicEsmComponent
|
|
2917
|
+
componentName="Icon"
|
|
2918
|
+
importUrl="https://esm.sh/some-icon-library"
|
|
2919
|
+
name="star"
|
|
2920
|
+
/>
|
|
2921
|
+
}
|
|
2922
|
+
level={1}
|
|
2923
|
+
>
|
|
2924
|
+
<p>
|
|
2925
|
+
Hello World
|
|
2926
|
+
</p>
|
|
2927
|
+
</Heading>
|
|
2928
|
+
<Cards
|
|
2929
|
+
actionButton={
|
|
2930
|
+
<DynamicEsmComponent
|
|
2931
|
+
componentName="default"
|
|
2932
|
+
importUrl="https://esm.sh/some-button-component"
|
|
2933
|
+
>
|
|
2934
|
+
Click me
|
|
2935
|
+
</DynamicEsmComponent>
|
|
2936
|
+
}
|
|
2937
|
+
>
|
|
2938
|
+
<p>
|
|
2939
|
+
Some content
|
|
2940
|
+
</p>
|
|
2941
|
+
</Cards>
|
|
2942
|
+
</React.Fragment>
|
|
2943
|
+
`);
|
|
2944
|
+
});
|
|
2945
|
+
test('ESM imports disabled by default', () => {
|
|
2946
|
+
const code = dedent `
|
|
2947
|
+
import Button from 'https://esm.sh/some-button-component'
|
|
2948
|
+
|
|
2949
|
+
# Test Default Behavior
|
|
2950
|
+
|
|
2951
|
+
<Button>This should not work</Button>
|
|
2952
|
+
`;
|
|
2953
|
+
const { result, errors, html } = render(code); // No allowClientEsmImports flag
|
|
2954
|
+
// ESM imports should not be processed when disabled
|
|
2955
|
+
const mdast = mdxParse(code);
|
|
2956
|
+
const visitor = new MdastToJsx({ markdown: code, mdast, components }); // Default allowClientEsmImports: false
|
|
2957
|
+
expect(visitor.esmImports.size).toBe(0);
|
|
2958
|
+
// Should have error for unsupported component
|
|
2959
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
2960
|
+
expect(errors.some(e => e.message.includes('Unsupported jsx component Button'))).toBe(true);
|
|
2961
|
+
// Should render heading but not the button
|
|
2962
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Test Default Behavior</h1>"`);
|
|
2963
|
+
});
|
|
2964
|
+
test("jsx components in attributes error handling", () => {
|
|
2965
|
+
const code = dedent `
|
|
2966
|
+
# Error Handling Test
|
|
2967
|
+
|
|
2968
|
+
<Heading icon={<UnsupportedComponent />} level={1}>
|
|
2969
|
+
Hello World
|
|
2970
|
+
</Heading>
|
|
2971
|
+
`;
|
|
2972
|
+
const { result, errors, html } = render(code);
|
|
2973
|
+
// Should have an error for the unsupported component
|
|
2974
|
+
expect(errors).toMatchInlineSnapshot(`
|
|
2975
|
+
[
|
|
2976
|
+
{
|
|
2977
|
+
"line": 3,
|
|
2978
|
+
"message": "Unsupported jsx component UnsupportedComponent in attribute",
|
|
2979
|
+
},
|
|
2980
|
+
{
|
|
2981
|
+
"line": 3,
|
|
2982
|
+
"message": "Failed to evaluate expression attribute: icon={<UnsupportedComponent />}",
|
|
2983
|
+
},
|
|
2984
|
+
{
|
|
2985
|
+
"line": 3,
|
|
2986
|
+
"message": "Expressions in jsx prop not evaluated: (icon={<UnsupportedComponent />})",
|
|
2987
|
+
},
|
|
2988
|
+
]
|
|
2989
|
+
`);
|
|
2990
|
+
// Should still render the rest of the content
|
|
2991
|
+
expect(html).toMatchInlineSnapshot(`"<h1>Error Handling Test</h1><h1><p>Hello World</p></h1>"`);
|
|
2992
|
+
expect(result).toMatchInlineSnapshot(`
|
|
2993
|
+
<React.Fragment>
|
|
2994
|
+
<h1>
|
|
2995
|
+
Error Handling Test
|
|
2996
|
+
</h1>
|
|
2997
|
+
<Heading
|
|
2998
|
+
level={1}
|
|
2999
|
+
>
|
|
3000
|
+
<p>
|
|
3001
|
+
Hello World
|
|
3002
|
+
</p>
|
|
3003
|
+
</Heading>
|
|
3004
|
+
</React.Fragment>
|
|
3005
|
+
`);
|
|
3006
|
+
});
|
|
2438
3007
|
//# sourceMappingURL=safe-mdx.test.js.map
|