safe-mdx 1.1.0 → 1.3.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,17 @@ 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 {
7
+ JSXElement,
8
+ JSXAttribute,
9
+ JSXText,
10
+ JSXExpressionContainer,
11
+ } from 'estree-jsx'
12
+ import Evaluate from 'eval-estree-expression'
6
13
 
7
14
  import { Fragment, ReactNode } from 'react'
15
+ import { DynamicEsmComponent } from './dynamic-esm-component.js'
16
+ import { parseEsmImports, extractComponentInfo } from './esm-parser.js'
8
17
 
9
18
  const HtmlToJsxConverter = React.lazy(() =>
10
19
  import('./HtmlToJsxConverter.js').then((module) => ({
@@ -49,6 +58,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
49
58
  renderNode,
50
59
  componentPropsSchema,
51
60
  createElement,
61
+ allowClientEsmImports = false,
62
+ addMarkdownLineNumbers = false,
52
63
  }: {
53
64
  components?: ComponentsMap
54
65
  markdown?: string
@@ -56,6 +67,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
56
67
  renderNode?: RenderNode
57
68
  componentPropsSchema?: ComponentPropsSchema
58
69
  createElement?: CreateElementFunction
70
+ allowClientEsmImports?: boolean
71
+ addMarkdownLineNumbers?: boolean
59
72
  }) {
60
73
  const visitor = new MdastToJsx({
61
74
  markdown,
@@ -64,6 +77,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
64
77
  renderNode,
65
78
  componentPropsSchema,
66
79
  createElement,
80
+ allowClientEsmImports,
81
+ addMarkdownLineNumbers,
67
82
  })
68
83
  const result = visitor.run()
69
84
  return result
@@ -78,6 +93,9 @@ export class MdastToJsx {
78
93
  renderNode?: RenderNode
79
94
  componentPropsSchema?: ComponentPropsSchema
80
95
  createElement: CreateElementFunction
96
+ esmImports: Map<string, string> = new Map()
97
+ allowClientEsmImports: boolean
98
+ addMarkdownLineNumbers: boolean
81
99
 
82
100
  constructor({
83
101
  markdown: code = '',
@@ -86,6 +104,8 @@ export class MdastToJsx {
86
104
  renderNode,
87
105
  componentPropsSchema,
88
106
  createElement = React.createElement,
107
+ allowClientEsmImports = false,
108
+ addMarkdownLineNumbers = false,
89
109
  }: {
90
110
  markdown?: string
91
111
  mdast: MyRootContent
@@ -96,6 +116,8 @@ export class MdastToJsx {
96
116
  ) => ReactNode | undefined
97
117
  componentPropsSchema?: ComponentPropsSchema
98
118
  createElement?: CreateElementFunction
119
+ allowClientEsmImports?: boolean
120
+ addMarkdownLineNumbers?: boolean
99
121
  }) {
100
122
  this.str = code
101
123
 
@@ -107,6 +129,10 @@ export class MdastToJsx {
107
129
 
108
130
  this.createElement = createElement
109
131
 
132
+ this.allowClientEsmImports = allowClientEsmImports
133
+
134
+ this.addMarkdownLineNumbers = addMarkdownLineNumbers
135
+
110
136
  this.c = {
111
137
  ...Object.fromEntries(
112
138
  nativeTags.map((tag) => {
@@ -117,12 +143,33 @@ export class MdastToJsx {
117
143
  }
118
144
  }
119
145
 
146
+ addLineNumberToProps(
147
+ props: Record<string, any> | undefined,
148
+ node: MyRootContent,
149
+ ): Record<string, any> {
150
+ if (!this.addMarkdownLineNumbers) {
151
+ return props || {}
152
+ }
153
+
154
+ const lineNumber = node.position?.start?.line
155
+ if (lineNumber) {
156
+ return {
157
+ ...props,
158
+ 'data-markdown-line': lineNumber,
159
+ }
160
+ }
161
+ return props || {}
162
+ }
163
+
120
164
  validateComponentProps(
121
165
  componentName: string,
122
166
  props: Record<string, any>,
123
- line?: number
167
+ line?: number,
124
168
  ): void {
125
- if (!this.componentPropsSchema || !this.componentPropsSchema[componentName]) {
169
+ if (
170
+ !this.componentPropsSchema ||
171
+ !this.componentPropsSchema[componentName]
172
+ ) {
126
173
  return
127
174
  }
128
175
 
@@ -192,26 +239,58 @@ export class MdastToJsx {
192
239
  return []
193
240
  }
194
241
 
195
- const Component = accessWithDot(this.c, node.name)
242
+ // Check if this is an ESM imported component (only if allowed)
243
+ const esmImportInfo = this.allowClientEsmImports
244
+ ? this.esmImports.get(node.name)
245
+ : null
246
+ let Component
196
247
 
197
- if (!Component) {
198
- this.errors.push({
199
- message: `Unsupported jsx component ${node.name}`,
200
- line: node.position?.start?.line,
248
+ if (esmImportInfo) {
249
+ // Handle ESM imported component
250
+ const { importUrl, componentName } =
251
+ extractComponentInfo(esmImportInfo)
252
+
253
+ Component = DynamicEsmComponent
254
+ let attrsList = this.getJsxAttrs(node, (err) => {
255
+ this.errors.push(err)
201
256
  })
202
- return null
257
+ let attrs = Object.fromEntries(attrsList)
258
+
259
+ return this.createElement(
260
+ Component,
261
+ { ...attrs, importUrl, componentName },
262
+ this.mapJsxChildren(node),
263
+ )
264
+ } else {
265
+ Component = accessWithDot(this.c, node.name)
266
+
267
+ if (!Component) {
268
+ this.errors.push({
269
+ message: `Unsupported jsx component ${node.name}`,
270
+ line: node.position?.start?.line,
271
+ })
272
+ return null
273
+ }
203
274
  }
204
275
 
205
- let attrsList = getJsxAttrs(node, (err) => {
276
+ let attrsList = this.getJsxAttrs(node, (err) => {
206
277
  this.errors.push(err)
207
278
  })
208
279
 
209
280
  let attrs = Object.fromEntries(attrsList)
210
281
 
211
282
  // Validate component props with schema if available
212
- this.validateComponentProps(node.name, attrs, node.position?.start?.line)
283
+ this.validateComponentProps(
284
+ node.name,
285
+ attrs,
286
+ node.position?.start?.line,
287
+ )
213
288
 
214
- return this.createElement(Component, attrs, this.mapJsxChildren(node))
289
+ return this.createElement(
290
+ Component,
291
+ attrs,
292
+ this.mapJsxChildren(node),
293
+ )
215
294
  }
216
295
  default: {
217
296
  return this.mdastTransformer(node)
@@ -219,6 +298,263 @@ export class MdastToJsx {
219
298
  }
220
299
  }
221
300
 
301
+ transformJsxElement(
302
+ jsxElement: JSXElement,
303
+ onError?: (err: SafeMdxError) => void,
304
+ line?: number,
305
+ ): ReactNode {
306
+ try {
307
+ // Handle JSX opening element
308
+ if (jsxElement.openingElement) {
309
+ const tagName =
310
+ jsxElement.openingElement.name?.type === 'JSXIdentifier'
311
+ ? jsxElement.openingElement.name.name
312
+ : null
313
+ if (!tagName) {
314
+ onError?.({
315
+ message: 'JSX element missing component name',
316
+ line: line,
317
+ })
318
+ return null
319
+ }
320
+
321
+ // Check if this is an ESM imported component (only if allowed)
322
+ const esmImportInfo = this.allowClientEsmImports
323
+ ? this.esmImports.get(tagName)
324
+ : null
325
+ let Component
326
+
327
+ if (esmImportInfo) {
328
+ // Handle ESM imported component
329
+ const { importUrl, componentName } =
330
+ extractComponentInfo(esmImportInfo)
331
+ Component = DynamicEsmComponent
332
+ } else {
333
+ // Get the component from the regular component map
334
+ Component = accessWithDot(this.c, tagName)
335
+ if (!Component) {
336
+ onError?.({
337
+ message: `Unsupported jsx component ${tagName} in attribute`,
338
+ line: line,
339
+ })
340
+ return null
341
+ }
342
+ }
343
+
344
+ // Extract attributes
345
+ const props: Record<string, any> = {}
346
+ if (jsxElement.openingElement.attributes) {
347
+ for (const attr of jsxElement.openingElement.attributes) {
348
+ if (
349
+ attr.type === 'JSXAttribute' &&
350
+ attr.name?.type === 'JSXIdentifier' &&
351
+ attr.name.name
352
+ ) {
353
+ if (attr.value) {
354
+ if (attr.value.type === 'Literal') {
355
+ props[attr.name.name] = attr.value.value
356
+ } else if (
357
+ attr.value.type === 'JSXExpressionContainer'
358
+ ) {
359
+ if (
360
+ attr.value.expression.type === 'Literal'
361
+ ) {
362
+ props[attr.name.name] =
363
+ attr.value.expression.value
364
+ }
365
+ }
366
+ } else {
367
+ props[attr.name.name] = true
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ // Extract children
374
+ const children: ReactNode[] = []
375
+ if (jsxElement.children) {
376
+ for (const child of jsxElement.children) {
377
+ if (child.type === 'JSXText') {
378
+ children.push(child.value)
379
+ } else if (child.type === 'JSXElement') {
380
+ const childElement = this.transformJsxElement(child)
381
+ if (childElement) {
382
+ children.push(childElement)
383
+ }
384
+ }
385
+ }
386
+ }
387
+
388
+ // Handle ESM imported components by adding required props
389
+ if (esmImportInfo) {
390
+ const { importUrl, componentName } =
391
+ extractComponentInfo(esmImportInfo)
392
+ return this.createElement(
393
+ Component,
394
+ { ...props, importUrl, componentName },
395
+ ...children,
396
+ )
397
+ } else {
398
+ return this.createElement(Component, props, ...children)
399
+ }
400
+ }
401
+ } catch (error) {
402
+ // Return null if transformation fails
403
+ onError?.({
404
+ message: `Failed to transform JSX element: ${
405
+ error instanceof Error ? error.message : 'Unknown error'
406
+ }`,
407
+ line: line,
408
+ })
409
+ return null
410
+ }
411
+ return null
412
+ }
413
+
414
+ getJsxAttrs(
415
+ node: MdxJsxFlowElement | MdxJsxTextElement,
416
+ onError: (err: SafeMdxError) => void = console.error,
417
+ ) {
418
+ let attrsList: [string, any][] = []
419
+
420
+ for (const attr of node.attributes) {
421
+ if (attr.type === 'mdxJsxExpressionAttribute') {
422
+ // Handle spread expressions like {...{key: '1'}}
423
+ if (attr.data?.estree) {
424
+ try {
425
+ const program = attr.data.estree
426
+ if (
427
+ program.body?.length > 0 &&
428
+ program.body[0].type === 'ExpressionStatement'
429
+ ) {
430
+ const expression = program.body[0].expression
431
+ try {
432
+ const result =
433
+ Evaluate.evaluate.sync(expression)
434
+
435
+ // Handle spread syntax - merge the evaluated object
436
+ if (
437
+ typeof result === 'object' &&
438
+ result != null
439
+ ) {
440
+ const entries = Object.entries(result)
441
+ attrsList.push(...entries)
442
+ }
443
+ } catch (error) {
444
+ onError({
445
+ message: `Failed to evaluate expression attribute: ${attr.value
446
+ .replace(/\n+/g, ' ')
447
+ .replace(/ +/g, ' ')}`,
448
+ line: attr.position?.start?.line,
449
+ })
450
+ }
451
+ }
452
+ } catch (error) {
453
+ onError({
454
+ message: `Failed to evaluate expression attribute: ${attr.value
455
+ .replace(/\n+/g, ' ')
456
+ .replace(/ +/g, ' ')}`,
457
+ line: attr.position?.start?.line,
458
+ })
459
+ }
460
+ } else {
461
+ onError({
462
+ message: `Expressions in jsx props are not supported (${attr.value
463
+ .replace(/\n+/g, ' ')
464
+ .replace(/ +/g, ' ')})`,
465
+ line: attr.position?.start?.line,
466
+ })
467
+ }
468
+ continue
469
+ }
470
+
471
+ if (attr.type !== 'mdxJsxAttribute') {
472
+ onError({
473
+ message: `non mdxJsxAttribute attribute is not supported: ${attr}`,
474
+ line: node.position?.start?.line,
475
+ })
476
+ continue
477
+ }
478
+
479
+ const v = attr.value
480
+ if (typeof v === 'string' || typeof v === 'number') {
481
+ attrsList.push([attr.name, v])
482
+ continue
483
+ }
484
+ if (v === null) {
485
+ attrsList.push([attr.name, true])
486
+ continue
487
+ }
488
+ if (v?.type === 'mdxJsxAttributeValueExpression') {
489
+ // Manual parsing fallback for simple values
490
+ if (v.value === 'true') {
491
+ attrsList.push([attr.name, true])
492
+ continue
493
+ }
494
+ if (v.value === 'false') {
495
+ attrsList.push([attr.name, false])
496
+ continue
497
+ }
498
+ if (v.value === 'null') {
499
+ attrsList.push([attr.name, null])
500
+ continue
501
+ }
502
+ if (v.value === 'undefined') {
503
+ attrsList.push([attr.name, undefined])
504
+ continue
505
+ }
506
+
507
+ if (v.data?.estree) {
508
+ try {
509
+ // Extract the expression from the Program body
510
+ const program = v.data.estree
511
+ if (
512
+ program.body?.length > 0 &&
513
+ program.body[0].type === 'ExpressionStatement'
514
+ ) {
515
+ const expression = program.body[0].expression
516
+
517
+ // Check if this is a JSX element
518
+ if (expression.type === 'JSXElement') {
519
+ // Transform JSX element to React element
520
+ const jsxElement = this.transformJsxElement(
521
+ expression,
522
+ onError,
523
+ attr.position?.start?.line,
524
+ )
525
+ if (jsxElement) {
526
+ attrsList.push([attr.name, jsxElement])
527
+ continue
528
+ }
529
+ }
530
+
531
+ try {
532
+ // Evaluate the expression synchronously
533
+ const result =
534
+ Evaluate.evaluate.sync(expression)
535
+ attrsList.push([attr.name, result])
536
+ continue
537
+ } catch (error) {
538
+ onError({
539
+ message: `Failed to evaluate expression attribute: ${attr.name}={${v.value}}`,
540
+ line: attr.position?.start?.line,
541
+ })
542
+ }
543
+ }
544
+ } catch (error) {
545
+ // Fall back to the original manual parsing for backwards compatibility
546
+ }
547
+ }
548
+
549
+ onError({
550
+ message: `Expressions in jsx prop not evaluated: (${attr.name}={${v.value}})`,
551
+ line: attr.position?.start?.line,
552
+ })
553
+ }
554
+ }
555
+ return attrsList
556
+ }
557
+
222
558
  run() {
223
559
  const res = this.mdastTransformer(this.mdast) as ReactNode
224
560
  if (Array.isArray(res) && res.length === 1) {
@@ -245,10 +581,15 @@ export class MdastToJsx {
245
581
 
246
582
  switch (node.type) {
247
583
  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
-
584
+ // Parse ESM imports and merge into our imports map (only if allowed)
585
+ if (this.allowClientEsmImports) {
586
+ const parsedImports = parseEsmImports(node, (err) =>
587
+ this.errors.push(err),
588
+ )
589
+ parsedImports.forEach((value, key) => {
590
+ this.esmImports.set(key, value)
591
+ })
592
+ }
252
593
  return []
253
594
  }
254
595
  case 'mdxJsxTextElement':
@@ -275,6 +616,37 @@ export class MdastToJsx {
275
616
  if (!node.value) {
276
617
  return []
277
618
  }
619
+
620
+ // Check if we have an estree AST
621
+ if (node.data?.estree) {
622
+ try {
623
+ // Extract the expression from the Program body
624
+ const program = node.data.estree
625
+ if (
626
+ program.body?.length > 0 &&
627
+ program.body[0].type === 'ExpressionStatement'
628
+ ) {
629
+ const expression = program.body[0].expression
630
+ try {
631
+ // Evaluate the expression synchronously
632
+ const result =
633
+ Evaluate.evaluate.sync(expression)
634
+ return result
635
+ } catch (error) {
636
+ this.errors.push({
637
+ message: `Failed to evaluate expression: ${node.value}`,
638
+ line: node.position?.start?.line,
639
+ })
640
+ }
641
+ }
642
+ } catch (error) {
643
+ this.errors.push({
644
+ message: `Failed to evaluate expression: ${node.value}`,
645
+ line: node.position?.start?.line,
646
+ })
647
+ }
648
+ }
649
+
278
650
  return []
279
651
  }
280
652
  case 'yaml': {
@@ -287,16 +659,31 @@ export class MdastToJsx {
287
659
  const level = node.depth
288
660
  const Tag = this.c[`h${level}`] ?? `h${level}`
289
661
 
290
- return this.createElement(Tag, node.data?.hProperties, this.mapMdastChildren(node))
662
+ return this.createElement(
663
+ Tag,
664
+ this.addLineNumberToProps(node.data?.hProperties, node),
665
+ this.mapMdastChildren(node),
666
+ )
291
667
  }
292
668
  case 'paragraph': {
293
- return this.createElement(this.c.p, node.data?.hProperties, this.mapMdastChildren(node))
669
+ return this.createElement(
670
+ this.c.p,
671
+ this.addLineNumberToProps(node.data?.hProperties, node),
672
+ this.mapMdastChildren(node),
673
+ )
294
674
  }
295
675
  case 'blockquote': {
296
- return this.createElement(this.c.blockquote, node.data?.hProperties, this.mapMdastChildren(node))
676
+ return this.createElement(
677
+ this.c.blockquote,
678
+ this.addLineNumberToProps(node.data?.hProperties, node),
679
+ this.mapMdastChildren(node),
680
+ )
297
681
  }
298
682
  case 'thematicBreak': {
299
- return this.createElement(this.c.hr, node.data?.hProperties)
683
+ return this.createElement(
684
+ this.c.hr,
685
+ this.addLineNumberToProps(node.data?.hProperties, node),
686
+ )
300
687
  }
301
688
  case 'code': {
302
689
  if (!node.value) {
@@ -304,11 +691,12 @@ export class MdastToJsx {
304
691
  }
305
692
  const language = node.lang || ''
306
693
  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
- )
694
+ const codeBlock = (className?: string) =>
695
+ this.createElement(
696
+ this.c.pre,
697
+ this.addLineNumberToProps(node.data?.hProperties, node),
698
+ this.createElement(this.c.code, { className }, code),
699
+ )
312
700
 
313
701
  if (language) {
314
702
  return codeBlock(`language-${language}`)
@@ -320,29 +708,50 @@ export class MdastToJsx {
320
708
  if (node.ordered) {
321
709
  return this.createElement(
322
710
  this.c.ol,
323
- { start: node.start!, ...node.data?.hProperties },
324
- this.mapMdastChildren(node)
711
+ this.addLineNumberToProps(
712
+ { start: node.start!, ...node.data?.hProperties },
713
+ node,
714
+ ),
715
+ this.mapMdastChildren(node),
325
716
  )
326
717
  }
327
- return this.createElement(this.c.ul, node.data?.hProperties, this.mapMdastChildren(node))
718
+ return this.createElement(
719
+ this.c.ul,
720
+ this.addLineNumberToProps(node.data?.hProperties, node),
721
+ this.mapMdastChildren(node),
722
+ )
328
723
  }
329
724
  case 'listItem': {
330
725
  // https://github.com/syntax-tree/mdast-util-gfm-task-list-item#syntax-tree
331
726
  if (node?.checked != null) {
332
727
  return this.createElement(
333
728
  this.c.li,
334
- { 'data-checked': node.checked, ...node.data?.hProperties },
335
- this.mapMdastChildren(node)
729
+ this.addLineNumberToProps(
730
+ {
731
+ 'data-checked': node.checked,
732
+ ...node.data?.hProperties,
733
+ },
734
+ node,
735
+ ),
736
+ this.mapMdastChildren(node),
336
737
  )
337
738
  }
338
- return this.createElement(this.c.li, node.data?.hProperties, this.mapMdastChildren(node))
739
+ return this.createElement(
740
+ this.c.li,
741
+ this.addLineNumberToProps(node.data?.hProperties, node),
742
+ this.mapMdastChildren(node),
743
+ )
339
744
  }
340
745
  case 'text': {
341
746
  if (!node.value) {
342
747
  return []
343
748
  }
344
749
  if (node.data?.hProperties) {
345
- return this.createElement(this.c.span, node.data.hProperties, node.value)
750
+ return this.createElement(
751
+ this.c.span,
752
+ this.addLineNumberToProps(node.data.hProperties, node),
753
+ node.value,
754
+ )
346
755
  }
347
756
  return node.value
348
757
  }
@@ -350,45 +759,81 @@ export class MdastToJsx {
350
759
  const src = node.url || ''
351
760
  const alt = node.alt || ''
352
761
  const title = node.title || ''
353
- return this.createElement(this.c.img, {
354
- src,
355
- alt,
356
- title,
357
- ...node.data?.hProperties
358
- })
762
+ return this.createElement(
763
+ this.c.img,
764
+ this.addLineNumberToProps(
765
+ {
766
+ src,
767
+ alt,
768
+ title,
769
+ ...node.data?.hProperties,
770
+ },
771
+ node,
772
+ ),
773
+ )
359
774
  }
360
775
  case 'link': {
361
776
  const href = node.url || ''
362
777
  const title = node.title || ''
363
778
  return this.createElement(
364
779
  this.c.a,
365
- { href, title, ...node.data?.hProperties },
366
- this.mapMdastChildren(node)
780
+ this.addLineNumberToProps(
781
+ { href, title, ...node.data?.hProperties },
782
+ node,
783
+ ),
784
+ this.mapMdastChildren(node),
367
785
  )
368
786
  }
369
787
  case 'strong': {
370
- return this.createElement(this.c.strong, node.data?.hProperties, this.mapMdastChildren(node))
788
+ return this.createElement(
789
+ this.c.strong,
790
+ this.addLineNumberToProps(node.data?.hProperties, node),
791
+ this.mapMdastChildren(node),
792
+ )
371
793
  }
372
794
  case 'emphasis': {
373
- return this.createElement(this.c.em, node.data?.hProperties, this.mapMdastChildren(node))
795
+ return this.createElement(
796
+ this.c.em,
797
+ this.addLineNumberToProps(node.data?.hProperties, node),
798
+ this.mapMdastChildren(node),
799
+ )
374
800
  }
375
801
  case 'delete': {
376
- return this.createElement(this.c.del, node.data?.hProperties, this.mapMdastChildren(node))
802
+ return this.createElement(
803
+ this.c.del,
804
+ this.addLineNumberToProps(node.data?.hProperties, node),
805
+ this.mapMdastChildren(node),
806
+ )
377
807
  }
378
808
  case 'inlineCode': {
379
809
  if (!node.value) {
380
810
  return []
381
811
  }
382
- return this.createElement(this.c.code, node.data?.hProperties, node.value)
812
+ return this.createElement(
813
+ this.c.code,
814
+ this.addLineNumberToProps(node.data?.hProperties, node),
815
+ node.value,
816
+ )
383
817
  }
384
818
  case 'break': {
385
- return this.createElement(this.c.br, node.data?.hProperties)
819
+ return this.createElement(
820
+ this.c.br,
821
+ this.addLineNumberToProps(node.data?.hProperties, node),
822
+ )
386
823
  }
387
824
  case 'root': {
388
825
  if (node.data?.hProperties) {
389
- return this.createElement(this.c.div, node.data.hProperties, this.mapMdastChildren(node))
826
+ return this.createElement(
827
+ this.c.div,
828
+ this.addLineNumberToProps(node.data.hProperties, node),
829
+ this.mapMdastChildren(node),
830
+ )
390
831
  }
391
- return this.createElement(Fragment, null, this.mapMdastChildren(node))
832
+ return this.createElement(
833
+ Fragment,
834
+ null,
835
+ this.mapMdastChildren(node),
836
+ )
392
837
  }
393
838
  case 'table': {
394
839
  const [head, ...body] = React.Children.toArray(
@@ -396,24 +841,31 @@ export class MdastToJsx {
396
841
  )
397
842
  return this.createElement(
398
843
  this.c.table,
399
- node.data?.hProperties,
844
+ this.addLineNumberToProps(node.data?.hProperties, node),
400
845
  head && this.createElement(this.c.thead, null, head),
401
- !!body?.length && this.createElement(this.c.tbody, null, body)
846
+ !!body?.length &&
847
+ this.createElement(this.c.tbody, null, body),
402
848
  )
403
849
  }
404
850
  case 'tableRow': {
405
851
  return this.createElement(
406
852
  this.c.tr,
407
- { className: '', ...node.data?.hProperties },
408
- this.mapMdastChildren(node)
853
+ this.addLineNumberToProps(
854
+ { className: '', ...node.data?.hProperties },
855
+ node,
856
+ ),
857
+ this.mapMdastChildren(node),
409
858
  )
410
859
  }
411
860
  case 'tableCell': {
412
861
  let content = this.mapMdastChildren(node)
413
862
  return this.createElement(
414
863
  this.c.td,
415
- { className: '', ...node.data?.hProperties },
416
- content
864
+ this.addLineNumberToProps(
865
+ { className: '', ...node.data?.hProperties },
866
+ node,
867
+ ),
868
+ content,
417
869
  )
418
870
  }
419
871
  case 'definition': {
@@ -421,19 +873,24 @@ export class MdastToJsx {
421
873
  }
422
874
  case 'linkReference': {
423
875
  let href = ''
876
+ let title = ''
424
877
  mdastBfs(this.mdast, (child: any) => {
425
878
  if (
426
879
  child.type === 'definition' &&
427
880
  child.identifier === node.identifier
428
881
  ) {
429
- href = child.url
882
+ href = child.url || ''
883
+ title = child.title || ''
430
884
  }
431
885
  })
432
886
 
433
887
  return this.createElement(
434
888
  this.c.a,
435
- { href, ...node.data?.hProperties },
436
- this.mapMdastChildren(node)
889
+ this.addLineNumberToProps(
890
+ { href, title, ...node.data?.hProperties },
891
+ node,
892
+ ),
893
+ this.mapMdastChildren(node),
437
894
  )
438
895
  }
439
896
  case 'footnoteReference': {
@@ -457,8 +914,8 @@ export class MdastToJsx {
457
914
  this.createElement(HtmlToJsxConverter, {
458
915
  htmlText: text,
459
916
  instance: this,
460
- node
461
- })
917
+ node,
918
+ }),
462
919
  )
463
920
  }
464
921
  case 'imageReference': {
@@ -480,80 +937,6 @@ export class MdastToJsx {
480
937
  }
481
938
  }
482
939
 
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
940
  function isTruthy<T>(val: T | undefined | null | false): val is T {
558
941
  return Boolean(val)
559
942
  }