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/src/safe-mdx.tsx CHANGED
@@ -3,8 +3,12 @@ import React, { Suspense, cloneElement } from 'react'
3
3
  import type { StandardSchemaV1 } from '@standard-schema/spec'
4
4
  import type { Node, Parent, Root, RootContent } from 'mdast'
5
5
  import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'
6
+ import type { JSXElement, JSXAttribute, JSXText, JSXExpressionContainer } from 'estree-jsx'
7
+ import Evaluate from 'eval-estree-expression'
6
8
 
7
9
  import { Fragment, ReactNode } from 'react'
10
+ import { DynamicEsmComponent } from './dynamic-esm-component.js'
11
+ import { parseEsmImports, extractComponentInfo } from './esm-parser.js'
8
12
 
9
13
  const HtmlToJsxConverter = React.lazy(() =>
10
14
  import('./HtmlToJsxConverter.js').then((module) => ({
@@ -49,6 +53,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
49
53
  renderNode,
50
54
  componentPropsSchema,
51
55
  createElement,
56
+ allowClientEsmImports = false,
52
57
  }: {
53
58
  components?: ComponentsMap
54
59
  markdown?: string
@@ -56,6 +61,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
56
61
  renderNode?: RenderNode
57
62
  componentPropsSchema?: ComponentPropsSchema
58
63
  createElement?: CreateElementFunction
64
+ allowClientEsmImports?: boolean
59
65
  }) {
60
66
  const visitor = new MdastToJsx({
61
67
  markdown,
@@ -64,6 +70,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
64
70
  renderNode,
65
71
  componentPropsSchema,
66
72
  createElement,
73
+ allowClientEsmImports,
67
74
  })
68
75
  const result = visitor.run()
69
76
  return result
@@ -78,6 +85,8 @@ export class MdastToJsx {
78
85
  renderNode?: RenderNode
79
86
  componentPropsSchema?: ComponentPropsSchema
80
87
  createElement: CreateElementFunction
88
+ esmImports: Map<string, string> = new Map()
89
+ allowClientEsmImports: boolean
81
90
 
82
91
  constructor({
83
92
  markdown: code = '',
@@ -86,6 +95,7 @@ export class MdastToJsx {
86
95
  renderNode,
87
96
  componentPropsSchema,
88
97
  createElement = React.createElement,
98
+ allowClientEsmImports = false,
89
99
  }: {
90
100
  markdown?: string
91
101
  mdast: MyRootContent
@@ -96,6 +106,7 @@ export class MdastToJsx {
96
106
  ) => ReactNode | undefined
97
107
  componentPropsSchema?: ComponentPropsSchema
98
108
  createElement?: CreateElementFunction
109
+ allowClientEsmImports?: boolean
99
110
  }) {
100
111
  this.str = code
101
112
 
@@ -107,6 +118,8 @@ export class MdastToJsx {
107
118
 
108
119
  this.createElement = createElement
109
120
 
121
+ this.allowClientEsmImports = allowClientEsmImports
122
+
110
123
  this.c = {
111
124
  ...Object.fromEntries(
112
125
  nativeTags.map((tag) => {
@@ -120,9 +133,12 @@ export class MdastToJsx {
120
133
  validateComponentProps(
121
134
  componentName: string,
122
135
  props: Record<string, any>,
123
- line?: number
136
+ line?: number,
124
137
  ): void {
125
- if (!this.componentPropsSchema || !this.componentPropsSchema[componentName]) {
138
+ if (
139
+ !this.componentPropsSchema ||
140
+ !this.componentPropsSchema[componentName]
141
+ ) {
126
142
  return
127
143
  }
128
144
 
@@ -192,26 +208,55 @@ export class MdastToJsx {
192
208
  return []
193
209
  }
194
210
 
195
- const Component = accessWithDot(this.c, node.name)
196
-
197
- if (!Component) {
198
- this.errors.push({
199
- message: `Unsupported jsx component ${node.name}`,
200
- line: node.position?.start?.line,
211
+ // Check if this is an ESM imported component (only if allowed)
212
+ const esmImportInfo = this.allowClientEsmImports ? this.esmImports.get(node.name) : null
213
+ let Component
214
+
215
+ if (esmImportInfo) {
216
+ // Handle ESM imported component
217
+ const { importUrl, componentName } = extractComponentInfo(esmImportInfo)
218
+
219
+ Component = DynamicEsmComponent
220
+ let attrsList = this.getJsxAttrs(node, (err) => {
221
+ this.errors.push(err)
201
222
  })
202
- return null
223
+ let attrs = Object.fromEntries(attrsList)
224
+
225
+ return this.createElement(
226
+ Component,
227
+ { ...attrs, importUrl, componentName },
228
+ this.mapJsxChildren(node),
229
+ )
230
+ } else {
231
+ Component = accessWithDot(this.c, node.name)
232
+
233
+ if (!Component) {
234
+ this.errors.push({
235
+ message: `Unsupported jsx component ${node.name}`,
236
+ line: node.position?.start?.line,
237
+ })
238
+ return null
239
+ }
203
240
  }
204
241
 
205
- let attrsList = getJsxAttrs(node, (err) => {
242
+ let attrsList = this.getJsxAttrs(node, (err) => {
206
243
  this.errors.push(err)
207
244
  })
208
245
 
209
246
  let attrs = Object.fromEntries(attrsList)
210
247
 
211
248
  // Validate component props with schema if available
212
- this.validateComponentProps(node.name, attrs, node.position?.start?.line)
249
+ this.validateComponentProps(
250
+ node.name,
251
+ attrs,
252
+ node.position?.start?.line,
253
+ )
213
254
 
214
- return this.createElement(Component, attrs, this.mapJsxChildren(node))
255
+ return this.createElement(
256
+ Component,
257
+ attrs,
258
+ this.mapJsxChildren(node),
259
+ )
215
260
  }
216
261
  default: {
217
262
  return this.mdastTransformer(node)
@@ -219,6 +264,232 @@ export class MdastToJsx {
219
264
  }
220
265
  }
221
266
 
267
+ transformJsxElement(jsxElement: JSXElement, onError?: (err: SafeMdxError) => void, line?: number): ReactNode {
268
+ try {
269
+ // Handle JSX opening element
270
+ if (jsxElement.openingElement) {
271
+ const tagName = jsxElement.openingElement.name?.type === 'JSXIdentifier' ? jsxElement.openingElement.name.name : null
272
+ if (!tagName) {
273
+ onError?.({
274
+ message: 'JSX element missing component name',
275
+ line: line,
276
+ })
277
+ return null
278
+ }
279
+
280
+ // Check if this is an ESM imported component (only if allowed)
281
+ const esmImportInfo = this.allowClientEsmImports ? this.esmImports.get(tagName) : null
282
+ let Component
283
+
284
+ if (esmImportInfo) {
285
+ // Handle ESM imported component
286
+ const { importUrl, componentName } = extractComponentInfo(esmImportInfo)
287
+ Component = DynamicEsmComponent
288
+ } else {
289
+ // Get the component from the regular component map
290
+ Component = accessWithDot(this.c, tagName)
291
+ if (!Component) {
292
+ onError?.({
293
+ message: `Unsupported jsx component ${tagName} in attribute`,
294
+ line: line,
295
+ })
296
+ return null
297
+ }
298
+ }
299
+
300
+ // Extract attributes
301
+ const props: Record<string, any> = {}
302
+ if (jsxElement.openingElement.attributes) {
303
+ for (const attr of jsxElement.openingElement.attributes) {
304
+ if (attr.type === 'JSXAttribute' && attr.name?.type === 'JSXIdentifier' && attr.name.name) {
305
+ if (attr.value) {
306
+ if (attr.value.type === 'Literal') {
307
+ props[attr.name.name] = attr.value.value
308
+ } else if (attr.value.type === 'JSXExpressionContainer') {
309
+ if (attr.value.expression.type === 'Literal') {
310
+ props[attr.name.name] = attr.value.expression.value
311
+ }
312
+ }
313
+ } else {
314
+ props[attr.name.name] = true
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ // Extract children
321
+ const children: ReactNode[] = []
322
+ if (jsxElement.children) {
323
+ for (const child of jsxElement.children) {
324
+ if (child.type === 'JSXText') {
325
+ children.push(child.value)
326
+ } else if (child.type === 'JSXElement') {
327
+ const childElement = this.transformJsxElement(child)
328
+ if (childElement) {
329
+ children.push(childElement)
330
+ }
331
+ }
332
+ }
333
+ }
334
+
335
+ // Handle ESM imported components by adding required props
336
+ if (esmImportInfo) {
337
+ const { importUrl, componentName } = extractComponentInfo(esmImportInfo)
338
+ return this.createElement(
339
+ Component,
340
+ { ...props, importUrl, componentName },
341
+ ...children
342
+ )
343
+ } else {
344
+ return this.createElement(Component, props, ...children)
345
+ }
346
+ }
347
+ } catch (error) {
348
+ // Return null if transformation fails
349
+ onError?.({
350
+ message: `Failed to transform JSX element: ${error instanceof Error ? error.message : 'Unknown error'}`,
351
+ line: line,
352
+ })
353
+ return null
354
+ }
355
+ return null
356
+ }
357
+
358
+ getJsxAttrs(
359
+ node: MdxJsxFlowElement | MdxJsxTextElement,
360
+ onError: (err: SafeMdxError) => void = console.error,
361
+ ) {
362
+ let attrsList: [string, any][] = []
363
+
364
+ for (const attr of node.attributes) {
365
+ if (attr.type === 'mdxJsxExpressionAttribute') {
366
+ // Handle spread expressions like {...{key: '1'}}
367
+ if (attr.data?.estree) {
368
+ try {
369
+ const program = attr.data.estree
370
+ if (
371
+ program.body?.length > 0 &&
372
+ program.body[0].type === 'ExpressionStatement'
373
+ ) {
374
+ const expression = program.body[0].expression
375
+ try {
376
+ const result = Evaluate.evaluate.sync(expression)
377
+
378
+ // Handle spread syntax - merge the evaluated object
379
+ if (typeof result === 'object' && result != null) {
380
+ const entries = Object.entries(result)
381
+ attrsList.push(...entries)
382
+ }
383
+ } catch (error) {
384
+ onError({
385
+ message: `Failed to evaluate expression attribute: ${attr.value
386
+ .replace(/\n+/g, ' ')
387
+ .replace(/ +/g, ' ')}`,
388
+ line: attr.position?.start?.line,
389
+ })
390
+ }
391
+ }
392
+ } catch (error) {
393
+ onError({
394
+ message: `Failed to evaluate expression attribute: ${attr.value
395
+ .replace(/\n+/g, ' ')
396
+ .replace(/ +/g, ' ')}`,
397
+ line: attr.position?.start?.line,
398
+ })
399
+ }
400
+ } else {
401
+ onError({
402
+ message: `Expressions in jsx props are not supported (${attr.value
403
+ .replace(/\n+/g, ' ')
404
+ .replace(/ +/g, ' ')})`,
405
+ line: attr.position?.start?.line,
406
+ })
407
+ }
408
+ continue
409
+ }
410
+
411
+ if (attr.type !== 'mdxJsxAttribute') {
412
+ onError({
413
+ message: `non mdxJsxAttribute attribute is not supported: ${attr}`,
414
+ line: node.position?.start?.line,
415
+ })
416
+ continue
417
+ }
418
+
419
+ const v = attr.value
420
+ if (typeof v === 'string' || typeof v === 'number') {
421
+ attrsList.push([attr.name, v])
422
+ continue
423
+ }
424
+ if (v === null) {
425
+ attrsList.push([attr.name, true])
426
+ continue
427
+ }
428
+ if (v?.type === 'mdxJsxAttributeValueExpression') {
429
+ // Manual parsing fallback for simple values
430
+ if (v.value === 'true') {
431
+ attrsList.push([attr.name, true])
432
+ continue
433
+ }
434
+ if (v.value === 'false') {
435
+ attrsList.push([attr.name, false])
436
+ continue
437
+ }
438
+ if (v.value === 'null') {
439
+ attrsList.push([attr.name, null])
440
+ continue
441
+ }
442
+ if (v.value === 'undefined') {
443
+ attrsList.push([attr.name, undefined])
444
+ continue
445
+ }
446
+
447
+ if (v.data?.estree) {
448
+ try {
449
+ // Extract the expression from the Program body
450
+ const program = v.data.estree
451
+ if (
452
+ program.body?.length > 0 &&
453
+ program.body[0].type === 'ExpressionStatement'
454
+ ) {
455
+ const expression = program.body[0].expression
456
+
457
+ // Check if this is a JSX element
458
+ if (expression.type === 'JSXElement') {
459
+ // Transform JSX element to React element
460
+ const jsxElement = this.transformJsxElement(expression, onError, attr.position?.start?.line)
461
+ if (jsxElement) {
462
+ attrsList.push([attr.name, jsxElement])
463
+ continue
464
+ }
465
+ }
466
+
467
+ try {
468
+ // Evaluate the expression synchronously
469
+ const result = Evaluate.evaluate.sync(expression)
470
+ attrsList.push([attr.name, result])
471
+ continue
472
+ } catch (error) {
473
+ onError({
474
+ message: `Failed to evaluate expression attribute: ${attr.name}={${v.value}}`,
475
+ line: attr.position?.start?.line,
476
+ })
477
+ }
478
+ }
479
+ } catch (error) {
480
+ // Fall back to the original manual parsing for backwards compatibility
481
+ }
482
+ }
483
+
484
+ onError({
485
+ message: `Expressions in jsx prop not evaluated: (${attr.name}={${v.value}})`,
486
+ line: attr.position?.start?.line,
487
+ })
488
+ }
489
+ }
490
+ return attrsList
491
+ }
492
+
222
493
  run() {
223
494
  const res = this.mdastTransformer(this.mdast) as ReactNode
224
495
  if (Array.isArray(res) && res.length === 1) {
@@ -245,10 +516,13 @@ export class MdastToJsx {
245
516
 
246
517
  switch (node.type) {
247
518
  case 'mdxjsEsm': {
248
- const start = node.position?.start?.offset
249
- const end = node.position?.end?.offset
250
- let text = this.str.slice(start, end)
251
-
519
+ // Parse ESM imports and merge into our imports map (only if allowed)
520
+ if (this.allowClientEsmImports) {
521
+ const parsedImports = parseEsmImports(node, (err) => this.errors.push(err))
522
+ parsedImports.forEach((value, key) => {
523
+ this.esmImports.set(key, value)
524
+ })
525
+ }
252
526
  return []
253
527
  }
254
528
  case 'mdxJsxTextElement':
@@ -275,6 +549,36 @@ export class MdastToJsx {
275
549
  if (!node.value) {
276
550
  return []
277
551
  }
552
+
553
+ // Check if we have an estree AST
554
+ if (node.data?.estree) {
555
+ try {
556
+ // Extract the expression from the Program body
557
+ const program = node.data.estree
558
+ if (
559
+ program.body?.length > 0 &&
560
+ program.body[0].type === 'ExpressionStatement'
561
+ ) {
562
+ const expression = program.body[0].expression
563
+ try {
564
+ // Evaluate the expression synchronously
565
+ const result = Evaluate.evaluate.sync(expression)
566
+ return result
567
+ } catch (error) {
568
+ this.errors.push({
569
+ message: `Failed to evaluate expression: ${node.value}`,
570
+ line: node.position?.start?.line,
571
+ })
572
+ }
573
+ }
574
+ } catch (error) {
575
+ this.errors.push({
576
+ message: `Failed to evaluate expression: ${node.value}`,
577
+ line: node.position?.start?.line,
578
+ })
579
+ }
580
+ }
581
+
278
582
  return []
279
583
  }
280
584
  case 'yaml': {
@@ -287,13 +591,25 @@ export class MdastToJsx {
287
591
  const level = node.depth
288
592
  const Tag = this.c[`h${level}`] ?? `h${level}`
289
593
 
290
- return this.createElement(Tag, node.data?.hProperties, this.mapMdastChildren(node))
594
+ return this.createElement(
595
+ Tag,
596
+ node.data?.hProperties,
597
+ this.mapMdastChildren(node),
598
+ )
291
599
  }
292
600
  case 'paragraph': {
293
- return this.createElement(this.c.p, node.data?.hProperties, this.mapMdastChildren(node))
601
+ return this.createElement(
602
+ this.c.p,
603
+ node.data?.hProperties,
604
+ this.mapMdastChildren(node),
605
+ )
294
606
  }
295
607
  case 'blockquote': {
296
- return this.createElement(this.c.blockquote, node.data?.hProperties, this.mapMdastChildren(node))
608
+ return this.createElement(
609
+ this.c.blockquote,
610
+ node.data?.hProperties,
611
+ this.mapMdastChildren(node),
612
+ )
297
613
  }
298
614
  case 'thematicBreak': {
299
615
  return this.createElement(this.c.hr, node.data?.hProperties)
@@ -304,11 +620,12 @@ export class MdastToJsx {
304
620
  }
305
621
  const language = node.lang || ''
306
622
  const code = node.value
307
- const codeBlock = (className?: string) => this.createElement(
308
- this.c.pre,
309
- node.data?.hProperties,
310
- this.createElement(this.c.code, { className }, code)
311
- )
623
+ const codeBlock = (className?: string) =>
624
+ this.createElement(
625
+ this.c.pre,
626
+ node.data?.hProperties,
627
+ this.createElement(this.c.code, { className }, code),
628
+ )
312
629
 
313
630
  if (language) {
314
631
  return codeBlock(`language-${language}`)
@@ -321,28 +638,43 @@ export class MdastToJsx {
321
638
  return this.createElement(
322
639
  this.c.ol,
323
640
  { start: node.start!, ...node.data?.hProperties },
324
- this.mapMdastChildren(node)
641
+ this.mapMdastChildren(node),
325
642
  )
326
643
  }
327
- return this.createElement(this.c.ul, node.data?.hProperties, this.mapMdastChildren(node))
644
+ return this.createElement(
645
+ this.c.ul,
646
+ node.data?.hProperties,
647
+ this.mapMdastChildren(node),
648
+ )
328
649
  }
329
650
  case 'listItem': {
330
651
  // https://github.com/syntax-tree/mdast-util-gfm-task-list-item#syntax-tree
331
652
  if (node?.checked != null) {
332
653
  return this.createElement(
333
654
  this.c.li,
334
- { 'data-checked': node.checked, ...node.data?.hProperties },
335
- this.mapMdastChildren(node)
655
+ {
656
+ 'data-checked': node.checked,
657
+ ...node.data?.hProperties,
658
+ },
659
+ this.mapMdastChildren(node),
336
660
  )
337
661
  }
338
- return this.createElement(this.c.li, node.data?.hProperties, this.mapMdastChildren(node))
662
+ return this.createElement(
663
+ this.c.li,
664
+ node.data?.hProperties,
665
+ this.mapMdastChildren(node),
666
+ )
339
667
  }
340
668
  case 'text': {
341
669
  if (!node.value) {
342
670
  return []
343
671
  }
344
672
  if (node.data?.hProperties) {
345
- return this.createElement(this.c.span, node.data.hProperties, node.value)
673
+ return this.createElement(
674
+ this.c.span,
675
+ node.data.hProperties,
676
+ node.value,
677
+ )
346
678
  }
347
679
  return node.value
348
680
  }
@@ -354,7 +686,7 @@ export class MdastToJsx {
354
686
  src,
355
687
  alt,
356
688
  title,
357
- ...node.data?.hProperties
689
+ ...node.data?.hProperties,
358
690
  })
359
691
  }
360
692
  case 'link': {
@@ -363,32 +695,56 @@ export class MdastToJsx {
363
695
  return this.createElement(
364
696
  this.c.a,
365
697
  { href, title, ...node.data?.hProperties },
366
- this.mapMdastChildren(node)
698
+ this.mapMdastChildren(node),
367
699
  )
368
700
  }
369
701
  case 'strong': {
370
- return this.createElement(this.c.strong, node.data?.hProperties, this.mapMdastChildren(node))
702
+ return this.createElement(
703
+ this.c.strong,
704
+ node.data?.hProperties,
705
+ this.mapMdastChildren(node),
706
+ )
371
707
  }
372
708
  case 'emphasis': {
373
- return this.createElement(this.c.em, node.data?.hProperties, this.mapMdastChildren(node))
709
+ return this.createElement(
710
+ this.c.em,
711
+ node.data?.hProperties,
712
+ this.mapMdastChildren(node),
713
+ )
374
714
  }
375
715
  case 'delete': {
376
- return this.createElement(this.c.del, node.data?.hProperties, this.mapMdastChildren(node))
716
+ return this.createElement(
717
+ this.c.del,
718
+ node.data?.hProperties,
719
+ this.mapMdastChildren(node),
720
+ )
377
721
  }
378
722
  case 'inlineCode': {
379
723
  if (!node.value) {
380
724
  return []
381
725
  }
382
- return this.createElement(this.c.code, node.data?.hProperties, node.value)
726
+ return this.createElement(
727
+ this.c.code,
728
+ node.data?.hProperties,
729
+ node.value,
730
+ )
383
731
  }
384
732
  case 'break': {
385
733
  return this.createElement(this.c.br, node.data?.hProperties)
386
734
  }
387
735
  case 'root': {
388
736
  if (node.data?.hProperties) {
389
- return this.createElement(this.c.div, node.data.hProperties, this.mapMdastChildren(node))
737
+ return this.createElement(
738
+ this.c.div,
739
+ node.data.hProperties,
740
+ this.mapMdastChildren(node),
741
+ )
390
742
  }
391
- return this.createElement(Fragment, null, this.mapMdastChildren(node))
743
+ return this.createElement(
744
+ Fragment,
745
+ null,
746
+ this.mapMdastChildren(node),
747
+ )
392
748
  }
393
749
  case 'table': {
394
750
  const [head, ...body] = React.Children.toArray(
@@ -398,14 +754,15 @@ export class MdastToJsx {
398
754
  this.c.table,
399
755
  node.data?.hProperties,
400
756
  head && this.createElement(this.c.thead, null, head),
401
- !!body?.length && this.createElement(this.c.tbody, null, body)
757
+ !!body?.length &&
758
+ this.createElement(this.c.tbody, null, body),
402
759
  )
403
760
  }
404
761
  case 'tableRow': {
405
762
  return this.createElement(
406
763
  this.c.tr,
407
764
  { className: '', ...node.data?.hProperties },
408
- this.mapMdastChildren(node)
765
+ this.mapMdastChildren(node),
409
766
  )
410
767
  }
411
768
  case 'tableCell': {
@@ -413,7 +770,7 @@ export class MdastToJsx {
413
770
  return this.createElement(
414
771
  this.c.td,
415
772
  { className: '', ...node.data?.hProperties },
416
- content
773
+ content,
417
774
  )
418
775
  }
419
776
  case 'definition': {
@@ -421,19 +778,21 @@ export class MdastToJsx {
421
778
  }
422
779
  case 'linkReference': {
423
780
  let href = ''
781
+ let title = ''
424
782
  mdastBfs(this.mdast, (child: any) => {
425
783
  if (
426
784
  child.type === 'definition' &&
427
785
  child.identifier === node.identifier
428
786
  ) {
429
- href = child.url
787
+ href = child.url || ''
788
+ title = child.title || ''
430
789
  }
431
790
  })
432
791
 
433
792
  return this.createElement(
434
793
  this.c.a,
435
- { href, ...node.data?.hProperties },
436
- this.mapMdastChildren(node)
794
+ { href, title, ...node.data?.hProperties },
795
+ this.mapMdastChildren(node),
437
796
  )
438
797
  }
439
798
  case 'footnoteReference': {
@@ -457,8 +816,8 @@ export class MdastToJsx {
457
816
  this.createElement(HtmlToJsxConverter, {
458
817
  htmlText: text,
459
818
  instance: this,
460
- node
461
- })
819
+ node,
820
+ }),
462
821
  )
463
822
  }
464
823
  case 'imageReference': {
@@ -480,80 +839,6 @@ export class MdastToJsx {
480
839
  }
481
840
  }
482
841
 
483
- export function getJsxAttrs(
484
- node: MdxJsxFlowElement | MdxJsxTextElement,
485
- onError: (err: SafeMdxError) => void = console.error,
486
- ) {
487
- let attrsList = node.attributes
488
- .map((attr) => {
489
- if (attr.type === 'mdxJsxExpressionAttribute') {
490
-
491
- onError({
492
- message: `Expressions in jsx props are not supported (${attr.value
493
- .replace(/\n+/g, ' ')
494
- .replace(/ +/g, ' ')})`,
495
- line: attr.position?.start?.line,
496
- })
497
- return
498
- }
499
- if (attr.type !== 'mdxJsxAttribute') {
500
- throw new Error(`non mdxJsxAttribute is not supported: ${attr}`)
501
- }
502
-
503
- const v = attr.value
504
- if (typeof v === 'string' || typeof v === 'number') {
505
- return [attr.name, v]
506
- }
507
- if (v === null) {
508
- return [attr.name, true]
509
- }
510
- if (v?.type === 'mdxJsxAttributeValueExpression') {
511
- if (v.value === 'true') {
512
- return [attr.name, true]
513
- }
514
- if (v.value === 'false') {
515
- return [attr.name, false]
516
- }
517
- if (v.value === 'null') {
518
- return [attr.name, null]
519
- }
520
- if (v.value === 'undefined') {
521
- return [attr.name, undefined]
522
- }
523
- let quote = ['"', "'", '`'].find(
524
- (q) => v.value.startsWith(q) && v.value.endsWith(q),
525
- )
526
- if (quote) {
527
- let value = v.value
528
- if (quote !== '"') {
529
- value = v.value.replace(new RegExp(quote, 'g'), '"')
530
- }
531
- return [attr.name, JSON.parse(value)]
532
- }
533
-
534
- const number = Number(v.value)
535
- if (!isNaN(number)) {
536
- return [attr.name, number]
537
- }
538
- const parsedJson = safeJsonParse(v.value)
539
- if (parsedJson) {
540
- return [attr.name, parsedJson]
541
- }
542
-
543
- onError({
544
- message: `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
545
- line: attr.position?.start?.line,
546
- })
547
- } else {
548
- console.log('unhandled attr', { attr }, attr.type)
549
- }
550
-
551
- return
552
- })
553
- .filter(isTruthy) as [string, any][]
554
- return attrsList
555
- }
556
-
557
842
  function isTruthy<T>(val: T | undefined | null | false): val is T {
558
843
  return Boolean(val)
559
844
  }