safe-mdx 1.3.2 → 1.3.3
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 +14 -14
- package/dist/assets/HtmlToJsxConverter-Ds0bTjpw.js +24 -0
- package/dist/assets/_commonjsHelpers-CqkleIqs.js +1 -0
- package/dist/assets/index-B5fPOjPt.css +1 -0
- package/dist/assets/index-B7ATSoRE.js +9 -0
- package/dist/assets/index-BwZ2FTRd.js +146 -0
- package/dist/assets/index-R1UqLMGJ.js +1 -0
- package/dist/assets/index-c0qeY2gs.js +9 -0
- package/dist/assets/jsx-runtime-BhZZLbvw.js +9 -0
- package/dist/assets/jsx-runtime-NArryeSM.js +1 -0
- package/dist/assets/react-Ca6JzGpx.js +1 -0
- package/dist/assets/react-dom-BYRHYqYl.js +1 -0
- package/dist/html/attributes.d.ts +19 -0
- package/dist/html/attributes.d.ts.map +1 -0
- package/dist/html/attributes.js +289 -0
- package/dist/html/attributes.js.map +1 -0
- package/dist/html/convert-attributes.d.ts +6 -0
- package/dist/html/convert-attributes.d.ts.map +1 -0
- package/dist/html/convert-attributes.js +43 -0
- package/dist/html/convert-attributes.js.map +1 -0
- package/dist/html/domparser-browser.d.ts +4 -0
- package/dist/html/domparser-browser.d.ts.map +1 -0
- package/dist/html/domparser-browser.js +7 -0
- package/dist/html/domparser-browser.js.map +1 -0
- package/dist/html/domparser.d.ts +2 -0
- package/dist/html/domparser.d.ts.map +1 -0
- package/dist/html/domparser.js +5 -0
- package/dist/html/domparser.js.map +1 -0
- package/dist/html/html-to-mdx-ast.d.ts +23 -0
- package/dist/html/html-to-mdx-ast.d.ts.map +1 -0
- package/dist/html/html-to-mdx-ast.js +227 -0
- package/dist/html/html-to-mdx-ast.js.map +1 -0
- package/dist/html/html-to-mdx-ast.test.d.ts +2 -0
- package/dist/html/html-to-mdx-ast.test.d.ts.map +1 -0
- package/dist/html/html-to-mdx-ast.test.js +324 -0
- package/dist/html/html-to-mdx-ast.test.js.map +1 -0
- package/dist/html/remark-mdx-jsx-normalize.d.ts +10 -0
- package/dist/html/remark-mdx-jsx-normalize.d.ts.map +1 -0
- package/dist/html/remark-mdx-jsx-normalize.js +117 -0
- package/dist/html/remark-mdx-jsx-normalize.js.map +1 -0
- package/dist/html/valid-html-elements.d.ts +10 -0
- package/dist/html/valid-html-elements.d.ts.map +1 -0
- package/dist/html/valid-html-elements.js +50 -0
- package/dist/html/valid-html-elements.js.map +1 -0
- package/dist/index.html +19 -0
- package/dist/parse.d.ts +2 -0
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +2 -0
- package/dist/parse.js.map +1 -1
- package/dist/safe-mdx.d.ts +1 -1
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +23 -71
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +161 -8
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +27 -6
- package/src/html/README +17 -0
- package/src/html/attributes.ts +297 -0
- package/src/html/convert-attributes.ts +59 -0
- package/src/html/domparser-browser.ts +6 -0
- package/src/html/domparser.ts +5 -0
- package/src/html/html-to-mdx-ast.test.ts +365 -0
- package/src/html/html-to-mdx-ast.ts +304 -0
- package/src/html/remark-mdx-jsx-normalize.ts +128 -0
- package/src/html/valid-html-elements.ts +65 -0
- package/src/parse.ts +3 -0
- package/src/safe-mdx.test.tsx +178 -12
- package/src/safe-mdx.tsx +23 -79
- package/dist/HtmlToJsxConverter.d.ts +0 -10
- package/dist/HtmlToJsxConverter.d.ts.map +0 -1
- package/dist/HtmlToJsxConverter.js +0 -22
- package/dist/HtmlToJsxConverter.js.map +0 -1
- package/dist/plugins.d.ts +0 -12
- package/dist/plugins.d.ts.map +0 -1
- package/dist/plugins.js +0 -68
- package/dist/plugins.js.map +0 -1
- package/src/HtmlToJsxConverter.tsx +0 -37
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
coerceToBooleanAttributes,
|
|
4
|
+
eventHandlerAttributes,
|
|
5
|
+
lowercasedAttributes,
|
|
6
|
+
numberAttributes,
|
|
7
|
+
renamedAttributes,
|
|
8
|
+
styleDontStripPx,
|
|
9
|
+
svgCamelizedAttributes,
|
|
10
|
+
svgCoerceToBooleanAttributes,
|
|
11
|
+
} from "./attributes.js";
|
|
12
|
+
|
|
13
|
+
// Export function to convert HTML attribute name to JSX attribute name
|
|
14
|
+
export function convertAttributeNameToJSX(htmlName: string): string {
|
|
15
|
+
// Check renamed attributes
|
|
16
|
+
for (const [html, jsx] of renamedAttributes) {
|
|
17
|
+
if (html === htmlName) {
|
|
18
|
+
return jsx;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check event handler attributes
|
|
23
|
+
for (const jsxAttribute of eventHandlerAttributes) {
|
|
24
|
+
if (htmlName === jsxAttribute.toLowerCase()) {
|
|
25
|
+
return jsxAttribute;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check lowercased attributes
|
|
30
|
+
for (const jsxAttribute of lowercasedAttributes) {
|
|
31
|
+
if (htmlName === jsxAttribute.toLowerCase()) {
|
|
32
|
+
return jsxAttribute;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check SVG camelized attributes
|
|
37
|
+
for (const [jsxAttribute] of svgCamelizedAttributes) {
|
|
38
|
+
if (htmlName === jsxAttribute) {
|
|
39
|
+
return camelize(htmlName);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return htmlName;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CAMELIZE = /[\-\:]([a-z])/g;
|
|
47
|
+
const capitalize = (token: string) => token[1]!.toUpperCase();
|
|
48
|
+
|
|
49
|
+
const IS_CSS_VARIBLE = /^--\w+/;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts kebab-case or colon:case to camelCase
|
|
53
|
+
*/
|
|
54
|
+
export function camelize(string: string) {
|
|
55
|
+
// Skip the attribute if it is a css variable.
|
|
56
|
+
// It looks something like this: style="--bgColor: red"
|
|
57
|
+
if (IS_CSS_VARIBLE.test(string)) return `"${string}"`;
|
|
58
|
+
return string.replace(CAMELIZE, capitalize);
|
|
59
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { test, expect, describe } from 'vitest'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
4
|
+
import {
|
|
5
|
+
parseHtmlToMdxAst,
|
|
6
|
+
htmlToMdxAst,
|
|
7
|
+
remarkMdxJsxNormalize,
|
|
8
|
+
} from './html-to-mdx-ast.js'
|
|
9
|
+
import { unified } from 'unified'
|
|
10
|
+
import remarkMdx from 'remark-mdx'
|
|
11
|
+
import remarkStringify from 'remark-stringify'
|
|
12
|
+
import remarkParse from 'remark-parse'
|
|
13
|
+
import type { RootContent } from 'mdast'
|
|
14
|
+
import { mdxParse } from '../parse.js'
|
|
15
|
+
import { MdastToJsx } from '../safe-mdx.js'
|
|
16
|
+
|
|
17
|
+
// Default components for testing
|
|
18
|
+
const components = {
|
|
19
|
+
Heading({ level, children, ...props }) {
|
|
20
|
+
return React.createElement('h1', props, children)
|
|
21
|
+
},
|
|
22
|
+
Cards({ level, children, ...props }) {
|
|
23
|
+
return React.createElement('div', props, children)
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Helper to convert HTML to MDX string and rendered HTML
|
|
28
|
+
async function htmlToMdxString({
|
|
29
|
+
html,
|
|
30
|
+
withProcessor = false,
|
|
31
|
+
onError,
|
|
32
|
+
convertTagName,
|
|
33
|
+
convertAttributeValue,
|
|
34
|
+
}: {
|
|
35
|
+
html: string
|
|
36
|
+
withProcessor?: boolean
|
|
37
|
+
onError?: (error: unknown, text: string) => void
|
|
38
|
+
convertTagName?: (args: { tagName: string }) => string
|
|
39
|
+
convertAttributeValue?: (args: {
|
|
40
|
+
name: string
|
|
41
|
+
value: string
|
|
42
|
+
tagName: string
|
|
43
|
+
}) => string
|
|
44
|
+
}): Promise<{ mdx: string; html: string }> {
|
|
45
|
+
// If withProcessor is true, create a textToMdast that parses markdown
|
|
46
|
+
const textToMdast = withProcessor
|
|
47
|
+
? ({ text }: { text: string }) => {
|
|
48
|
+
const markdownProcessor = unified()
|
|
49
|
+
.use(remarkParse)
|
|
50
|
+
.use(remarkMdx)
|
|
51
|
+
const mdast = markdownProcessor.parse(text) as any
|
|
52
|
+
markdownProcessor.runSync(mdast)
|
|
53
|
+
// Return the children of the root node
|
|
54
|
+
return mdast.children || []
|
|
55
|
+
}
|
|
56
|
+
: undefined
|
|
57
|
+
|
|
58
|
+
const mdxAst = parseHtmlToMdxAst({
|
|
59
|
+
html,
|
|
60
|
+
textToMdast,
|
|
61
|
+
onError,
|
|
62
|
+
convertTagName,
|
|
63
|
+
convertAttributeValue,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Generate MDX string
|
|
67
|
+
const processor = unified().use(remarkMdx).use(remarkStringify, {
|
|
68
|
+
// bullet: '-',
|
|
69
|
+
// fence: '`',
|
|
70
|
+
// fences: true
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Create a root node with the content
|
|
74
|
+
const root = {
|
|
75
|
+
type: 'root',
|
|
76
|
+
children: mdxAst,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const mdx = processor.stringify(root as any)
|
|
80
|
+
|
|
81
|
+
// Generate HTML using MdastToJsx like in safe-mdx.test.tsx
|
|
82
|
+
// First parse the MDX to get the full AST
|
|
83
|
+
const mdast = mdxParse(mdx)
|
|
84
|
+
const visitor = new MdastToJsx({ markdown: mdx, mdast, components })
|
|
85
|
+
const jsx = visitor.run()
|
|
86
|
+
const renderedHtml = renderToStaticMarkup(jsx)
|
|
87
|
+
|
|
88
|
+
return { mdx, html: renderedHtml }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('parseHtmlToMdxAst', () => {
|
|
92
|
+
test('parses simple HTML element', () => {
|
|
93
|
+
const result = parseHtmlToMdxAst({ html: '<div>Hello</div>' })
|
|
94
|
+
expect(result).toMatchInlineSnapshot(`
|
|
95
|
+
[
|
|
96
|
+
{
|
|
97
|
+
"attributes": [],
|
|
98
|
+
"children": [
|
|
99
|
+
{
|
|
100
|
+
"type": "text",
|
|
101
|
+
"value": "Hello",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
"name": "div",
|
|
105
|
+
"type": "mdxJsxTextElement",
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
`)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('filters out non-HTML elements when convertTagName returns empty string', () => {
|
|
112
|
+
const result = parseHtmlToMdxAst({
|
|
113
|
+
html: '<custom-element>Hello <span>world</span></custom-element>',
|
|
114
|
+
convertTagName: ({ tagName }) => {
|
|
115
|
+
// Only keep span, filter out custom-element
|
|
116
|
+
if (tagName === 'span') return 'span'
|
|
117
|
+
return tagName
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
expect(result).toMatchInlineSnapshot(`
|
|
121
|
+
[
|
|
122
|
+
{
|
|
123
|
+
"attributes": [],
|
|
124
|
+
"children": [
|
|
125
|
+
{
|
|
126
|
+
"type": "text",
|
|
127
|
+
"value": "Hello ",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"attributes": [],
|
|
131
|
+
"children": [
|
|
132
|
+
{
|
|
133
|
+
"type": "text",
|
|
134
|
+
"value": "world",
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
"name": "span",
|
|
138
|
+
"type": "mdxJsxTextElement",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
"name": "custom-element",
|
|
142
|
+
"type": "mdxJsxTextElement",
|
|
143
|
+
},
|
|
144
|
+
]
|
|
145
|
+
`)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('handles self-closing tags', () => {
|
|
149
|
+
const result = parseHtmlToMdxAst({
|
|
150
|
+
html: '<img src="https://example.com/img.jpg" />',
|
|
151
|
+
})
|
|
152
|
+
expect(result).toMatchInlineSnapshot(`
|
|
153
|
+
[
|
|
154
|
+
{
|
|
155
|
+
"attributes": [
|
|
156
|
+
{
|
|
157
|
+
"name": "src",
|
|
158
|
+
"type": "mdxJsxAttribute",
|
|
159
|
+
"value": "https://example.com/img.jpg",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
"children": [],
|
|
163
|
+
"name": "img",
|
|
164
|
+
"type": "mdxJsxTextElement",
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
`)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('handles span with attributes', () => {
|
|
171
|
+
const result = parseHtmlToMdxAst({
|
|
172
|
+
html: '<span color="blue">colored text</span>',
|
|
173
|
+
})
|
|
174
|
+
expect(result).toMatchInlineSnapshot(`
|
|
175
|
+
[
|
|
176
|
+
{
|
|
177
|
+
"attributes": [
|
|
178
|
+
{
|
|
179
|
+
"name": "color",
|
|
180
|
+
"type": "mdxJsxAttribute",
|
|
181
|
+
"value": "blue",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
"children": [
|
|
185
|
+
{
|
|
186
|
+
"type": "text",
|
|
187
|
+
"value": "colored text",
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
"name": "span",
|
|
191
|
+
"type": "mdxJsxTextElement",
|
|
192
|
+
},
|
|
193
|
+
]
|
|
194
|
+
`)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('handles mixed content', () => {
|
|
198
|
+
const result = parseHtmlToMdxAst({
|
|
199
|
+
html: 'Some text <strong>bold</strong> more text',
|
|
200
|
+
})
|
|
201
|
+
expect(result).toMatchInlineSnapshot(`
|
|
202
|
+
[
|
|
203
|
+
{
|
|
204
|
+
"type": "text",
|
|
205
|
+
"value": "Some text ",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"attributes": [],
|
|
209
|
+
"children": [
|
|
210
|
+
{
|
|
211
|
+
"type": "text",
|
|
212
|
+
"value": "bold",
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
"name": "strong",
|
|
216
|
+
"type": "mdxJsxTextElement",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"type": "text",
|
|
220
|
+
"value": " more text",
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
`)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('handles comments', () => {
|
|
227
|
+
const result = parseHtmlToMdxAst({ html: '<!-- This is a comment -->' })
|
|
228
|
+
expect(result).toMatchInlineSnapshot(`[]`)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('handles table with attributes', () => {
|
|
232
|
+
const result = parseHtmlToMdxAst({
|
|
233
|
+
html: '<table class="data-table"><tr><td>Cell</td></tr></table>',
|
|
234
|
+
})
|
|
235
|
+
expect(result).toMatchInlineSnapshot(`
|
|
236
|
+
[
|
|
237
|
+
{
|
|
238
|
+
"attributes": [
|
|
239
|
+
{
|
|
240
|
+
"name": "className",
|
|
241
|
+
"type": "mdxJsxAttribute",
|
|
242
|
+
"value": "data-table",
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
"children": [
|
|
246
|
+
{
|
|
247
|
+
"attributes": [],
|
|
248
|
+
"children": [
|
|
249
|
+
{
|
|
250
|
+
"attributes": [],
|
|
251
|
+
"children": [
|
|
252
|
+
{
|
|
253
|
+
"type": "text",
|
|
254
|
+
"value": "Cell",
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
"name": "td",
|
|
258
|
+
"type": "mdxJsxTextElement",
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
"name": "tr",
|
|
262
|
+
"type": "mdxJsxTextElement",
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
"name": "table",
|
|
266
|
+
"type": "mdxJsxTextElement",
|
|
267
|
+
},
|
|
268
|
+
]
|
|
269
|
+
`)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('parseHtmlToMdxAst with markdown processor', () => {
|
|
274
|
+
test('parses markdown inside HTML tags', async () => {
|
|
275
|
+
const htmlToConvert = '<div>This is **bold** text</div>'
|
|
276
|
+
const result = await htmlToMdxString({
|
|
277
|
+
html: htmlToConvert,
|
|
278
|
+
withProcessor: true,
|
|
279
|
+
onError: (e) => {
|
|
280
|
+
throw e
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
expect(result).toMatchInlineSnapshot(`
|
|
284
|
+
{
|
|
285
|
+
"html": "<div>This is <strong>bold</strong> text</div>",
|
|
286
|
+
"mdx": "<div>This is **bold** text</div>
|
|
287
|
+
",
|
|
288
|
+
}
|
|
289
|
+
`)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('handles mixed markdown and HTML inside tags', async () => {
|
|
293
|
+
const htmlToConvert = '<div>**Bold text:** <a href="#">link</a></div>'
|
|
294
|
+
const result = await htmlToMdxString({
|
|
295
|
+
html: htmlToConvert,
|
|
296
|
+
withProcessor: true,
|
|
297
|
+
onError: (e) => {
|
|
298
|
+
throw e
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
expect(result).toMatchInlineSnapshot(`
|
|
302
|
+
{
|
|
303
|
+
"html": "<div><strong>Bold text:</strong><a href="#">link</a></div>",
|
|
304
|
+
"mdx": "<div>**Bold text:**<a href="#">link</a></div>
|
|
305
|
+
",
|
|
306
|
+
}
|
|
307
|
+
`)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test('handles markdown inside table cells', async () => {
|
|
311
|
+
const htmlToConvert =
|
|
312
|
+
'<table><tr><td>**Bold** text and [link](http://example.com)</td></tr></table>'
|
|
313
|
+
const result = await htmlToMdxString({
|
|
314
|
+
html: htmlToConvert,
|
|
315
|
+
withProcessor: true,
|
|
316
|
+
onError: (e) => {
|
|
317
|
+
throw e
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
expect(result).toMatchInlineSnapshot(`
|
|
321
|
+
{
|
|
322
|
+
"html": "<table><tr><td><strong>Bold</strong> text and <a href="http://example.com" title="">link</a></td></tr></table>",
|
|
323
|
+
"mdx": "<table><tr><td>**Bold** text and [link](http://example.com)</td></tr></table>
|
|
324
|
+
",
|
|
325
|
+
}
|
|
326
|
+
`)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('preserves plain text when no markdown', async () => {
|
|
330
|
+
const htmlToConvert = '<div>Plain text without markdown</div>'
|
|
331
|
+
const result = await htmlToMdxString({
|
|
332
|
+
html: htmlToConvert,
|
|
333
|
+
withProcessor: true,
|
|
334
|
+
onError: (e) => {
|
|
335
|
+
throw e
|
|
336
|
+
},
|
|
337
|
+
})
|
|
338
|
+
expect(result).toMatchInlineSnapshot(`
|
|
339
|
+
{
|
|
340
|
+
"html": "<div>Plain text without markdown</div>",
|
|
341
|
+
"mdx": "<div>Plain text without markdown</div>
|
|
342
|
+
",
|
|
343
|
+
}
|
|
344
|
+
`)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test('handles nested HTML tags with markdown', async () => {
|
|
348
|
+
const htmlToConvert =
|
|
349
|
+
'<div><span>**Bold** and <a href="#">link</a></span></div>'
|
|
350
|
+
const result = await htmlToMdxString({
|
|
351
|
+
html: htmlToConvert,
|
|
352
|
+
withProcessor: true,
|
|
353
|
+
onError: (e) => {
|
|
354
|
+
throw e
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
expect(result).toMatchInlineSnapshot(`
|
|
358
|
+
{
|
|
359
|
+
"html": "<div><span><strong>Bold</strong> and<a href="#">link</a></span></div>",
|
|
360
|
+
"mdx": "<div><span>**Bold** and<a href="#">link</a></span></div>
|
|
361
|
+
",
|
|
362
|
+
}
|
|
363
|
+
`)
|
|
364
|
+
})
|
|
365
|
+
})
|