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.
package/src/safe-mdx.tsx CHANGED
@@ -3,7 +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'
6
+ import type {
7
+ JSXElement,
8
+ JSXAttribute,
9
+ JSXText,
10
+ JSXExpressionContainer,
11
+ } from 'estree-jsx'
7
12
  import Evaluate from 'eval-estree-expression'
8
13
 
9
14
  import { Fragment, ReactNode } from 'react'
@@ -54,6 +59,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
54
59
  componentPropsSchema,
55
60
  createElement,
56
61
  allowClientEsmImports = false,
62
+ addMarkdownLineNumbers = false,
57
63
  }: {
58
64
  components?: ComponentsMap
59
65
  markdown?: string
@@ -62,6 +68,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
62
68
  componentPropsSchema?: ComponentPropsSchema
63
69
  createElement?: CreateElementFunction
64
70
  allowClientEsmImports?: boolean
71
+ addMarkdownLineNumbers?: boolean
65
72
  }) {
66
73
  const visitor = new MdastToJsx({
67
74
  markdown,
@@ -71,6 +78,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
71
78
  componentPropsSchema,
72
79
  createElement,
73
80
  allowClientEsmImports,
81
+ addMarkdownLineNumbers,
74
82
  })
75
83
  const result = visitor.run()
76
84
  return result
@@ -87,6 +95,7 @@ export class MdastToJsx {
87
95
  createElement: CreateElementFunction
88
96
  esmImports: Map<string, string> = new Map()
89
97
  allowClientEsmImports: boolean
98
+ addMarkdownLineNumbers: boolean
90
99
 
91
100
  constructor({
92
101
  markdown: code = '',
@@ -96,6 +105,7 @@ export class MdastToJsx {
96
105
  componentPropsSchema,
97
106
  createElement = React.createElement,
98
107
  allowClientEsmImports = false,
108
+ addMarkdownLineNumbers = false,
99
109
  }: {
100
110
  markdown?: string
101
111
  mdast: MyRootContent
@@ -107,6 +117,7 @@ export class MdastToJsx {
107
117
  componentPropsSchema?: ComponentPropsSchema
108
118
  createElement?: CreateElementFunction
109
119
  allowClientEsmImports?: boolean
120
+ addMarkdownLineNumbers?: boolean
110
121
  }) {
111
122
  this.str = code
112
123
 
@@ -120,6 +131,8 @@ export class MdastToJsx {
120
131
 
121
132
  this.allowClientEsmImports = allowClientEsmImports
122
133
 
134
+ this.addMarkdownLineNumbers = addMarkdownLineNumbers
135
+
123
136
  this.c = {
124
137
  ...Object.fromEntries(
125
138
  nativeTags.map((tag) => {
@@ -130,6 +143,24 @@ export class MdastToJsx {
130
143
  }
131
144
  }
132
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
+
133
164
  validateComponentProps(
134
165
  componentName: string,
135
166
  props: Record<string, any>,
@@ -209,22 +240,25 @@ export class MdastToJsx {
209
240
  }
210
241
 
211
242
  // Check if this is an ESM imported component (only if allowed)
212
- const esmImportInfo = this.allowClientEsmImports ? this.esmImports.get(node.name) : null
243
+ const esmImportInfo = this.allowClientEsmImports
244
+ ? this.esmImports.get(node.name)
245
+ : null
213
246
  let Component
214
-
247
+
215
248
  if (esmImportInfo) {
216
249
  // Handle ESM imported component
217
- const { importUrl, componentName } = extractComponentInfo(esmImportInfo)
218
-
250
+ const { importUrl, componentName } =
251
+ extractComponentInfo(esmImportInfo)
252
+
219
253
  Component = DynamicEsmComponent
220
254
  let attrsList = this.getJsxAttrs(node, (err) => {
221
255
  this.errors.push(err)
222
256
  })
223
257
  let attrs = Object.fromEntries(attrsList)
224
-
258
+
225
259
  return this.createElement(
226
260
  Component,
227
- { ...attrs, importUrl, componentName },
261
+ this.addLineNumberToProps({ ...attrs, importUrl, componentName }, node),
228
262
  this.mapJsxChildren(node),
229
263
  )
230
264
  } else {
@@ -254,7 +288,7 @@ export class MdastToJsx {
254
288
 
255
289
  return this.createElement(
256
290
  Component,
257
- attrs,
291
+ this.addLineNumberToProps(attrs, node),
258
292
  this.mapJsxChildren(node),
259
293
  )
260
294
  }
@@ -264,11 +298,18 @@ export class MdastToJsx {
264
298
  }
265
299
  }
266
300
 
267
- transformJsxElement(jsxElement: JSXElement, onError?: (err: SafeMdxError) => void, line?: number): ReactNode {
301
+ transformJsxElement(
302
+ jsxElement: JSXElement,
303
+ onError?: (err: SafeMdxError) => void,
304
+ line?: number,
305
+ ): ReactNode {
268
306
  try {
269
307
  // Handle JSX opening element
270
308
  if (jsxElement.openingElement) {
271
- const tagName = jsxElement.openingElement.name?.type === 'JSXIdentifier' ? jsxElement.openingElement.name.name : null
309
+ const tagName =
310
+ jsxElement.openingElement.name?.type === 'JSXIdentifier'
311
+ ? jsxElement.openingElement.name.name
312
+ : null
272
313
  if (!tagName) {
273
314
  onError?.({
274
315
  message: 'JSX element missing component name',
@@ -278,12 +319,15 @@ export class MdastToJsx {
278
319
  }
279
320
 
280
321
  // Check if this is an ESM imported component (only if allowed)
281
- const esmImportInfo = this.allowClientEsmImports ? this.esmImports.get(tagName) : null
322
+ const esmImportInfo = this.allowClientEsmImports
323
+ ? this.esmImports.get(tagName)
324
+ : null
282
325
  let Component
283
-
326
+
284
327
  if (esmImportInfo) {
285
328
  // Handle ESM imported component
286
- const { importUrl, componentName } = extractComponentInfo(esmImportInfo)
329
+ const { importUrl, componentName } =
330
+ extractComponentInfo(esmImportInfo)
287
331
  Component = DynamicEsmComponent
288
332
  } else {
289
333
  // Get the component from the regular component map
@@ -301,13 +345,22 @@ export class MdastToJsx {
301
345
  const props: Record<string, any> = {}
302
346
  if (jsxElement.openingElement.attributes) {
303
347
  for (const attr of jsxElement.openingElement.attributes) {
304
- if (attr.type === 'JSXAttribute' && attr.name?.type === 'JSXIdentifier' && attr.name.name) {
348
+ if (
349
+ attr.type === 'JSXAttribute' &&
350
+ attr.name?.type === 'JSXIdentifier' &&
351
+ attr.name.name
352
+ ) {
305
353
  if (attr.value) {
306
354
  if (attr.value.type === 'Literal') {
307
355
  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
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
311
364
  }
312
365
  }
313
366
  } else {
@@ -334,11 +387,12 @@ export class MdastToJsx {
334
387
 
335
388
  // Handle ESM imported components by adding required props
336
389
  if (esmImportInfo) {
337
- const { importUrl, componentName } = extractComponentInfo(esmImportInfo)
390
+ const { importUrl, componentName } =
391
+ extractComponentInfo(esmImportInfo)
338
392
  return this.createElement(
339
393
  Component,
340
394
  { ...props, importUrl, componentName },
341
- ...children
395
+ ...children,
342
396
  )
343
397
  } else {
344
398
  return this.createElement(Component, props, ...children)
@@ -347,7 +401,9 @@ export class MdastToJsx {
347
401
  } catch (error) {
348
402
  // Return null if transformation fails
349
403
  onError?.({
350
- message: `Failed to transform JSX element: ${error instanceof Error ? error.message : 'Unknown error'}`,
404
+ message: `Failed to transform JSX element: ${
405
+ error instanceof Error ? error.message : 'Unknown error'
406
+ }`,
351
407
  line: line,
352
408
  })
353
409
  return null
@@ -373,10 +429,14 @@ export class MdastToJsx {
373
429
  ) {
374
430
  const expression = program.body[0].expression
375
431
  try {
376
- const result = Evaluate.evaluate.sync(expression)
432
+ const result =
433
+ Evaluate.evaluate.sync(expression)
377
434
 
378
435
  // Handle spread syntax - merge the evaluated object
379
- if (typeof result === 'object' && result != null) {
436
+ if (
437
+ typeof result === 'object' &&
438
+ result != null
439
+ ) {
380
440
  const entries = Object.entries(result)
381
441
  attrsList.push(...entries)
382
442
  }
@@ -384,7 +444,7 @@ export class MdastToJsx {
384
444
  onError({
385
445
  message: `Failed to evaluate expression attribute: ${attr.value
386
446
  .replace(/\n+/g, ' ')
387
- .replace(/ +/g, ' ')}`,
447
+ .replace(/ +/g, ' ')}. ${error instanceof Error ? error.message : String(error)}`,
388
448
  line: attr.position?.start?.line,
389
449
  })
390
450
  }
@@ -393,7 +453,7 @@ export class MdastToJsx {
393
453
  onError({
394
454
  message: `Failed to evaluate expression attribute: ${attr.value
395
455
  .replace(/\n+/g, ' ')
396
- .replace(/ +/g, ' ')}`,
456
+ .replace(/ +/g, ' ')}. ${error instanceof Error ? error.message : String(error)}`,
397
457
  line: attr.position?.start?.line,
398
458
  })
399
459
  }
@@ -453,25 +513,30 @@ export class MdastToJsx {
453
513
  program.body[0].type === 'ExpressionStatement'
454
514
  ) {
455
515
  const expression = program.body[0].expression
456
-
516
+
457
517
  // Check if this is a JSX element
458
518
  if (expression.type === 'JSXElement') {
459
519
  // Transform JSX element to React element
460
- const jsxElement = this.transformJsxElement(expression, onError, attr.position?.start?.line)
520
+ const jsxElement = this.transformJsxElement(
521
+ expression,
522
+ onError,
523
+ attr.position?.start?.line,
524
+ )
461
525
  if (jsxElement) {
462
526
  attrsList.push([attr.name, jsxElement])
463
527
  continue
464
528
  }
465
529
  }
466
-
530
+
467
531
  try {
468
532
  // Evaluate the expression synchronously
469
- const result = Evaluate.evaluate.sync(expression)
533
+ const result =
534
+ Evaluate.evaluate.sync(expression)
470
535
  attrsList.push([attr.name, result])
471
536
  continue
472
537
  } catch (error) {
473
538
  onError({
474
- message: `Failed to evaluate expression attribute: ${attr.name}={${v.value}}`,
539
+ message: `Failed to evaluate expression attribute: ${attr.name}={${v.value}}. ${error instanceof Error ? error.message : String(error)}`,
475
540
  line: attr.position?.start?.line,
476
541
  })
477
542
  }
@@ -518,7 +583,9 @@ export class MdastToJsx {
518
583
  case 'mdxjsEsm': {
519
584
  // Parse ESM imports and merge into our imports map (only if allowed)
520
585
  if (this.allowClientEsmImports) {
521
- const parsedImports = parseEsmImports(node, (err) => this.errors.push(err))
586
+ const parsedImports = parseEsmImports(node, (err) =>
587
+ this.errors.push(err),
588
+ )
522
589
  parsedImports.forEach((value, key) => {
523
590
  this.esmImports.set(key, value)
524
591
  })
@@ -562,18 +629,19 @@ export class MdastToJsx {
562
629
  const expression = program.body[0].expression
563
630
  try {
564
631
  // Evaluate the expression synchronously
565
- const result = Evaluate.evaluate.sync(expression)
632
+ const result =
633
+ Evaluate.evaluate.sync(expression)
566
634
  return result
567
635
  } catch (error) {
568
636
  this.errors.push({
569
- message: `Failed to evaluate expression: ${node.value}`,
637
+ message: `Failed to evaluate expression: ${node.value}. ${error instanceof Error ? error.message : String(error)}`,
570
638
  line: node.position?.start?.line,
571
639
  })
572
640
  }
573
641
  }
574
642
  } catch (error) {
575
643
  this.errors.push({
576
- message: `Failed to evaluate expression: ${node.value}`,
644
+ message: `Failed to evaluate expression: ${node.value}. ${error instanceof Error ? error.message : String(error)}`,
577
645
  line: node.position?.start?.line,
578
646
  })
579
647
  }
@@ -593,26 +661,29 @@ export class MdastToJsx {
593
661
 
594
662
  return this.createElement(
595
663
  Tag,
596
- node.data?.hProperties,
664
+ this.addLineNumberToProps(node.data?.hProperties, node),
597
665
  this.mapMdastChildren(node),
598
666
  )
599
667
  }
600
668
  case 'paragraph': {
601
669
  return this.createElement(
602
670
  this.c.p,
603
- node.data?.hProperties,
671
+ this.addLineNumberToProps(node.data?.hProperties, node),
604
672
  this.mapMdastChildren(node),
605
673
  )
606
674
  }
607
675
  case 'blockquote': {
608
676
  return this.createElement(
609
677
  this.c.blockquote,
610
- node.data?.hProperties,
678
+ this.addLineNumberToProps(node.data?.hProperties, node),
611
679
  this.mapMdastChildren(node),
612
680
  )
613
681
  }
614
682
  case 'thematicBreak': {
615
- 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
+ )
616
687
  }
617
688
  case 'code': {
618
689
  if (!node.value) {
@@ -623,7 +694,7 @@ export class MdastToJsx {
623
694
  const codeBlock = (className?: string) =>
624
695
  this.createElement(
625
696
  this.c.pre,
626
- node.data?.hProperties,
697
+ this.addLineNumberToProps(node.data?.hProperties, node),
627
698
  this.createElement(this.c.code, { className }, code),
628
699
  )
629
700
 
@@ -637,13 +708,16 @@ export class MdastToJsx {
637
708
  if (node.ordered) {
638
709
  return this.createElement(
639
710
  this.c.ol,
640
- { start: node.start!, ...node.data?.hProperties },
711
+ this.addLineNumberToProps(
712
+ { start: node.start!, ...node.data?.hProperties },
713
+ node,
714
+ ),
641
715
  this.mapMdastChildren(node),
642
716
  )
643
717
  }
644
718
  return this.createElement(
645
719
  this.c.ul,
646
- node.data?.hProperties,
720
+ this.addLineNumberToProps(node.data?.hProperties, node),
647
721
  this.mapMdastChildren(node),
648
722
  )
649
723
  }
@@ -652,16 +726,19 @@ export class MdastToJsx {
652
726
  if (node?.checked != null) {
653
727
  return this.createElement(
654
728
  this.c.li,
655
- {
656
- 'data-checked': node.checked,
657
- ...node.data?.hProperties,
658
- },
729
+ this.addLineNumberToProps(
730
+ {
731
+ 'data-checked': node.checked,
732
+ ...node.data?.hProperties,
733
+ },
734
+ node,
735
+ ),
659
736
  this.mapMdastChildren(node),
660
737
  )
661
738
  }
662
739
  return this.createElement(
663
740
  this.c.li,
664
- node.data?.hProperties,
741
+ this.addLineNumberToProps(node.data?.hProperties, node),
665
742
  this.mapMdastChildren(node),
666
743
  )
667
744
  }
@@ -672,7 +749,7 @@ export class MdastToJsx {
672
749
  if (node.data?.hProperties) {
673
750
  return this.createElement(
674
751
  this.c.span,
675
- node.data.hProperties,
752
+ this.addLineNumberToProps(node.data.hProperties, node),
676
753
  node.value,
677
754
  )
678
755
  }
@@ -682,40 +759,49 @@ export class MdastToJsx {
682
759
  const src = node.url || ''
683
760
  const alt = node.alt || ''
684
761
  const title = node.title || ''
685
- return this.createElement(this.c.img, {
686
- src,
687
- alt,
688
- title,
689
- ...node.data?.hProperties,
690
- })
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
+ )
691
774
  }
692
775
  case 'link': {
693
776
  const href = node.url || ''
694
777
  const title = node.title || ''
695
778
  return this.createElement(
696
779
  this.c.a,
697
- { href, title, ...node.data?.hProperties },
780
+ this.addLineNumberToProps(
781
+ { href, title, ...node.data?.hProperties },
782
+ node,
783
+ ),
698
784
  this.mapMdastChildren(node),
699
785
  )
700
786
  }
701
787
  case 'strong': {
702
788
  return this.createElement(
703
789
  this.c.strong,
704
- node.data?.hProperties,
790
+ this.addLineNumberToProps(node.data?.hProperties, node),
705
791
  this.mapMdastChildren(node),
706
792
  )
707
793
  }
708
794
  case 'emphasis': {
709
795
  return this.createElement(
710
796
  this.c.em,
711
- node.data?.hProperties,
797
+ this.addLineNumberToProps(node.data?.hProperties, node),
712
798
  this.mapMdastChildren(node),
713
799
  )
714
800
  }
715
801
  case 'delete': {
716
802
  return this.createElement(
717
803
  this.c.del,
718
- node.data?.hProperties,
804
+ this.addLineNumberToProps(node.data?.hProperties, node),
719
805
  this.mapMdastChildren(node),
720
806
  )
721
807
  }
@@ -725,18 +811,21 @@ export class MdastToJsx {
725
811
  }
726
812
  return this.createElement(
727
813
  this.c.code,
728
- node.data?.hProperties,
814
+ this.addLineNumberToProps(node.data?.hProperties, node),
729
815
  node.value,
730
816
  )
731
817
  }
732
818
  case 'break': {
733
- 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
+ )
734
823
  }
735
824
  case 'root': {
736
825
  if (node.data?.hProperties) {
737
826
  return this.createElement(
738
827
  this.c.div,
739
- node.data.hProperties,
828
+ this.addLineNumberToProps(node.data.hProperties, node),
740
829
  this.mapMdastChildren(node),
741
830
  )
742
831
  }
@@ -752,7 +841,7 @@ export class MdastToJsx {
752
841
  )
753
842
  return this.createElement(
754
843
  this.c.table,
755
- node.data?.hProperties,
844
+ this.addLineNumberToProps(node.data?.hProperties, node),
756
845
  head && this.createElement(this.c.thead, null, head),
757
846
  !!body?.length &&
758
847
  this.createElement(this.c.tbody, null, body),
@@ -761,7 +850,10 @@ export class MdastToJsx {
761
850
  case 'tableRow': {
762
851
  return this.createElement(
763
852
  this.c.tr,
764
- { className: '', ...node.data?.hProperties },
853
+ this.addLineNumberToProps(
854
+ { className: '', ...node.data?.hProperties },
855
+ node,
856
+ ),
765
857
  this.mapMdastChildren(node),
766
858
  )
767
859
  }
@@ -769,7 +861,10 @@ export class MdastToJsx {
769
861
  let content = this.mapMdastChildren(node)
770
862
  return this.createElement(
771
863
  this.c.td,
772
- { className: '', ...node.data?.hProperties },
864
+ this.addLineNumberToProps(
865
+ { className: '', ...node.data?.hProperties },
866
+ node,
867
+ ),
773
868
  content,
774
869
  )
775
870
  }
@@ -791,7 +886,10 @@ export class MdastToJsx {
791
886
 
792
887
  return this.createElement(
793
888
  this.c.a,
794
- { href, title, ...node.data?.hProperties },
889
+ this.addLineNumberToProps(
890
+ { href, title, ...node.data?.hProperties },
891
+ node,
892
+ ),
795
893
  this.mapMdastChildren(node),
796
894
  )
797
895
  }