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.
Files changed (77) hide show
  1. package/README.md +14 -14
  2. package/dist/assets/HtmlToJsxConverter-Ds0bTjpw.js +24 -0
  3. package/dist/assets/_commonjsHelpers-CqkleIqs.js +1 -0
  4. package/dist/assets/index-B5fPOjPt.css +1 -0
  5. package/dist/assets/index-B7ATSoRE.js +9 -0
  6. package/dist/assets/index-BwZ2FTRd.js +146 -0
  7. package/dist/assets/index-R1UqLMGJ.js +1 -0
  8. package/dist/assets/index-c0qeY2gs.js +9 -0
  9. package/dist/assets/jsx-runtime-BhZZLbvw.js +9 -0
  10. package/dist/assets/jsx-runtime-NArryeSM.js +1 -0
  11. package/dist/assets/react-Ca6JzGpx.js +1 -0
  12. package/dist/assets/react-dom-BYRHYqYl.js +1 -0
  13. package/dist/html/attributes.d.ts +19 -0
  14. package/dist/html/attributes.d.ts.map +1 -0
  15. package/dist/html/attributes.js +289 -0
  16. package/dist/html/attributes.js.map +1 -0
  17. package/dist/html/convert-attributes.d.ts +6 -0
  18. package/dist/html/convert-attributes.d.ts.map +1 -0
  19. package/dist/html/convert-attributes.js +43 -0
  20. package/dist/html/convert-attributes.js.map +1 -0
  21. package/dist/html/domparser-browser.d.ts +4 -0
  22. package/dist/html/domparser-browser.d.ts.map +1 -0
  23. package/dist/html/domparser-browser.js +7 -0
  24. package/dist/html/domparser-browser.js.map +1 -0
  25. package/dist/html/domparser.d.ts +2 -0
  26. package/dist/html/domparser.d.ts.map +1 -0
  27. package/dist/html/domparser.js +5 -0
  28. package/dist/html/domparser.js.map +1 -0
  29. package/dist/html/html-to-mdx-ast.d.ts +23 -0
  30. package/dist/html/html-to-mdx-ast.d.ts.map +1 -0
  31. package/dist/html/html-to-mdx-ast.js +227 -0
  32. package/dist/html/html-to-mdx-ast.js.map +1 -0
  33. package/dist/html/html-to-mdx-ast.test.d.ts +2 -0
  34. package/dist/html/html-to-mdx-ast.test.d.ts.map +1 -0
  35. package/dist/html/html-to-mdx-ast.test.js +324 -0
  36. package/dist/html/html-to-mdx-ast.test.js.map +1 -0
  37. package/dist/html/remark-mdx-jsx-normalize.d.ts +10 -0
  38. package/dist/html/remark-mdx-jsx-normalize.d.ts.map +1 -0
  39. package/dist/html/remark-mdx-jsx-normalize.js +117 -0
  40. package/dist/html/remark-mdx-jsx-normalize.js.map +1 -0
  41. package/dist/html/valid-html-elements.d.ts +10 -0
  42. package/dist/html/valid-html-elements.d.ts.map +1 -0
  43. package/dist/html/valid-html-elements.js +50 -0
  44. package/dist/html/valid-html-elements.js.map +1 -0
  45. package/dist/index.html +19 -0
  46. package/dist/parse.d.ts +2 -0
  47. package/dist/parse.d.ts.map +1 -1
  48. package/dist/parse.js +2 -0
  49. package/dist/parse.js.map +1 -1
  50. package/dist/safe-mdx.d.ts +1 -1
  51. package/dist/safe-mdx.d.ts.map +1 -1
  52. package/dist/safe-mdx.js +23 -71
  53. package/dist/safe-mdx.js.map +1 -1
  54. package/dist/safe-mdx.test.js +161 -8
  55. package/dist/safe-mdx.test.js.map +1 -1
  56. package/package.json +27 -6
  57. package/src/html/README +17 -0
  58. package/src/html/attributes.ts +297 -0
  59. package/src/html/convert-attributes.ts +59 -0
  60. package/src/html/domparser-browser.ts +6 -0
  61. package/src/html/domparser.ts +5 -0
  62. package/src/html/html-to-mdx-ast.test.ts +365 -0
  63. package/src/html/html-to-mdx-ast.ts +304 -0
  64. package/src/html/remark-mdx-jsx-normalize.ts +128 -0
  65. package/src/html/valid-html-elements.ts +65 -0
  66. package/src/parse.ts +3 -0
  67. package/src/safe-mdx.test.tsx +178 -12
  68. package/src/safe-mdx.tsx +23 -79
  69. package/dist/HtmlToJsxConverter.d.ts +0 -10
  70. package/dist/HtmlToJsxConverter.d.ts.map +0 -1
  71. package/dist/HtmlToJsxConverter.js +0 -22
  72. package/dist/HtmlToJsxConverter.js.map +0 -1
  73. package/dist/plugins.d.ts +0 -12
  74. package/dist/plugins.d.ts.map +0 -1
  75. package/dist/plugins.js +0 -68
  76. package/dist/plugins.js.map +0 -1
  77. 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,6 @@
1
+ // Browser-specific DOMParser implementation
2
+ export function parseHTML(html: string) {
3
+ const parser = new DOMParser();
4
+ const doc = parser.parseFromString(html, 'text/html');
5
+ return { document: doc };
6
+ }
@@ -0,0 +1,5 @@
1
+ import { parseHTML as linkedomParseHTML } from 'linkedom';
2
+
3
+ export function parseHTML(html: string) {
4
+ return linkedomParseHTML(html);
5
+ }
@@ -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
+ })