safe-mdx 1.1.0 → 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 +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 +8 -2
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +291 -83
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +593 -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 +648 -24
- package/src/safe-mdx.tsx +405 -120
package/src/safe-mdx.tsx
CHANGED
|
@@ -3,8 +3,12 @@ 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 { JSXElement, JSXAttribute, JSXText, JSXExpressionContainer } from 'estree-jsx'
|
|
7
|
+
import Evaluate from 'eval-estree-expression'
|
|
6
8
|
|
|
7
9
|
import { Fragment, ReactNode } from 'react'
|
|
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) => ({
|
|
@@ -49,6 +53,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
49
53
|
renderNode,
|
|
50
54
|
componentPropsSchema,
|
|
51
55
|
createElement,
|
|
56
|
+
allowClientEsmImports = false,
|
|
52
57
|
}: {
|
|
53
58
|
components?: ComponentsMap
|
|
54
59
|
markdown?: string
|
|
@@ -56,6 +61,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
56
61
|
renderNode?: RenderNode
|
|
57
62
|
componentPropsSchema?: ComponentPropsSchema
|
|
58
63
|
createElement?: CreateElementFunction
|
|
64
|
+
allowClientEsmImports?: boolean
|
|
59
65
|
}) {
|
|
60
66
|
const visitor = new MdastToJsx({
|
|
61
67
|
markdown,
|
|
@@ -64,6 +70,7 @@ export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({
|
|
|
64
70
|
renderNode,
|
|
65
71
|
componentPropsSchema,
|
|
66
72
|
createElement,
|
|
73
|
+
allowClientEsmImports,
|
|
67
74
|
})
|
|
68
75
|
const result = visitor.run()
|
|
69
76
|
return result
|
|
@@ -78,6 +85,8 @@ export class MdastToJsx {
|
|
|
78
85
|
renderNode?: RenderNode
|
|
79
86
|
componentPropsSchema?: ComponentPropsSchema
|
|
80
87
|
createElement: CreateElementFunction
|
|
88
|
+
esmImports: Map<string, string> = new Map()
|
|
89
|
+
allowClientEsmImports: boolean
|
|
81
90
|
|
|
82
91
|
constructor({
|
|
83
92
|
markdown: code = '',
|
|
@@ -86,6 +95,7 @@ export class MdastToJsx {
|
|
|
86
95
|
renderNode,
|
|
87
96
|
componentPropsSchema,
|
|
88
97
|
createElement = React.createElement,
|
|
98
|
+
allowClientEsmImports = false,
|
|
89
99
|
}: {
|
|
90
100
|
markdown?: string
|
|
91
101
|
mdast: MyRootContent
|
|
@@ -96,6 +106,7 @@ export class MdastToJsx {
|
|
|
96
106
|
) => ReactNode | undefined
|
|
97
107
|
componentPropsSchema?: ComponentPropsSchema
|
|
98
108
|
createElement?: CreateElementFunction
|
|
109
|
+
allowClientEsmImports?: boolean
|
|
99
110
|
}) {
|
|
100
111
|
this.str = code
|
|
101
112
|
|
|
@@ -107,6 +118,8 @@ export class MdastToJsx {
|
|
|
107
118
|
|
|
108
119
|
this.createElement = createElement
|
|
109
120
|
|
|
121
|
+
this.allowClientEsmImports = allowClientEsmImports
|
|
122
|
+
|
|
110
123
|
this.c = {
|
|
111
124
|
...Object.fromEntries(
|
|
112
125
|
nativeTags.map((tag) => {
|
|
@@ -120,9 +133,12 @@ export class MdastToJsx {
|
|
|
120
133
|
validateComponentProps(
|
|
121
134
|
componentName: string,
|
|
122
135
|
props: Record<string, any>,
|
|
123
|
-
line?: number
|
|
136
|
+
line?: number,
|
|
124
137
|
): void {
|
|
125
|
-
if (
|
|
138
|
+
if (
|
|
139
|
+
!this.componentPropsSchema ||
|
|
140
|
+
!this.componentPropsSchema[componentName]
|
|
141
|
+
) {
|
|
126
142
|
return
|
|
127
143
|
}
|
|
128
144
|
|
|
@@ -192,26 +208,55 @@ export class MdastToJsx {
|
|
|
192
208
|
return []
|
|
193
209
|
}
|
|
194
210
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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)
|
|
201
222
|
})
|
|
202
|
-
|
|
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
|
+
}
|
|
203
240
|
}
|
|
204
241
|
|
|
205
|
-
let attrsList = getJsxAttrs(node, (err) => {
|
|
242
|
+
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
206
243
|
this.errors.push(err)
|
|
207
244
|
})
|
|
208
245
|
|
|
209
246
|
let attrs = Object.fromEntries(attrsList)
|
|
210
247
|
|
|
211
248
|
// Validate component props with schema if available
|
|
212
|
-
this.validateComponentProps(
|
|
249
|
+
this.validateComponentProps(
|
|
250
|
+
node.name,
|
|
251
|
+
attrs,
|
|
252
|
+
node.position?.start?.line,
|
|
253
|
+
)
|
|
213
254
|
|
|
214
|
-
return this.createElement(
|
|
255
|
+
return this.createElement(
|
|
256
|
+
Component,
|
|
257
|
+
attrs,
|
|
258
|
+
this.mapJsxChildren(node),
|
|
259
|
+
)
|
|
215
260
|
}
|
|
216
261
|
default: {
|
|
217
262
|
return this.mdastTransformer(node)
|
|
@@ -219,6 +264,232 @@ export class MdastToJsx {
|
|
|
219
264
|
}
|
|
220
265
|
}
|
|
221
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
|
+
|
|
222
493
|
run() {
|
|
223
494
|
const res = this.mdastTransformer(this.mdast) as ReactNode
|
|
224
495
|
if (Array.isArray(res) && res.length === 1) {
|
|
@@ -245,10 +516,13 @@ export class MdastToJsx {
|
|
|
245
516
|
|
|
246
517
|
switch (node.type) {
|
|
247
518
|
case 'mdxjsEsm': {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
}
|
|
252
526
|
return []
|
|
253
527
|
}
|
|
254
528
|
case 'mdxJsxTextElement':
|
|
@@ -275,6 +549,36 @@ export class MdastToJsx {
|
|
|
275
549
|
if (!node.value) {
|
|
276
550
|
return []
|
|
277
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
|
+
|
|
278
582
|
return []
|
|
279
583
|
}
|
|
280
584
|
case 'yaml': {
|
|
@@ -287,13 +591,25 @@ export class MdastToJsx {
|
|
|
287
591
|
const level = node.depth
|
|
288
592
|
const Tag = this.c[`h${level}`] ?? `h${level}`
|
|
289
593
|
|
|
290
|
-
return this.createElement(
|
|
594
|
+
return this.createElement(
|
|
595
|
+
Tag,
|
|
596
|
+
node.data?.hProperties,
|
|
597
|
+
this.mapMdastChildren(node),
|
|
598
|
+
)
|
|
291
599
|
}
|
|
292
600
|
case 'paragraph': {
|
|
293
|
-
return this.createElement(
|
|
601
|
+
return this.createElement(
|
|
602
|
+
this.c.p,
|
|
603
|
+
node.data?.hProperties,
|
|
604
|
+
this.mapMdastChildren(node),
|
|
605
|
+
)
|
|
294
606
|
}
|
|
295
607
|
case 'blockquote': {
|
|
296
|
-
return this.createElement(
|
|
608
|
+
return this.createElement(
|
|
609
|
+
this.c.blockquote,
|
|
610
|
+
node.data?.hProperties,
|
|
611
|
+
this.mapMdastChildren(node),
|
|
612
|
+
)
|
|
297
613
|
}
|
|
298
614
|
case 'thematicBreak': {
|
|
299
615
|
return this.createElement(this.c.hr, node.data?.hProperties)
|
|
@@ -304,11 +620,12 @@ export class MdastToJsx {
|
|
|
304
620
|
}
|
|
305
621
|
const language = node.lang || ''
|
|
306
622
|
const code = node.value
|
|
307
|
-
const codeBlock = (className?: string) =>
|
|
308
|
-
this.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
+
)
|
|
312
629
|
|
|
313
630
|
if (language) {
|
|
314
631
|
return codeBlock(`language-${language}`)
|
|
@@ -321,28 +638,43 @@ export class MdastToJsx {
|
|
|
321
638
|
return this.createElement(
|
|
322
639
|
this.c.ol,
|
|
323
640
|
{ start: node.start!, ...node.data?.hProperties },
|
|
324
|
-
this.mapMdastChildren(node)
|
|
641
|
+
this.mapMdastChildren(node),
|
|
325
642
|
)
|
|
326
643
|
}
|
|
327
|
-
return this.createElement(
|
|
644
|
+
return this.createElement(
|
|
645
|
+
this.c.ul,
|
|
646
|
+
node.data?.hProperties,
|
|
647
|
+
this.mapMdastChildren(node),
|
|
648
|
+
)
|
|
328
649
|
}
|
|
329
650
|
case 'listItem': {
|
|
330
651
|
// https://github.com/syntax-tree/mdast-util-gfm-task-list-item#syntax-tree
|
|
331
652
|
if (node?.checked != null) {
|
|
332
653
|
return this.createElement(
|
|
333
654
|
this.c.li,
|
|
334
|
-
{
|
|
335
|
-
|
|
655
|
+
{
|
|
656
|
+
'data-checked': node.checked,
|
|
657
|
+
...node.data?.hProperties,
|
|
658
|
+
},
|
|
659
|
+
this.mapMdastChildren(node),
|
|
336
660
|
)
|
|
337
661
|
}
|
|
338
|
-
return this.createElement(
|
|
662
|
+
return this.createElement(
|
|
663
|
+
this.c.li,
|
|
664
|
+
node.data?.hProperties,
|
|
665
|
+
this.mapMdastChildren(node),
|
|
666
|
+
)
|
|
339
667
|
}
|
|
340
668
|
case 'text': {
|
|
341
669
|
if (!node.value) {
|
|
342
670
|
return []
|
|
343
671
|
}
|
|
344
672
|
if (node.data?.hProperties) {
|
|
345
|
-
return this.createElement(
|
|
673
|
+
return this.createElement(
|
|
674
|
+
this.c.span,
|
|
675
|
+
node.data.hProperties,
|
|
676
|
+
node.value,
|
|
677
|
+
)
|
|
346
678
|
}
|
|
347
679
|
return node.value
|
|
348
680
|
}
|
|
@@ -354,7 +686,7 @@ export class MdastToJsx {
|
|
|
354
686
|
src,
|
|
355
687
|
alt,
|
|
356
688
|
title,
|
|
357
|
-
...node.data?.hProperties
|
|
689
|
+
...node.data?.hProperties,
|
|
358
690
|
})
|
|
359
691
|
}
|
|
360
692
|
case 'link': {
|
|
@@ -363,32 +695,56 @@ export class MdastToJsx {
|
|
|
363
695
|
return this.createElement(
|
|
364
696
|
this.c.a,
|
|
365
697
|
{ href, title, ...node.data?.hProperties },
|
|
366
|
-
this.mapMdastChildren(node)
|
|
698
|
+
this.mapMdastChildren(node),
|
|
367
699
|
)
|
|
368
700
|
}
|
|
369
701
|
case 'strong': {
|
|
370
|
-
return this.createElement(
|
|
702
|
+
return this.createElement(
|
|
703
|
+
this.c.strong,
|
|
704
|
+
node.data?.hProperties,
|
|
705
|
+
this.mapMdastChildren(node),
|
|
706
|
+
)
|
|
371
707
|
}
|
|
372
708
|
case 'emphasis': {
|
|
373
|
-
return this.createElement(
|
|
709
|
+
return this.createElement(
|
|
710
|
+
this.c.em,
|
|
711
|
+
node.data?.hProperties,
|
|
712
|
+
this.mapMdastChildren(node),
|
|
713
|
+
)
|
|
374
714
|
}
|
|
375
715
|
case 'delete': {
|
|
376
|
-
return this.createElement(
|
|
716
|
+
return this.createElement(
|
|
717
|
+
this.c.del,
|
|
718
|
+
node.data?.hProperties,
|
|
719
|
+
this.mapMdastChildren(node),
|
|
720
|
+
)
|
|
377
721
|
}
|
|
378
722
|
case 'inlineCode': {
|
|
379
723
|
if (!node.value) {
|
|
380
724
|
return []
|
|
381
725
|
}
|
|
382
|
-
return this.createElement(
|
|
726
|
+
return this.createElement(
|
|
727
|
+
this.c.code,
|
|
728
|
+
node.data?.hProperties,
|
|
729
|
+
node.value,
|
|
730
|
+
)
|
|
383
731
|
}
|
|
384
732
|
case 'break': {
|
|
385
733
|
return this.createElement(this.c.br, node.data?.hProperties)
|
|
386
734
|
}
|
|
387
735
|
case 'root': {
|
|
388
736
|
if (node.data?.hProperties) {
|
|
389
|
-
return this.createElement(
|
|
737
|
+
return this.createElement(
|
|
738
|
+
this.c.div,
|
|
739
|
+
node.data.hProperties,
|
|
740
|
+
this.mapMdastChildren(node),
|
|
741
|
+
)
|
|
390
742
|
}
|
|
391
|
-
return this.createElement(
|
|
743
|
+
return this.createElement(
|
|
744
|
+
Fragment,
|
|
745
|
+
null,
|
|
746
|
+
this.mapMdastChildren(node),
|
|
747
|
+
)
|
|
392
748
|
}
|
|
393
749
|
case 'table': {
|
|
394
750
|
const [head, ...body] = React.Children.toArray(
|
|
@@ -398,14 +754,15 @@ export class MdastToJsx {
|
|
|
398
754
|
this.c.table,
|
|
399
755
|
node.data?.hProperties,
|
|
400
756
|
head && this.createElement(this.c.thead, null, head),
|
|
401
|
-
!!body?.length &&
|
|
757
|
+
!!body?.length &&
|
|
758
|
+
this.createElement(this.c.tbody, null, body),
|
|
402
759
|
)
|
|
403
760
|
}
|
|
404
761
|
case 'tableRow': {
|
|
405
762
|
return this.createElement(
|
|
406
763
|
this.c.tr,
|
|
407
764
|
{ className: '', ...node.data?.hProperties },
|
|
408
|
-
this.mapMdastChildren(node)
|
|
765
|
+
this.mapMdastChildren(node),
|
|
409
766
|
)
|
|
410
767
|
}
|
|
411
768
|
case 'tableCell': {
|
|
@@ -413,7 +770,7 @@ export class MdastToJsx {
|
|
|
413
770
|
return this.createElement(
|
|
414
771
|
this.c.td,
|
|
415
772
|
{ className: '', ...node.data?.hProperties },
|
|
416
|
-
content
|
|
773
|
+
content,
|
|
417
774
|
)
|
|
418
775
|
}
|
|
419
776
|
case 'definition': {
|
|
@@ -421,19 +778,21 @@ export class MdastToJsx {
|
|
|
421
778
|
}
|
|
422
779
|
case 'linkReference': {
|
|
423
780
|
let href = ''
|
|
781
|
+
let title = ''
|
|
424
782
|
mdastBfs(this.mdast, (child: any) => {
|
|
425
783
|
if (
|
|
426
784
|
child.type === 'definition' &&
|
|
427
785
|
child.identifier === node.identifier
|
|
428
786
|
) {
|
|
429
|
-
href = child.url
|
|
787
|
+
href = child.url || ''
|
|
788
|
+
title = child.title || ''
|
|
430
789
|
}
|
|
431
790
|
})
|
|
432
791
|
|
|
433
792
|
return this.createElement(
|
|
434
793
|
this.c.a,
|
|
435
|
-
{ href, ...node.data?.hProperties },
|
|
436
|
-
this.mapMdastChildren(node)
|
|
794
|
+
{ href, title, ...node.data?.hProperties },
|
|
795
|
+
this.mapMdastChildren(node),
|
|
437
796
|
)
|
|
438
797
|
}
|
|
439
798
|
case 'footnoteReference': {
|
|
@@ -457,8 +816,8 @@ export class MdastToJsx {
|
|
|
457
816
|
this.createElement(HtmlToJsxConverter, {
|
|
458
817
|
htmlText: text,
|
|
459
818
|
instance: this,
|
|
460
|
-
node
|
|
461
|
-
})
|
|
819
|
+
node,
|
|
820
|
+
}),
|
|
462
821
|
)
|
|
463
822
|
}
|
|
464
823
|
case 'imageReference': {
|
|
@@ -480,80 +839,6 @@ export class MdastToJsx {
|
|
|
480
839
|
}
|
|
481
840
|
}
|
|
482
841
|
|
|
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
842
|
function isTruthy<T>(val: T | undefined | null | false): val is T {
|
|
558
843
|
return Boolean(val)
|
|
559
844
|
}
|