safe-mdx 1.1.0 → 1.3.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 +109 -3
- 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.js +6 -0
- package/dist/safe-mdx.bench.js.map +1 -1
- package/dist/safe-mdx.d.ts +12 -2
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +337 -103
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +717 -24
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +3 -1
- 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 +6 -0
- package/src/safe-mdx.test.tsx +777 -24
- package/src/safe-mdx.tsx +514 -131
package/src/safe-mdx.tsx
CHANGED
|
@@ -3,8 +3,17 @@ import React, { Suspense, cloneElement } from 'react'
|
|
|
3
3
|
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
4
4
|
import type { Node, Parent, Root, RootContent } from 'mdast'
|
|
5
5
|
import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'
|
|
6
|
+
import type {
|
|
7
|
+
JSXElement,
|
|
8
|
+
JSXAttribute,
|
|
9
|
+
JSXText,
|
|
10
|
+
JSXExpressionContainer,
|
|
11
|
+
} from 'estree-jsx'
|
|
12
|
+
import Evaluate from 'eval-estree-expression'
|
|
6
13
|
|
|
7
14
|
import { Fragment, ReactNode } from 'react'
|
|
15
|
+
import { DynamicEsmComponent } from './dynamic-esm-component.js'
|
|
16
|
+
import { parseEsmImports, extractComponentInfo } from './esm-parser.js'
|
|
8
17
|
|
|
9
18
|
const HtmlToJsxConverter = React.lazy(() =>
|
|
10
19
|
import('./HtmlToJsxConverter.js').then((module) => ({
|
|
@@ -49,6 +58,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
49
58
|
renderNode,
|
|
50
59
|
componentPropsSchema,
|
|
51
60
|
createElement,
|
|
61
|
+
allowClientEsmImports = false,
|
|
62
|
+
addMarkdownLineNumbers = false,
|
|
52
63
|
}: {
|
|
53
64
|
components?: ComponentsMap
|
|
54
65
|
markdown?: string
|
|
@@ -56,6 +67,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
56
67
|
renderNode?: RenderNode
|
|
57
68
|
componentPropsSchema?: ComponentPropsSchema
|
|
58
69
|
createElement?: CreateElementFunction
|
|
70
|
+
allowClientEsmImports?: boolean
|
|
71
|
+
addMarkdownLineNumbers?: boolean
|
|
59
72
|
}) {
|
|
60
73
|
const visitor = new MdastToJsx({
|
|
61
74
|
markdown,
|
|
@@ -64,6 +77,8 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
64
77
|
renderNode,
|
|
65
78
|
componentPropsSchema,
|
|
66
79
|
createElement,
|
|
80
|
+
allowClientEsmImports,
|
|
81
|
+
addMarkdownLineNumbers,
|
|
67
82
|
})
|
|
68
83
|
const result = visitor.run()
|
|
69
84
|
return result
|
|
@@ -78,6 +93,9 @@ export class MdastToJsx {
|
|
|
78
93
|
renderNode?: RenderNode
|
|
79
94
|
componentPropsSchema?: ComponentPropsSchema
|
|
80
95
|
createElement: CreateElementFunction
|
|
96
|
+
esmImports: Map<string, string> = new Map()
|
|
97
|
+
allowClientEsmImports: boolean
|
|
98
|
+
addMarkdownLineNumbers: boolean
|
|
81
99
|
|
|
82
100
|
constructor({
|
|
83
101
|
markdown: code = '',
|
|
@@ -86,6 +104,8 @@ export class MdastToJsx {
|
|
|
86
104
|
renderNode,
|
|
87
105
|
componentPropsSchema,
|
|
88
106
|
createElement = React.createElement,
|
|
107
|
+
allowClientEsmImports = false,
|
|
108
|
+
addMarkdownLineNumbers = false,
|
|
89
109
|
}: {
|
|
90
110
|
markdown?: string
|
|
91
111
|
mdast: MyRootContent
|
|
@@ -96,6 +116,8 @@ export class MdastToJsx {
|
|
|
96
116
|
) => ReactNode | undefined
|
|
97
117
|
componentPropsSchema?: ComponentPropsSchema
|
|
98
118
|
createElement?: CreateElementFunction
|
|
119
|
+
allowClientEsmImports?: boolean
|
|
120
|
+
addMarkdownLineNumbers?: boolean
|
|
99
121
|
}) {
|
|
100
122
|
this.str = code
|
|
101
123
|
|
|
@@ -107,6 +129,10 @@ export class MdastToJsx {
|
|
|
107
129
|
|
|
108
130
|
this.createElement = createElement
|
|
109
131
|
|
|
132
|
+
this.allowClientEsmImports = allowClientEsmImports
|
|
133
|
+
|
|
134
|
+
this.addMarkdownLineNumbers = addMarkdownLineNumbers
|
|
135
|
+
|
|
110
136
|
this.c = {
|
|
111
137
|
...Object.fromEntries(
|
|
112
138
|
nativeTags.map((tag) => {
|
|
@@ -117,12 +143,33 @@ export class MdastToJsx {
|
|
|
117
143
|
}
|
|
118
144
|
}
|
|
119
145
|
|
|
146
|
+
addLineNumberToProps(
|
|
147
|
+
props: Record<string, any> | undefined,
|
|
148
|
+
node: MyRootContent,
|
|
149
|
+
): Record<string, any> {
|
|
150
|
+
if (!this.addMarkdownLineNumbers) {
|
|
151
|
+
return props || {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const lineNumber = node.position?.start?.line
|
|
155
|
+
if (lineNumber) {
|
|
156
|
+
return {
|
|
157
|
+
...props,
|
|
158
|
+
'data-markdown-line': lineNumber,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return props || {}
|
|
162
|
+
}
|
|
163
|
+
|
|
120
164
|
validateComponentProps(
|
|
121
165
|
componentName: string,
|
|
122
166
|
props: Record<string, any>,
|
|
123
|
-
line?: number
|
|
167
|
+
line?: number,
|
|
124
168
|
): void {
|
|
125
|
-
if (
|
|
169
|
+
if (
|
|
170
|
+
!this.componentPropsSchema ||
|
|
171
|
+
!this.componentPropsSchema[componentName]
|
|
172
|
+
) {
|
|
126
173
|
return
|
|
127
174
|
}
|
|
128
175
|
|
|
@@ -192,26 +239,58 @@ export class MdastToJsx {
|
|
|
192
239
|
return []
|
|
193
240
|
}
|
|
194
241
|
|
|
195
|
-
|
|
242
|
+
// Check if this is an ESM imported component (only if allowed)
|
|
243
|
+
const esmImportInfo = this.allowClientEsmImports
|
|
244
|
+
? this.esmImports.get(node.name)
|
|
245
|
+
: null
|
|
246
|
+
let Component
|
|
196
247
|
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
248
|
+
if (esmImportInfo) {
|
|
249
|
+
// Handle ESM imported component
|
|
250
|
+
const { importUrl, componentName } =
|
|
251
|
+
extractComponentInfo(esmImportInfo)
|
|
252
|
+
|
|
253
|
+
Component = DynamicEsmComponent
|
|
254
|
+
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
255
|
+
this.errors.push(err)
|
|
201
256
|
})
|
|
202
|
-
|
|
257
|
+
let attrs = Object.fromEntries(attrsList)
|
|
258
|
+
|
|
259
|
+
return this.createElement(
|
|
260
|
+
Component,
|
|
261
|
+
{ ...attrs, importUrl, componentName },
|
|
262
|
+
this.mapJsxChildren(node),
|
|
263
|
+
)
|
|
264
|
+
} else {
|
|
265
|
+
Component = accessWithDot(this.c, node.name)
|
|
266
|
+
|
|
267
|
+
if (!Component) {
|
|
268
|
+
this.errors.push({
|
|
269
|
+
message: `Unsupported jsx component ${node.name}`,
|
|
270
|
+
line: node.position?.start?.line,
|
|
271
|
+
})
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
203
274
|
}
|
|
204
275
|
|
|
205
|
-
let attrsList = getJsxAttrs(node, (err) => {
|
|
276
|
+
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
206
277
|
this.errors.push(err)
|
|
207
278
|
})
|
|
208
279
|
|
|
209
280
|
let attrs = Object.fromEntries(attrsList)
|
|
210
281
|
|
|
211
282
|
// Validate component props with schema if available
|
|
212
|
-
this.validateComponentProps(
|
|
283
|
+
this.validateComponentProps(
|
|
284
|
+
node.name,
|
|
285
|
+
attrs,
|
|
286
|
+
node.position?.start?.line,
|
|
287
|
+
)
|
|
213
288
|
|
|
214
|
-
return this.createElement(
|
|
289
|
+
return this.createElement(
|
|
290
|
+
Component,
|
|
291
|
+
attrs,
|
|
292
|
+
this.mapJsxChildren(node),
|
|
293
|
+
)
|
|
215
294
|
}
|
|
216
295
|
default: {
|
|
217
296
|
return this.mdastTransformer(node)
|
|
@@ -219,6 +298,263 @@ export class MdastToJsx {
|
|
|
219
298
|
}
|
|
220
299
|
}
|
|
221
300
|
|
|
301
|
+
transformJsxElement(
|
|
302
|
+
jsxElement: JSXElement,
|
|
303
|
+
onError?: (err: SafeMdxError) => void,
|
|
304
|
+
line?: number,
|
|
305
|
+
): ReactNode {
|
|
306
|
+
try {
|
|
307
|
+
// Handle JSX opening element
|
|
308
|
+
if (jsxElement.openingElement) {
|
|
309
|
+
const tagName =
|
|
310
|
+
jsxElement.openingElement.name?.type === 'JSXIdentifier'
|
|
311
|
+
? jsxElement.openingElement.name.name
|
|
312
|
+
: null
|
|
313
|
+
if (!tagName) {
|
|
314
|
+
onError?.({
|
|
315
|
+
message: 'JSX element missing component name',
|
|
316
|
+
line: line,
|
|
317
|
+
})
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check if this is an ESM imported component (only if allowed)
|
|
322
|
+
const esmImportInfo = this.allowClientEsmImports
|
|
323
|
+
? this.esmImports.get(tagName)
|
|
324
|
+
: null
|
|
325
|
+
let Component
|
|
326
|
+
|
|
327
|
+
if (esmImportInfo) {
|
|
328
|
+
// Handle ESM imported component
|
|
329
|
+
const { importUrl, componentName } =
|
|
330
|
+
extractComponentInfo(esmImportInfo)
|
|
331
|
+
Component = DynamicEsmComponent
|
|
332
|
+
} else {
|
|
333
|
+
// Get the component from the regular component map
|
|
334
|
+
Component = accessWithDot(this.c, tagName)
|
|
335
|
+
if (!Component) {
|
|
336
|
+
onError?.({
|
|
337
|
+
message: `Unsupported jsx component ${tagName} in attribute`,
|
|
338
|
+
line: line,
|
|
339
|
+
})
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Extract attributes
|
|
345
|
+
const props: Record<string, any> = {}
|
|
346
|
+
if (jsxElement.openingElement.attributes) {
|
|
347
|
+
for (const attr of jsxElement.openingElement.attributes) {
|
|
348
|
+
if (
|
|
349
|
+
attr.type === 'JSXAttribute' &&
|
|
350
|
+
attr.name?.type === 'JSXIdentifier' &&
|
|
351
|
+
attr.name.name
|
|
352
|
+
) {
|
|
353
|
+
if (attr.value) {
|
|
354
|
+
if (attr.value.type === 'Literal') {
|
|
355
|
+
props[attr.name.name] = attr.value.value
|
|
356
|
+
} else if (
|
|
357
|
+
attr.value.type === 'JSXExpressionContainer'
|
|
358
|
+
) {
|
|
359
|
+
if (
|
|
360
|
+
attr.value.expression.type === 'Literal'
|
|
361
|
+
) {
|
|
362
|
+
props[attr.name.name] =
|
|
363
|
+
attr.value.expression.value
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
props[attr.name.name] = true
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Extract children
|
|
374
|
+
const children: ReactNode[] = []
|
|
375
|
+
if (jsxElement.children) {
|
|
376
|
+
for (const child of jsxElement.children) {
|
|
377
|
+
if (child.type === 'JSXText') {
|
|
378
|
+
children.push(child.value)
|
|
379
|
+
} else if (child.type === 'JSXElement') {
|
|
380
|
+
const childElement = this.transformJsxElement(child)
|
|
381
|
+
if (childElement) {
|
|
382
|
+
children.push(childElement)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Handle ESM imported components by adding required props
|
|
389
|
+
if (esmImportInfo) {
|
|
390
|
+
const { importUrl, componentName } =
|
|
391
|
+
extractComponentInfo(esmImportInfo)
|
|
392
|
+
return this.createElement(
|
|
393
|
+
Component,
|
|
394
|
+
{ ...props, importUrl, componentName },
|
|
395
|
+
...children,
|
|
396
|
+
)
|
|
397
|
+
} else {
|
|
398
|
+
return this.createElement(Component, props, ...children)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
// Return null if transformation fails
|
|
403
|
+
onError?.({
|
|
404
|
+
message: `Failed to transform JSX element: ${
|
|
405
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
406
|
+
}`,
|
|
407
|
+
line: line,
|
|
408
|
+
})
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
411
|
+
return null
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
getJsxAttrs(
|
|
415
|
+
node: MdxJsxFlowElement | MdxJsxTextElement,
|
|
416
|
+
onError: (err: SafeMdxError) => void = console.error,
|
|
417
|
+
) {
|
|
418
|
+
let attrsList: [string, any][] = []
|
|
419
|
+
|
|
420
|
+
for (const attr of node.attributes) {
|
|
421
|
+
if (attr.type === 'mdxJsxExpressionAttribute') {
|
|
422
|
+
// Handle spread expressions like {...{key: '1'}}
|
|
423
|
+
if (attr.data?.estree) {
|
|
424
|
+
try {
|
|
425
|
+
const program = attr.data.estree
|
|
426
|
+
if (
|
|
427
|
+
program.body?.length > 0 &&
|
|
428
|
+
program.body[0].type === 'ExpressionStatement'
|
|
429
|
+
) {
|
|
430
|
+
const expression = program.body[0].expression
|
|
431
|
+
try {
|
|
432
|
+
const result =
|
|
433
|
+
Evaluate.evaluate.sync(expression)
|
|
434
|
+
|
|
435
|
+
// Handle spread syntax - merge the evaluated object
|
|
436
|
+
if (
|
|
437
|
+
typeof result === 'object' &&
|
|
438
|
+
result != null
|
|
439
|
+
) {
|
|
440
|
+
const entries = Object.entries(result)
|
|
441
|
+
attrsList.push(...entries)
|
|
442
|
+
}
|
|
443
|
+
} catch (error) {
|
|
444
|
+
onError({
|
|
445
|
+
message: `Failed to evaluate expression attribute: ${attr.value
|
|
446
|
+
.replace(/\n+/g, ' ')
|
|
447
|
+
.replace(/ +/g, ' ')}`,
|
|
448
|
+
line: attr.position?.start?.line,
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
onError({
|
|
454
|
+
message: `Failed to evaluate expression attribute: ${attr.value
|
|
455
|
+
.replace(/\n+/g, ' ')
|
|
456
|
+
.replace(/ +/g, ' ')}`,
|
|
457
|
+
line: attr.position?.start?.line,
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
onError({
|
|
462
|
+
message: `Expressions in jsx props are not supported (${attr.value
|
|
463
|
+
.replace(/\n+/g, ' ')
|
|
464
|
+
.replace(/ +/g, ' ')})`,
|
|
465
|
+
line: attr.position?.start?.line,
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
continue
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (attr.type !== 'mdxJsxAttribute') {
|
|
472
|
+
onError({
|
|
473
|
+
message: `non mdxJsxAttribute attribute is not supported: ${attr}`,
|
|
474
|
+
line: node.position?.start?.line,
|
|
475
|
+
})
|
|
476
|
+
continue
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const v = attr.value
|
|
480
|
+
if (typeof v === 'string' || typeof v === 'number') {
|
|
481
|
+
attrsList.push([attr.name, v])
|
|
482
|
+
continue
|
|
483
|
+
}
|
|
484
|
+
if (v === null) {
|
|
485
|
+
attrsList.push([attr.name, true])
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
if (v?.type === 'mdxJsxAttributeValueExpression') {
|
|
489
|
+
// Manual parsing fallback for simple values
|
|
490
|
+
if (v.value === 'true') {
|
|
491
|
+
attrsList.push([attr.name, true])
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
if (v.value === 'false') {
|
|
495
|
+
attrsList.push([attr.name, false])
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
498
|
+
if (v.value === 'null') {
|
|
499
|
+
attrsList.push([attr.name, null])
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
if (v.value === 'undefined') {
|
|
503
|
+
attrsList.push([attr.name, undefined])
|
|
504
|
+
continue
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (v.data?.estree) {
|
|
508
|
+
try {
|
|
509
|
+
// Extract the expression from the Program body
|
|
510
|
+
const program = v.data.estree
|
|
511
|
+
if (
|
|
512
|
+
program.body?.length > 0 &&
|
|
513
|
+
program.body[0].type === 'ExpressionStatement'
|
|
514
|
+
) {
|
|
515
|
+
const expression = program.body[0].expression
|
|
516
|
+
|
|
517
|
+
// Check if this is a JSX element
|
|
518
|
+
if (expression.type === 'JSXElement') {
|
|
519
|
+
// Transform JSX element to React element
|
|
520
|
+
const jsxElement = this.transformJsxElement(
|
|
521
|
+
expression,
|
|
522
|
+
onError,
|
|
523
|
+
attr.position?.start?.line,
|
|
524
|
+
)
|
|
525
|
+
if (jsxElement) {
|
|
526
|
+
attrsList.push([attr.name, jsxElement])
|
|
527
|
+
continue
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
// Evaluate the expression synchronously
|
|
533
|
+
const result =
|
|
534
|
+
Evaluate.evaluate.sync(expression)
|
|
535
|
+
attrsList.push([attr.name, result])
|
|
536
|
+
continue
|
|
537
|
+
} catch (error) {
|
|
538
|
+
onError({
|
|
539
|
+
message: `Failed to evaluate expression attribute: ${attr.name}={${v.value}}`,
|
|
540
|
+
line: attr.position?.start?.line,
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} catch (error) {
|
|
545
|
+
// Fall back to the original manual parsing for backwards compatibility
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
onError({
|
|
550
|
+
message: `Expressions in jsx prop not evaluated: (${attr.name}={${v.value}})`,
|
|
551
|
+
line: attr.position?.start?.line,
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return attrsList
|
|
556
|
+
}
|
|
557
|
+
|
|
222
558
|
run() {
|
|
223
559
|
const res = this.mdastTransformer(this.mdast) as ReactNode
|
|
224
560
|
if (Array.isArray(res) && res.length === 1) {
|
|
@@ -245,10 +581,15 @@ export class MdastToJsx {
|
|
|
245
581
|
|
|
246
582
|
switch (node.type) {
|
|
247
583
|
case 'mdxjsEsm': {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
584
|
+
// Parse ESM imports and merge into our imports map (only if allowed)
|
|
585
|
+
if (this.allowClientEsmImports) {
|
|
586
|
+
const parsedImports = parseEsmImports(node, (err) =>
|
|
587
|
+
this.errors.push(err),
|
|
588
|
+
)
|
|
589
|
+
parsedImports.forEach((value, key) => {
|
|
590
|
+
this.esmImports.set(key, value)
|
|
591
|
+
})
|
|
592
|
+
}
|
|
252
593
|
return []
|
|
253
594
|
}
|
|
254
595
|
case 'mdxJsxTextElement':
|
|
@@ -275,6 +616,37 @@ export class MdastToJsx {
|
|
|
275
616
|
if (!node.value) {
|
|
276
617
|
return []
|
|
277
618
|
}
|
|
619
|
+
|
|
620
|
+
// Check if we have an estree AST
|
|
621
|
+
if (node.data?.estree) {
|
|
622
|
+
try {
|
|
623
|
+
// Extract the expression from the Program body
|
|
624
|
+
const program = node.data.estree
|
|
625
|
+
if (
|
|
626
|
+
program.body?.length > 0 &&
|
|
627
|
+
program.body[0].type === 'ExpressionStatement'
|
|
628
|
+
) {
|
|
629
|
+
const expression = program.body[0].expression
|
|
630
|
+
try {
|
|
631
|
+
// Evaluate the expression synchronously
|
|
632
|
+
const result =
|
|
633
|
+
Evaluate.evaluate.sync(expression)
|
|
634
|
+
return result
|
|
635
|
+
} catch (error) {
|
|
636
|
+
this.errors.push({
|
|
637
|
+
message: `Failed to evaluate expression: ${node.value}`,
|
|
638
|
+
line: node.position?.start?.line,
|
|
639
|
+
})
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
this.errors.push({
|
|
644
|
+
message: `Failed to evaluate expression: ${node.value}`,
|
|
645
|
+
line: node.position?.start?.line,
|
|
646
|
+
})
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
278
650
|
return []
|
|
279
651
|
}
|
|
280
652
|
case 'yaml': {
|
|
@@ -287,16 +659,31 @@ export class MdastToJsx {
|
|
|
287
659
|
const level = node.depth
|
|
288
660
|
const Tag = this.c[`h${level}`] ?? `h${level}`
|
|
289
661
|
|
|
290
|
-
return this.createElement(
|
|
662
|
+
return this.createElement(
|
|
663
|
+
Tag,
|
|
664
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
665
|
+
this.mapMdastChildren(node),
|
|
666
|
+
)
|
|
291
667
|
}
|
|
292
668
|
case 'paragraph': {
|
|
293
|
-
return this.createElement(
|
|
669
|
+
return this.createElement(
|
|
670
|
+
this.c.p,
|
|
671
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
672
|
+
this.mapMdastChildren(node),
|
|
673
|
+
)
|
|
294
674
|
}
|
|
295
675
|
case 'blockquote': {
|
|
296
|
-
return this.createElement(
|
|
676
|
+
return this.createElement(
|
|
677
|
+
this.c.blockquote,
|
|
678
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
679
|
+
this.mapMdastChildren(node),
|
|
680
|
+
)
|
|
297
681
|
}
|
|
298
682
|
case 'thematicBreak': {
|
|
299
|
-
return this.createElement(
|
|
683
|
+
return this.createElement(
|
|
684
|
+
this.c.hr,
|
|
685
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
686
|
+
)
|
|
300
687
|
}
|
|
301
688
|
case 'code': {
|
|
302
689
|
if (!node.value) {
|
|
@@ -304,11 +691,12 @@ export class MdastToJsx {
|
|
|
304
691
|
}
|
|
305
692
|
const language = node.lang || ''
|
|
306
693
|
const code = node.value
|
|
307
|
-
const codeBlock = (className?: string) =>
|
|
308
|
-
this.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
694
|
+
const codeBlock = (className?: string) =>
|
|
695
|
+
this.createElement(
|
|
696
|
+
this.c.pre,
|
|
697
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
698
|
+
this.createElement(this.c.code, { className }, code),
|
|
699
|
+
)
|
|
312
700
|
|
|
313
701
|
if (language) {
|
|
314
702
|
return codeBlock(`language-${language}`)
|
|
@@ -320,29 +708,50 @@ export class MdastToJsx {
|
|
|
320
708
|
if (node.ordered) {
|
|
321
709
|
return this.createElement(
|
|
322
710
|
this.c.ol,
|
|
323
|
-
|
|
324
|
-
|
|
711
|
+
this.addLineNumberToProps(
|
|
712
|
+
{ start: node.start!, ...node.data?.hProperties },
|
|
713
|
+
node,
|
|
714
|
+
),
|
|
715
|
+
this.mapMdastChildren(node),
|
|
325
716
|
)
|
|
326
717
|
}
|
|
327
|
-
return this.createElement(
|
|
718
|
+
return this.createElement(
|
|
719
|
+
this.c.ul,
|
|
720
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
721
|
+
this.mapMdastChildren(node),
|
|
722
|
+
)
|
|
328
723
|
}
|
|
329
724
|
case 'listItem': {
|
|
330
725
|
// https://github.com/syntax-tree/mdast-util-gfm-task-list-item#syntax-tree
|
|
331
726
|
if (node?.checked != null) {
|
|
332
727
|
return this.createElement(
|
|
333
728
|
this.c.li,
|
|
334
|
-
|
|
335
|
-
|
|
729
|
+
this.addLineNumberToProps(
|
|
730
|
+
{
|
|
731
|
+
'data-checked': node.checked,
|
|
732
|
+
...node.data?.hProperties,
|
|
733
|
+
},
|
|
734
|
+
node,
|
|
735
|
+
),
|
|
736
|
+
this.mapMdastChildren(node),
|
|
336
737
|
)
|
|
337
738
|
}
|
|
338
|
-
return this.createElement(
|
|
739
|
+
return this.createElement(
|
|
740
|
+
this.c.li,
|
|
741
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
742
|
+
this.mapMdastChildren(node),
|
|
743
|
+
)
|
|
339
744
|
}
|
|
340
745
|
case 'text': {
|
|
341
746
|
if (!node.value) {
|
|
342
747
|
return []
|
|
343
748
|
}
|
|
344
749
|
if (node.data?.hProperties) {
|
|
345
|
-
return this.createElement(
|
|
750
|
+
return this.createElement(
|
|
751
|
+
this.c.span,
|
|
752
|
+
this.addLineNumberToProps(node.data.hProperties, node),
|
|
753
|
+
node.value,
|
|
754
|
+
)
|
|
346
755
|
}
|
|
347
756
|
return node.value
|
|
348
757
|
}
|
|
@@ -350,45 +759,81 @@ export class MdastToJsx {
|
|
|
350
759
|
const src = node.url || ''
|
|
351
760
|
const alt = node.alt || ''
|
|
352
761
|
const title = node.title || ''
|
|
353
|
-
return this.createElement(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
762
|
+
return this.createElement(
|
|
763
|
+
this.c.img,
|
|
764
|
+
this.addLineNumberToProps(
|
|
765
|
+
{
|
|
766
|
+
src,
|
|
767
|
+
alt,
|
|
768
|
+
title,
|
|
769
|
+
...node.data?.hProperties,
|
|
770
|
+
},
|
|
771
|
+
node,
|
|
772
|
+
),
|
|
773
|
+
)
|
|
359
774
|
}
|
|
360
775
|
case 'link': {
|
|
361
776
|
const href = node.url || ''
|
|
362
777
|
const title = node.title || ''
|
|
363
778
|
return this.createElement(
|
|
364
779
|
this.c.a,
|
|
365
|
-
|
|
366
|
-
|
|
780
|
+
this.addLineNumberToProps(
|
|
781
|
+
{ href, title, ...node.data?.hProperties },
|
|
782
|
+
node,
|
|
783
|
+
),
|
|
784
|
+
this.mapMdastChildren(node),
|
|
367
785
|
)
|
|
368
786
|
}
|
|
369
787
|
case 'strong': {
|
|
370
|
-
return this.createElement(
|
|
788
|
+
return this.createElement(
|
|
789
|
+
this.c.strong,
|
|
790
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
791
|
+
this.mapMdastChildren(node),
|
|
792
|
+
)
|
|
371
793
|
}
|
|
372
794
|
case 'emphasis': {
|
|
373
|
-
return this.createElement(
|
|
795
|
+
return this.createElement(
|
|
796
|
+
this.c.em,
|
|
797
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
798
|
+
this.mapMdastChildren(node),
|
|
799
|
+
)
|
|
374
800
|
}
|
|
375
801
|
case 'delete': {
|
|
376
|
-
return this.createElement(
|
|
802
|
+
return this.createElement(
|
|
803
|
+
this.c.del,
|
|
804
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
805
|
+
this.mapMdastChildren(node),
|
|
806
|
+
)
|
|
377
807
|
}
|
|
378
808
|
case 'inlineCode': {
|
|
379
809
|
if (!node.value) {
|
|
380
810
|
return []
|
|
381
811
|
}
|
|
382
|
-
return this.createElement(
|
|
812
|
+
return this.createElement(
|
|
813
|
+
this.c.code,
|
|
814
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
815
|
+
node.value,
|
|
816
|
+
)
|
|
383
817
|
}
|
|
384
818
|
case 'break': {
|
|
385
|
-
return this.createElement(
|
|
819
|
+
return this.createElement(
|
|
820
|
+
this.c.br,
|
|
821
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
822
|
+
)
|
|
386
823
|
}
|
|
387
824
|
case 'root': {
|
|
388
825
|
if (node.data?.hProperties) {
|
|
389
|
-
return this.createElement(
|
|
826
|
+
return this.createElement(
|
|
827
|
+
this.c.div,
|
|
828
|
+
this.addLineNumberToProps(node.data.hProperties, node),
|
|
829
|
+
this.mapMdastChildren(node),
|
|
830
|
+
)
|
|
390
831
|
}
|
|
391
|
-
return this.createElement(
|
|
832
|
+
return this.createElement(
|
|
833
|
+
Fragment,
|
|
834
|
+
null,
|
|
835
|
+
this.mapMdastChildren(node),
|
|
836
|
+
)
|
|
392
837
|
}
|
|
393
838
|
case 'table': {
|
|
394
839
|
const [head, ...body] = React.Children.toArray(
|
|
@@ -396,24 +841,31 @@ export class MdastToJsx {
|
|
|
396
841
|
)
|
|
397
842
|
return this.createElement(
|
|
398
843
|
this.c.table,
|
|
399
|
-
node.data?.hProperties,
|
|
844
|
+
this.addLineNumberToProps(node.data?.hProperties, node),
|
|
400
845
|
head && this.createElement(this.c.thead, null, head),
|
|
401
|
-
!!body?.length &&
|
|
846
|
+
!!body?.length &&
|
|
847
|
+
this.createElement(this.c.tbody, null, body),
|
|
402
848
|
)
|
|
403
849
|
}
|
|
404
850
|
case 'tableRow': {
|
|
405
851
|
return this.createElement(
|
|
406
852
|
this.c.tr,
|
|
407
|
-
|
|
408
|
-
|
|
853
|
+
this.addLineNumberToProps(
|
|
854
|
+
{ className: '', ...node.data?.hProperties },
|
|
855
|
+
node,
|
|
856
|
+
),
|
|
857
|
+
this.mapMdastChildren(node),
|
|
409
858
|
)
|
|
410
859
|
}
|
|
411
860
|
case 'tableCell': {
|
|
412
861
|
let content = this.mapMdastChildren(node)
|
|
413
862
|
return this.createElement(
|
|
414
863
|
this.c.td,
|
|
415
|
-
|
|
416
|
-
|
|
864
|
+
this.addLineNumberToProps(
|
|
865
|
+
{ className: '', ...node.data?.hProperties },
|
|
866
|
+
node,
|
|
867
|
+
),
|
|
868
|
+
content,
|
|
417
869
|
)
|
|
418
870
|
}
|
|
419
871
|
case 'definition': {
|
|
@@ -421,19 +873,24 @@ export class MdastToJsx {
|
|
|
421
873
|
}
|
|
422
874
|
case 'linkReference': {
|
|
423
875
|
let href = ''
|
|
876
|
+
let title = ''
|
|
424
877
|
mdastBfs(this.mdast, (child: any) => {
|
|
425
878
|
if (
|
|
426
879
|
child.type === 'definition' &&
|
|
427
880
|
child.identifier === node.identifier
|
|
428
881
|
) {
|
|
429
|
-
href = child.url
|
|
882
|
+
href = child.url || ''
|
|
883
|
+
title = child.title || ''
|
|
430
884
|
}
|
|
431
885
|
})
|
|
432
886
|
|
|
433
887
|
return this.createElement(
|
|
434
888
|
this.c.a,
|
|
435
|
-
|
|
436
|
-
|
|
889
|
+
this.addLineNumberToProps(
|
|
890
|
+
{ href, title, ...node.data?.hProperties },
|
|
891
|
+
node,
|
|
892
|
+
),
|
|
893
|
+
this.mapMdastChildren(node),
|
|
437
894
|
)
|
|
438
895
|
}
|
|
439
896
|
case 'footnoteReference': {
|
|
@@ -457,8 +914,8 @@ export class MdastToJsx {
|
|
|
457
914
|
this.createElement(HtmlToJsxConverter, {
|
|
458
915
|
htmlText: text,
|
|
459
916
|
instance: this,
|
|
460
|
-
node
|
|
461
|
-
})
|
|
917
|
+
node,
|
|
918
|
+
}),
|
|
462
919
|
)
|
|
463
920
|
}
|
|
464
921
|
case 'imageReference': {
|
|
@@ -480,80 +937,6 @@ export class MdastToJsx {
|
|
|
480
937
|
}
|
|
481
938
|
}
|
|
482
939
|
|
|
483
|
-
export function getJsxAttrs(
|
|
484
|
-
node: MdxJsxFlowElement | MdxJsxTextElement,
|
|
485
|
-
onError: (err: SafeMdxError) => void = console.error,
|
|
486
|
-
) {
|
|
487
|
-
let attrsList = node.attributes
|
|
488
|
-
.map((attr) => {
|
|
489
|
-
if (attr.type === 'mdxJsxExpressionAttribute') {
|
|
490
|
-
|
|
491
|
-
onError({
|
|
492
|
-
message: `Expressions in jsx props are not supported (${attr.value
|
|
493
|
-
.replace(/\n+/g, ' ')
|
|
494
|
-
.replace(/ +/g, ' ')})`,
|
|
495
|
-
line: attr.position?.start?.line,
|
|
496
|
-
})
|
|
497
|
-
return
|
|
498
|
-
}
|
|
499
|
-
if (attr.type !== 'mdxJsxAttribute') {
|
|
500
|
-
throw new Error(`non mdxJsxAttribute is not supported: ${attr}`)
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const v = attr.value
|
|
504
|
-
if (typeof v === 'string' || typeof v === 'number') {
|
|
505
|
-
return [attr.name, v]
|
|
506
|
-
}
|
|
507
|
-
if (v === null) {
|
|
508
|
-
return [attr.name, true]
|
|
509
|
-
}
|
|
510
|
-
if (v?.type === 'mdxJsxAttributeValueExpression') {
|
|
511
|
-
if (v.value === 'true') {
|
|
512
|
-
return [attr.name, true]
|
|
513
|
-
}
|
|
514
|
-
if (v.value === 'false') {
|
|
515
|
-
return [attr.name, false]
|
|
516
|
-
}
|
|
517
|
-
if (v.value === 'null') {
|
|
518
|
-
return [attr.name, null]
|
|
519
|
-
}
|
|
520
|
-
if (v.value === 'undefined') {
|
|
521
|
-
return [attr.name, undefined]
|
|
522
|
-
}
|
|
523
|
-
let quote = ['"', "'", '`'].find(
|
|
524
|
-
(q) => v.value.startsWith(q) && v.value.endsWith(q),
|
|
525
|
-
)
|
|
526
|
-
if (quote) {
|
|
527
|
-
let value = v.value
|
|
528
|
-
if (quote !== '"') {
|
|
529
|
-
value = v.value.replace(new RegExp(quote, 'g'), '"')
|
|
530
|
-
}
|
|
531
|
-
return [attr.name, JSON.parse(value)]
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const number = Number(v.value)
|
|
535
|
-
if (!isNaN(number)) {
|
|
536
|
-
return [attr.name, number]
|
|
537
|
-
}
|
|
538
|
-
const parsedJson = safeJsonParse(v.value)
|
|
539
|
-
if (parsedJson) {
|
|
540
|
-
return [attr.name, parsedJson]
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
onError({
|
|
544
|
-
message: `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
|
|
545
|
-
line: attr.position?.start?.line,
|
|
546
|
-
})
|
|
547
|
-
} else {
|
|
548
|
-
console.log('unhandled attr', { attr }, attr.type)
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
return
|
|
552
|
-
})
|
|
553
|
-
.filter(isTruthy) as [string, any][]
|
|
554
|
-
return attrsList
|
|
555
|
-
}
|
|
556
|
-
|
|
557
940
|
function isTruthy<T>(val: T | undefined | null | false): val is T {
|
|
558
941
|
return Boolean(val)
|
|
559
942
|
}
|