safe-mdx 1.6.0 → 1.8.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 +78 -12
- package/dist/html/html-and-md.test.js +14 -41
- package/dist/html/html-and-md.test.js.map +1 -1
- package/dist/html/html-to-mdx-ast.d.ts +26 -1
- package/dist/html/html-to-mdx-ast.d.ts.map +1 -1
- package/dist/html/html-to-mdx-ast.js +40 -0
- package/dist/html/html-to-mdx-ast.js.map +1 -1
- package/dist/incremental-parse.d.ts +41 -0
- package/dist/incremental-parse.d.ts.map +1 -0
- package/dist/incremental-parse.js +139 -0
- package/dist/incremental-parse.js.map +1 -0
- package/dist/incremental-parse.test.d.ts +2 -0
- package/dist/incremental-parse.test.d.ts.map +1 -0
- package/dist/incremental-parse.test.js +299 -0
- package/dist/incremental-parse.test.js.map +1 -0
- package/dist/markdown-html.test.d.ts +2 -0
- package/dist/markdown-html.test.d.ts.map +1 -0
- package/dist/markdown-html.test.js +129 -0
- package/dist/markdown-html.test.js.map +1 -0
- package/dist/markdown.d.ts +3 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +4 -0
- package/dist/markdown.js.map +1 -0
- package/dist/parse.d.ts +9 -2
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +24 -12
- package/dist/parse.js.map +1 -1
- package/dist/safe-mdx.d.ts +13 -0
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +193 -24
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +284 -11
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +9 -1
- package/src/html/html-and-md.test.ts +15 -47
- package/src/html/html-to-mdx-ast.ts +53 -1
- package/src/incremental-parse.test.ts +315 -0
- package/src/incremental-parse.ts +219 -0
- package/src/markdown-html.test.tsx +144 -0
- package/src/markdown.ts +4 -0
- package/src/parse.ts +36 -13
- package/src/safe-mdx.test.tsx +357 -11
- package/src/safe-mdx.tsx +252 -26
package/src/safe-mdx.tsx
CHANGED
|
@@ -10,8 +10,7 @@ import { Fragment, ReactNode } from 'react'
|
|
|
10
10
|
import { DynamicEsmComponent } from 'safe-mdx/client'
|
|
11
11
|
import { extractComponentInfo, parseEsmImports } from './esm-parser.ts'
|
|
12
12
|
import { resolveModulePath, type EagerModules } from './parse.ts'
|
|
13
|
-
import {
|
|
14
|
-
import { validHtmlElements, nativeTags } from './html/valid-html-elements.ts'
|
|
13
|
+
import { nativeTags } from './html/valid-html-elements.ts'
|
|
15
14
|
|
|
16
15
|
export type MyRootContent = RootContent | Root
|
|
17
16
|
|
|
@@ -534,6 +533,19 @@ export class MdastToJsx {
|
|
|
534
533
|
const options = hasScope || this.evaluateOptions
|
|
535
534
|
? { ...(hasScope ? { functions: true } : {}), ...this.evaluateOptions }
|
|
536
535
|
: undefined
|
|
536
|
+
|
|
537
|
+
// When functions are enabled and the user hasn't provided their own
|
|
538
|
+
// `generate` (escodegen), inject our safe AST-interpreting visitors
|
|
539
|
+
// that handle ArrowFunctionExpression and FunctionExpression without
|
|
540
|
+
// using `new Function()` or `eval()`. This makes arrow function
|
|
541
|
+
// callbacks like `.map(x => x.name)` work in Cloudflare Workers.
|
|
542
|
+
if (options && options.functions && !options.generate) {
|
|
543
|
+
;(options as any).visitors = {
|
|
544
|
+
...(options as any).visitors,
|
|
545
|
+
...createSafeFunctionVisitors(),
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
537
549
|
return Evaluate.evaluate.sync(expression, context, options)
|
|
538
550
|
}
|
|
539
551
|
|
|
@@ -1068,30 +1080,11 @@ export class MdastToJsx {
|
|
|
1068
1080
|
return []
|
|
1069
1081
|
}
|
|
1070
1082
|
case 'html': {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// Parse HTML to MDX AST using the new approach - always returns an array
|
|
1079
|
-
const mdxAst = htmlToMdxAst({
|
|
1080
|
-
html: text,
|
|
1081
|
-
parentType: parentType || 'root',
|
|
1082
|
-
convertTagName: ({ tagName }) => {
|
|
1083
|
-
const lowerTag = tagName.toLowerCase()
|
|
1084
|
-
// Only keep valid HTML elements
|
|
1085
|
-
if (validHtmlElements.has(lowerTag)) {
|
|
1086
|
-
return lowerTag
|
|
1087
|
-
}
|
|
1088
|
-
// Return empty string for non-HTML elements
|
|
1089
|
-
return ''
|
|
1090
|
-
}
|
|
1091
|
-
})
|
|
1092
|
-
|
|
1093
|
-
// Process the MDX AST nodes
|
|
1094
|
-
return mdxAst.map(child => this.mdastTransformer(child, 'html'))
|
|
1083
|
+
// html nodes appear when rendering plain markdown (not MDX) without
|
|
1084
|
+
// the remarkHtmlToMdx pre-processing plugin. They are intentionally
|
|
1085
|
+
// ignored here — use remarkHtmlToMdx from 'safe-mdx/markdown' to convert
|
|
1086
|
+
// them to mdxJsx nodes before passing the AST to MdastToJsx.
|
|
1087
|
+
return []
|
|
1095
1088
|
}
|
|
1096
1089
|
case 'imageReference': {
|
|
1097
1090
|
return []
|
|
@@ -1145,3 +1138,236 @@ export function mdastBfs(
|
|
|
1145
1138
|
type ComponentsMap = { [k in (typeof nativeTags)[number]]?: any } & {
|
|
1146
1139
|
[key: string]: any
|
|
1147
1140
|
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Bind function parameters to argument values, handling Identifier,
|
|
1144
|
+
* ObjectPattern, ArrayPattern, RestElement, and AssignmentPattern nodes.
|
|
1145
|
+
* Writes bindings into `ctx` in place.
|
|
1146
|
+
*/
|
|
1147
|
+
function bindParams(
|
|
1148
|
+
params: any[],
|
|
1149
|
+
args: any[],
|
|
1150
|
+
ctx: Record<string, any>,
|
|
1151
|
+
visit: (node: any, context: any, parent?: any) => any,
|
|
1152
|
+
) {
|
|
1153
|
+
for (let i = 0; i < params.length; i++) {
|
|
1154
|
+
const param = params[i]
|
|
1155
|
+
switch (param.type) {
|
|
1156
|
+
case 'Identifier':
|
|
1157
|
+
ctx[param.name] = args[i]
|
|
1158
|
+
break
|
|
1159
|
+
case 'RestElement':
|
|
1160
|
+
if (param.argument.type === 'Identifier') {
|
|
1161
|
+
ctx[param.argument.name] = args.slice(i)
|
|
1162
|
+
}
|
|
1163
|
+
break
|
|
1164
|
+
case 'AssignmentPattern': {
|
|
1165
|
+
const val =
|
|
1166
|
+
args[i] !== undefined
|
|
1167
|
+
? args[i]
|
|
1168
|
+
: visit(param.right, ctx, param)
|
|
1169
|
+
if (param.left.type === 'Identifier') {
|
|
1170
|
+
ctx[param.left.name] = val
|
|
1171
|
+
}
|
|
1172
|
+
break
|
|
1173
|
+
}
|
|
1174
|
+
case 'ObjectPattern': {
|
|
1175
|
+
const obj = args[i] || {}
|
|
1176
|
+
for (const prop of param.properties) {
|
|
1177
|
+
if (prop.type === 'RestElement') {
|
|
1178
|
+
const used = new Set(
|
|
1179
|
+
param.properties
|
|
1180
|
+
.filter((p: any) => p !== prop)
|
|
1181
|
+
.map(
|
|
1182
|
+
(p: any) =>
|
|
1183
|
+
p.key?.name ?? p.key?.value,
|
|
1184
|
+
),
|
|
1185
|
+
)
|
|
1186
|
+
const rest: Record<string, any> = {}
|
|
1187
|
+
for (const key of Object.keys(obj)) {
|
|
1188
|
+
if (!used.has(key)) rest[key] = obj[key]
|
|
1189
|
+
}
|
|
1190
|
+
if (prop.argument.type === 'Identifier') {
|
|
1191
|
+
ctx[prop.argument.name] = rest
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
const key =
|
|
1195
|
+
prop.key.type === 'Identifier'
|
|
1196
|
+
? prop.key.name
|
|
1197
|
+
: prop.key.value
|
|
1198
|
+
if (prop.value.type === 'Identifier') {
|
|
1199
|
+
ctx[prop.value.name] = obj[key]
|
|
1200
|
+
} else if (
|
|
1201
|
+
prop.value.type === 'AssignmentPattern'
|
|
1202
|
+
) {
|
|
1203
|
+
const val =
|
|
1204
|
+
obj[key] !== undefined
|
|
1205
|
+
? obj[key]
|
|
1206
|
+
: visit(
|
|
1207
|
+
prop.value.right,
|
|
1208
|
+
ctx,
|
|
1209
|
+
prop.value,
|
|
1210
|
+
)
|
|
1211
|
+
if (
|
|
1212
|
+
prop.value.left.type === 'Identifier'
|
|
1213
|
+
) {
|
|
1214
|
+
ctx[prop.value.left.name] = val
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
break
|
|
1220
|
+
}
|
|
1221
|
+
case 'ArrayPattern': {
|
|
1222
|
+
const arr = args[i] || []
|
|
1223
|
+
for (let j = 0; j < param.elements.length; j++) {
|
|
1224
|
+
const elem = param.elements[j]
|
|
1225
|
+
if (!elem) continue
|
|
1226
|
+
if (elem.type === 'Identifier') {
|
|
1227
|
+
ctx[elem.name] = arr[j]
|
|
1228
|
+
} else if (
|
|
1229
|
+
elem.type === 'RestElement' &&
|
|
1230
|
+
elem.argument.type === 'Identifier'
|
|
1231
|
+
) {
|
|
1232
|
+
ctx[elem.argument.name] = arr.slice(j)
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
break
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Sentinel value to signal a return from inside a block body
|
|
1242
|
+
const RETURN_SENTINEL = Symbol('return')
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Execute a block statement body (array of statements) using the
|
|
1246
|
+
* eval-estree-expression visitor's `this.visit`. Returns the value
|
|
1247
|
+
* from the first ReturnStatement encountered, or undefined.
|
|
1248
|
+
*/
|
|
1249
|
+
function executeBlockBody(
|
|
1250
|
+
body: any[],
|
|
1251
|
+
ctx: Record<string, any>,
|
|
1252
|
+
visit: (node: any, context: any, parent?: any) => any,
|
|
1253
|
+
parentNode: any,
|
|
1254
|
+
): any {
|
|
1255
|
+
for (const stmt of body) {
|
|
1256
|
+
switch (stmt.type) {
|
|
1257
|
+
case 'ReturnStatement':
|
|
1258
|
+
return stmt.argument
|
|
1259
|
+
? visit(stmt.argument, ctx, stmt)
|
|
1260
|
+
: undefined
|
|
1261
|
+
case 'ExpressionStatement':
|
|
1262
|
+
visit(stmt.expression, ctx, stmt)
|
|
1263
|
+
break
|
|
1264
|
+
case 'VariableDeclaration':
|
|
1265
|
+
for (const decl of stmt.declarations) {
|
|
1266
|
+
const value = decl.init
|
|
1267
|
+
? visit(decl.init, ctx, decl)
|
|
1268
|
+
: undefined
|
|
1269
|
+
if (decl.id.type === 'Identifier') {
|
|
1270
|
+
ctx[decl.id.name] = value
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
break
|
|
1274
|
+
case 'IfStatement': {
|
|
1275
|
+
const test = visit(stmt.test, ctx, stmt)
|
|
1276
|
+
if (test) {
|
|
1277
|
+
if (stmt.consequent.type === 'BlockStatement') {
|
|
1278
|
+
const result = executeBlockBody(
|
|
1279
|
+
stmt.consequent.body,
|
|
1280
|
+
ctx,
|
|
1281
|
+
visit,
|
|
1282
|
+
stmt,
|
|
1283
|
+
)
|
|
1284
|
+
if (result !== undefined) return result
|
|
1285
|
+
} else if (
|
|
1286
|
+
stmt.consequent.type === 'ReturnStatement'
|
|
1287
|
+
) {
|
|
1288
|
+
return stmt.consequent.argument
|
|
1289
|
+
? visit(
|
|
1290
|
+
stmt.consequent.argument,
|
|
1291
|
+
ctx,
|
|
1292
|
+
stmt.consequent,
|
|
1293
|
+
)
|
|
1294
|
+
: undefined
|
|
1295
|
+
} else {
|
|
1296
|
+
visit(stmt.consequent, ctx, stmt)
|
|
1297
|
+
}
|
|
1298
|
+
} else if (stmt.alternate) {
|
|
1299
|
+
if (stmt.alternate.type === 'BlockStatement') {
|
|
1300
|
+
const result = executeBlockBody(
|
|
1301
|
+
stmt.alternate.body,
|
|
1302
|
+
ctx,
|
|
1303
|
+
visit,
|
|
1304
|
+
stmt,
|
|
1305
|
+
)
|
|
1306
|
+
if (result !== undefined) return result
|
|
1307
|
+
} else if (
|
|
1308
|
+
stmt.alternate.type === 'ReturnStatement'
|
|
1309
|
+
) {
|
|
1310
|
+
return stmt.alternate.argument
|
|
1311
|
+
? visit(
|
|
1312
|
+
stmt.alternate.argument,
|
|
1313
|
+
ctx,
|
|
1314
|
+
stmt.alternate,
|
|
1315
|
+
)
|
|
1316
|
+
: undefined
|
|
1317
|
+
} else {
|
|
1318
|
+
visit(stmt.alternate, ctx, stmt)
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
break
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return undefined
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Custom visitors for eval-estree-expression that interpret arrow functions
|
|
1330
|
+
* and function expressions by walking the AST recursively, without using
|
|
1331
|
+
* `new Function()` or `eval()`. This makes them safe for Cloudflare Workers
|
|
1332
|
+
* and other edge runtimes that block dynamic code evaluation.
|
|
1333
|
+
*
|
|
1334
|
+
* The visitors are called with `this` bound to the Expression evaluator
|
|
1335
|
+
* instance, giving access to `this.visit()` for recursive evaluation.
|
|
1336
|
+
*/
|
|
1337
|
+
export function createSafeFunctionVisitors() {
|
|
1338
|
+
// Using a regular function (not arrow) so `this` is the Expression instance
|
|
1339
|
+
function functionExpressionVisitor(
|
|
1340
|
+
this: any,
|
|
1341
|
+
node: any,
|
|
1342
|
+
context: any,
|
|
1343
|
+
) {
|
|
1344
|
+
const self = this
|
|
1345
|
+
return function (this: any, ...args: any[]) {
|
|
1346
|
+
const newContext = { ...context }
|
|
1347
|
+
bindParams(node.params, args, newContext, (n, ctx, p) =>
|
|
1348
|
+
self.visit(n, ctx, p),
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
if (
|
|
1352
|
+
node.expression ||
|
|
1353
|
+
node.body.type !== 'BlockStatement'
|
|
1354
|
+
) {
|
|
1355
|
+
// Expression body: x => x.name
|
|
1356
|
+
return self.visit(node.body, newContext, node)
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Block body: x => { ... return ... }
|
|
1360
|
+
return executeBlockBody(
|
|
1361
|
+
node.body.body,
|
|
1362
|
+
newContext,
|
|
1363
|
+
(n, ctx, p) => self.visit(n, ctx, p),
|
|
1364
|
+
node,
|
|
1365
|
+
)
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
return {
|
|
1370
|
+
ArrowFunctionExpression: functionExpressionVisitor,
|
|
1371
|
+
FunctionExpression: functionExpressionVisitor,
|
|
1372
|
+
}
|
|
1373
|
+
}
|