safe-mdx 1.0.4 → 1.2.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 +112 -4
- package/dist/dynamic-esm-component.d.ts +6 -0
- package/dist/dynamic-esm-component.d.ts.map +1 -0
- package/dist/dynamic-esm-component.js +58 -0
- package/dist/dynamic-esm-component.js.map +1 -0
- package/dist/esm-parser.d.ts +19 -0
- package/dist/esm-parser.d.ts.map +1 -0
- package/dist/esm-parser.js +69 -0
- package/dist/esm-parser.js.map +1 -0
- package/dist/esm-parser.test.d.ts +2 -0
- package/dist/esm-parser.test.d.ts.map +1 -0
- package/dist/esm-parser.test.js +124 -0
- package/dist/esm-parser.test.js.map +1 -0
- 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 +41 -0
- package/dist/safe-mdx.bench.js.map +1 -0
- package/dist/safe-mdx.d.ts +24 -7
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +351 -101
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +770 -9
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +6 -2
- package/src/dynamic-esm-component.tsx +85 -0
- package/src/esm-parser.test.ts +141 -0
- package/src/esm-parser.ts +89 -0
- package/src/safe-mdx.bench.tsx +52 -0
- package/src/safe-mdx.test.tsx +843 -10
- package/src/safe-mdx.tsx +490 -193
package/src/safe-mdx.tsx
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
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'
|
|
6
|
+
import type { JSXElement, JSXAttribute, JSXText, JSXExpressionContainer } from 'estree-jsx'
|
|
7
|
+
import Evaluate from 'eval-estree-expression'
|
|
5
8
|
|
|
6
9
|
import { Fragment, ReactNode } from 'react'
|
|
7
|
-
import {
|
|
10
|
+
import { DynamicEsmComponent } from './dynamic-esm-component.js'
|
|
11
|
+
import { parseEsmImports, extractComponentInfo } from './esm-parser.js'
|
|
8
12
|
|
|
9
13
|
const HtmlToJsxConverter = React.lazy(() =>
|
|
10
14
|
import('./HtmlToJsxConverter.js').then((module) => ({
|
|
@@ -28,22 +32,45 @@ export type RenderNode = (
|
|
|
28
32
|
transform: (node: MyRootContent) => ReactNode,
|
|
29
33
|
) => ReactNode | undefined
|
|
30
34
|
|
|
35
|
+
export interface SafeMdxError {
|
|
36
|
+
message: string
|
|
37
|
+
line?: number
|
|
38
|
+
schemaPath?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ComponentPropsSchema = Record<string, StandardSchemaV1>
|
|
42
|
+
|
|
43
|
+
export type CreateElementFunction = (
|
|
44
|
+
type: any,
|
|
45
|
+
props?: any,
|
|
46
|
+
...children: ReactNode[]
|
|
47
|
+
) => ReactNode
|
|
48
|
+
|
|
31
49
|
export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
32
50
|
components,
|
|
33
51
|
markdown = '',
|
|
34
52
|
mdast = null as any,
|
|
35
53
|
renderNode,
|
|
54
|
+
componentPropsSchema,
|
|
55
|
+
createElement,
|
|
56
|
+
allowClientEsmImports = false,
|
|
36
57
|
}: {
|
|
37
58
|
components?: ComponentsMap
|
|
38
59
|
markdown?: string
|
|
39
60
|
mdast: MyRootContent
|
|
40
61
|
renderNode?: RenderNode
|
|
62
|
+
componentPropsSchema?: ComponentPropsSchema
|
|
63
|
+
createElement?: CreateElementFunction
|
|
64
|
+
allowClientEsmImports?: boolean
|
|
41
65
|
}) {
|
|
42
66
|
const visitor = new MdastToJsx({
|
|
43
67
|
markdown,
|
|
44
68
|
mdast,
|
|
45
69
|
components,
|
|
46
70
|
renderNode,
|
|
71
|
+
componentPropsSchema,
|
|
72
|
+
createElement,
|
|
73
|
+
allowClientEsmImports,
|
|
47
74
|
})
|
|
48
75
|
const result = visitor.run()
|
|
49
76
|
return result
|
|
@@ -54,14 +81,21 @@ export class MdastToJsx {
|
|
|
54
81
|
str: string
|
|
55
82
|
jsxStr: string = ''
|
|
56
83
|
c: ComponentsMap
|
|
57
|
-
errors:
|
|
84
|
+
errors: SafeMdxError[] = []
|
|
58
85
|
renderNode?: RenderNode
|
|
86
|
+
componentPropsSchema?: ComponentPropsSchema
|
|
87
|
+
createElement: CreateElementFunction
|
|
88
|
+
esmImports: Map<string, string> = new Map()
|
|
89
|
+
allowClientEsmImports: boolean
|
|
59
90
|
|
|
60
91
|
constructor({
|
|
61
92
|
markdown: code = '',
|
|
62
93
|
mdast,
|
|
63
94
|
components = {} as ComponentsMap,
|
|
64
95
|
renderNode,
|
|
96
|
+
componentPropsSchema,
|
|
97
|
+
createElement = React.createElement,
|
|
98
|
+
allowClientEsmImports = false,
|
|
65
99
|
}: {
|
|
66
100
|
markdown?: string
|
|
67
101
|
mdast: MyRootContent
|
|
@@ -70,6 +104,9 @@ export class MdastToJsx {
|
|
|
70
104
|
node: MyRootContent,
|
|
71
105
|
transform: (node: MyRootContent) => ReactNode,
|
|
72
106
|
) => ReactNode | undefined
|
|
107
|
+
componentPropsSchema?: ComponentPropsSchema
|
|
108
|
+
createElement?: CreateElementFunction
|
|
109
|
+
allowClientEsmImports?: boolean
|
|
73
110
|
}) {
|
|
74
111
|
this.str = code
|
|
75
112
|
|
|
@@ -77,6 +114,12 @@ export class MdastToJsx {
|
|
|
77
114
|
|
|
78
115
|
this.renderNode = renderNode
|
|
79
116
|
|
|
117
|
+
this.componentPropsSchema = componentPropsSchema
|
|
118
|
+
|
|
119
|
+
this.createElement = createElement
|
|
120
|
+
|
|
121
|
+
this.allowClientEsmImports = allowClientEsmImports
|
|
122
|
+
|
|
80
123
|
this.c = {
|
|
81
124
|
...Object.fromEntries(
|
|
82
125
|
nativeTags.map((tag) => {
|
|
@@ -86,6 +129,39 @@ export class MdastToJsx {
|
|
|
86
129
|
...components,
|
|
87
130
|
}
|
|
88
131
|
}
|
|
132
|
+
|
|
133
|
+
validateComponentProps(
|
|
134
|
+
componentName: string,
|
|
135
|
+
props: Record<string, any>,
|
|
136
|
+
line?: number,
|
|
137
|
+
): void {
|
|
138
|
+
if (
|
|
139
|
+
!this.componentPropsSchema ||
|
|
140
|
+
!this.componentPropsSchema[componentName]
|
|
141
|
+
) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const schema = this.componentPropsSchema[componentName]
|
|
146
|
+
let result = schema['~standard'].validate(props)
|
|
147
|
+
|
|
148
|
+
if (result instanceof Promise) {
|
|
149
|
+
// Ignore async validation errors as requested
|
|
150
|
+
return
|
|
151
|
+
} else {
|
|
152
|
+
if (result.issues) {
|
|
153
|
+
result.issues.forEach((issue) => {
|
|
154
|
+
const propPath = issue.path?.join('.') || 'unknown'
|
|
155
|
+
this.errors.push({
|
|
156
|
+
message: `Invalid props for component "${componentName}" at "${propPath}": ${issue.message}`,
|
|
157
|
+
line,
|
|
158
|
+
schemaPath: issue.path?.join('.'),
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
89
165
|
mapMdastChildren(node: any) {
|
|
90
166
|
const res = node.children
|
|
91
167
|
?.flatMap((child) => this.mdastTransformer(child))
|
|
@@ -132,24 +208,54 @@ export class MdastToJsx {
|
|
|
132
208
|
return []
|
|
133
209
|
}
|
|
134
210
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
211
|
+
// Check if this is an ESM imported component (only if allowed)
|
|
212
|
+
const esmImportInfo = this.allowClientEsmImports ? this.esmImports.get(node.name) : null
|
|
213
|
+
let Component
|
|
214
|
+
|
|
215
|
+
if (esmImportInfo) {
|
|
216
|
+
// Handle ESM imported component
|
|
217
|
+
const { importUrl, componentName } = extractComponentInfo(esmImportInfo)
|
|
218
|
+
|
|
219
|
+
Component = DynamicEsmComponent
|
|
220
|
+
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
221
|
+
this.errors.push(err)
|
|
140
222
|
})
|
|
141
|
-
|
|
223
|
+
let attrs = Object.fromEntries(attrsList)
|
|
224
|
+
|
|
225
|
+
return this.createElement(
|
|
226
|
+
Component,
|
|
227
|
+
{ ...attrs, importUrl, componentName },
|
|
228
|
+
this.mapJsxChildren(node),
|
|
229
|
+
)
|
|
230
|
+
} else {
|
|
231
|
+
Component = accessWithDot(this.c, node.name)
|
|
232
|
+
|
|
233
|
+
if (!Component) {
|
|
234
|
+
this.errors.push({
|
|
235
|
+
message: `Unsupported jsx component ${node.name}`,
|
|
236
|
+
line: node.position?.start?.line,
|
|
237
|
+
})
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
142
240
|
}
|
|
143
241
|
|
|
144
|
-
let attrsList = getJsxAttrs(node, (err) => {
|
|
242
|
+
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
145
243
|
this.errors.push(err)
|
|
146
244
|
})
|
|
147
245
|
|
|
148
246
|
let attrs = Object.fromEntries(attrsList)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
247
|
+
|
|
248
|
+
// Validate component props with schema if available
|
|
249
|
+
this.validateComponentProps(
|
|
250
|
+
node.name,
|
|
251
|
+
attrs,
|
|
252
|
+
node.position?.start?.line,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return this.createElement(
|
|
256
|
+
Component,
|
|
257
|
+
attrs,
|
|
258
|
+
this.mapJsxChildren(node),
|
|
153
259
|
)
|
|
154
260
|
}
|
|
155
261
|
default: {
|
|
@@ -158,6 +264,232 @@ export class MdastToJsx {
|
|
|
158
264
|
}
|
|
159
265
|
}
|
|
160
266
|
|
|
267
|
+
transformJsxElement(jsxElement: JSXElement, onError?: (err: SafeMdxError) => void, line?: number): ReactNode {
|
|
268
|
+
try {
|
|
269
|
+
// Handle JSX opening element
|
|
270
|
+
if (jsxElement.openingElement) {
|
|
271
|
+
const tagName = jsxElement.openingElement.name?.type === 'JSXIdentifier' ? jsxElement.openingElement.name.name : null
|
|
272
|
+
if (!tagName) {
|
|
273
|
+
onError?.({
|
|
274
|
+
message: 'JSX element missing component name',
|
|
275
|
+
line: line,
|
|
276
|
+
})
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check if this is an ESM imported component (only if allowed)
|
|
281
|
+
const esmImportInfo = this.allowClientEsmImports ? this.esmImports.get(tagName) : null
|
|
282
|
+
let Component
|
|
283
|
+
|
|
284
|
+
if (esmImportInfo) {
|
|
285
|
+
// Handle ESM imported component
|
|
286
|
+
const { importUrl, componentName } = extractComponentInfo(esmImportInfo)
|
|
287
|
+
Component = DynamicEsmComponent
|
|
288
|
+
} else {
|
|
289
|
+
// Get the component from the regular component map
|
|
290
|
+
Component = accessWithDot(this.c, tagName)
|
|
291
|
+
if (!Component) {
|
|
292
|
+
onError?.({
|
|
293
|
+
message: `Unsupported jsx component ${tagName} in attribute`,
|
|
294
|
+
line: line,
|
|
295
|
+
})
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Extract attributes
|
|
301
|
+
const props: Record<string, any> = {}
|
|
302
|
+
if (jsxElement.openingElement.attributes) {
|
|
303
|
+
for (const attr of jsxElement.openingElement.attributes) {
|
|
304
|
+
if (attr.type === 'JSXAttribute' && attr.name?.type === 'JSXIdentifier' && attr.name.name) {
|
|
305
|
+
if (attr.value) {
|
|
306
|
+
if (attr.value.type === 'Literal') {
|
|
307
|
+
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
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
props[attr.name.name] = true
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Extract children
|
|
321
|
+
const children: ReactNode[] = []
|
|
322
|
+
if (jsxElement.children) {
|
|
323
|
+
for (const child of jsxElement.children) {
|
|
324
|
+
if (child.type === 'JSXText') {
|
|
325
|
+
children.push(child.value)
|
|
326
|
+
} else if (child.type === 'JSXElement') {
|
|
327
|
+
const childElement = this.transformJsxElement(child)
|
|
328
|
+
if (childElement) {
|
|
329
|
+
children.push(childElement)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Handle ESM imported components by adding required props
|
|
336
|
+
if (esmImportInfo) {
|
|
337
|
+
const { importUrl, componentName } = extractComponentInfo(esmImportInfo)
|
|
338
|
+
return this.createElement(
|
|
339
|
+
Component,
|
|
340
|
+
{ ...props, importUrl, componentName },
|
|
341
|
+
...children
|
|
342
|
+
)
|
|
343
|
+
} else {
|
|
344
|
+
return this.createElement(Component, props, ...children)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
// Return null if transformation fails
|
|
349
|
+
onError?.({
|
|
350
|
+
message: `Failed to transform JSX element: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
351
|
+
line: line,
|
|
352
|
+
})
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
return null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getJsxAttrs(
|
|
359
|
+
node: MdxJsxFlowElement | MdxJsxTextElement,
|
|
360
|
+
onError: (err: SafeMdxError) => void = console.error,
|
|
361
|
+
) {
|
|
362
|
+
let attrsList: [string, any][] = []
|
|
363
|
+
|
|
364
|
+
for (const attr of node.attributes) {
|
|
365
|
+
if (attr.type === 'mdxJsxExpressionAttribute') {
|
|
366
|
+
// Handle spread expressions like {...{key: '1'}}
|
|
367
|
+
if (attr.data?.estree) {
|
|
368
|
+
try {
|
|
369
|
+
const program = attr.data.estree
|
|
370
|
+
if (
|
|
371
|
+
program.body?.length > 0 &&
|
|
372
|
+
program.body[0].type === 'ExpressionStatement'
|
|
373
|
+
) {
|
|
374
|
+
const expression = program.body[0].expression
|
|
375
|
+
try {
|
|
376
|
+
const result = Evaluate.evaluate.sync(expression)
|
|
377
|
+
|
|
378
|
+
// Handle spread syntax - merge the evaluated object
|
|
379
|
+
if (typeof result === 'object' && result != null) {
|
|
380
|
+
const entries = Object.entries(result)
|
|
381
|
+
attrsList.push(...entries)
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
onError({
|
|
385
|
+
message: `Failed to evaluate expression attribute: ${attr.value
|
|
386
|
+
.replace(/\n+/g, ' ')
|
|
387
|
+
.replace(/ +/g, ' ')}`,
|
|
388
|
+
line: attr.position?.start?.line,
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
onError({
|
|
394
|
+
message: `Failed to evaluate expression attribute: ${attr.value
|
|
395
|
+
.replace(/\n+/g, ' ')
|
|
396
|
+
.replace(/ +/g, ' ')}`,
|
|
397
|
+
line: attr.position?.start?.line,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
onError({
|
|
402
|
+
message: `Expressions in jsx props are not supported (${attr.value
|
|
403
|
+
.replace(/\n+/g, ' ')
|
|
404
|
+
.replace(/ +/g, ' ')})`,
|
|
405
|
+
line: attr.position?.start?.line,
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (attr.type !== 'mdxJsxAttribute') {
|
|
412
|
+
onError({
|
|
413
|
+
message: `non mdxJsxAttribute attribute is not supported: ${attr}`,
|
|
414
|
+
line: node.position?.start?.line,
|
|
415
|
+
})
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const v = attr.value
|
|
420
|
+
if (typeof v === 'string' || typeof v === 'number') {
|
|
421
|
+
attrsList.push([attr.name, v])
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
if (v === null) {
|
|
425
|
+
attrsList.push([attr.name, true])
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
if (v?.type === 'mdxJsxAttributeValueExpression') {
|
|
429
|
+
// Manual parsing fallback for simple values
|
|
430
|
+
if (v.value === 'true') {
|
|
431
|
+
attrsList.push([attr.name, true])
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
if (v.value === 'false') {
|
|
435
|
+
attrsList.push([attr.name, false])
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
if (v.value === 'null') {
|
|
439
|
+
attrsList.push([attr.name, null])
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
442
|
+
if (v.value === 'undefined') {
|
|
443
|
+
attrsList.push([attr.name, undefined])
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (v.data?.estree) {
|
|
448
|
+
try {
|
|
449
|
+
// Extract the expression from the Program body
|
|
450
|
+
const program = v.data.estree
|
|
451
|
+
if (
|
|
452
|
+
program.body?.length > 0 &&
|
|
453
|
+
program.body[0].type === 'ExpressionStatement'
|
|
454
|
+
) {
|
|
455
|
+
const expression = program.body[0].expression
|
|
456
|
+
|
|
457
|
+
// Check if this is a JSX element
|
|
458
|
+
if (expression.type === 'JSXElement') {
|
|
459
|
+
// Transform JSX element to React element
|
|
460
|
+
const jsxElement = this.transformJsxElement(expression, onError, attr.position?.start?.line)
|
|
461
|
+
if (jsxElement) {
|
|
462
|
+
attrsList.push([attr.name, jsxElement])
|
|
463
|
+
continue
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
// Evaluate the expression synchronously
|
|
469
|
+
const result = Evaluate.evaluate.sync(expression)
|
|
470
|
+
attrsList.push([attr.name, result])
|
|
471
|
+
continue
|
|
472
|
+
} catch (error) {
|
|
473
|
+
onError({
|
|
474
|
+
message: `Failed to evaluate expression attribute: ${attr.name}={${v.value}}`,
|
|
475
|
+
line: attr.position?.start?.line,
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
// Fall back to the original manual parsing for backwards compatibility
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
onError({
|
|
485
|
+
message: `Expressions in jsx prop not evaluated: (${attr.name}={${v.value}})`,
|
|
486
|
+
line: attr.position?.start?.line,
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return attrsList
|
|
491
|
+
}
|
|
492
|
+
|
|
161
493
|
run() {
|
|
162
494
|
const res = this.mdastTransformer(this.mdast) as ReactNode
|
|
163
495
|
if (Array.isArray(res) && res.length === 1) {
|
|
@@ -184,10 +516,13 @@ export class MdastToJsx {
|
|
|
184
516
|
|
|
185
517
|
switch (node.type) {
|
|
186
518
|
case 'mdxjsEsm': {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
519
|
+
// Parse ESM imports and merge into our imports map (only if allowed)
|
|
520
|
+
if (this.allowClientEsmImports) {
|
|
521
|
+
const parsedImports = parseEsmImports(node, (err) => this.errors.push(err))
|
|
522
|
+
parsedImports.forEach((value, key) => {
|
|
523
|
+
this.esmImports.set(key, value)
|
|
524
|
+
})
|
|
525
|
+
}
|
|
191
526
|
return []
|
|
192
527
|
}
|
|
193
528
|
case 'mdxJsxTextElement':
|
|
@@ -214,6 +549,36 @@ export class MdastToJsx {
|
|
|
214
549
|
if (!node.value) {
|
|
215
550
|
return []
|
|
216
551
|
}
|
|
552
|
+
|
|
553
|
+
// Check if we have an estree AST
|
|
554
|
+
if (node.data?.estree) {
|
|
555
|
+
try {
|
|
556
|
+
// Extract the expression from the Program body
|
|
557
|
+
const program = node.data.estree
|
|
558
|
+
if (
|
|
559
|
+
program.body?.length > 0 &&
|
|
560
|
+
program.body[0].type === 'ExpressionStatement'
|
|
561
|
+
) {
|
|
562
|
+
const expression = program.body[0].expression
|
|
563
|
+
try {
|
|
564
|
+
// Evaluate the expression synchronously
|
|
565
|
+
const result = Evaluate.evaluate.sync(expression)
|
|
566
|
+
return result
|
|
567
|
+
} catch (error) {
|
|
568
|
+
this.errors.push({
|
|
569
|
+
message: `Failed to evaluate expression: ${node.value}`,
|
|
570
|
+
line: node.position?.start?.line,
|
|
571
|
+
})
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
} catch (error) {
|
|
575
|
+
this.errors.push({
|
|
576
|
+
message: `Failed to evaluate expression: ${node.value}`,
|
|
577
|
+
line: node.position?.start?.line,
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
217
582
|
return []
|
|
218
583
|
}
|
|
219
584
|
case 'yaml': {
|
|
@@ -226,28 +591,28 @@ export class MdastToJsx {
|
|
|
226
591
|
const level = node.depth
|
|
227
592
|
const Tag = this.c[`h${level}`] ?? `h${level}`
|
|
228
593
|
|
|
229
|
-
return (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
594
|
+
return this.createElement(
|
|
595
|
+
Tag,
|
|
596
|
+
node.data?.hProperties,
|
|
597
|
+
this.mapMdastChildren(node),
|
|
233
598
|
)
|
|
234
599
|
}
|
|
235
600
|
case 'paragraph': {
|
|
236
|
-
return (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
601
|
+
return this.createElement(
|
|
602
|
+
this.c.p,
|
|
603
|
+
node.data?.hProperties,
|
|
604
|
+
this.mapMdastChildren(node),
|
|
240
605
|
)
|
|
241
606
|
}
|
|
242
607
|
case 'blockquote': {
|
|
243
|
-
return (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
608
|
+
return this.createElement(
|
|
609
|
+
this.c.blockquote,
|
|
610
|
+
node.data?.hProperties,
|
|
611
|
+
this.mapMdastChildren(node),
|
|
247
612
|
)
|
|
248
613
|
}
|
|
249
614
|
case 'thematicBreak': {
|
|
250
|
-
return
|
|
615
|
+
return this.createElement(this.c.hr, node.data?.hProperties)
|
|
251
616
|
}
|
|
252
617
|
case 'code': {
|
|
253
618
|
if (!node.value) {
|
|
@@ -255,11 +620,12 @@ export class MdastToJsx {
|
|
|
255
620
|
}
|
|
256
621
|
const language = node.lang || ''
|
|
257
622
|
const code = node.value
|
|
258
|
-
const codeBlock = (className?: string) =>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
623
|
+
const codeBlock = (className?: string) =>
|
|
624
|
+
this.createElement(
|
|
625
|
+
this.c.pre,
|
|
626
|
+
node.data?.hProperties,
|
|
627
|
+
this.createElement(this.c.code, { className }, code),
|
|
628
|
+
)
|
|
263
629
|
|
|
264
630
|
if (language) {
|
|
265
631
|
return codeBlock(`language-${language}`)
|
|
@@ -269,37 +635,34 @@ export class MdastToJsx {
|
|
|
269
635
|
|
|
270
636
|
case 'list': {
|
|
271
637
|
if (node.ordered) {
|
|
272
|
-
return (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
>
|
|
277
|
-
{this.mapMdastChildren(node)}
|
|
278
|
-
</this.c.ol>
|
|
638
|
+
return this.createElement(
|
|
639
|
+
this.c.ol,
|
|
640
|
+
{ start: node.start!, ...node.data?.hProperties },
|
|
641
|
+
this.mapMdastChildren(node),
|
|
279
642
|
)
|
|
280
643
|
}
|
|
281
|
-
return (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
644
|
+
return this.createElement(
|
|
645
|
+
this.c.ul,
|
|
646
|
+
node.data?.hProperties,
|
|
647
|
+
this.mapMdastChildren(node),
|
|
285
648
|
)
|
|
286
649
|
}
|
|
287
650
|
case 'listItem': {
|
|
288
651
|
// https://github.com/syntax-tree/mdast-util-gfm-task-list-item#syntax-tree
|
|
289
652
|
if (node?.checked != null) {
|
|
290
|
-
return (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
653
|
+
return this.createElement(
|
|
654
|
+
this.c.li,
|
|
655
|
+
{
|
|
656
|
+
'data-checked': node.checked,
|
|
657
|
+
...node.data?.hProperties,
|
|
658
|
+
},
|
|
659
|
+
this.mapMdastChildren(node),
|
|
297
660
|
)
|
|
298
661
|
}
|
|
299
|
-
return (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
662
|
+
return this.createElement(
|
|
663
|
+
this.c.li,
|
|
664
|
+
node.data?.hProperties,
|
|
665
|
+
this.mapMdastChildren(node),
|
|
303
666
|
)
|
|
304
667
|
}
|
|
305
668
|
case 'text': {
|
|
@@ -307,10 +670,10 @@ export class MdastToJsx {
|
|
|
307
670
|
return []
|
|
308
671
|
}
|
|
309
672
|
if (node.data?.hProperties) {
|
|
310
|
-
return (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
673
|
+
return this.createElement(
|
|
674
|
+
this.c.span,
|
|
675
|
+
node.data.hProperties,
|
|
676
|
+
node.value,
|
|
314
677
|
)
|
|
315
678
|
}
|
|
316
679
|
return node.value
|
|
@@ -319,92 +682,95 @@ export class MdastToJsx {
|
|
|
319
682
|
const src = node.url || ''
|
|
320
683
|
const alt = node.alt || ''
|
|
321
684
|
const title = node.title || ''
|
|
322
|
-
return (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
/>
|
|
329
|
-
)
|
|
685
|
+
return this.createElement(this.c.img, {
|
|
686
|
+
src,
|
|
687
|
+
alt,
|
|
688
|
+
title,
|
|
689
|
+
...node.data?.hProperties,
|
|
690
|
+
})
|
|
330
691
|
}
|
|
331
692
|
case 'link': {
|
|
332
693
|
const href = node.url || ''
|
|
333
694
|
const title = node.title || ''
|
|
334
|
-
return (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
695
|
+
return this.createElement(
|
|
696
|
+
this.c.a,
|
|
697
|
+
{ href, title, ...node.data?.hProperties },
|
|
698
|
+
this.mapMdastChildren(node),
|
|
338
699
|
)
|
|
339
700
|
}
|
|
340
701
|
case 'strong': {
|
|
341
|
-
return (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
702
|
+
return this.createElement(
|
|
703
|
+
this.c.strong,
|
|
704
|
+
node.data?.hProperties,
|
|
705
|
+
this.mapMdastChildren(node),
|
|
345
706
|
)
|
|
346
707
|
}
|
|
347
708
|
case 'emphasis': {
|
|
348
|
-
return (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
709
|
+
return this.createElement(
|
|
710
|
+
this.c.em,
|
|
711
|
+
node.data?.hProperties,
|
|
712
|
+
this.mapMdastChildren(node),
|
|
352
713
|
)
|
|
353
714
|
}
|
|
354
715
|
case 'delete': {
|
|
355
|
-
return (
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
716
|
+
return this.createElement(
|
|
717
|
+
this.c.del,
|
|
718
|
+
node.data?.hProperties,
|
|
719
|
+
this.mapMdastChildren(node),
|
|
359
720
|
)
|
|
360
721
|
}
|
|
361
722
|
case 'inlineCode': {
|
|
362
723
|
if (!node.value) {
|
|
363
724
|
return []
|
|
364
725
|
}
|
|
365
|
-
return (
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
726
|
+
return this.createElement(
|
|
727
|
+
this.c.code,
|
|
728
|
+
node.data?.hProperties,
|
|
729
|
+
node.value,
|
|
369
730
|
)
|
|
370
731
|
}
|
|
371
732
|
case 'break': {
|
|
372
|
-
return
|
|
733
|
+
return this.createElement(this.c.br, node.data?.hProperties)
|
|
373
734
|
}
|
|
374
735
|
case 'root': {
|
|
375
736
|
if (node.data?.hProperties) {
|
|
376
|
-
return (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
737
|
+
return this.createElement(
|
|
738
|
+
this.c.div,
|
|
739
|
+
node.data.hProperties,
|
|
740
|
+
this.mapMdastChildren(node),
|
|
380
741
|
)
|
|
381
742
|
}
|
|
382
|
-
return
|
|
743
|
+
return this.createElement(
|
|
744
|
+
Fragment,
|
|
745
|
+
null,
|
|
746
|
+
this.mapMdastChildren(node),
|
|
747
|
+
)
|
|
383
748
|
}
|
|
384
749
|
case 'table': {
|
|
385
750
|
const [head, ...body] = React.Children.toArray(
|
|
386
751
|
this.mapMdastChildren(node),
|
|
387
752
|
)
|
|
388
|
-
return (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
753
|
+
return this.createElement(
|
|
754
|
+
this.c.table,
|
|
755
|
+
node.data?.hProperties,
|
|
756
|
+
head && this.createElement(this.c.thead, null, head),
|
|
757
|
+
!!body?.length &&
|
|
758
|
+
this.createElement(this.c.tbody, null, body),
|
|
393
759
|
)
|
|
394
760
|
}
|
|
395
761
|
case 'tableRow': {
|
|
396
|
-
return (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
762
|
+
return this.createElement(
|
|
763
|
+
this.c.tr,
|
|
764
|
+
{ className: '', ...node.data?.hProperties },
|
|
765
|
+
this.mapMdastChildren(node),
|
|
400
766
|
)
|
|
401
767
|
}
|
|
402
768
|
case 'tableCell': {
|
|
403
769
|
let content = this.mapMdastChildren(node)
|
|
404
|
-
return (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
770
|
+
return this.createElement(
|
|
771
|
+
this.c.td,
|
|
772
|
+
{ className: '', ...node.data?.hProperties },
|
|
773
|
+
content,
|
|
408
774
|
)
|
|
409
775
|
}
|
|
410
776
|
case 'definition': {
|
|
@@ -412,19 +778,21 @@ export class MdastToJsx {
|
|
|
412
778
|
}
|
|
413
779
|
case 'linkReference': {
|
|
414
780
|
let href = ''
|
|
781
|
+
let title = ''
|
|
415
782
|
mdastBfs(this.mdast, (child: any) => {
|
|
416
783
|
if (
|
|
417
784
|
child.type === 'definition' &&
|
|
418
785
|
child.identifier === node.identifier
|
|
419
786
|
) {
|
|
420
|
-
href = child.url
|
|
787
|
+
href = child.url || ''
|
|
788
|
+
title = child.title || ''
|
|
421
789
|
}
|
|
422
790
|
})
|
|
423
791
|
|
|
424
|
-
return (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
792
|
+
return this.createElement(
|
|
793
|
+
this.c.a,
|
|
794
|
+
{ href, title, ...node.data?.hProperties },
|
|
795
|
+
this.mapMdastChildren(node),
|
|
428
796
|
)
|
|
429
797
|
}
|
|
430
798
|
case 'footnoteReference': {
|
|
@@ -442,14 +810,14 @@ export class MdastToJsx {
|
|
|
442
810
|
return []
|
|
443
811
|
}
|
|
444
812
|
|
|
445
|
-
return (
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
813
|
+
return this.createElement(
|
|
814
|
+
Suspense,
|
|
815
|
+
{ fallback: null },
|
|
816
|
+
this.createElement(HtmlToJsxConverter, {
|
|
817
|
+
htmlText: text,
|
|
818
|
+
instance: this,
|
|
819
|
+
node,
|
|
820
|
+
}),
|
|
453
821
|
)
|
|
454
822
|
}
|
|
455
823
|
case 'imageReference': {
|
|
@@ -471,77 +839,6 @@ export class MdastToJsx {
|
|
|
471
839
|
}
|
|
472
840
|
}
|
|
473
841
|
|
|
474
|
-
export function getJsxAttrs(
|
|
475
|
-
node: MdxJsxFlowElement | MdxJsxTextElement,
|
|
476
|
-
onError: (err: { message: string }) => void = console.error,
|
|
477
|
-
) {
|
|
478
|
-
let attrsList = node.attributes
|
|
479
|
-
.map((attr) => {
|
|
480
|
-
if (attr.type === 'mdxJsxExpressionAttribute') {
|
|
481
|
-
onError({
|
|
482
|
-
message: `Expressions in jsx props are not supported (${attr.value
|
|
483
|
-
.replace(/\n+/g, ' ')
|
|
484
|
-
.replace(/ +/g, ' ')})`,
|
|
485
|
-
})
|
|
486
|
-
return
|
|
487
|
-
}
|
|
488
|
-
if (attr.type !== 'mdxJsxAttribute') {
|
|
489
|
-
throw new Error(`non mdxJsxAttribute is not supported: ${attr}`)
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const v = attr.value
|
|
493
|
-
if (typeof v === 'string' || typeof v === 'number') {
|
|
494
|
-
return [attr.name, v]
|
|
495
|
-
}
|
|
496
|
-
if (v === null) {
|
|
497
|
-
return [attr.name, true]
|
|
498
|
-
}
|
|
499
|
-
if (v?.type === 'mdxJsxAttributeValueExpression') {
|
|
500
|
-
if (v.value === 'true') {
|
|
501
|
-
return [attr.name, true]
|
|
502
|
-
}
|
|
503
|
-
if (v.value === 'false') {
|
|
504
|
-
return [attr.name, false]
|
|
505
|
-
}
|
|
506
|
-
if (v.value === 'null') {
|
|
507
|
-
return [attr.name, null]
|
|
508
|
-
}
|
|
509
|
-
if (v.value === 'undefined') {
|
|
510
|
-
return [attr.name, undefined]
|
|
511
|
-
}
|
|
512
|
-
let quote = ['"', "'", '`'].find(
|
|
513
|
-
(q) => v.value.startsWith(q) && v.value.endsWith(q),
|
|
514
|
-
)
|
|
515
|
-
if (quote) {
|
|
516
|
-
let value = v.value
|
|
517
|
-
if (quote !== '"') {
|
|
518
|
-
value = v.value.replace(new RegExp(quote, 'g'), '"')
|
|
519
|
-
}
|
|
520
|
-
return [attr.name, JSON.parse(value)]
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const number = Number(v.value)
|
|
524
|
-
if (!isNaN(number)) {
|
|
525
|
-
return [attr.name, number]
|
|
526
|
-
}
|
|
527
|
-
const parsedJson = safeJsonParse(v.value)
|
|
528
|
-
if (parsedJson) {
|
|
529
|
-
return [attr.name, parsedJson]
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
onError({
|
|
533
|
-
message: `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
|
|
534
|
-
})
|
|
535
|
-
} else {
|
|
536
|
-
console.log('unhandled attr', { attr }, attr.type)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return
|
|
540
|
-
})
|
|
541
|
-
.filter(isTruthy) as [string, any][]
|
|
542
|
-
return attrsList
|
|
543
|
-
}
|
|
544
|
-
|
|
545
842
|
function isTruthy<T>(val: T | undefined | null | false): val is T {
|
|
546
843
|
return Boolean(val)
|
|
547
844
|
}
|