safe-mdx 1.0.4 → 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
@@ -1,10 +1,14 @@
1
- import React, { use, cloneElement, Suspense } from 'react'
1
+ import React, { Suspense, cloneElement } from 'react'
2
2
 
3
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
3
4
  import type { Node, Parent, Root, RootContent } from 'mdast'
4
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'
5
8
 
6
9
  import { Fragment, ReactNode } from 'react'
7
- import { completeJsxTags } from './streaming.js'
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) => ({
@@ -28,22 +32,45 @@ export type RenderNode = (
28
32
  transform: (node: MyRootContent) => ReactNode,
29
33
  ) => ReactNode | undefined
30
34
 
35
+ export interface SafeMdxError {
36
+ message: string
37
+ line?: number
38
+ schemaPath?: string
39
+ }
40
+
41
+ export type ComponentPropsSchema = Record<string, StandardSchemaV1>
42
+
43
+ export type CreateElementFunction = (
44
+ type: any,
45
+ props?: any,
46
+ ...children: ReactNode[]
47
+ ) => ReactNode
48
+
31
49
  export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
32
50
  components,
33
51
  markdown = '',
34
52
  mdast = null as any,
35
53
  renderNode,
54
+ componentPropsSchema,
55
+ createElement,
56
+ allowClientEsmImports = false,
36
57
  }: {
37
58
  components?: ComponentsMap
38
59
  markdown?: string
39
60
  mdast: MyRootContent
40
61
  renderNode?: RenderNode
62
+ componentPropsSchema?: ComponentPropsSchema
63
+ createElement?: CreateElementFunction
64
+ allowClientEsmImports?: boolean
41
65
  }) {
42
66
  const visitor = new MdastToJsx({
43
67
  markdown,
44
68
  mdast,
45
69
  components,
46
70
  renderNode,
71
+ componentPropsSchema,
72
+ createElement,
73
+ allowClientEsmImports,
47
74
  })
48
75
  const result = visitor.run()
49
76
  return result
@@ -54,14 +81,21 @@ export class MdastToJsx {
54
81
  str: string
55
82
  jsxStr: string = ''
56
83
  c: ComponentsMap
57
- errors: { message: string }[] = []
84
+ errors: SafeMdxError[] = []
58
85
  renderNode?: RenderNode
86
+ componentPropsSchema?: ComponentPropsSchema
87
+ createElement: CreateElementFunction
88
+ esmImports: Map<string, string> = new Map()
89
+ allowClientEsmImports: boolean
59
90
 
60
91
  constructor({
61
92
  markdown: code = '',
62
93
  mdast,
63
94
  components = {} as ComponentsMap,
64
95
  renderNode,
96
+ componentPropsSchema,
97
+ createElement = React.createElement,
98
+ allowClientEsmImports = false,
65
99
  }: {
66
100
  markdown?: string
67
101
  mdast: MyRootContent
@@ -70,6 +104,9 @@ export class MdastToJsx {
70
104
  node: MyRootContent,
71
105
  transform: (node: MyRootContent) => ReactNode,
72
106
  ) => ReactNode | undefined
107
+ componentPropsSchema?: ComponentPropsSchema
108
+ createElement?: CreateElementFunction
109
+ allowClientEsmImports?: boolean
73
110
  }) {
74
111
  this.str = code
75
112
 
@@ -77,6 +114,12 @@ export class MdastToJsx {
77
114
 
78
115
  this.renderNode = renderNode
79
116
 
117
+ this.componentPropsSchema = componentPropsSchema
118
+
119
+ this.createElement = createElement
120
+
121
+ this.allowClientEsmImports = allowClientEsmImports
122
+
80
123
  this.c = {
81
124
  ...Object.fromEntries(
82
125
  nativeTags.map((tag) => {
@@ -86,6 +129,39 @@ export class MdastToJsx {
86
129
  ...components,
87
130
  }
88
131
  }
132
+
133
+ validateComponentProps(
134
+ componentName: string,
135
+ props: Record<string, any>,
136
+ line?: number,
137
+ ): void {
138
+ if (
139
+ !this.componentPropsSchema ||
140
+ !this.componentPropsSchema[componentName]
141
+ ) {
142
+ return
143
+ }
144
+
145
+ const schema = this.componentPropsSchema[componentName]
146
+ let result = schema['~standard'].validate(props)
147
+
148
+ if (result instanceof Promise) {
149
+ // Ignore async validation errors as requested
150
+ return
151
+ } else {
152
+ if (result.issues) {
153
+ result.issues.forEach((issue) => {
154
+ const propPath = issue.path?.join('.') || 'unknown'
155
+ this.errors.push({
156
+ message: `Invalid props for component "${componentName}" at "${propPath}": ${issue.message}`,
157
+ line,
158
+ schemaPath: issue.path?.join('.'),
159
+ })
160
+ })
161
+ }
162
+ }
163
+ }
164
+
89
165
  mapMdastChildren(node: any) {
90
166
  const res = node.children
91
167
  ?.flatMap((child) => this.mdastTransformer(child))
@@ -132,24 +208,54 @@ export class MdastToJsx {
132
208
  return []
133
209
  }
134
210
 
135
- const Component = accessWithDot(this.c, node.name)
136
-
137
- if (!Component) {
138
- this.errors.push({
139
- message: `Unsupported jsx component ${node.name}`,
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)
140
222
  })
141
- 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
+ }
142
240
  }
143
241
 
144
- let attrsList = getJsxAttrs(node, (err) => {
242
+ let attrsList = this.getJsxAttrs(node, (err) => {
145
243
  this.errors.push(err)
146
244
  })
147
245
 
148
246
  let attrs = Object.fromEntries(attrsList)
149
- return (
150
- <Component {...attrs}>
151
- {this.mapJsxChildren(node)}
152
- </Component>
247
+
248
+ // Validate component props with schema if available
249
+ this.validateComponentProps(
250
+ node.name,
251
+ attrs,
252
+ node.position?.start?.line,
253
+ )
254
+
255
+ return this.createElement(
256
+ Component,
257
+ attrs,
258
+ this.mapJsxChildren(node),
153
259
  )
154
260
  }
155
261
  default: {
@@ -158,6 +264,232 @@ export class MdastToJsx {
158
264
  }
159
265
  }
160
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
+
161
493
  run() {
162
494
  const res = this.mdastTransformer(this.mdast) as ReactNode
163
495
  if (Array.isArray(res) && res.length === 1) {
@@ -184,10 +516,13 @@ export class MdastToJsx {
184
516
 
185
517
  switch (node.type) {
186
518
  case 'mdxjsEsm': {
187
- const start = node.position?.start?.offset
188
- const end = node.position?.end?.offset
189
- let text = this.str.slice(start, end)
190
-
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
+ }
191
526
  return []
192
527
  }
193
528
  case 'mdxJsxTextElement':
@@ -214,6 +549,36 @@ export class MdastToJsx {
214
549
  if (!node.value) {
215
550
  return []
216
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
+
217
582
  return []
218
583
  }
219
584
  case 'yaml': {
@@ -226,28 +591,28 @@ export class MdastToJsx {
226
591
  const level = node.depth
227
592
  const Tag = this.c[`h${level}`] ?? `h${level}`
228
593
 
229
- return (
230
- <Tag {...node.data?.hProperties}>
231
- {this.mapMdastChildren(node)}
232
- </Tag>
594
+ return this.createElement(
595
+ Tag,
596
+ node.data?.hProperties,
597
+ this.mapMdastChildren(node),
233
598
  )
234
599
  }
235
600
  case 'paragraph': {
236
- return (
237
- <this.c.p {...node.data?.hProperties}>
238
- {this.mapMdastChildren(node)}
239
- </this.c.p>
601
+ return this.createElement(
602
+ this.c.p,
603
+ node.data?.hProperties,
604
+ this.mapMdastChildren(node),
240
605
  )
241
606
  }
242
607
  case 'blockquote': {
243
- return (
244
- <this.c.blockquote {...node.data?.hProperties}>
245
- {this.mapMdastChildren(node)}
246
- </this.c.blockquote>
608
+ return this.createElement(
609
+ this.c.blockquote,
610
+ node.data?.hProperties,
611
+ this.mapMdastChildren(node),
247
612
  )
248
613
  }
249
614
  case 'thematicBreak': {
250
- return <this.c.hr {...node.data?.hProperties} />
615
+ return this.createElement(this.c.hr, node.data?.hProperties)
251
616
  }
252
617
  case 'code': {
253
618
  if (!node.value) {
@@ -255,11 +620,12 @@ export class MdastToJsx {
255
620
  }
256
621
  const language = node.lang || ''
257
622
  const code = node.value
258
- const codeBlock = (className?: string) => (
259
- <this.c.pre {...node.data?.hProperties}>
260
- <this.c.code className={className}>{code}</this.c.code>
261
- </this.c.pre>
262
- )
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
+ )
263
629
 
264
630
  if (language) {
265
631
  return codeBlock(`language-${language}`)
@@ -269,37 +635,34 @@ export class MdastToJsx {
269
635
 
270
636
  case 'list': {
271
637
  if (node.ordered) {
272
- return (
273
- <this.c.ol
274
- start={node.start!}
275
- {...node.data?.hProperties}
276
- >
277
- {this.mapMdastChildren(node)}
278
- </this.c.ol>
638
+ return this.createElement(
639
+ this.c.ol,
640
+ { start: node.start!, ...node.data?.hProperties },
641
+ this.mapMdastChildren(node),
279
642
  )
280
643
  }
281
- return (
282
- <this.c.ul {...node.data?.hProperties}>
283
- {this.mapMdastChildren(node)}
284
- </this.c.ul>
644
+ return this.createElement(
645
+ this.c.ul,
646
+ node.data?.hProperties,
647
+ this.mapMdastChildren(node),
285
648
  )
286
649
  }
287
650
  case 'listItem': {
288
651
  // https://github.com/syntax-tree/mdast-util-gfm-task-list-item#syntax-tree
289
652
  if (node?.checked != null) {
290
- return (
291
- <this.c.li
292
- data-checked={node.checked}
293
- {...node.data?.hProperties}
294
- >
295
- {this.mapMdastChildren(node)}
296
- </this.c.li>
653
+ return this.createElement(
654
+ this.c.li,
655
+ {
656
+ 'data-checked': node.checked,
657
+ ...node.data?.hProperties,
658
+ },
659
+ this.mapMdastChildren(node),
297
660
  )
298
661
  }
299
- return (
300
- <this.c.li {...node.data?.hProperties}>
301
- {this.mapMdastChildren(node)}
302
- </this.c.li>
662
+ return this.createElement(
663
+ this.c.li,
664
+ node.data?.hProperties,
665
+ this.mapMdastChildren(node),
303
666
  )
304
667
  }
305
668
  case 'text': {
@@ -307,10 +670,10 @@ export class MdastToJsx {
307
670
  return []
308
671
  }
309
672
  if (node.data?.hProperties) {
310
- return (
311
- <this.c.span {...node.data.hProperties}>
312
- {node.value}
313
- </this.c.span>
673
+ return this.createElement(
674
+ this.c.span,
675
+ node.data.hProperties,
676
+ node.value,
314
677
  )
315
678
  }
316
679
  return node.value
@@ -319,92 +682,95 @@ export class MdastToJsx {
319
682
  const src = node.url || ''
320
683
  const alt = node.alt || ''
321
684
  const title = node.title || ''
322
- return (
323
- <this.c.img
324
- src={src}
325
- alt={alt}
326
- title={title}
327
- {...node.data?.hProperties}
328
- />
329
- )
685
+ return this.createElement(this.c.img, {
686
+ src,
687
+ alt,
688
+ title,
689
+ ...node.data?.hProperties,
690
+ })
330
691
  }
331
692
  case 'link': {
332
693
  const href = node.url || ''
333
694
  const title = node.title || ''
334
- return (
335
- <this.c.a {...{ href, title }} {...node.data?.hProperties}>
336
- {this.mapMdastChildren(node)}
337
- </this.c.a>
695
+ return this.createElement(
696
+ this.c.a,
697
+ { href, title, ...node.data?.hProperties },
698
+ this.mapMdastChildren(node),
338
699
  )
339
700
  }
340
701
  case 'strong': {
341
- return (
342
- <this.c.strong {...node.data?.hProperties}>
343
- {this.mapMdastChildren(node)}
344
- </this.c.strong>
702
+ return this.createElement(
703
+ this.c.strong,
704
+ node.data?.hProperties,
705
+ this.mapMdastChildren(node),
345
706
  )
346
707
  }
347
708
  case 'emphasis': {
348
- return (
349
- <this.c.em {...node.data?.hProperties}>
350
- {this.mapMdastChildren(node)}
351
- </this.c.em>
709
+ return this.createElement(
710
+ this.c.em,
711
+ node.data?.hProperties,
712
+ this.mapMdastChildren(node),
352
713
  )
353
714
  }
354
715
  case 'delete': {
355
- return (
356
- <this.c.del {...node.data?.hProperties}>
357
- {this.mapMdastChildren(node)}
358
- </this.c.del>
716
+ return this.createElement(
717
+ this.c.del,
718
+ node.data?.hProperties,
719
+ this.mapMdastChildren(node),
359
720
  )
360
721
  }
361
722
  case 'inlineCode': {
362
723
  if (!node.value) {
363
724
  return []
364
725
  }
365
- return (
366
- <this.c.code {...node.data?.hProperties}>
367
- {node.value}
368
- </this.c.code>
726
+ return this.createElement(
727
+ this.c.code,
728
+ node.data?.hProperties,
729
+ node.value,
369
730
  )
370
731
  }
371
732
  case 'break': {
372
- return <this.c.br {...node.data?.hProperties} />
733
+ return this.createElement(this.c.br, node.data?.hProperties)
373
734
  }
374
735
  case 'root': {
375
736
  if (node.data?.hProperties) {
376
- return (
377
- <this.c.div {...node.data.hProperties}>
378
- {this.mapMdastChildren(node)}
379
- </this.c.div>
737
+ return this.createElement(
738
+ this.c.div,
739
+ node.data.hProperties,
740
+ this.mapMdastChildren(node),
380
741
  )
381
742
  }
382
- return <Fragment>{this.mapMdastChildren(node)}</Fragment>
743
+ return this.createElement(
744
+ Fragment,
745
+ null,
746
+ this.mapMdastChildren(node),
747
+ )
383
748
  }
384
749
  case 'table': {
385
750
  const [head, ...body] = React.Children.toArray(
386
751
  this.mapMdastChildren(node),
387
752
  )
388
- return (
389
- <this.c.table {...node.data?.hProperties}>
390
- {head && <this.c.thead>{head}</this.c.thead>}
391
- {!!body?.length && <this.c.tbody>{body}</this.c.tbody>}
392
- </this.c.table>
753
+ return this.createElement(
754
+ this.c.table,
755
+ node.data?.hProperties,
756
+ head && this.createElement(this.c.thead, null, head),
757
+ !!body?.length &&
758
+ this.createElement(this.c.tbody, null, body),
393
759
  )
394
760
  }
395
761
  case 'tableRow': {
396
- return (
397
- <this.c.tr className='' {...node.data?.hProperties}>
398
- {this.mapMdastChildren(node)}
399
- </this.c.tr>
762
+ return this.createElement(
763
+ this.c.tr,
764
+ { className: '', ...node.data?.hProperties },
765
+ this.mapMdastChildren(node),
400
766
  )
401
767
  }
402
768
  case 'tableCell': {
403
769
  let content = this.mapMdastChildren(node)
404
- return (
405
- <this.c.td className='' {...node.data?.hProperties}>
406
- {content}
407
- </this.c.td>
770
+ return this.createElement(
771
+ this.c.td,
772
+ { className: '', ...node.data?.hProperties },
773
+ content,
408
774
  )
409
775
  }
410
776
  case 'definition': {
@@ -412,19 +778,21 @@ export class MdastToJsx {
412
778
  }
413
779
  case 'linkReference': {
414
780
  let href = ''
781
+ let title = ''
415
782
  mdastBfs(this.mdast, (child: any) => {
416
783
  if (
417
784
  child.type === 'definition' &&
418
785
  child.identifier === node.identifier
419
786
  ) {
420
- href = child.url
787
+ href = child.url || ''
788
+ title = child.title || ''
421
789
  }
422
790
  })
423
791
 
424
- return (
425
- <this.c.a href={href} {...node.data?.hProperties}>
426
- {this.mapMdastChildren(node)}
427
- </this.c.a>
792
+ return this.createElement(
793
+ this.c.a,
794
+ { href, title, ...node.data?.hProperties },
795
+ this.mapMdastChildren(node),
428
796
  )
429
797
  }
430
798
  case 'footnoteReference': {
@@ -442,14 +810,14 @@ export class MdastToJsx {
442
810
  return []
443
811
  }
444
812
 
445
- return (
446
- <Suspense fallback={null}>
447
- <HtmlToJsxConverter
448
- htmlText={text}
449
- instance={this}
450
- node={node}
451
- />
452
- </Suspense>
813
+ return this.createElement(
814
+ Suspense,
815
+ { fallback: null },
816
+ this.createElement(HtmlToJsxConverter, {
817
+ htmlText: text,
818
+ instance: this,
819
+ node,
820
+ }),
453
821
  )
454
822
  }
455
823
  case 'imageReference': {
@@ -471,77 +839,6 @@ export class MdastToJsx {
471
839
  }
472
840
  }
473
841
 
474
- export function getJsxAttrs(
475
- node: MdxJsxFlowElement | MdxJsxTextElement,
476
- onError: (err: { message: string }) => void = console.error,
477
- ) {
478
- let attrsList = node.attributes
479
- .map((attr) => {
480
- if (attr.type === 'mdxJsxExpressionAttribute') {
481
- onError({
482
- message: `Expressions in jsx props are not supported (${attr.value
483
- .replace(/\n+/g, ' ')
484
- .replace(/ +/g, ' ')})`,
485
- })
486
- return
487
- }
488
- if (attr.type !== 'mdxJsxAttribute') {
489
- throw new Error(`non mdxJsxAttribute is not supported: ${attr}`)
490
- }
491
-
492
- const v = attr.value
493
- if (typeof v === 'string' || typeof v === 'number') {
494
- return [attr.name, v]
495
- }
496
- if (v === null) {
497
- return [attr.name, true]
498
- }
499
- if (v?.type === 'mdxJsxAttributeValueExpression') {
500
- if (v.value === 'true') {
501
- return [attr.name, true]
502
- }
503
- if (v.value === 'false') {
504
- return [attr.name, false]
505
- }
506
- if (v.value === 'null') {
507
- return [attr.name, null]
508
- }
509
- if (v.value === 'undefined') {
510
- return [attr.name, undefined]
511
- }
512
- let quote = ['"', "'", '`'].find(
513
- (q) => v.value.startsWith(q) && v.value.endsWith(q),
514
- )
515
- if (quote) {
516
- let value = v.value
517
- if (quote !== '"') {
518
- value = v.value.replace(new RegExp(quote, 'g'), '"')
519
- }
520
- return [attr.name, JSON.parse(value)]
521
- }
522
-
523
- const number = Number(v.value)
524
- if (!isNaN(number)) {
525
- return [attr.name, number]
526
- }
527
- const parsedJson = safeJsonParse(v.value)
528
- if (parsedJson) {
529
- return [attr.name, parsedJson]
530
- }
531
-
532
- onError({
533
- message: `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
534
- })
535
- } else {
536
- console.log('unhandled attr', { attr }, attr.type)
537
- }
538
-
539
- return
540
- })
541
- .filter(isTruthy) as [string, any][]
542
- return attrsList
543
- }
544
-
545
842
  function isTruthy<T>(val: T | undefined | null | false): val is T {
546
843
  return Boolean(val)
547
844
  }