safe-mdx 1.2.0 → 1.3.1

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.
@@ -9,17 +9,20 @@ import { MdastToJsx, mdastBfs, type ComponentPropsSchema } from './safe-mdx.js'
9
9
  import { completeJsxTags } from './streaming.js'
10
10
 
11
11
  const components = {
12
- Heading({ level, children }) {
13
- return <h1>{children}</h1>
12
+ Heading({ level, children, ...props }) {
13
+ return <h1 {...props}>{children}</h1>
14
14
  },
15
- Cards({ level, children }) {
16
- return <div>{children}</div>
15
+ Cards({ level, children, ...props }) {
16
+ return <div {...props}>{children}</div>
17
+ },
18
+ Tabs({ items, children, ...props }) {
19
+ return <div {...props}>{children}</div>
17
20
  },
18
21
  }
19
22
 
20
- function render(code, componentPropsSchema?: ComponentPropsSchema, allowClientEsmImports?: boolean) {
23
+ function render(code, componentPropsSchema?: ComponentPropsSchema, allowClientEsmImports?: boolean, addMarkdownLineNumbers?: boolean) {
21
24
  const mdast = mdxParse(code)
22
- const visitor = new MdastToJsx({ markdown: code, mdast, components, componentPropsSchema, allowClientEsmImports })
25
+ const visitor = new MdastToJsx({ markdown: code, mdast, components, componentPropsSchema, allowClientEsmImports, addMarkdownLineNumbers })
23
26
  const result = visitor.run()
24
27
  const html = renderToStaticMarkup(result)
25
28
  // console.log(JSON.stringify(result, null, 2))
@@ -68,7 +71,7 @@ test('markdown inside jsx', () => {
68
71
  expect(render(code)).toMatchInlineSnapshot(`
69
72
  {
70
73
  "errors": [],
71
- "html": "<h1>Hello</h1><h1><p>Component <em>children</em></p></h1><figure><p>some <em>bold</em> content</p></figure>",
74
+ "html": "<h1>Hello</h1><h1 prop="value"><p>Component <em>children</em></p></h1><figure><p>some <em>bold</em> content</p></figure>",
72
75
  "result": <React.Fragment>
73
76
  <h1>
74
77
  Hello
@@ -114,7 +117,7 @@ test('can complete jsx code with completeJsxTags', () => {
114
117
  expect(render(completeJsxTags(code))).toMatchInlineSnapshot(`
115
118
  {
116
119
  "errors": [],
117
- "html": "<h1>Hello</h1><div><h1></h1></div>",
120
+ "html": "<h1>Hello</h1><div><h1 prop="value"></h1></div>",
118
121
  "result": <React.Fragment>
119
122
  <h1>
120
123
  Hello
@@ -527,7 +530,7 @@ test('props parsing', () => {
527
530
  "errors": [
528
531
  {
529
532
  "line": 8,
530
- "message": "Failed to evaluate expression attribute: expression2={Boolean(1)}",
533
+ "message": "Failed to evaluate expression attribute: expression2={Boolean(1)}. Functions are not supported",
531
534
  },
532
535
  {
533
536
  "line": 8,
@@ -539,14 +542,14 @@ test('props parsing', () => {
539
542
  },
540
543
  {
541
544
  "line": 9,
542
- "message": "Failed to evaluate expression attribute: jsx={<SomeComponent />}",
545
+ "message": "Failed to evaluate expression attribute: jsx={<SomeComponent />}. visitor "JSXElement" is not supported",
543
546
  },
544
547
  {
545
548
  "line": 9,
546
549
  "message": "Expressions in jsx prop not evaluated: (jsx={<SomeComponent />})",
547
550
  },
548
551
  ],
549
- "html": "<h1><p>hi</p></h1>",
552
+ "html": "<h1 num="2" doublequote="a &quot; string" quote="a &#x27; string" backTick="some undefined value" expression1="4" someJson="[object Object]"><p>hi</p></h1>",
550
553
  "result": <React.Fragment>
551
554
  <Heading
552
555
  backTick="some undefined value"
@@ -586,7 +589,7 @@ test('jsx attributes with arithmetic expressions', () => {
586
589
  ).toMatchInlineSnapshot(`
587
590
  {
588
591
  "errors": [],
589
- "html": "<h1></h1>",
592
+ "html": "<h1 width="200" concat="hello world"></h1>",
590
593
  "result": <React.Fragment>
591
594
  <Heading
592
595
  active={true}
@@ -630,7 +633,7 @@ test('jsx attributes with complex objects and arrays', () => {
630
633
  ).toMatchInlineSnapshot(`
631
634
  {
632
635
  "errors": [],
633
- "html": "<h1></h1>",
636
+ "html": "<h1 simpleArray="1,2,3" stringArray="one,two,three" mixedArray="1,two,true," simpleObject="[object Object]" nestedObject="[object Object]" arrayOfObjects="[object Object],[object Object]"></h1>",
634
637
  "result": <React.Fragment>
635
638
  <Heading
636
639
  arrayOfObjects={
@@ -2491,7 +2494,7 @@ test('component props schema validation with zod', () => {
2491
2494
  "schemaPath": "count",
2492
2495
  },
2493
2496
  ],
2494
- "html": "<h1>Valid heading</h1><div>Valid cards</div><h1>Invalid heading - level too high</h1><div>Invalid cards - negative count</div><div>Invalid cards - wrong type</div>",
2497
+ "html": "<h1 title="test">Valid heading</h1><div count="3" variant="outline">Valid cards</div><h1 title="test">Invalid heading - level too high</h1><div count="-1">Invalid cards - negative count</div><div count="not a number">Invalid cards - wrong type</div>",
2495
2498
  "result": <React.Fragment>
2496
2499
  <Heading
2497
2500
  level={2}
@@ -2582,11 +2585,11 @@ test('mdx expressions with unsupported functions', () => {
2582
2585
  "errors": [
2583
2586
  {
2584
2587
  "line": 1,
2585
- "message": "Failed to evaluate expression: Math.max(5, 10)",
2588
+ "message": "Failed to evaluate expression: Math.max(5, 10). Functions are not supported",
2586
2589
  },
2587
2590
  {
2588
2591
  "line": 2,
2589
- "message": "Failed to evaluate expression: console.log("test")",
2592
+ "message": "Failed to evaluate expression: console.log("test"). Functions are not supported",
2590
2593
  },
2591
2594
  ],
2592
2595
  "html": "<p>Math function:
@@ -2620,7 +2623,7 @@ test('schema validation without errors', () => {
2620
2623
  expect(render(code, componentPropsSchema)).toMatchInlineSnapshot(`
2621
2624
  {
2622
2625
  "errors": [],
2623
- "html": "<h1>Valid heading</h1><h1>Another valid heading</h1>",
2626
+ "html": "<h1 title="test">Valid heading</h1><h1>Another valid heading</h1>",
2624
2627
  "result": <React.Fragment>
2625
2628
  <Heading
2626
2629
  level={2}
@@ -2655,7 +2658,7 @@ test('component without schema should not be validated', () => {
2655
2658
  expect(render(code, componentPropsSchema)).toMatchInlineSnapshot(`
2656
2659
  {
2657
2660
  "errors": [],
2658
- "html": "<h1>Valid heading with schema</h1><div>Cards without schema - should not be validated</div>",
2661
+ "html": "<h1>Valid heading with schema</h1><div invalidProp="anything">Cards without schema - should not be validated</div>",
2659
2662
  "result": <React.Fragment>
2660
2663
  <Heading
2661
2664
  level={2}
@@ -2705,7 +2708,7 @@ test('validation error includes schema path', () => {
2705
2708
  "schemaPath": "settings.theme",
2706
2709
  },
2707
2710
  ],
2708
- "html": "<h1>Complex validation</h1>",
2711
+ "html": "<h1 user="[object Object]" settings="[object Object]">Complex validation</h1>",
2709
2712
  "result": <React.Fragment>
2710
2713
  <Heading
2711
2714
  settings={
@@ -2740,7 +2743,7 @@ test('mdxJsxExpressionAttribute spread syntax', () => {
2740
2743
  ).toMatchInlineSnapshot(`
2741
2744
  {
2742
2745
  "errors": [],
2743
- "html": "<h1><p>Content with spread</p></h1>",
2746
+ "html": "<h1 title="test"><p>Content with spread</p></h1>",
2744
2747
  "result": <React.Fragment>
2745
2748
  <Heading
2746
2749
  level={2}
@@ -2783,7 +2786,7 @@ test('mdxJsxExpressionAttribute complex spread cases', () => {
2783
2786
  ).toMatchInlineSnapshot(`
2784
2787
  {
2785
2788
  "errors": [],
2786
- "html": "<h1><p>Complex spread test</p></h1><div><p>Multiple spreads</p></div>",
2789
+ "html": "<h1 count="42" title="spread title" nested="[object Object]"><p>Complex spread test</p></h1><div style="color:red;font-size:16px" class="test-class" id="test-id"><p>Multiple spreads</p></div>",
2787
2790
  "result": <React.Fragment>
2788
2791
  <Heading
2789
2792
  active={true}
@@ -2834,10 +2837,10 @@ test('mdxJsxExpressionAttribute edge cases', () => {
2834
2837
  "errors": [
2835
2838
  {
2836
2839
  "line": 3,
2837
- "message": "Failed to evaluate expression attribute: ...{null: null, undefined: undefined}",
2840
+ "message": "Failed to evaluate expression attribute: ...{null: null, undefined: undefined}. undefined is undefined",
2838
2841
  },
2839
2842
  ],
2840
- "html": "<h1>Empty spread</h1><h1>Null/undefined</h1><h1>Complex types</h1>",
2843
+ "html": "<h1 title="empty spread">Empty spread</h1><h1 title="null/undefined values">Null/undefined</h1><h1 array="1,2,3" object="[object Object]" title="complex values">Complex types</h1>",
2841
2844
  "result": <React.Fragment>
2842
2845
  <Heading
2843
2846
  title="empty spread"
@@ -2955,7 +2958,7 @@ test('jsx components in attributes', () => {
2955
2958
  expect(errors).toMatchInlineSnapshot(`[]`)
2956
2959
 
2957
2960
  // Should render correctly
2958
- expect(html).toMatchInlineSnapshot(`"<h1>JSX Components in Attributes</h1><h1><p>Hello World</p></h1><div><p>Some content</p></div>"`)
2961
+ expect(html).toMatchInlineSnapshot(`"<h1>JSX Components in Attributes</h1><h1 icon="[object Object]"><p>Hello World</p></h1><div items="[object Object]"><p>Some content</p></div>"`)
2959
2962
 
2960
2963
  expect(result).toMatchInlineSnapshot(`
2961
2964
  <React.Fragment>
@@ -3011,7 +3014,7 @@ test('jsx components in attributes with ESM imports', () => {
3011
3014
  expect(errors).toMatchInlineSnapshot(`[]`)
3012
3015
 
3013
3016
  // Should render correctly - ESM components should be wrapped in DynamicEsmComponent
3014
- expect(html).toMatchInlineSnapshot(`"<h1>ESM Components in Attributes</h1><h1><p>Hello World</p></h1><div><p>Some content</p></div>"`)
3017
+ expect(html).toMatchInlineSnapshot(`"<h1>ESM Components in Attributes</h1><h1 icon="[object Object]"><p>Hello World</p></h1><div actionButton="[object Object]"><p>Some content</p></div>"`)
3015
3018
 
3016
3019
  expect(result).toMatchInlineSnapshot(`
3017
3020
  <React.Fragment>
@@ -3094,7 +3097,7 @@ test("jsx components in attributes error handling", () => {
3094
3097
  },
3095
3098
  {
3096
3099
  "line": 3,
3097
- "message": "Failed to evaluate expression attribute: icon={<UnsupportedComponent />}",
3100
+ "message": "Failed to evaluate expression attribute: icon={<UnsupportedComponent />}. visitor "JSXElement" is not supported",
3098
3101
  },
3099
3102
  {
3100
3103
  "line": 3,
@@ -3121,3 +3124,213 @@ test("jsx components in attributes error handling", () => {
3121
3124
  </React.Fragment>
3122
3125
  `)
3123
3126
  })
3127
+
3128
+ test('addMarkdownLineNumbers adds data-markdown-line attributes', () => {
3129
+ const code = dedent`
3130
+ # Hello World
3131
+
3132
+ This is a **paragraph** with *emphasis*.
3133
+
3134
+ <Heading level={2}>
3135
+ Custom component
3136
+ </Heading>
3137
+
3138
+ - List item 1
3139
+ - List item 2
3140
+
3141
+ | Column 1 | Column 2 |
3142
+ |----------|----------|
3143
+ | Cell 1 | Cell 2 |
3144
+ `
3145
+
3146
+ const { result, errors, html } = render(code, undefined, false, true)
3147
+
3148
+ // Should not have any errors
3149
+ expect(errors).toMatchInlineSnapshot(`[]`)
3150
+
3151
+ // Check that data-markdown-line attributes are present in HTML
3152
+ expect(html).toContain('data-markdown-line="1"')
3153
+ expect(html).toContain('data-markdown-line="3"')
3154
+ expect(html).toContain('data-markdown-line="9"')
3155
+
3156
+ expect(result).toMatchInlineSnapshot(`
3157
+ <React.Fragment>
3158
+ <h1
3159
+ data-markdown-line={1}
3160
+ >
3161
+ Hello World
3162
+ </h1>
3163
+ <p
3164
+ data-markdown-line={3}
3165
+ >
3166
+ This is a
3167
+ <strong
3168
+ data-markdown-line={3}
3169
+ >
3170
+ paragraph
3171
+ </strong>
3172
+ with
3173
+ <em
3174
+ data-markdown-line={3}
3175
+ >
3176
+ emphasis
3177
+ </em>
3178
+ .
3179
+ </p>
3180
+ <Heading
3181
+ data-markdown-line={5}
3182
+ level={2}
3183
+ >
3184
+ <p
3185
+ data-markdown-line={6}
3186
+ >
3187
+ Custom component
3188
+ </p>
3189
+ </Heading>
3190
+ <ul
3191
+ data-markdown-line={9}
3192
+ >
3193
+ <li
3194
+ data-markdown-line={9}
3195
+ >
3196
+ <p
3197
+ data-markdown-line={9}
3198
+ >
3199
+ List item 1
3200
+ </p>
3201
+ </li>
3202
+ <li
3203
+ data-markdown-line={10}
3204
+ >
3205
+ <p
3206
+ data-markdown-line={10}
3207
+ >
3208
+ List item 2
3209
+ </p>
3210
+ </li>
3211
+ </ul>
3212
+ <table
3213
+ data-markdown-line={12}
3214
+ >
3215
+ <thead>
3216
+ <tr
3217
+ className=""
3218
+ data-markdown-line={12}
3219
+ >
3220
+ <td
3221
+ className=""
3222
+ data-markdown-line={12}
3223
+ >
3224
+ Column 1
3225
+ </td>
3226
+ <td
3227
+ className=""
3228
+ data-markdown-line={12}
3229
+ >
3230
+ Column 2
3231
+ </td>
3232
+ </tr>
3233
+ </thead>
3234
+ <tbody>
3235
+ <tr
3236
+ className=""
3237
+ data-markdown-line={14}
3238
+ >
3239
+ <td
3240
+ className=""
3241
+ data-markdown-line={14}
3242
+ >
3243
+ Cell 1
3244
+ </td>
3245
+ <td
3246
+ className=""
3247
+ data-markdown-line={14}
3248
+ >
3249
+ Cell 2
3250
+ </td>
3251
+ </tr>
3252
+ </tbody>
3253
+ </table>
3254
+ </React.Fragment>
3255
+ `)
3256
+ })
3257
+
3258
+ test('addMarkdownLineNumbers works with custom MDX components', () => {
3259
+ const code = dedent`
3260
+ # Regular Heading
3261
+
3262
+ <Heading level={1}>
3263
+ Custom component on line 3
3264
+ </Heading>
3265
+
3266
+ Regular paragraph.
3267
+
3268
+ <Cards count={2}>
3269
+ Another custom component on line 9
3270
+ </Cards>
3271
+ `
3272
+
3273
+ const { result, errors, html } = render(code, undefined, false, true)
3274
+
3275
+ // Should not have any errors
3276
+ expect(errors).toMatchInlineSnapshot(`[]`)
3277
+
3278
+ // Check that custom components have line numbers
3279
+ expect(html).toContain('data-markdown-line="3"')
3280
+ expect(html).toContain('data-markdown-line="9"')
3281
+
3282
+ expect(result).toMatchInlineSnapshot(`
3283
+ <React.Fragment>
3284
+ <h1
3285
+ data-markdown-line={1}
3286
+ >
3287
+ Regular Heading
3288
+ </h1>
3289
+ <Heading
3290
+ data-markdown-line={3}
3291
+ level={1}
3292
+ >
3293
+ <p
3294
+ data-markdown-line={4}
3295
+ >
3296
+ Custom component on line 3
3297
+ </p>
3298
+ </Heading>
3299
+ <p
3300
+ data-markdown-line={7}
3301
+ >
3302
+ Regular paragraph.
3303
+ </p>
3304
+ <Cards
3305
+ count={2}
3306
+ data-markdown-line={9}
3307
+ >
3308
+ <p
3309
+ data-markdown-line={10}
3310
+ >
3311
+ Another custom component on line 9
3312
+ </p>
3313
+ </Cards>
3314
+ </React.Fragment>
3315
+ `)
3316
+ })
3317
+
3318
+ test('jsx component with complex array props should show clear error message', () => {
3319
+ const code = dedent`
3320
+ <Tabs items={invalidFunction()} />
3321
+ `
3322
+
3323
+ const { errors } = render(code)
3324
+
3325
+ // Should have error with original error message included
3326
+ expect(errors.length).toBeGreaterThan(0)
3327
+
3328
+ // Find the error that contains the expression evaluation message
3329
+ const expressionError = errors.find(err =>
3330
+ err.message.includes('Failed to evaluate expression attribute: items={invalidFunction()}')
3331
+ )
3332
+
3333
+ expect(expressionError).toBeDefined()
3334
+ expect(expressionError!.message).toContain('Functions are not supported')
3335
+ expect(expressionError!.line).toBe(1)
3336
+ })