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.
Files changed (43) hide show
  1. package/README.md +78 -12
  2. package/dist/html/html-and-md.test.js +14 -41
  3. package/dist/html/html-and-md.test.js.map +1 -1
  4. package/dist/html/html-to-mdx-ast.d.ts +26 -1
  5. package/dist/html/html-to-mdx-ast.d.ts.map +1 -1
  6. package/dist/html/html-to-mdx-ast.js +40 -0
  7. package/dist/html/html-to-mdx-ast.js.map +1 -1
  8. package/dist/incremental-parse.d.ts +41 -0
  9. package/dist/incremental-parse.d.ts.map +1 -0
  10. package/dist/incremental-parse.js +139 -0
  11. package/dist/incremental-parse.js.map +1 -0
  12. package/dist/incremental-parse.test.d.ts +2 -0
  13. package/dist/incremental-parse.test.d.ts.map +1 -0
  14. package/dist/incremental-parse.test.js +299 -0
  15. package/dist/incremental-parse.test.js.map +1 -0
  16. package/dist/markdown-html.test.d.ts +2 -0
  17. package/dist/markdown-html.test.d.ts.map +1 -0
  18. package/dist/markdown-html.test.js +129 -0
  19. package/dist/markdown-html.test.js.map +1 -0
  20. package/dist/markdown.d.ts +3 -0
  21. package/dist/markdown.d.ts.map +1 -0
  22. package/dist/markdown.js +4 -0
  23. package/dist/markdown.js.map +1 -0
  24. package/dist/parse.d.ts +9 -2
  25. package/dist/parse.d.ts.map +1 -1
  26. package/dist/parse.js +24 -12
  27. package/dist/parse.js.map +1 -1
  28. package/dist/safe-mdx.d.ts +13 -0
  29. package/dist/safe-mdx.d.ts.map +1 -1
  30. package/dist/safe-mdx.js +193 -24
  31. package/dist/safe-mdx.js.map +1 -1
  32. package/dist/safe-mdx.test.js +284 -11
  33. package/dist/safe-mdx.test.js.map +1 -1
  34. package/package.json +9 -1
  35. package/src/html/html-and-md.test.ts +15 -47
  36. package/src/html/html-to-mdx-ast.ts +53 -1
  37. package/src/incremental-parse.test.ts +315 -0
  38. package/src/incremental-parse.ts +219 -0
  39. package/src/markdown-html.test.tsx +144 -0
  40. package/src/markdown.ts +4 -0
  41. package/src/parse.ts +36 -13
  42. package/src/safe-mdx.test.tsx +357 -11
  43. package/src/safe-mdx.tsx +252 -26
package/src/parse.ts CHANGED
@@ -5,9 +5,9 @@ import { Root, RootContent } from 'mdast'
5
5
  import { remark } from 'remark'
6
6
  import remarkGfm from 'remark-gfm'
7
7
  import remarkMdx from 'remark-mdx'
8
- import { parseHtmlToMdxAst, remarkMdxJsxNormalize } from './html/html-to-mdx-ast.ts'
8
+ import { remarkMdxJsxNormalize } from './html/remark-mdx-jsx-normalize.ts'
9
9
 
10
- export { parseHtmlToMdxAst, remarkMdxJsxNormalize }
10
+ export { remarkMdxJsxNormalize }
11
11
 
12
12
  /* ── Import extraction ──────────────────────────────────────────────── */
13
13
 
@@ -34,7 +34,7 @@ export function extractImports(ast: Root): MdxImport[] {
34
34
 
35
35
  for (const node of ast.children) {
36
36
  if (node.type !== 'mdxjsEsm') continue
37
- const estree = (node as any).data?.estree
37
+ const estree = node.data?.estree
38
38
  if (!estree) continue
39
39
 
40
40
  for (const statement of estree.body) {
@@ -70,6 +70,38 @@ export function mdxParse(code: string) {
70
70
  return file.data.ast as Root
71
71
  }
72
72
 
73
+ export type MdxProcessorOptions = {
74
+ /** Extra remark plugins appended after safe-mdx's default MDX, frontmatter, and GFM parsers. */
75
+ remarkPlugins?: any[]
76
+ }
77
+
78
+ export function createMdxProcessor({
79
+ remarkPlugins = [],
80
+ }: MdxProcessorOptions = {}) {
81
+ const processor = remark()
82
+ .use(remarkMdx)
83
+ .use(remarkFrontmatter, ['yaml', 'toml'])
84
+ .use(remarkGfm)
85
+
86
+ for (const plugin of remarkPlugins) {
87
+ if (Array.isArray(plugin)) {
88
+ processor.use(plugin[0], ...plugin.slice(1))
89
+ } else {
90
+ processor.use(plugin)
91
+ }
92
+ }
93
+
94
+ return processor
95
+ .use(remarkMarkAndUnravel)
96
+ .use(() => {
97
+ return (tree, file) => {
98
+ file.data.ast = tree
99
+ }
100
+ })
101
+ }
102
+
103
+ export type MdxProcessor = ReturnType<typeof createMdxProcessor>
104
+
73
105
  /**
74
106
  * https://github.com/mdx-js/mdx/blob/b3351fadcb6f78833a72757b7135dcfb8ab646fe/packages/mdx/lib/plugin/remark-mark-and-unravel.js
75
107
  * A tiny plugin that unravels `<p><h1>x</h1></p>` but also
@@ -262,13 +294,4 @@ export async function resolveModules({
262
294
  return result
263
295
  }
264
296
 
265
- const mdxProcessor = remark()
266
- .use(remarkMdx)
267
- .use(remarkFrontmatter, ['yaml', 'toml'])
268
- .use(remarkGfm)
269
- .use(remarkMarkAndUnravel)
270
- .use(() => {
271
- return (tree, file) => {
272
- file.data.ast = tree
273
- }
274
- })
297
+ export const mdxProcessor = createMdxProcessor()
@@ -3818,7 +3818,7 @@ test('scope with function in spread attribute', () => {
3818
3818
  expect(html).toMatchInlineSnapshot(`"<h1>Spread test</h1>"`)
3819
3819
  })
3820
3820
 
3821
- test('scope with .map and arrow function callback fails without generate', () => {
3821
+ test('scope with .map and arrow function callback works without generate (safe interpreter)', () => {
3822
3822
  const scope = {
3823
3823
  items: [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }],
3824
3824
  }
@@ -3828,16 +3828,8 @@ test('scope with .map and arrow function callback fails without generate', () =>
3828
3828
  `
3829
3829
 
3830
3830
  const { html, errors } = render(code, undefined, undefined, undefined, scope)
3831
- expect(errors).toMatchInlineSnapshot(`
3832
- [
3833
- {
3834
- "line": 1,
3835
- "message": "Failed to evaluate expression: items.map(item => item.name).join(", "). Expected options.generate to be the "generate" function from "escodegen"",
3836
- "type": "expression",
3837
- },
3838
- ]
3839
- `)
3840
- expect(html).toMatchInlineSnapshot(`""`)
3831
+ expect(errors).toMatchInlineSnapshot(`[]`)
3832
+ expect(html).toMatchInlineSnapshot(`"Alice, Bob, Charlie"`)
3841
3833
  })
3842
3834
 
3843
3835
  test('scope with .map and arrow function callback works with generate', () => {
@@ -3853,3 +3845,357 @@ test('scope with .map and arrow function callback works with generate', () => {
3853
3845
  expect(errors).toMatchInlineSnapshot(`[]`)
3854
3846
  expect(html).toMatchInlineSnapshot(`"Alice, Bob, Charlie"`)
3855
3847
  })
3848
+
3849
+ test('safe interpreter: arrow with block body and return', () => {
3850
+ const scope = {
3851
+ items: [1, 2, 3],
3852
+ }
3853
+
3854
+ const code = dedent`
3855
+ {items.map(x => { return x * 2 }).join(", ")}
3856
+ `
3857
+
3858
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3859
+ expect(errors).toMatchInlineSnapshot(`[]`)
3860
+ expect(html).toMatchInlineSnapshot(`"2, 4, 6"`)
3861
+ })
3862
+
3863
+ test('safe interpreter: arrow with multiple params', () => {
3864
+ const scope = {
3865
+ items: ['a', 'b', 'c'],
3866
+ }
3867
+
3868
+ const code = dedent`
3869
+ {items.map((item, i) => i + ":" + item).join(", ")}
3870
+ `
3871
+
3872
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3873
+ expect(errors).toMatchInlineSnapshot(`[]`)
3874
+ expect(html).toMatchInlineSnapshot(`"0:a, 1:b, 2:c"`)
3875
+ })
3876
+
3877
+ test('safe interpreter: arrow with object destructuring', () => {
3878
+ const scope = {
3879
+ items: [
3880
+ { name: 'Alice', age: 30 },
3881
+ { name: 'Bob', age: 25 },
3882
+ ],
3883
+ }
3884
+
3885
+ const code = dedent`
3886
+ {items.map(({ name, age }) => name + "(" + age + ")").join(", ")}
3887
+ `
3888
+
3889
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3890
+ expect(errors).toMatchInlineSnapshot(`[]`)
3891
+ expect(html).toMatchInlineSnapshot(`"Alice(30), Bob(25)"`)
3892
+ })
3893
+
3894
+ test('safe interpreter: arrow with ternary expression', () => {
3895
+ const scope = {
3896
+ items: [
3897
+ { name: 'Alice', active: true },
3898
+ { name: 'Bob', active: false },
3899
+ ],
3900
+ }
3901
+
3902
+ const code = dedent`
3903
+ {items.map(item => item.active ? item.name : "inactive").join(", ")}
3904
+ `
3905
+
3906
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3907
+ expect(errors).toMatchInlineSnapshot(`[]`)
3908
+ expect(html).toMatchInlineSnapshot(`"Alice, inactive"`)
3909
+ })
3910
+
3911
+ test('safe interpreter: .filter with arrow function', () => {
3912
+ const scope = {
3913
+ items: [1, 2, 3, 4, 5, 6],
3914
+ }
3915
+
3916
+ const code = dedent`
3917
+ {items.filter(x => x > 3).join(", ")}
3918
+ `
3919
+
3920
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3921
+ expect(errors).toMatchInlineSnapshot(`[]`)
3922
+ expect(html).toMatchInlineSnapshot(`"4, 5, 6"`)
3923
+ })
3924
+
3925
+ test('safe interpreter: .reduce with arrow function', () => {
3926
+ const scope = {
3927
+ items: [1, 2, 3, 4],
3928
+ }
3929
+
3930
+ const code = dedent`
3931
+ {items.reduce((acc, x) => acc + x, 0)}
3932
+ `
3933
+
3934
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3935
+ expect(errors).toMatchInlineSnapshot(`[]`)
3936
+ expect(html).toMatchInlineSnapshot(`"10"`)
3937
+ })
3938
+
3939
+ test('safe interpreter: chained .filter.map', () => {
3940
+ const scope = {
3941
+ users: [
3942
+ { name: 'Alice', role: 'admin' },
3943
+ { name: 'Bob', role: 'user' },
3944
+ { name: 'Charlie', role: 'admin' },
3945
+ ],
3946
+ }
3947
+
3948
+ const code = dedent`
3949
+ {users.filter(u => u.role === "admin").map(u => u.name).join(", ")}
3950
+ `
3951
+
3952
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3953
+ expect(errors).toMatchInlineSnapshot(`[]`)
3954
+ expect(html).toMatchInlineSnapshot(`"Alice, Charlie"`)
3955
+ })
3956
+
3957
+ test('safe interpreter: .find with arrow function', () => {
3958
+ const scope = {
3959
+ items: [
3960
+ { id: 1, name: 'Alice' },
3961
+ { id: 2, name: 'Bob' },
3962
+ ],
3963
+ }
3964
+
3965
+ const code = dedent`
3966
+ {items.find(item => item.id === 2).name}
3967
+ `
3968
+
3969
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3970
+ expect(errors).toMatchInlineSnapshot(`[]`)
3971
+ expect(html).toMatchInlineSnapshot(`"Bob"`)
3972
+ })
3973
+
3974
+ test('safe interpreter: .some and .every with arrow functions', () => {
3975
+ const scope = {
3976
+ nums: [2, 4, 6],
3977
+ }
3978
+
3979
+ const code = dedent`
3980
+ {nums.every(n => n > 0) ? "all positive" : "nope"}
3981
+ `
3982
+
3983
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3984
+ expect(errors).toMatchInlineSnapshot(`[]`)
3985
+ expect(html).toMatchInlineSnapshot(`"all positive"`)
3986
+ })
3987
+
3988
+ test('safe interpreter: nested arrow functions', () => {
3989
+ const scope = {
3990
+ matrix: [[1, 2], [3, 4], [5, 6]],
3991
+ }
3992
+
3993
+ const code = dedent`
3994
+ {matrix.map(row => row.map(x => x * 10).join("-")).join(", ")}
3995
+ `
3996
+
3997
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
3998
+ expect(errors).toMatchInlineSnapshot(`[]`)
3999
+ expect(html).toMatchInlineSnapshot(`"10-20, 30-40, 50-60"`)
4000
+ })
4001
+
4002
+ test('safe interpreter: arrow accessing outer scope variables', () => {
4003
+ const scope = {
4004
+ items: [1, 2, 3],
4005
+ multiplier: 5,
4006
+ }
4007
+
4008
+ const code = dedent`
4009
+ {items.map(x => x * multiplier).join(", ")}
4010
+ `
4011
+
4012
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4013
+ expect(errors).toMatchInlineSnapshot(`[]`)
4014
+ expect(html).toMatchInlineSnapshot(`"5, 10, 15"`)
4015
+ })
4016
+
4017
+ test('safe interpreter: arrow with block body and variable declaration', () => {
4018
+ const scope = {
4019
+ items: [{ first: 'John', last: 'Doe' }, { first: 'Jane', last: 'Smith' }],
4020
+ }
4021
+
4022
+ const code = dedent`
4023
+ {items.map(item => { const full = item.first + " " + item.last; return full }).join(", ")}
4024
+ `
4025
+
4026
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4027
+ expect(errors).toMatchInlineSnapshot(`[]`)
4028
+ expect(html).toMatchInlineSnapshot(`"John Doe, Jane Smith"`)
4029
+ })
4030
+
4031
+ test('safe interpreter: arrow with if/else in block body', () => {
4032
+ const scope = {
4033
+ items: [1, 2, 3, 4, 5],
4034
+ }
4035
+
4036
+ const code = dedent`
4037
+ {items.map(x => { if (x > 3) { return "big" } else { return "small" } }).join(", ")}
4038
+ `
4039
+
4040
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4041
+ expect(errors).toMatchInlineSnapshot(`[]`)
4042
+ expect(html).toMatchInlineSnapshot(`"small, small, small, big, big"`)
4043
+ })
4044
+
4045
+ test('safe interpreter: .sort with comparator arrow', () => {
4046
+ const scope = {
4047
+ items: [3, 1, 4, 1, 5],
4048
+ }
4049
+
4050
+ const code = dedent`
4051
+ {items.sort((a, b) => a - b).join(", ")}
4052
+ `
4053
+
4054
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4055
+ expect(errors).toMatchInlineSnapshot(`[]`)
4056
+ expect(html).toMatchInlineSnapshot(`"1, 1, 3, 4, 5"`)
4057
+ })
4058
+
4059
+ test('safe interpreter: arrow in JSX attribute', () => {
4060
+ const scope = {
4061
+ items: [{ name: 'Alice' }, { name: 'Bob' }],
4062
+ }
4063
+
4064
+ const code = dedent`
4065
+ <Heading level={items.map(i => i.name).join(", ")}>Title</Heading>
4066
+ `
4067
+
4068
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4069
+ expect(errors).toMatchInlineSnapshot(`[]`)
4070
+ expect(html).toMatchInlineSnapshot(`"<h1>Title</h1>"`)
4071
+ })
4072
+
4073
+ test('safe interpreter: arrow with array destructuring', () => {
4074
+ const scope = {
4075
+ pairs: [['a', 1], ['b', 2], ['c', 3]],
4076
+ }
4077
+
4078
+ const code = dedent`
4079
+ {pairs.map(([letter, num]) => letter + num).join(", ")}
4080
+ `
4081
+
4082
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4083
+ expect(errors).toMatchInlineSnapshot(`[]`)
4084
+ expect(html).toMatchInlineSnapshot(`"a1, b2, c3"`)
4085
+ })
4086
+
4087
+ test('safe interpreter: calling scope functions inside arrow callback', () => {
4088
+ const scope = {
4089
+ items: [{ name: 'alice' }, { name: 'bob' }],
4090
+ formatName: (s: string) => s.toUpperCase(),
4091
+ }
4092
+
4093
+ const code = dedent`
4094
+ {items.map(item => formatName(item.name)).join(", ")}
4095
+ `
4096
+
4097
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4098
+ expect(errors).toMatchInlineSnapshot(`[]`)
4099
+ expect(html).toMatchInlineSnapshot(`"ALICE, BOB"`)
4100
+ })
4101
+
4102
+ test('safe interpreter: calling scope function with multiple args inside arrow', () => {
4103
+ const scope = {
4104
+ items: [1, 2, 3],
4105
+ add: (a: number, b: number) => a + b,
4106
+ base: 10,
4107
+ }
4108
+
4109
+ const code = dedent`
4110
+ {items.map(x => add(x, base)).join(", ")}
4111
+ `
4112
+
4113
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4114
+ expect(errors).toMatchInlineSnapshot(`[]`)
4115
+ expect(html).toMatchInlineSnapshot(`"11, 12, 13"`)
4116
+ })
4117
+
4118
+ test('safe interpreter: scope function returning object used in arrow', () => {
4119
+ const scope = {
4120
+ ids: [1, 2, 3],
4121
+ getUser: (id: number) => ({ id, name: 'User' + id }),
4122
+ }
4123
+
4124
+ const code = dedent`
4125
+ {ids.map(id => getUser(id).name).join(", ")}
4126
+ `
4127
+
4128
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4129
+ expect(errors).toMatchInlineSnapshot(`[]`)
4130
+ expect(html).toMatchInlineSnapshot(`"User1, User2, User3"`)
4131
+ })
4132
+
4133
+ test('safe interpreter: arrow with default parameter', () => {
4134
+ const scope = {
4135
+ items: [undefined, 'hello', undefined],
4136
+ fallback: 'default',
4137
+ }
4138
+
4139
+ const code = dedent`
4140
+ {items.map((x = fallback) => x).join(", ")}
4141
+ `
4142
+
4143
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4144
+ expect(errors).toMatchInlineSnapshot(`[]`)
4145
+ expect(html).toMatchInlineSnapshot(`"default, hello, default"`)
4146
+ })
4147
+
4148
+ test('scope with template literal in expression', () => {
4149
+ const scope = {
4150
+ name: 'World',
4151
+ count: 3,
4152
+ }
4153
+
4154
+ const code = dedent`
4155
+ {${'`'}Hello ${'${'}name${'}'}, you have ${'${'}count${'}'} items${'`'}}
4156
+ `
4157
+
4158
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4159
+ expect(errors).toMatchInlineSnapshot(`[]`)
4160
+ expect(html).toMatchInlineSnapshot(`"Hello World, you have 3 items"`)
4161
+ })
4162
+
4163
+ test('scope with tagged template literal function', () => {
4164
+ const myTag = (strings: TemplateStringsArray, ...values: any[]) => {
4165
+ return strings.reduce((result, str, i) => {
4166
+ return result + str + (values[i] !== undefined ? String(values[i]).toUpperCase() : '')
4167
+ }, '')
4168
+ }
4169
+
4170
+ const scope = {
4171
+ myTag,
4172
+ name: 'world',
4173
+ }
4174
+
4175
+ const code = `{myTag${'`'}hello ${'${'}name${'}'}${'`'}}`
4176
+
4177
+ const { html, errors } = render(code, undefined, undefined, undefined, scope, { generate })
4178
+ expect(errors).toMatchInlineSnapshot(`[]`)
4179
+ expect(html).toMatchInlineSnapshot(`"hello WORLD"`)
4180
+ })
4181
+
4182
+ test('scope with tagged template literal without generate', () => {
4183
+ const myTag = (strings: TemplateStringsArray, ...values: any[]) => {
4184
+ return strings.reduce((result, str, i) => {
4185
+ return result + str + (values[i] !== undefined ? String(values[i]).toUpperCase() : '')
4186
+ }, '')
4187
+ }
4188
+
4189
+ const scope = {
4190
+ myTag,
4191
+ name: 'world',
4192
+ }
4193
+
4194
+ const code = `{myTag${'`'}hello ${'${'}name${'}'}${'`'}}`
4195
+
4196
+ const { html, errors } = render(code, undefined, undefined, undefined, scope)
4197
+ expect(errors).toMatchInlineSnapshot(`[]`)
4198
+ expect(html).toMatchInlineSnapshot(`"hello WORLD"`)
4199
+ })
4200
+
4201
+