safe-mdx 0.0.0 → 0.0.2

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,11 +145,15 @@ 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
 
143
- let attrsList = this.getJsxAttrs(node)
154
+ let attrsList = getJsxAttrs(node, (err) => {
155
+ this.errors.push(err)
156
+ })
144
157
 
145
158
  let attrs = Object.fromEntries(attrsList)
146
159
  return (
@@ -173,8 +186,6 @@ class MdastToJsx {
173
186
  const start = node.position?.start?.offset
174
187
  const end = node.position?.end?.offset
175
188
  let text = this.str.slice(start, end)
176
- // console.log('esm', node)
177
- const tree = node.data?.estree
178
189
 
179
190
  return []
180
191
  }
@@ -237,9 +248,7 @@ class MdastToJsx {
237
248
  const code = node.value
238
249
  return (
239
250
  <this.c.pre>
240
- <this.c.code className={`language-${language}`}>
241
- {code}
242
- </this.c.code>
251
+ <this.c.code>{code}</this.c.code>
243
252
  </this.c.pre>
244
253
  )
245
254
  }
@@ -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)
@@ -383,71 +403,78 @@ class MdastToJsx {
383
403
  }
384
404
  }
385
405
  }
386
- getJsxAttrs(node: MdxJsxFlowElement | MdxJsxTextElement) {
387
- let attrsList = node.attributes
388
- .map((attr) => {
389
- // TODO what is mdxJsxExpressionAttribute
390
- if (attr.type === 'mdxJsxExpressionAttribute') {
391
- throw new Error(
392
- `mdxJsxExpressionAttribute is not supported: ${attr.value}`,
393
- )
406
+ }
407
+
408
+ export function getJsxAttrs(
409
+ node: MdxJsxFlowElement | MdxJsxTextElement,
410
+ onError: (err: { message: string }) => void = console.error,
411
+ ) {
412
+ let attrsList = node.attributes
413
+ .map((attr) => {
414
+ if (attr.type === 'mdxJsxExpressionAttribute') {
415
+ onError({
416
+ message: `Expressions in jsx props are not supported (${attr.value.replace(
417
+ /\n+/g,
418
+ ' ',
419
+ )})`,
420
+ })
421
+ return
422
+ }
423
+ if (attr.type !== 'mdxJsxAttribute') {
424
+ throw new Error(`non mdxJsxAttribute is not supported: ${attr}`)
425
+ }
426
+
427
+ const v = attr.value
428
+ if (typeof v === 'string' || typeof v === 'number') {
429
+ return [attr.name, v]
430
+ }
431
+ if (v === null) {
432
+ return [attr.name, true]
433
+ }
434
+ if (v?.type === 'mdxJsxAttributeValueExpression') {
435
+ if (v.value === 'true') {
436
+ return [attr.name, true]
394
437
  }
395
- if (attr.type !== 'mdxJsxAttribute') {
396
- throw new Error(
397
- `non mdxJsxAttribute is not supported: ${attr}`,
398
- )
438
+ if (v.value === 'false') {
439
+ return [attr.name, false]
399
440
  }
400
-
401
- const v = attr.value
402
- if (typeof v === 'string' || typeof v === 'number') {
403
- return [attr.name, v]
441
+ if (v.value === 'null') {
442
+ return [attr.name, null]
404
443
  }
405
- if (v === null) {
406
- return [attr.name, true]
444
+ if (v.value === 'undefined') {
445
+ return [attr.name, undefined]
407
446
  }
408
- if (v?.type === 'mdxJsxAttributeValueExpression') {
409
- // logger.json({value})
410
- // if it's a number, just return it
411
- if (v.value === 'true') {
412
- return [attr.name, true]
413
- }
414
- if (v.value === 'false') {
415
- return [attr.name, false]
416
- }
417
- if (v.value === 'null') {
418
- return [attr.name, null]
419
- }
420
- if (v.value === 'undefined') {
421
- return [attr.name, undefined]
422
- }
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)]
430
- }
431
-
432
- const number = Number(v.value)
433
- if (!isNaN(number)) {
434
- return [attr.name, number]
447
+ let quote = ['"', "'", '`'].find(
448
+ (q) => v.value.startsWith(q) && v.value.endsWith(q),
449
+ )
450
+ if (quote) {
451
+ let value = v.value
452
+ if (quote !== '"') {
453
+ value = v.value.replace(new RegExp(quote, 'g'), '"')
435
454
  }
455
+ return [attr.name, JSON.parse(value)]
456
+ }
436
457
 
437
- this.errors.push(
438
- `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
439
- )
440
-
441
- // return [attr.name, `{${v.value}}`]
442
- } else {
443
- console.log('unhandled attr', { attr }, attr.type)
458
+ const number = Number(v.value)
459
+ if (!isNaN(number)) {
460
+ return [attr.name, number]
461
+ }
462
+ const parsedJson = safeJsonParse(v.value)
463
+ if (parsedJson) {
464
+ return [attr.name, parsedJson]
444
465
  }
445
466
 
446
- return
447
- })
448
- .filter(isTruthy) as [string, any][]
449
- return attrsList
450
- }
467
+ onError({
468
+ message: `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
469
+ })
470
+ } else {
471
+ console.log('unhandled attr', { attr }, attr.type)
472
+ }
473
+
474
+ return
475
+ })
476
+ .filter(isTruthy) as [string, any][]
477
+ return attrsList
451
478
  }
452
479
 
453
480
  function isTruthy<T>(val: T | undefined | null | false): val is T {
@@ -462,7 +489,10 @@ function accessWithDot(obj, path: string) {
462
489
  .reduce((o, i) => o[i], obj)
463
490
  }
464
491
 
465
- function mdastBfs(node: Parent | Node, cb?: (node: Node | Parent) => any) {
492
+ export function mdastBfs(
493
+ node: Parent | Node,
494
+ cb?: (node: Node | Parent) => any,
495
+ ) {
466
496
  const queue = [node]
467
497
  const result: any[] = []
468
498
  while (queue.length) {
@@ -479,3 +509,11 @@ function mdastBfs(node: Parent | Node, cb?: (node: Node | Parent) => any) {
479
509
  }
480
510
  return result
481
511
  }
512
+
513
+ function safeJsonParse(str: string) {
514
+ try {
515
+ return JSON.parse(str)
516
+ } catch (err) {
517
+ return null
518
+ }
519
+ }