safe-mdx 0.0.0 → 0.0.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
@@ -1,3 +1,4 @@
1
+ import React, { cloneElement } from 'react'
1
2
  import { htmlToJsx } from 'html-to-jsx-transform'
2
3
  import { Node, Parent } from 'mdast'
3
4
  import remarkFrontmatter from 'remark-frontmatter'
@@ -17,6 +18,8 @@ const mdxParser = remark()
17
18
  .use(remarkGfm)
18
19
  .use(remarkMdx) as any
19
20
 
21
+ void React
22
+
20
23
  export function SafeMdxRenderer({
21
24
  components,
22
25
  code = '',
@@ -32,7 +35,6 @@ const nativeTags = [
32
35
  'strong',
33
36
  'em',
34
37
  'del',
35
- 'br',
36
38
  'hr',
37
39
  'a',
38
40
  'b',
@@ -74,18 +76,21 @@ const nativeTags = [
74
76
  'pre',
75
77
  ] as const
76
78
 
77
- type ComponentsMap = { [k in (typeof nativeTags)[number]]: any }
79
+ type ComponentsMap = { [k in (typeof nativeTags)[number]]?: any }
78
80
 
79
- class MdastToJsx {
81
+ export class MdastToJsx {
80
82
  mdast: MyRootContent
81
83
  str: string
82
84
  jsxStr: string = ''
83
85
  c: ComponentsMap
84
- errors: string[] = []
85
- constructor({ code = '', mdast, components = {} as ComponentsMap }) {
86
+ errors: { message: string }[] = []
87
+ constructor({
88
+ code = '',
89
+ mdast = undefined as any,
90
+ components = {} as ComponentsMap,
91
+ }) {
86
92
  this.str = code
87
93
  this.mdast = mdast || mdxParser.parse(code)
88
- // TODO add tags and their allowed import sources
89
94
  this.c = {
90
95
  ...Object.fromEntries(
91
96
  nativeTags.map((tag) => {
@@ -96,27 +101,31 @@ class MdastToJsx {
96
101
  }
97
102
  }
98
103
  mapMdastChildren(node: any) {
99
- const res = node.children?.flatMap((child) =>
100
- this.mdastTransformer(child),
101
- )
104
+ const res = node.children
105
+ ?.flatMap((child) => this.mdastTransformer(child))
106
+ .filter(Boolean)
102
107
  if (Array.isArray(res)) {
103
108
  if (res.length === 1) {
104
109
  return res[0]
105
110
  } else {
106
- return res.map((x, i) => <Fragment key={i}>{x}</Fragment>)
111
+ return res.map((x, i) =>
112
+ React.isValidElement(x) ? cloneElement(x, { key: i }) : x,
113
+ )
107
114
  }
108
115
  }
109
116
  return res || null
110
117
  }
111
118
  mapJsxChildren(node: any) {
112
- const res = node.children?.flatMap((child, i) =>
113
- this.jsxTransformer(child),
114
- )
119
+ const res = node.children
120
+ ?.flatMap((child, i) => this.jsxTransformer(child))
121
+ .filter(Boolean)
115
122
  if (Array.isArray(res)) {
116
123
  if (res.length === 1) {
117
124
  return res[0]
118
125
  } else {
119
- return res.map((x, i) => <Fragment key={i}>{x}</Fragment>)
126
+ return res.map((x, i) =>
127
+ React.isValidElement(x) ? cloneElement(x, { key: i }) : x,
128
+ )
120
129
  }
121
130
  }
122
131
  return res || null
@@ -136,7 +145,9 @@ class MdastToJsx {
136
145
  const Component = accessWithDot(this.c, node.name)
137
146
 
138
147
  if (!Component) {
139
- this.errors.push(`Unsupported jsx tag ${node.name}`)
148
+ this.errors.push({
149
+ message: `Unsupported jsx component ${node.name}`,
150
+ })
140
151
  return null
141
152
  }
142
153
 
@@ -173,8 +184,6 @@ class MdastToJsx {
173
184
  const start = node.position?.start?.offset
174
185
  const end = node.position?.end?.offset
175
186
  let text = this.str.slice(start, end)
176
- // console.log('esm', node)
177
- const tree = node.data?.estree
178
187
 
179
188
  return []
180
189
  }
@@ -307,11 +316,7 @@ class MdastToJsx {
307
316
  return <this.c.br />
308
317
  }
309
318
  case 'root': {
310
- return (
311
- <this.c.div className=''>
312
- {this.mapMdastChildren(node)}
313
- </this.c.div>
314
- )
319
+ return <Fragment>{this.mapMdastChildren(node)}</Fragment>
315
320
  }
316
321
  case 'table': {
317
322
  const align = node.align
@@ -334,6 +339,23 @@ class MdastToJsx {
334
339
  case 'definition': {
335
340
  return []
336
341
  }
342
+ case 'linkReference': {
343
+ let href = ''
344
+ mdastBfs(this.mdast, (child: any) => {
345
+ if (
346
+ child.type === 'definition' &&
347
+ child.identifier === node.identifier
348
+ ) {
349
+ href = child.url
350
+ }
351
+ })
352
+
353
+ return (
354
+ <this.c.a href={href}>
355
+ {this.mapMdastChildren(node)}
356
+ </this.c.a>
357
+ )
358
+ }
337
359
  case 'footnoteReference': {
338
360
  return []
339
361
  }
@@ -342,8 +364,6 @@ class MdastToJsx {
342
364
  return []
343
365
  }
344
366
  case 'html': {
345
- // console.log('html', node)
346
-
347
367
  const start = node.position?.start?.offset
348
368
  const end = node.position?.end?.offset
349
369
  const text = this.str.slice(start, end)
@@ -386,11 +406,14 @@ class MdastToJsx {
386
406
  getJsxAttrs(node: MdxJsxFlowElement | MdxJsxTextElement) {
387
407
  let attrsList = node.attributes
388
408
  .map((attr) => {
389
- // TODO what is mdxJsxExpressionAttribute
390
409
  if (attr.type === 'mdxJsxExpressionAttribute') {
391
- throw new Error(
392
- `mdxJsxExpressionAttribute is not supported: ${attr.value}`,
393
- )
410
+ this.errors.push({
411
+ message: `Expressions in jsx props are not supported (${attr.value.replace(
412
+ /\n+/g,
413
+ ' ',
414
+ )})`,
415
+ })
416
+ return
394
417
  }
395
418
  if (attr.type !== 'mdxJsxAttribute') {
396
419
  throw new Error(
@@ -406,8 +429,6 @@ class MdastToJsx {
406
429
  return [attr.name, true]
407
430
  }
408
431
  if (v?.type === 'mdxJsxAttributeValueExpression') {
409
- // logger.json({value})
410
- // if it's a number, just return it
411
432
  if (v.value === 'true') {
412
433
  return [attr.name, true]
413
434
  }
@@ -420,13 +441,15 @@ class MdastToJsx {
420
441
  if (v.value === 'undefined') {
421
442
  return [attr.name, undefined]
422
443
  }
423
- if (
424
- // TODO add a way to parse ' and `
425
- ['"'].some(
426
- (q) => v.value.startsWith(q) && v.value.endsWith(q),
427
- )
428
- ) {
429
- return [attr.name, JSON.parse(v.value)]
444
+ let quote = ['"', "'", '`'].find(
445
+ (q) => v.value.startsWith(q) && v.value.endsWith(q),
446
+ )
447
+ if (quote) {
448
+ let value = v.value
449
+ if (quote !== '"') {
450
+ value = v.value.replace(new RegExp(quote, 'g'), '"')
451
+ }
452
+ return [attr.name, JSON.parse(value)]
430
453
  }
431
454
 
432
455
  const number = Number(v.value)
@@ -434,11 +457,9 @@ class MdastToJsx {
434
457
  return [attr.name, number]
435
458
  }
436
459
 
437
- this.errors.push(
438
- `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
439
- )
440
-
441
- // return [attr.name, `{${v.value}}`]
460
+ this.errors.push({
461
+ message: `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
462
+ })
442
463
  } else {
443
464
  console.log('unhandled attr', { attr }, attr.type)
444
465
  }
@@ -462,7 +483,10 @@ function accessWithDot(obj, path: string) {
462
483
  .reduce((o, i) => o[i], obj)
463
484
  }
464
485
 
465
- function mdastBfs(node: Parent | Node, cb?: (node: Node | Parent) => any) {
486
+ export function mdastBfs(
487
+ node: Parent | Node,
488
+ cb?: (node: Node | Parent) => any,
489
+ ) {
466
490
  const queue = [node]
467
491
  const result: any[] = []
468
492
  while (queue.length) {