safe-mdx 1.3.1 → 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 (81) 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/dynamic-esm-component.d.ts.map +1 -1
  14. package/dist/dynamic-esm-component.js +16 -1
  15. package/dist/dynamic-esm-component.js.map +1 -1
  16. package/dist/html/attributes.d.ts +19 -0
  17. package/dist/html/attributes.d.ts.map +1 -0
  18. package/dist/html/attributes.js +289 -0
  19. package/dist/html/attributes.js.map +1 -0
  20. package/dist/html/convert-attributes.d.ts +6 -0
  21. package/dist/html/convert-attributes.d.ts.map +1 -0
  22. package/dist/html/convert-attributes.js +43 -0
  23. package/dist/html/convert-attributes.js.map +1 -0
  24. package/dist/html/domparser-browser.d.ts +4 -0
  25. package/dist/html/domparser-browser.d.ts.map +1 -0
  26. package/dist/html/domparser-browser.js +7 -0
  27. package/dist/html/domparser-browser.js.map +1 -0
  28. package/dist/html/domparser.d.ts +2 -0
  29. package/dist/html/domparser.d.ts.map +1 -0
  30. package/dist/html/domparser.js +5 -0
  31. package/dist/html/domparser.js.map +1 -0
  32. package/dist/html/html-to-mdx-ast.d.ts +23 -0
  33. package/dist/html/html-to-mdx-ast.d.ts.map +1 -0
  34. package/dist/html/html-to-mdx-ast.js +227 -0
  35. package/dist/html/html-to-mdx-ast.js.map +1 -0
  36. package/dist/html/html-to-mdx-ast.test.d.ts +2 -0
  37. package/dist/html/html-to-mdx-ast.test.d.ts.map +1 -0
  38. package/dist/html/html-to-mdx-ast.test.js +324 -0
  39. package/dist/html/html-to-mdx-ast.test.js.map +1 -0
  40. package/dist/html/remark-mdx-jsx-normalize.d.ts +10 -0
  41. package/dist/html/remark-mdx-jsx-normalize.d.ts.map +1 -0
  42. package/dist/html/remark-mdx-jsx-normalize.js +117 -0
  43. package/dist/html/remark-mdx-jsx-normalize.js.map +1 -0
  44. package/dist/html/valid-html-elements.d.ts +10 -0
  45. package/dist/html/valid-html-elements.d.ts.map +1 -0
  46. package/dist/html/valid-html-elements.js +50 -0
  47. package/dist/html/valid-html-elements.js.map +1 -0
  48. package/dist/index.html +19 -0
  49. package/dist/parse.d.ts +2 -0
  50. package/dist/parse.d.ts.map +1 -1
  51. package/dist/parse.js +2 -0
  52. package/dist/parse.js.map +1 -1
  53. package/dist/safe-mdx.d.ts +2 -2
  54. package/dist/safe-mdx.d.ts.map +1 -1
  55. package/dist/safe-mdx.js +39 -77
  56. package/dist/safe-mdx.js.map +1 -1
  57. package/dist/safe-mdx.test.js +161 -8
  58. package/dist/safe-mdx.test.js.map +1 -1
  59. package/package.json +27 -6
  60. package/src/dynamic-esm-component.tsx +40 -10
  61. package/src/html/README +17 -0
  62. package/src/html/attributes.ts +297 -0
  63. package/src/html/convert-attributes.ts +59 -0
  64. package/src/html/domparser-browser.ts +6 -0
  65. package/src/html/domparser.ts +5 -0
  66. package/src/html/html-to-mdx-ast.test.ts +365 -0
  67. package/src/html/html-to-mdx-ast.ts +304 -0
  68. package/src/html/remark-mdx-jsx-normalize.ts +128 -0
  69. package/src/html/valid-html-elements.ts +65 -0
  70. package/src/parse.ts +3 -0
  71. package/src/safe-mdx.test.tsx +178 -12
  72. package/src/safe-mdx.tsx +61 -93
  73. package/dist/HtmlToJsxConverter.d.ts +0 -10
  74. package/dist/HtmlToJsxConverter.d.ts.map +0 -1
  75. package/dist/HtmlToJsxConverter.js +0 -22
  76. package/dist/HtmlToJsxConverter.js.map +0 -1
  77. package/dist/plugins.d.ts +0 -12
  78. package/dist/plugins.d.ts.map +0 -1
  79. package/dist/plugins.js +0 -68
  80. package/dist/plugins.js.map +0 -1
  81. package/src/HtmlToJsxConverter.tsx +0 -37
@@ -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
+ })
@@ -0,0 +1,304 @@
1
+ import type { Root, RootContent, Text as MdastText } from 'mdast'
2
+ import type {
3
+ MdxJsxAttribute,
4
+ MdxJsxAttributeValueExpression,
5
+ MdxJsxTextElement,
6
+ } from 'mdast-util-mdx-jsx'
7
+ import type { Processor } from 'unified'
8
+ import { convertAttributeNameToJSX } from './convert-attributes.js'
9
+ import { parseHTML } from './domparser.js'
10
+
11
+ // Re-export the normalize plugin
12
+ export { default as remarkMdxJsxNormalize } from './remark-mdx-jsx-normalize.js'
13
+
14
+ // Type for converting tag names
15
+ export type ConvertTagName = (args: { tagName: string }) => string
16
+
17
+ // Type for converting text to mdast nodes - now returns AST nodes directly
18
+ export type TextToMdast = (args: {
19
+ text: string
20
+ }) => RootContent | RootContent[]
21
+
22
+ // Type for converting attribute values
23
+ export type ConvertAttributeValue = (args: {
24
+ name: string
25
+ value: string
26
+ tagName: string
27
+ }) => string
28
+
29
+ // Options for parsing HTML to MDX AST
30
+ export interface ParseHtmlToMdxAstOptions {
31
+ html: string
32
+ onError?: (error: unknown, text: string) => void
33
+ convertTagName?: ConvertTagName
34
+ textToMdast?: TextToMdast
35
+ convertAttributeValue?: ConvertAttributeValue
36
+ }
37
+
38
+ // Type guard functions for DOM nodes
39
+ function isCommentNode(node: Node): node is Comment {
40
+ return node.nodeType === 8 // Node.COMMENT_NODE
41
+ }
42
+
43
+ function isTextNode(node: Node): node is Text {
44
+ return node.nodeType === 3 // Node.TEXT_NODE
45
+ }
46
+
47
+ function isElementNode(node: Node): node is Element {
48
+ return node.nodeType === 1 // Node.ELEMENT_NODE
49
+ }
50
+
51
+ // Default tag name converter (no transformation)
52
+ function defaultConvertTagName({ tagName }: { tagName: string }): string {
53
+ return tagName.toLowerCase()
54
+ }
55
+
56
+ // Default attribute value converter (no transformation)
57
+ function defaultConvertAttributeValue({
58
+ value,
59
+ }: {
60
+ name: string
61
+ value: string
62
+ tagName: string
63
+ }): string {
64
+ return value
65
+ }
66
+
67
+ // Convert HTML attribute to MDX JSX attribute
68
+ function convertAttribute(
69
+ attr: Attr,
70
+ tagName: string,
71
+ options?: ParseHtmlToMdxAstOptions,
72
+ ): MdxJsxAttribute {
73
+ let jsxName = convertAttributeNameToJSX(attr.name)
74
+
75
+ // Apply attribute value transformation
76
+ const convertAttrValue =
77
+ options?.convertAttributeValue || defaultConvertAttributeValue
78
+ let value = convertAttrValue({
79
+ name: attr.name,
80
+ value: attr.value,
81
+ tagName,
82
+ })
83
+
84
+ // Handle boolean attributes
85
+ if (value === '' || value === attr.name) {
86
+ return {
87
+ type: 'mdxJsxAttribute',
88
+ name: jsxName,
89
+ value: null, // boolean true
90
+ }
91
+ }
92
+
93
+ // Handle special number attributes
94
+ const numberAttrs = [
95
+ 'tabIndex',
96
+ 'cols',
97
+ 'rows',
98
+ 'size',
99
+ 'span',
100
+ 'colSpan',
101
+ 'rowSpan',
102
+ 'border',
103
+ ]
104
+ if (numberAttrs.includes(jsxName) && value && !isNaN(Number(value))) {
105
+ return {
106
+ type: 'mdxJsxAttribute',
107
+ name: jsxName,
108
+ value: {
109
+ type: 'mdxJsxAttributeValueExpression',
110
+ value: value,
111
+ data: {
112
+ estree: {
113
+ type: 'Program',
114
+ sourceType: 'module',
115
+ body: [
116
+ {
117
+ type: 'ExpressionStatement',
118
+ expression: {
119
+ type: 'Literal',
120
+ value: Number(value),
121
+ },
122
+ },
123
+ ],
124
+ },
125
+ },
126
+ } satisfies MdxJsxAttributeValueExpression,
127
+ }
128
+ }
129
+
130
+ // Handle style attribute - for now keep as string
131
+ // if (jsxName === 'style' && value.includes(':')) {
132
+ // // Could enhance to parse CSS to object
133
+ // return {
134
+ // type: 'mdxJsxAttribute',
135
+ // name: jsxName,
136
+ // value: {
137
+ // type: 'mdxJsxAttributeValueExpression',
138
+ // value: `{${JSON.stringify(parseStyleString(value))}}`,
139
+ // data: {
140
+ // estree: parseExpression(JSON.stringify(parseStyleString(value))),
141
+ // },
142
+ // },
143
+ // }
144
+ // }
145
+
146
+ // String value
147
+ return {
148
+ type: 'mdxJsxAttribute',
149
+ name: jsxName,
150
+ value: value,
151
+ }
152
+ }
153
+
154
+ // Convert DOM node to MDX AST nodes
155
+ function htmlNodeToMdxAst(
156
+ node: Node,
157
+ options?: ParseHtmlToMdxAstOptions,
158
+ ): RootContent | RootContent[] {
159
+ if (isCommentNode(node)) {
160
+ // Convert comments to MDX JSX expression with comment
161
+ // For now, return as HTML node
162
+ // return {
163
+ // type: 'html',
164
+ // value: `<!-- ${node.data} -->`
165
+ // } as Html
166
+ return []
167
+ }
168
+
169
+ if (isTextNode(node)) {
170
+ const textValue = node.textContent || ''
171
+
172
+ // If we have a textToMdast converter, use it
173
+ if (options?.textToMdast) {
174
+ try {
175
+ const result = options.textToMdast({ text: textValue })
176
+ return result
177
+ } catch (error) {
178
+ // Call onError callback if provided, otherwise log
179
+ if (options.onError) {
180
+ options.onError(error, textValue)
181
+ } else {
182
+ console.error('Failed to convert text to mdast:', error)
183
+ console.error('Text content:', textValue)
184
+ }
185
+ // Fallback to simple text node
186
+ return {
187
+ type: 'text',
188
+ value: textValue,
189
+ } satisfies MdastText
190
+ }
191
+ }
192
+
193
+ // Default: return simple text node
194
+ return {
195
+ type: 'text',
196
+ value: textValue,
197
+ } satisfies MdastText
198
+ }
199
+
200
+ if (!isElementNode(node)) {
201
+ return []
202
+ }
203
+
204
+ const convertTagNameFn = options?.convertTagName || defaultConvertTagName
205
+ // Use localName which is always lowercase in both browser and linkedom
206
+ const componentName = convertTagNameFn({ tagName: node.localName })
207
+
208
+ // If convertTagName returns empty string, skip this element and only return its children
209
+ if (componentName === '') {
210
+ // Process children but skip the element wrapper
211
+ const children: RootContent[] = []
212
+ for (const child of Array.from(node.childNodes)) {
213
+ const result = htmlNodeToMdxAst(child, options)
214
+ if (Array.isArray(result)) {
215
+ children.push(...result)
216
+ } else {
217
+ children.push(result)
218
+ }
219
+ }
220
+ return children
221
+ }
222
+
223
+ // Convert attributes
224
+ const attributes: MdxJsxAttribute[] = []
225
+ for (const attr of Array.from(node.attributes)) {
226
+ attributes.push(convertAttribute(attr, node.tagName, options))
227
+ }
228
+
229
+ // Process children
230
+ const children: RootContent[] = []
231
+ for (const child of Array.from(node.childNodes)) {
232
+ const result = htmlNodeToMdxAst(child, options)
233
+ if (Array.isArray(result)) {
234
+ children.push(...result)
235
+ } else {
236
+ children.push(result)
237
+ }
238
+ }
239
+
240
+ // Always create MdxJsxTextElement initially
241
+ // The conversion to MdxJsxFlowElement will be handled by a separate plugin
242
+ const element: MdxJsxTextElement = {
243
+ type: 'mdxJsxTextElement',
244
+ name: componentName,
245
+ attributes,
246
+ children: children as any,
247
+ }
248
+ return element
249
+ }
250
+
251
+ // Main function to parse HTML and return MDX AST
252
+ export function htmlToMdxAst(
253
+ options: ParseHtmlToMdxAstOptions,
254
+ ): RootContent | RootContent[] {
255
+ // Parse HTML with linkedom
256
+ const { document } = parseHTML(options.html.trim())
257
+
258
+ // linkedom behavior:
259
+ // - If input is a fragment (like "<div>Hello</div>"), the content becomes direct children of document
260
+ // - If input has body tag, it creates proper body element
261
+ // - We need to handle both cases
262
+
263
+ // linkedom behavior:
264
+ // - When parsing fragments, content becomes direct children of document
265
+ // - Accessing document.body on fragments auto-creates HEAD and BODY as children
266
+ // - We must avoid accessing document.body to prevent this
267
+
268
+ // Just use document's direct children and filter for relevant nodes
269
+ const childNodes = Array.from(document.childNodes).filter(
270
+ (node) =>
271
+ node.nodeType === 1 || // Element nodes
272
+ node.nodeType === 3 || // Text nodes
273
+ node.nodeType === 8, // Comment nodes
274
+ )
275
+
276
+ if (childNodes.length === 0) {
277
+ return []
278
+ }
279
+
280
+ if (childNodes.length === 1) {
281
+ return htmlNodeToMdxAst(childNodes[0]!, options)
282
+ }
283
+
284
+ // Multiple nodes - return as array
285
+ const results: RootContent[] = []
286
+ for (const node of childNodes) {
287
+ const result = htmlNodeToMdxAst(node, options)
288
+ if (Array.isArray(result)) {
289
+ results.push(...result)
290
+ } else {
291
+ results.push(result)
292
+ }
293
+ }
294
+
295
+ return results
296
+ }
297
+
298
+ // Export a wrapper that always returns an array for consistency
299
+ export function parseHtmlToMdxAst(
300
+ options: ParseHtmlToMdxAstOptions,
301
+ ): RootContent[] {
302
+ const result = htmlToMdxAst(options)
303
+ return Array.isArray(result) ? result : [result]
304
+ }