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/README.md +45 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/safe-mdx.d.ts +33 -0
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +119 -66
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.d.ts +2 -0
- package/dist/safe-mdx.test.d.ts.map +1 -0
- package/dist/safe-mdx.test.js +1519 -0
- package/dist/safe-mdx.test.js.map +1 -0
- package/package.json +3 -1
- package/src/index.ts +1 -0
- package/src/safe-mdx.test.tsx +1534 -0
- package/src/safe-mdx.tsx +124 -86
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]]
|
|
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({
|
|
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
|
|
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) =>
|
|
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
|
|
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) =>
|
|
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(
|
|
148
|
+
this.errors.push({
|
|
149
|
+
message: `Unsupported jsx component ${node.name}`,
|
|
150
|
+
})
|
|
140
151
|
return null
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
let attrsList =
|
|
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
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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 (
|
|
396
|
-
|
|
397
|
-
`non mdxJsxAttribute is not supported: ${attr}`,
|
|
398
|
-
)
|
|
438
|
+
if (v.value === 'false') {
|
|
439
|
+
return [attr.name, false]
|
|
399
440
|
}
|
|
400
|
-
|
|
401
|
-
|
|
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 ===
|
|
406
|
-
return [attr.name,
|
|
444
|
+
if (v.value === 'undefined') {
|
|
445
|
+
return [attr.name, undefined]
|
|
407
446
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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(
|
|
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
|
+
}
|