safe-mdx 1.0.3 → 1.1.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/README.md +3 -1
- package/dist/safe-mdx.bench.d.ts +2 -0
- package/dist/safe-mdx.bench.d.ts.map +1 -0
- package/dist/safe-mdx.bench.js +35 -0
- package/dist/safe-mdx.bench.js.map +1 -0
- package/dist/safe-mdx.d.ts +21 -10
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +71 -29
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +194 -2
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +4 -2
- package/src/safe-mdx.bench.tsx +46 -0
- package/src/safe-mdx.test.tsx +212 -3
- package/src/safe-mdx.tsx +137 -125
package/src/safe-mdx.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { Suspense, cloneElement } from 'react'
|
|
2
2
|
|
|
3
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
3
4
|
import type { Node, Parent, Root, RootContent } from 'mdast'
|
|
4
5
|
import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'
|
|
5
6
|
|
|
6
7
|
import { Fragment, ReactNode } from 'react'
|
|
7
|
-
import { completeJsxTags } from './streaming.js'
|
|
8
8
|
|
|
9
9
|
const HtmlToJsxConverter = React.lazy(() =>
|
|
10
10
|
import('./HtmlToJsxConverter.js').then((module) => ({
|
|
@@ -23,27 +23,47 @@ declare module 'mdast' {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export type
|
|
26
|
+
export type RenderNode = (
|
|
27
27
|
node: MyRootContent,
|
|
28
28
|
transform: (node: MyRootContent) => ReactNode,
|
|
29
29
|
) => ReactNode | undefined
|
|
30
30
|
|
|
31
|
+
export interface SafeMdxError {
|
|
32
|
+
message: string
|
|
33
|
+
line?: number
|
|
34
|
+
schemaPath?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ComponentPropsSchema = Record<string, StandardSchemaV1>
|
|
38
|
+
|
|
39
|
+
export type CreateElementFunction = (
|
|
40
|
+
type: any,
|
|
41
|
+
props?: any,
|
|
42
|
+
...children: ReactNode[]
|
|
43
|
+
) => ReactNode
|
|
44
|
+
|
|
31
45
|
export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
32
46
|
components,
|
|
33
47
|
markdown = '',
|
|
34
48
|
mdast = null as any,
|
|
35
49
|
renderNode,
|
|
50
|
+
componentPropsSchema,
|
|
51
|
+
createElement,
|
|
36
52
|
}: {
|
|
37
53
|
components?: ComponentsMap
|
|
38
54
|
markdown?: string
|
|
39
55
|
mdast: MyRootContent
|
|
40
|
-
renderNode?:
|
|
56
|
+
renderNode?: RenderNode
|
|
57
|
+
componentPropsSchema?: ComponentPropsSchema
|
|
58
|
+
createElement?: CreateElementFunction
|
|
41
59
|
}) {
|
|
42
60
|
const visitor = new MdastToJsx({
|
|
43
61
|
markdown,
|
|
44
62
|
mdast,
|
|
45
63
|
components,
|
|
46
64
|
renderNode,
|
|
65
|
+
componentPropsSchema,
|
|
66
|
+
createElement,
|
|
47
67
|
})
|
|
48
68
|
const result = visitor.run()
|
|
49
69
|
return result
|
|
@@ -54,14 +74,18 @@ export class MdastToJsx {
|
|
|
54
74
|
str: string
|
|
55
75
|
jsxStr: string = ''
|
|
56
76
|
c: ComponentsMap
|
|
57
|
-
errors:
|
|
58
|
-
renderNode?:
|
|
77
|
+
errors: SafeMdxError[] = []
|
|
78
|
+
renderNode?: RenderNode
|
|
79
|
+
componentPropsSchema?: ComponentPropsSchema
|
|
80
|
+
createElement: CreateElementFunction
|
|
59
81
|
|
|
60
82
|
constructor({
|
|
61
83
|
markdown: code = '',
|
|
62
84
|
mdast,
|
|
63
85
|
components = {} as ComponentsMap,
|
|
64
86
|
renderNode,
|
|
87
|
+
componentPropsSchema,
|
|
88
|
+
createElement = React.createElement,
|
|
65
89
|
}: {
|
|
66
90
|
markdown?: string
|
|
67
91
|
mdast: MyRootContent
|
|
@@ -70,6 +94,8 @@ export class MdastToJsx {
|
|
|
70
94
|
node: MyRootContent,
|
|
71
95
|
transform: (node: MyRootContent) => ReactNode,
|
|
72
96
|
) => ReactNode | undefined
|
|
97
|
+
componentPropsSchema?: ComponentPropsSchema
|
|
98
|
+
createElement?: CreateElementFunction
|
|
73
99
|
}) {
|
|
74
100
|
this.str = code
|
|
75
101
|
|
|
@@ -77,6 +103,10 @@ export class MdastToJsx {
|
|
|
77
103
|
|
|
78
104
|
this.renderNode = renderNode
|
|
79
105
|
|
|
106
|
+
this.componentPropsSchema = componentPropsSchema
|
|
107
|
+
|
|
108
|
+
this.createElement = createElement
|
|
109
|
+
|
|
80
110
|
this.c = {
|
|
81
111
|
...Object.fromEntries(
|
|
82
112
|
nativeTags.map((tag) => {
|
|
@@ -86,6 +116,36 @@ export class MdastToJsx {
|
|
|
86
116
|
...components,
|
|
87
117
|
}
|
|
88
118
|
}
|
|
119
|
+
|
|
120
|
+
validateComponentProps(
|
|
121
|
+
componentName: string,
|
|
122
|
+
props: Record<string, any>,
|
|
123
|
+
line?: number
|
|
124
|
+
): void {
|
|
125
|
+
if (!this.componentPropsSchema || !this.componentPropsSchema[componentName]) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const schema = this.componentPropsSchema[componentName]
|
|
130
|
+
let result = schema['~standard'].validate(props)
|
|
131
|
+
|
|
132
|
+
if (result instanceof Promise) {
|
|
133
|
+
// Ignore async validation errors as requested
|
|
134
|
+
return
|
|
135
|
+
} else {
|
|
136
|
+
if (result.issues) {
|
|
137
|
+
result.issues.forEach((issue) => {
|
|
138
|
+
const propPath = issue.path?.join('.') || 'unknown'
|
|
139
|
+
this.errors.push({
|
|
140
|
+
message: `Invalid props for component "${componentName}" at "${propPath}": ${issue.message}`,
|
|
141
|
+
line,
|
|
142
|
+
schemaPath: issue.path?.join('.'),
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
89
149
|
mapMdastChildren(node: any) {
|
|
90
150
|
const res = node.children
|
|
91
151
|
?.flatMap((child) => this.mdastTransformer(child))
|
|
@@ -137,6 +197,7 @@ export class MdastToJsx {
|
|
|
137
197
|
if (!Component) {
|
|
138
198
|
this.errors.push({
|
|
139
199
|
message: `Unsupported jsx component ${node.name}`,
|
|
200
|
+
line: node.position?.start?.line,
|
|
140
201
|
})
|
|
141
202
|
return null
|
|
142
203
|
}
|
|
@@ -146,11 +207,11 @@ export class MdastToJsx {
|
|
|
146
207
|
})
|
|
147
208
|
|
|
148
209
|
let attrs = Object.fromEntries(attrsList)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
210
|
+
|
|
211
|
+
// Validate component props with schema if available
|
|
212
|
+
this.validateComponentProps(node.name, attrs, node.position?.start?.line)
|
|
213
|
+
|
|
214
|
+
return this.createElement(Component, attrs, this.mapJsxChildren(node))
|
|
154
215
|
}
|
|
155
216
|
default: {
|
|
156
217
|
return this.mdastTransformer(node)
|
|
@@ -226,28 +287,16 @@ export class MdastToJsx {
|
|
|
226
287
|
const level = node.depth
|
|
227
288
|
const Tag = this.c[`h${level}`] ?? `h${level}`
|
|
228
289
|
|
|
229
|
-
return (
|
|
230
|
-
<Tag {...node.data?.hProperties}>
|
|
231
|
-
{this.mapMdastChildren(node)}
|
|
232
|
-
</Tag>
|
|
233
|
-
)
|
|
290
|
+
return this.createElement(Tag, node.data?.hProperties, this.mapMdastChildren(node))
|
|
234
291
|
}
|
|
235
292
|
case 'paragraph': {
|
|
236
|
-
return (
|
|
237
|
-
<this.c.p {...node.data?.hProperties}>
|
|
238
|
-
{this.mapMdastChildren(node)}
|
|
239
|
-
</this.c.p>
|
|
240
|
-
)
|
|
293
|
+
return this.createElement(this.c.p, node.data?.hProperties, this.mapMdastChildren(node))
|
|
241
294
|
}
|
|
242
295
|
case 'blockquote': {
|
|
243
|
-
return (
|
|
244
|
-
<this.c.blockquote {...node.data?.hProperties}>
|
|
245
|
-
{this.mapMdastChildren(node)}
|
|
246
|
-
</this.c.blockquote>
|
|
247
|
-
)
|
|
296
|
+
return this.createElement(this.c.blockquote, node.data?.hProperties, this.mapMdastChildren(node))
|
|
248
297
|
}
|
|
249
298
|
case 'thematicBreak': {
|
|
250
|
-
return
|
|
299
|
+
return this.createElement(this.c.hr, node.data?.hProperties)
|
|
251
300
|
}
|
|
252
301
|
case 'code': {
|
|
253
302
|
if (!node.value) {
|
|
@@ -255,10 +304,10 @@ export class MdastToJsx {
|
|
|
255
304
|
}
|
|
256
305
|
const language = node.lang || ''
|
|
257
306
|
const code = node.value
|
|
258
|
-
const codeBlock = (className?: string) => (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
307
|
+
const codeBlock = (className?: string) => this.createElement(
|
|
308
|
+
this.c.pre,
|
|
309
|
+
node.data?.hProperties,
|
|
310
|
+
this.createElement(this.c.code, { className }, code)
|
|
262
311
|
)
|
|
263
312
|
|
|
264
313
|
if (language) {
|
|
@@ -269,49 +318,31 @@ export class MdastToJsx {
|
|
|
269
318
|
|
|
270
319
|
case 'list': {
|
|
271
320
|
if (node.ordered) {
|
|
272
|
-
return (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
>
|
|
277
|
-
{this.mapMdastChildren(node)}
|
|
278
|
-
</this.c.ol>
|
|
321
|
+
return this.createElement(
|
|
322
|
+
this.c.ol,
|
|
323
|
+
{ start: node.start!, ...node.data?.hProperties },
|
|
324
|
+
this.mapMdastChildren(node)
|
|
279
325
|
)
|
|
280
326
|
}
|
|
281
|
-
return (
|
|
282
|
-
<this.c.ul {...node.data?.hProperties}>
|
|
283
|
-
{this.mapMdastChildren(node)}
|
|
284
|
-
</this.c.ul>
|
|
285
|
-
)
|
|
327
|
+
return this.createElement(this.c.ul, node.data?.hProperties, this.mapMdastChildren(node))
|
|
286
328
|
}
|
|
287
329
|
case 'listItem': {
|
|
288
330
|
// https://github.com/syntax-tree/mdast-util-gfm-task-list-item#syntax-tree
|
|
289
331
|
if (node?.checked != null) {
|
|
290
|
-
return (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
>
|
|
295
|
-
{this.mapMdastChildren(node)}
|
|
296
|
-
</this.c.li>
|
|
332
|
+
return this.createElement(
|
|
333
|
+
this.c.li,
|
|
334
|
+
{ 'data-checked': node.checked, ...node.data?.hProperties },
|
|
335
|
+
this.mapMdastChildren(node)
|
|
297
336
|
)
|
|
298
337
|
}
|
|
299
|
-
return (
|
|
300
|
-
<this.c.li {...node.data?.hProperties}>
|
|
301
|
-
{this.mapMdastChildren(node)}
|
|
302
|
-
</this.c.li>
|
|
303
|
-
)
|
|
338
|
+
return this.createElement(this.c.li, node.data?.hProperties, this.mapMdastChildren(node))
|
|
304
339
|
}
|
|
305
340
|
case 'text': {
|
|
306
341
|
if (!node.value) {
|
|
307
342
|
return []
|
|
308
343
|
}
|
|
309
344
|
if (node.data?.hProperties) {
|
|
310
|
-
return (
|
|
311
|
-
<this.c.span {...node.data.hProperties}>
|
|
312
|
-
{node.value}
|
|
313
|
-
</this.c.span>
|
|
314
|
-
)
|
|
345
|
+
return this.createElement(this.c.span, node.data.hProperties, node.value)
|
|
315
346
|
}
|
|
316
347
|
return node.value
|
|
317
348
|
}
|
|
@@ -319,92 +350,70 @@ export class MdastToJsx {
|
|
|
319
350
|
const src = node.url || ''
|
|
320
351
|
const alt = node.alt || ''
|
|
321
352
|
const title = node.title || ''
|
|
322
|
-
return (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
/>
|
|
329
|
-
)
|
|
353
|
+
return this.createElement(this.c.img, {
|
|
354
|
+
src,
|
|
355
|
+
alt,
|
|
356
|
+
title,
|
|
357
|
+
...node.data?.hProperties
|
|
358
|
+
})
|
|
330
359
|
}
|
|
331
360
|
case 'link': {
|
|
332
361
|
const href = node.url || ''
|
|
333
362
|
const title = node.title || ''
|
|
334
|
-
return (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
363
|
+
return this.createElement(
|
|
364
|
+
this.c.a,
|
|
365
|
+
{ href, title, ...node.data?.hProperties },
|
|
366
|
+
this.mapMdastChildren(node)
|
|
338
367
|
)
|
|
339
368
|
}
|
|
340
369
|
case 'strong': {
|
|
341
|
-
return (
|
|
342
|
-
<this.c.strong {...node.data?.hProperties}>
|
|
343
|
-
{this.mapMdastChildren(node)}
|
|
344
|
-
</this.c.strong>
|
|
345
|
-
)
|
|
370
|
+
return this.createElement(this.c.strong, node.data?.hProperties, this.mapMdastChildren(node))
|
|
346
371
|
}
|
|
347
372
|
case 'emphasis': {
|
|
348
|
-
return (
|
|
349
|
-
<this.c.em {...node.data?.hProperties}>
|
|
350
|
-
{this.mapMdastChildren(node)}
|
|
351
|
-
</this.c.em>
|
|
352
|
-
)
|
|
373
|
+
return this.createElement(this.c.em, node.data?.hProperties, this.mapMdastChildren(node))
|
|
353
374
|
}
|
|
354
375
|
case 'delete': {
|
|
355
|
-
return (
|
|
356
|
-
<this.c.del {...node.data?.hProperties}>
|
|
357
|
-
{this.mapMdastChildren(node)}
|
|
358
|
-
</this.c.del>
|
|
359
|
-
)
|
|
376
|
+
return this.createElement(this.c.del, node.data?.hProperties, this.mapMdastChildren(node))
|
|
360
377
|
}
|
|
361
378
|
case 'inlineCode': {
|
|
362
379
|
if (!node.value) {
|
|
363
380
|
return []
|
|
364
381
|
}
|
|
365
|
-
return (
|
|
366
|
-
<this.c.code {...node.data?.hProperties}>
|
|
367
|
-
{node.value}
|
|
368
|
-
</this.c.code>
|
|
369
|
-
)
|
|
382
|
+
return this.createElement(this.c.code, node.data?.hProperties, node.value)
|
|
370
383
|
}
|
|
371
384
|
case 'break': {
|
|
372
|
-
return
|
|
385
|
+
return this.createElement(this.c.br, node.data?.hProperties)
|
|
373
386
|
}
|
|
374
387
|
case 'root': {
|
|
375
388
|
if (node.data?.hProperties) {
|
|
376
|
-
return (
|
|
377
|
-
<this.c.div {...node.data.hProperties}>
|
|
378
|
-
{this.mapMdastChildren(node)}
|
|
379
|
-
</this.c.div>
|
|
380
|
-
)
|
|
389
|
+
return this.createElement(this.c.div, node.data.hProperties, this.mapMdastChildren(node))
|
|
381
390
|
}
|
|
382
|
-
return
|
|
391
|
+
return this.createElement(Fragment, null, this.mapMdastChildren(node))
|
|
383
392
|
}
|
|
384
393
|
case 'table': {
|
|
385
394
|
const [head, ...body] = React.Children.toArray(
|
|
386
395
|
this.mapMdastChildren(node),
|
|
387
396
|
)
|
|
388
|
-
return (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
397
|
+
return this.createElement(
|
|
398
|
+
this.c.table,
|
|
399
|
+
node.data?.hProperties,
|
|
400
|
+
head && this.createElement(this.c.thead, null, head),
|
|
401
|
+
!!body?.length && this.createElement(this.c.tbody, null, body)
|
|
393
402
|
)
|
|
394
403
|
}
|
|
395
404
|
case 'tableRow': {
|
|
396
|
-
return (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
405
|
+
return this.createElement(
|
|
406
|
+
this.c.tr,
|
|
407
|
+
{ className: '', ...node.data?.hProperties },
|
|
408
|
+
this.mapMdastChildren(node)
|
|
400
409
|
)
|
|
401
410
|
}
|
|
402
411
|
case 'tableCell': {
|
|
403
412
|
let content = this.mapMdastChildren(node)
|
|
404
|
-
return (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
413
|
+
return this.createElement(
|
|
414
|
+
this.c.td,
|
|
415
|
+
{ className: '', ...node.data?.hProperties },
|
|
416
|
+
content
|
|
408
417
|
)
|
|
409
418
|
}
|
|
410
419
|
case 'definition': {
|
|
@@ -421,10 +430,10 @@ export class MdastToJsx {
|
|
|
421
430
|
}
|
|
422
431
|
})
|
|
423
432
|
|
|
424
|
-
return (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
433
|
+
return this.createElement(
|
|
434
|
+
this.c.a,
|
|
435
|
+
{ href, ...node.data?.hProperties },
|
|
436
|
+
this.mapMdastChildren(node)
|
|
428
437
|
)
|
|
429
438
|
}
|
|
430
439
|
case 'footnoteReference': {
|
|
@@ -442,14 +451,14 @@ export class MdastToJsx {
|
|
|
442
451
|
return []
|
|
443
452
|
}
|
|
444
453
|
|
|
445
|
-
return (
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
454
|
+
return this.createElement(
|
|
455
|
+
Suspense,
|
|
456
|
+
{ fallback: null },
|
|
457
|
+
this.createElement(HtmlToJsxConverter, {
|
|
458
|
+
htmlText: text,
|
|
459
|
+
instance: this,
|
|
460
|
+
node
|
|
461
|
+
})
|
|
453
462
|
)
|
|
454
463
|
}
|
|
455
464
|
case 'imageReference': {
|
|
@@ -473,15 +482,17 @@ export class MdastToJsx {
|
|
|
473
482
|
|
|
474
483
|
export function getJsxAttrs(
|
|
475
484
|
node: MdxJsxFlowElement | MdxJsxTextElement,
|
|
476
|
-
onError: (err:
|
|
485
|
+
onError: (err: SafeMdxError) => void = console.error,
|
|
477
486
|
) {
|
|
478
487
|
let attrsList = node.attributes
|
|
479
488
|
.map((attr) => {
|
|
480
489
|
if (attr.type === 'mdxJsxExpressionAttribute') {
|
|
490
|
+
|
|
481
491
|
onError({
|
|
482
492
|
message: `Expressions in jsx props are not supported (${attr.value
|
|
483
493
|
.replace(/\n+/g, ' ')
|
|
484
494
|
.replace(/ +/g, ' ')})`,
|
|
495
|
+
line: attr.position?.start?.line,
|
|
485
496
|
})
|
|
486
497
|
return
|
|
487
498
|
}
|
|
@@ -531,6 +542,7 @@ export function getJsxAttrs(
|
|
|
531
542
|
|
|
532
543
|
onError({
|
|
533
544
|
message: `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
|
|
545
|
+
line: attr.position?.start?.line,
|
|
534
546
|
})
|
|
535
547
|
} else {
|
|
536
548
|
console.log('unhandled attr', { attr }, attr.type)
|