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/dist/safe-mdx.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import React, { Suspense, cloneElement } from 'react';
|
|
2
|
+
import Evaluate from 'eval-estree-expression';
|
|
3
3
|
import { Fragment } from 'react';
|
|
4
|
+
import { DynamicEsmComponent } from './dynamic-esm-component.js';
|
|
5
|
+
import { parseEsmImports, extractComponentInfo } from './esm-parser.js';
|
|
4
6
|
const HtmlToJsxConverter = React.lazy(() => import('./HtmlToJsxConverter.js').then((module) => ({
|
|
5
7
|
default: module.HtmlToJsxConverter,
|
|
6
8
|
})));
|
|
7
|
-
export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({ components, markdown = '', mdast = null, renderNode, }) {
|
|
9
|
+
export const SafeMdxRenderer = React.memo(function SafeMdxRenderer({ components, markdown = '', mdast = null, renderNode, componentPropsSchema, createElement, allowClientEsmImports = false, }) {
|
|
8
10
|
const visitor = new MdastToJsx({
|
|
9
11
|
markdown,
|
|
10
12
|
mdast,
|
|
11
13
|
components,
|
|
12
14
|
renderNode,
|
|
15
|
+
componentPropsSchema,
|
|
16
|
+
createElement,
|
|
17
|
+
allowClientEsmImports,
|
|
13
18
|
});
|
|
14
19
|
const result = visitor.run();
|
|
15
20
|
return result;
|
|
@@ -21,10 +26,17 @@ export class MdastToJsx {
|
|
|
21
26
|
c;
|
|
22
27
|
errors = [];
|
|
23
28
|
renderNode;
|
|
24
|
-
|
|
29
|
+
componentPropsSchema;
|
|
30
|
+
createElement;
|
|
31
|
+
esmImports = new Map();
|
|
32
|
+
allowClientEsmImports;
|
|
33
|
+
constructor({ markdown: code = '', mdast, components = {}, renderNode, componentPropsSchema, createElement = React.createElement, allowClientEsmImports = false, }) {
|
|
25
34
|
this.str = code;
|
|
26
35
|
this.mdast = mdast;
|
|
27
36
|
this.renderNode = renderNode;
|
|
37
|
+
this.componentPropsSchema = componentPropsSchema;
|
|
38
|
+
this.createElement = createElement;
|
|
39
|
+
this.allowClientEsmImports = allowClientEsmImports;
|
|
28
40
|
this.c = {
|
|
29
41
|
...Object.fromEntries(nativeTags.map((tag) => {
|
|
30
42
|
return [tag, tag];
|
|
@@ -32,6 +44,30 @@ export class MdastToJsx {
|
|
|
32
44
|
...components,
|
|
33
45
|
};
|
|
34
46
|
}
|
|
47
|
+
validateComponentProps(componentName, props, line) {
|
|
48
|
+
if (!this.componentPropsSchema ||
|
|
49
|
+
!this.componentPropsSchema[componentName]) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const schema = this.componentPropsSchema[componentName];
|
|
53
|
+
let result = schema['~standard'].validate(props);
|
|
54
|
+
if (result instanceof Promise) {
|
|
55
|
+
// Ignore async validation errors as requested
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
if (result.issues) {
|
|
60
|
+
result.issues.forEach((issue) => {
|
|
61
|
+
const propPath = issue.path?.join('.') || 'unknown';
|
|
62
|
+
this.errors.push({
|
|
63
|
+
message: `Invalid props for component "${componentName}" at "${propPath}": ${issue.message}`,
|
|
64
|
+
line,
|
|
65
|
+
schemaPath: issue.path?.join('.'),
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
35
71
|
mapMdastChildren(node) {
|
|
36
72
|
const res = node.children
|
|
37
73
|
?.flatMap((child) => this.mdastTransformer(child))
|
|
@@ -76,24 +112,253 @@ export class MdastToJsx {
|
|
|
76
112
|
if (!node.name) {
|
|
77
113
|
return [];
|
|
78
114
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
// Check if this is an ESM imported component (only if allowed)
|
|
116
|
+
const esmImportInfo = this.allowClientEsmImports ? this.esmImports.get(node.name) : null;
|
|
117
|
+
let Component;
|
|
118
|
+
if (esmImportInfo) {
|
|
119
|
+
// Handle ESM imported component
|
|
120
|
+
const { importUrl, componentName } = extractComponentInfo(esmImportInfo);
|
|
121
|
+
Component = DynamicEsmComponent;
|
|
122
|
+
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
123
|
+
this.errors.push(err);
|
|
83
124
|
});
|
|
84
|
-
|
|
125
|
+
let attrs = Object.fromEntries(attrsList);
|
|
126
|
+
return this.createElement(Component, { ...attrs, importUrl, componentName }, this.mapJsxChildren(node));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
Component = accessWithDot(this.c, node.name);
|
|
130
|
+
if (!Component) {
|
|
131
|
+
this.errors.push({
|
|
132
|
+
message: `Unsupported jsx component ${node.name}`,
|
|
133
|
+
line: node.position?.start?.line,
|
|
134
|
+
});
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
85
137
|
}
|
|
86
|
-
let attrsList = getJsxAttrs(node, (err) => {
|
|
138
|
+
let attrsList = this.getJsxAttrs(node, (err) => {
|
|
87
139
|
this.errors.push(err);
|
|
88
140
|
});
|
|
89
141
|
let attrs = Object.fromEntries(attrsList);
|
|
90
|
-
|
|
142
|
+
// Validate component props with schema if available
|
|
143
|
+
this.validateComponentProps(node.name, attrs, node.position?.start?.line);
|
|
144
|
+
return this.createElement(Component, attrs, this.mapJsxChildren(node));
|
|
91
145
|
}
|
|
92
146
|
default: {
|
|
93
147
|
return this.mdastTransformer(node);
|
|
94
148
|
}
|
|
95
149
|
}
|
|
96
150
|
}
|
|
151
|
+
transformJsxElement(jsxElement, onError, line) {
|
|
152
|
+
try {
|
|
153
|
+
// Handle JSX opening element
|
|
154
|
+
if (jsxElement.openingElement) {
|
|
155
|
+
const tagName = jsxElement.openingElement.name?.type === 'JSXIdentifier' ? jsxElement.openingElement.name.name : null;
|
|
156
|
+
if (!tagName) {
|
|
157
|
+
onError?.({
|
|
158
|
+
message: 'JSX element missing component name',
|
|
159
|
+
line: line,
|
|
160
|
+
});
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
// Check if this is an ESM imported component (only if allowed)
|
|
164
|
+
const esmImportInfo = this.allowClientEsmImports ? this.esmImports.get(tagName) : null;
|
|
165
|
+
let Component;
|
|
166
|
+
if (esmImportInfo) {
|
|
167
|
+
// Handle ESM imported component
|
|
168
|
+
const { importUrl, componentName } = extractComponentInfo(esmImportInfo);
|
|
169
|
+
Component = DynamicEsmComponent;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// Get the component from the regular component map
|
|
173
|
+
Component = accessWithDot(this.c, tagName);
|
|
174
|
+
if (!Component) {
|
|
175
|
+
onError?.({
|
|
176
|
+
message: `Unsupported jsx component ${tagName} in attribute`,
|
|
177
|
+
line: line,
|
|
178
|
+
});
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Extract attributes
|
|
183
|
+
const props = {};
|
|
184
|
+
if (jsxElement.openingElement.attributes) {
|
|
185
|
+
for (const attr of jsxElement.openingElement.attributes) {
|
|
186
|
+
if (attr.type === 'JSXAttribute' && attr.name?.type === 'JSXIdentifier' && attr.name.name) {
|
|
187
|
+
if (attr.value) {
|
|
188
|
+
if (attr.value.type === 'Literal') {
|
|
189
|
+
props[attr.name.name] = attr.value.value;
|
|
190
|
+
}
|
|
191
|
+
else if (attr.value.type === 'JSXExpressionContainer') {
|
|
192
|
+
if (attr.value.expression.type === 'Literal') {
|
|
193
|
+
props[attr.name.name] = attr.value.expression.value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
props[attr.name.name] = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Extract children
|
|
204
|
+
const children = [];
|
|
205
|
+
if (jsxElement.children) {
|
|
206
|
+
for (const child of jsxElement.children) {
|
|
207
|
+
if (child.type === 'JSXText') {
|
|
208
|
+
children.push(child.value);
|
|
209
|
+
}
|
|
210
|
+
else if (child.type === 'JSXElement') {
|
|
211
|
+
const childElement = this.transformJsxElement(child);
|
|
212
|
+
if (childElement) {
|
|
213
|
+
children.push(childElement);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Handle ESM imported components by adding required props
|
|
219
|
+
if (esmImportInfo) {
|
|
220
|
+
const { importUrl, componentName } = extractComponentInfo(esmImportInfo);
|
|
221
|
+
return this.createElement(Component, { ...props, importUrl, componentName }, ...children);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
return this.createElement(Component, props, ...children);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
// Return null if transformation fails
|
|
230
|
+
onError?.({
|
|
231
|
+
message: `Failed to transform JSX element: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
232
|
+
line: line,
|
|
233
|
+
});
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
getJsxAttrs(node, onError = console.error) {
|
|
239
|
+
let attrsList = [];
|
|
240
|
+
for (const attr of node.attributes) {
|
|
241
|
+
if (attr.type === 'mdxJsxExpressionAttribute') {
|
|
242
|
+
// Handle spread expressions like {...{key: '1'}}
|
|
243
|
+
if (attr.data?.estree) {
|
|
244
|
+
try {
|
|
245
|
+
const program = attr.data.estree;
|
|
246
|
+
if (program.body?.length > 0 &&
|
|
247
|
+
program.body[0].type === 'ExpressionStatement') {
|
|
248
|
+
const expression = program.body[0].expression;
|
|
249
|
+
try {
|
|
250
|
+
const result = Evaluate.evaluate.sync(expression);
|
|
251
|
+
// Handle spread syntax - merge the evaluated object
|
|
252
|
+
if (typeof result === 'object' && result != null) {
|
|
253
|
+
const entries = Object.entries(result);
|
|
254
|
+
attrsList.push(...entries);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
onError({
|
|
259
|
+
message: `Failed to evaluate expression attribute: ${attr.value
|
|
260
|
+
.replace(/\n+/g, ' ')
|
|
261
|
+
.replace(/ +/g, ' ')}`,
|
|
262
|
+
line: attr.position?.start?.line,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
onError({
|
|
269
|
+
message: `Failed to evaluate expression attribute: ${attr.value
|
|
270
|
+
.replace(/\n+/g, ' ')
|
|
271
|
+
.replace(/ +/g, ' ')}`,
|
|
272
|
+
line: attr.position?.start?.line,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
onError({
|
|
278
|
+
message: `Expressions in jsx props are not supported (${attr.value
|
|
279
|
+
.replace(/\n+/g, ' ')
|
|
280
|
+
.replace(/ +/g, ' ')})`,
|
|
281
|
+
line: attr.position?.start?.line,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (attr.type !== 'mdxJsxAttribute') {
|
|
287
|
+
onError({
|
|
288
|
+
message: `non mdxJsxAttribute attribute is not supported: ${attr}`,
|
|
289
|
+
line: node.position?.start?.line,
|
|
290
|
+
});
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const v = attr.value;
|
|
294
|
+
if (typeof v === 'string' || typeof v === 'number') {
|
|
295
|
+
attrsList.push([attr.name, v]);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (v === null) {
|
|
299
|
+
attrsList.push([attr.name, true]);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (v?.type === 'mdxJsxAttributeValueExpression') {
|
|
303
|
+
// Manual parsing fallback for simple values
|
|
304
|
+
if (v.value === 'true') {
|
|
305
|
+
attrsList.push([attr.name, true]);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (v.value === 'false') {
|
|
309
|
+
attrsList.push([attr.name, false]);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (v.value === 'null') {
|
|
313
|
+
attrsList.push([attr.name, null]);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (v.value === 'undefined') {
|
|
317
|
+
attrsList.push([attr.name, undefined]);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (v.data?.estree) {
|
|
321
|
+
try {
|
|
322
|
+
// Extract the expression from the Program body
|
|
323
|
+
const program = v.data.estree;
|
|
324
|
+
if (program.body?.length > 0 &&
|
|
325
|
+
program.body[0].type === 'ExpressionStatement') {
|
|
326
|
+
const expression = program.body[0].expression;
|
|
327
|
+
// Check if this is a JSX element
|
|
328
|
+
if (expression.type === 'JSXElement') {
|
|
329
|
+
// Transform JSX element to React element
|
|
330
|
+
const jsxElement = this.transformJsxElement(expression, onError, attr.position?.start?.line);
|
|
331
|
+
if (jsxElement) {
|
|
332
|
+
attrsList.push([attr.name, jsxElement]);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
// Evaluate the expression synchronously
|
|
338
|
+
const result = Evaluate.evaluate.sync(expression);
|
|
339
|
+
attrsList.push([attr.name, result]);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
onError({
|
|
344
|
+
message: `Failed to evaluate expression attribute: ${attr.name}={${v.value}}`,
|
|
345
|
+
line: attr.position?.start?.line,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
// Fall back to the original manual parsing for backwards compatibility
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
onError({
|
|
355
|
+
message: `Expressions in jsx prop not evaluated: (${attr.name}={${v.value}})`,
|
|
356
|
+
line: attr.position?.start?.line,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return attrsList;
|
|
361
|
+
}
|
|
97
362
|
run() {
|
|
98
363
|
const res = this.mdastTransformer(this.mdast);
|
|
99
364
|
if (Array.isArray(res) && res.length === 1) {
|
|
@@ -114,9 +379,13 @@ export class MdastToJsx {
|
|
|
114
379
|
}
|
|
115
380
|
switch (node.type) {
|
|
116
381
|
case 'mdxjsEsm': {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
382
|
+
// Parse ESM imports and merge into our imports map (only if allowed)
|
|
383
|
+
if (this.allowClientEsmImports) {
|
|
384
|
+
const parsedImports = parseEsmImports(node, (err) => this.errors.push(err));
|
|
385
|
+
parsedImports.forEach((value, key) => {
|
|
386
|
+
this.esmImports.set(key, value);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
120
389
|
return [];
|
|
121
390
|
}
|
|
122
391
|
case 'mdxJsxTextElement':
|
|
@@ -144,6 +413,34 @@ export class MdastToJsx {
|
|
|
144
413
|
if (!node.value) {
|
|
145
414
|
return [];
|
|
146
415
|
}
|
|
416
|
+
// Check if we have an estree AST
|
|
417
|
+
if (node.data?.estree) {
|
|
418
|
+
try {
|
|
419
|
+
// Extract the expression from the Program body
|
|
420
|
+
const program = node.data.estree;
|
|
421
|
+
if (program.body?.length > 0 &&
|
|
422
|
+
program.body[0].type === 'ExpressionStatement') {
|
|
423
|
+
const expression = program.body[0].expression;
|
|
424
|
+
try {
|
|
425
|
+
// Evaluate the expression synchronously
|
|
426
|
+
const result = Evaluate.evaluate.sync(expression);
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
this.errors.push({
|
|
431
|
+
message: `Failed to evaluate expression: ${node.value}`,
|
|
432
|
+
line: node.position?.start?.line,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
this.errors.push({
|
|
439
|
+
message: `Failed to evaluate expression: ${node.value}`,
|
|
440
|
+
line: node.position?.start?.line,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
147
444
|
return [];
|
|
148
445
|
}
|
|
149
446
|
case 'yaml': {
|
|
@@ -155,16 +452,16 @@ export class MdastToJsx {
|
|
|
155
452
|
case 'heading': {
|
|
156
453
|
const level = node.depth;
|
|
157
454
|
const Tag = this.c[`h${level}`] ?? `h${level}`;
|
|
158
|
-
return (
|
|
455
|
+
return this.createElement(Tag, node.data?.hProperties, this.mapMdastChildren(node));
|
|
159
456
|
}
|
|
160
457
|
case 'paragraph': {
|
|
161
|
-
return (
|
|
458
|
+
return this.createElement(this.c.p, node.data?.hProperties, this.mapMdastChildren(node));
|
|
162
459
|
}
|
|
163
460
|
case 'blockquote': {
|
|
164
|
-
return (
|
|
461
|
+
return this.createElement(this.c.blockquote, node.data?.hProperties, this.mapMdastChildren(node));
|
|
165
462
|
}
|
|
166
463
|
case 'thematicBreak': {
|
|
167
|
-
return
|
|
464
|
+
return this.createElement(this.c.hr, node.data?.hProperties);
|
|
168
465
|
}
|
|
169
466
|
case 'code': {
|
|
170
467
|
if (!node.value) {
|
|
@@ -172,7 +469,7 @@ export class MdastToJsx {
|
|
|
172
469
|
}
|
|
173
470
|
const language = node.lang || '';
|
|
174
471
|
const code = node.value;
|
|
175
|
-
const codeBlock = (className) => (
|
|
472
|
+
const codeBlock = (className) => this.createElement(this.c.pre, node.data?.hProperties, this.createElement(this.c.code, { className }, code));
|
|
176
473
|
if (language) {
|
|
177
474
|
return codeBlock(`language-${language}`);
|
|
178
475
|
}
|
|
@@ -180,23 +477,26 @@ export class MdastToJsx {
|
|
|
180
477
|
}
|
|
181
478
|
case 'list': {
|
|
182
479
|
if (node.ordered) {
|
|
183
|
-
return (
|
|
480
|
+
return this.createElement(this.c.ol, { start: node.start, ...node.data?.hProperties }, this.mapMdastChildren(node));
|
|
184
481
|
}
|
|
185
|
-
return (
|
|
482
|
+
return this.createElement(this.c.ul, node.data?.hProperties, this.mapMdastChildren(node));
|
|
186
483
|
}
|
|
187
484
|
case 'listItem': {
|
|
188
485
|
// https://github.com/syntax-tree/mdast-util-gfm-task-list-item#syntax-tree
|
|
189
486
|
if (node?.checked != null) {
|
|
190
|
-
return (
|
|
487
|
+
return this.createElement(this.c.li, {
|
|
488
|
+
'data-checked': node.checked,
|
|
489
|
+
...node.data?.hProperties,
|
|
490
|
+
}, this.mapMdastChildren(node));
|
|
191
491
|
}
|
|
192
|
-
return (
|
|
492
|
+
return this.createElement(this.c.li, node.data?.hProperties, this.mapMdastChildren(node));
|
|
193
493
|
}
|
|
194
494
|
case 'text': {
|
|
195
495
|
if (!node.value) {
|
|
196
496
|
return [];
|
|
197
497
|
}
|
|
198
498
|
if (node.data?.hProperties) {
|
|
199
|
-
return (
|
|
499
|
+
return this.createElement(this.c.span, node.data.hProperties, node.value);
|
|
200
500
|
}
|
|
201
501
|
return node.value;
|
|
202
502
|
}
|
|
@@ -204,60 +504,68 @@ export class MdastToJsx {
|
|
|
204
504
|
const src = node.url || '';
|
|
205
505
|
const alt = node.alt || '';
|
|
206
506
|
const title = node.title || '';
|
|
207
|
-
return (
|
|
507
|
+
return this.createElement(this.c.img, {
|
|
508
|
+
src,
|
|
509
|
+
alt,
|
|
510
|
+
title,
|
|
511
|
+
...node.data?.hProperties,
|
|
512
|
+
});
|
|
208
513
|
}
|
|
209
514
|
case 'link': {
|
|
210
515
|
const href = node.url || '';
|
|
211
516
|
const title = node.title || '';
|
|
212
|
-
return (
|
|
517
|
+
return this.createElement(this.c.a, { href, title, ...node.data?.hProperties }, this.mapMdastChildren(node));
|
|
213
518
|
}
|
|
214
519
|
case 'strong': {
|
|
215
|
-
return (
|
|
520
|
+
return this.createElement(this.c.strong, node.data?.hProperties, this.mapMdastChildren(node));
|
|
216
521
|
}
|
|
217
522
|
case 'emphasis': {
|
|
218
|
-
return (
|
|
523
|
+
return this.createElement(this.c.em, node.data?.hProperties, this.mapMdastChildren(node));
|
|
219
524
|
}
|
|
220
525
|
case 'delete': {
|
|
221
|
-
return (
|
|
526
|
+
return this.createElement(this.c.del, node.data?.hProperties, this.mapMdastChildren(node));
|
|
222
527
|
}
|
|
223
528
|
case 'inlineCode': {
|
|
224
529
|
if (!node.value) {
|
|
225
530
|
return [];
|
|
226
531
|
}
|
|
227
|
-
return (
|
|
532
|
+
return this.createElement(this.c.code, node.data?.hProperties, node.value);
|
|
228
533
|
}
|
|
229
534
|
case 'break': {
|
|
230
|
-
return
|
|
535
|
+
return this.createElement(this.c.br, node.data?.hProperties);
|
|
231
536
|
}
|
|
232
537
|
case 'root': {
|
|
233
538
|
if (node.data?.hProperties) {
|
|
234
|
-
return (
|
|
539
|
+
return this.createElement(this.c.div, node.data.hProperties, this.mapMdastChildren(node));
|
|
235
540
|
}
|
|
236
|
-
return
|
|
541
|
+
return this.createElement(Fragment, null, this.mapMdastChildren(node));
|
|
237
542
|
}
|
|
238
543
|
case 'table': {
|
|
239
544
|
const [head, ...body] = React.Children.toArray(this.mapMdastChildren(node));
|
|
240
|
-
return (
|
|
545
|
+
return this.createElement(this.c.table, node.data?.hProperties, head && this.createElement(this.c.thead, null, head), !!body?.length &&
|
|
546
|
+
this.createElement(this.c.tbody, null, body));
|
|
241
547
|
}
|
|
242
548
|
case 'tableRow': {
|
|
243
|
-
return (
|
|
549
|
+
return this.createElement(this.c.tr, { className: '', ...node.data?.hProperties }, this.mapMdastChildren(node));
|
|
244
550
|
}
|
|
245
551
|
case 'tableCell': {
|
|
246
552
|
let content = this.mapMdastChildren(node);
|
|
247
|
-
return (
|
|
553
|
+
return this.createElement(this.c.td, { className: '', ...node.data?.hProperties }, content);
|
|
248
554
|
}
|
|
249
555
|
case 'definition': {
|
|
250
556
|
return [];
|
|
251
557
|
}
|
|
252
558
|
case 'linkReference': {
|
|
253
559
|
let href = '';
|
|
560
|
+
let title = '';
|
|
254
561
|
mdastBfs(this.mdast, (child) => {
|
|
255
562
|
if (child.type === 'definition' &&
|
|
256
563
|
child.identifier === node.identifier) {
|
|
257
|
-
href = child.url;
|
|
564
|
+
href = child.url || '';
|
|
565
|
+
title = child.title || '';
|
|
258
566
|
}
|
|
259
567
|
});
|
|
260
|
-
return (
|
|
568
|
+
return this.createElement(this.c.a, { href, title, ...node.data?.hProperties }, this.mapMdastChildren(node));
|
|
261
569
|
}
|
|
262
570
|
case 'footnoteReference': {
|
|
263
571
|
return [];
|
|
@@ -272,7 +580,11 @@ export class MdastToJsx {
|
|
|
272
580
|
if (!text) {
|
|
273
581
|
return [];
|
|
274
582
|
}
|
|
275
|
-
return (
|
|
583
|
+
return this.createElement(Suspense, { fallback: null }, this.createElement(HtmlToJsxConverter, {
|
|
584
|
+
htmlText: text,
|
|
585
|
+
instance: this,
|
|
586
|
+
node,
|
|
587
|
+
}));
|
|
276
588
|
}
|
|
277
589
|
case 'imageReference': {
|
|
278
590
|
return [];
|
|
@@ -287,68 +599,6 @@ export class MdastToJsx {
|
|
|
287
599
|
}
|
|
288
600
|
}
|
|
289
601
|
}
|
|
290
|
-
export function getJsxAttrs(node, onError = console.error) {
|
|
291
|
-
let attrsList = node.attributes
|
|
292
|
-
.map((attr) => {
|
|
293
|
-
if (attr.type === 'mdxJsxExpressionAttribute') {
|
|
294
|
-
onError({
|
|
295
|
-
message: `Expressions in jsx props are not supported (${attr.value
|
|
296
|
-
.replace(/\n+/g, ' ')
|
|
297
|
-
.replace(/ +/g, ' ')})`,
|
|
298
|
-
});
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (attr.type !== 'mdxJsxAttribute') {
|
|
302
|
-
throw new Error(`non mdxJsxAttribute is not supported: ${attr}`);
|
|
303
|
-
}
|
|
304
|
-
const v = attr.value;
|
|
305
|
-
if (typeof v === 'string' || typeof v === 'number') {
|
|
306
|
-
return [attr.name, v];
|
|
307
|
-
}
|
|
308
|
-
if (v === null) {
|
|
309
|
-
return [attr.name, true];
|
|
310
|
-
}
|
|
311
|
-
if (v?.type === 'mdxJsxAttributeValueExpression') {
|
|
312
|
-
if (v.value === 'true') {
|
|
313
|
-
return [attr.name, true];
|
|
314
|
-
}
|
|
315
|
-
if (v.value === 'false') {
|
|
316
|
-
return [attr.name, false];
|
|
317
|
-
}
|
|
318
|
-
if (v.value === 'null') {
|
|
319
|
-
return [attr.name, null];
|
|
320
|
-
}
|
|
321
|
-
if (v.value === 'undefined') {
|
|
322
|
-
return [attr.name, undefined];
|
|
323
|
-
}
|
|
324
|
-
let quote = ['"', "'", '`'].find((q) => v.value.startsWith(q) && v.value.endsWith(q));
|
|
325
|
-
if (quote) {
|
|
326
|
-
let value = v.value;
|
|
327
|
-
if (quote !== '"') {
|
|
328
|
-
value = v.value.replace(new RegExp(quote, 'g'), '"');
|
|
329
|
-
}
|
|
330
|
-
return [attr.name, JSON.parse(value)];
|
|
331
|
-
}
|
|
332
|
-
const number = Number(v.value);
|
|
333
|
-
if (!isNaN(number)) {
|
|
334
|
-
return [attr.name, number];
|
|
335
|
-
}
|
|
336
|
-
const parsedJson = safeJsonParse(v.value);
|
|
337
|
-
if (parsedJson) {
|
|
338
|
-
return [attr.name, parsedJson];
|
|
339
|
-
}
|
|
340
|
-
onError({
|
|
341
|
-
message: `Expressions in jsx props are not supported (${attr.name}={${v.value}})`,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
console.log('unhandled attr', { attr }, attr.type);
|
|
346
|
-
}
|
|
347
|
-
return;
|
|
348
|
-
})
|
|
349
|
-
.filter(isTruthy);
|
|
350
|
-
return attrsList;
|
|
351
|
-
}
|
|
352
602
|
function isTruthy(val) {
|
|
353
603
|
return Boolean(val);
|
|
354
604
|
}
|