safe-mdx 1.3.2 → 1.3.6

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 +25 -0
  30. package/dist/html/html-to-mdx-ast.d.ts.map +1 -0
  31. package/dist/html/html-to-mdx-ast.js +247 -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 +411 -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 +194 -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 +2 -2
  51. package/dist/safe-mdx.d.ts.map +1 -1
  52. package/dist/safe-mdx.js +24 -76
  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 +28 -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 +459 -0
  63. package/src/html/html-to-mdx-ast.ts +327 -0
  64. package/src/html/remark-mdx-jsx-normalize.ts +209 -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 +25 -84
  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,247 @@
1
+ import { unified } from 'unified';
2
+ import { convertAttributeNameToJSX } from './convert-attributes.js';
3
+ import { parseHTML } from './domparser.js';
4
+ import { remarkMdxJsxNormalize } from './remark-mdx-jsx-normalize.js';
5
+ // Re-export the normalize plugin
6
+ export { remarkMdxJsxNormalize };
7
+ // Type guard functions for DOM nodes
8
+ function isCommentNode(node) {
9
+ return node.nodeType === 8; // Node.COMMENT_NODE
10
+ }
11
+ function isTextNode(node) {
12
+ return node.nodeType === 3; // Node.TEXT_NODE
13
+ }
14
+ function isElementNode(node) {
15
+ return node.nodeType === 1; // Node.ELEMENT_NODE
16
+ }
17
+ // Default tag name converter (no transformation)
18
+ function defaultConvertTagName({ tagName }) {
19
+ return tagName.toLowerCase();
20
+ }
21
+ // Default attribute value converter (no transformation)
22
+ function defaultConvertAttributeValue({ value, }) {
23
+ return value;
24
+ }
25
+ // Convert HTML attribute to MDX JSX attribute
26
+ function convertAttribute(attr, tagName, options) {
27
+ let jsxName = convertAttributeNameToJSX(attr.name);
28
+ // Apply attribute value transformation
29
+ const convertAttrValue = options?.convertAttributeValue || defaultConvertAttributeValue;
30
+ let value = convertAttrValue({
31
+ name: attr.name,
32
+ value: attr.value,
33
+ tagName,
34
+ });
35
+ // Handle boolean attributes
36
+ if (value === '' || value === attr.name) {
37
+ return {
38
+ type: 'mdxJsxAttribute',
39
+ name: jsxName,
40
+ value: null, // boolean true
41
+ };
42
+ }
43
+ // Handle special number attributes
44
+ const numberAttrs = [
45
+ 'tabIndex',
46
+ 'cols',
47
+ 'rows',
48
+ 'size',
49
+ 'span',
50
+ 'colSpan',
51
+ 'rowSpan',
52
+ 'border',
53
+ ];
54
+ if (numberAttrs.includes(jsxName) && value && !isNaN(Number(value))) {
55
+ return {
56
+ type: 'mdxJsxAttribute',
57
+ name: jsxName,
58
+ value: {
59
+ type: 'mdxJsxAttributeValueExpression',
60
+ value: value,
61
+ data: {
62
+ estree: {
63
+ type: 'Program',
64
+ sourceType: 'module',
65
+ body: [
66
+ {
67
+ type: 'ExpressionStatement',
68
+ expression: {
69
+ type: 'Literal',
70
+ value: Number(value),
71
+ },
72
+ },
73
+ ],
74
+ },
75
+ },
76
+ },
77
+ };
78
+ }
79
+ // Handle style attribute - for now keep as string
80
+ // if (jsxName === 'style' && value.includes(':')) {
81
+ // // Could enhance to parse CSS to object
82
+ // return {
83
+ // type: 'mdxJsxAttribute',
84
+ // name: jsxName,
85
+ // value: {
86
+ // type: 'mdxJsxAttributeValueExpression',
87
+ // value: `{${JSON.stringify(parseStyleString(value))}}`,
88
+ // data: {
89
+ // estree: parseExpression(JSON.stringify(parseStyleString(value))),
90
+ // },
91
+ // },
92
+ // }
93
+ // }
94
+ // String value
95
+ return {
96
+ type: 'mdxJsxAttribute',
97
+ name: jsxName,
98
+ value: value,
99
+ };
100
+ }
101
+ // Convert DOM node to MDX AST nodes - always returns an array
102
+ function htmlNodeToMdxAst(node, options) {
103
+ if (isCommentNode(node)) {
104
+ // Convert comments to MDX JSX expression with comment
105
+ // For now, return empty array
106
+ // return [{
107
+ // type: 'html',
108
+ // value: `<!-- ${node.data} -->`
109
+ // }] as Html[]
110
+ return [];
111
+ }
112
+ if (isTextNode(node)) {
113
+ const textValue = node.textContent || '';
114
+ // If we have a textToMdast converter, use it
115
+ if (options?.textToMdast) {
116
+ try {
117
+ const result = options.textToMdast({ text: textValue });
118
+ return Array.isArray(result) ? result : [result];
119
+ }
120
+ catch (error) {
121
+ // Call onError callback if provided, otherwise log
122
+ if (options.onError) {
123
+ options.onError(error, textValue);
124
+ }
125
+ else {
126
+ console.error('Failed to convert text to mdast:', error);
127
+ console.error('Text content:', textValue);
128
+ }
129
+ // Fallback to simple text node
130
+ return [
131
+ {
132
+ type: 'text',
133
+ value: textValue,
134
+ },
135
+ ];
136
+ }
137
+ }
138
+ // Default: return simple text node
139
+ return [
140
+ {
141
+ type: 'text',
142
+ value: textValue,
143
+ },
144
+ ];
145
+ }
146
+ if (!isElementNode(node)) {
147
+ return [];
148
+ }
149
+ const convertTagNameFn = options?.convertTagName || defaultConvertTagName;
150
+ // Use localName which is always lowercase in both browser and linkedom
151
+ const componentName = convertTagNameFn({ tagName: node.localName });
152
+ // If convertTagName returns empty string, skip this element and only return its children
153
+ if (componentName === '') {
154
+ // Process children but skip the element wrapper
155
+ const children = [];
156
+ for (const child of Array.from(node.childNodes)) {
157
+ children.push(...htmlNodeToMdxAst(child, options));
158
+ }
159
+ return children;
160
+ }
161
+ // Convert attributes
162
+ const attributes = [];
163
+ for (const attr of Array.from(node.attributes)) {
164
+ attributes.push(convertAttribute(attr, node.tagName, options));
165
+ }
166
+ // Process children
167
+ const children = [];
168
+ for (const child of Array.from(node.childNodes)) {
169
+ children.push(...htmlNodeToMdxAst(child, options));
170
+ }
171
+ // Always create MdxJsxTextElement initially
172
+ // The conversion to MdxJsxFlowElement will be handled by a separate plugin
173
+ const element = {
174
+ type: 'mdxJsxTextElement',
175
+ name: componentName,
176
+ attributes,
177
+ children: children,
178
+ };
179
+ return [element];
180
+ }
181
+ // Main function to parse HTML and return MDX AST - always returns an array
182
+ export function htmlToMdxAst(options) {
183
+ // Parse HTML with linkedom
184
+ const { document } = parseHTML(options.html.trim());
185
+ // linkedom behavior:
186
+ // - If input is a fragment (like "<div>Hello</div>"), the content becomes direct children of document
187
+ // - If input has body tag, it creates proper body element
188
+ // - We need to handle both cases
189
+ // linkedom behavior:
190
+ // - When parsing fragments, content becomes direct children of document
191
+ // - Accessing document.body on fragments auto-creates HEAD and BODY as children
192
+ // - We must avoid accessing document.body to prevent this
193
+ // Just use document's direct children and filter for relevant nodes
194
+ const childNodes = Array.from(document.childNodes).filter((node) => node.nodeType === 1 || // Element nodes
195
+ node.nodeType === 3 || // Text nodes
196
+ node.nodeType === 8);
197
+ let results = [];
198
+ for (const node of childNodes) {
199
+ results.push(...htmlNodeToMdxAst(node, options));
200
+ }
201
+ // Apply the normalize plugin if we have a parentType
202
+ if (options.parentType && results.length > 0) {
203
+ // Create a temporary AST node with the same parent type
204
+ const parentType = options.parentType;
205
+ const tempRoot = {
206
+ type: 'root',
207
+ children: results,
208
+ };
209
+ // If we have a specific parent type, wrap the content in that parent
210
+ // to provide proper context for the normalize plugin
211
+ let astToProcess;
212
+ if (parentType !== 'root') {
213
+ // Create a parent node of the specified type with our content as children
214
+ const parentNode = {
215
+ type: parentType,
216
+ children: tempRoot.children,
217
+ };
218
+ astToProcess = {
219
+ type: 'root',
220
+ children: [parentNode],
221
+ };
222
+ }
223
+ else {
224
+ astToProcess = tempRoot;
225
+ }
226
+ // Create a simple processor and run the normalize plugin
227
+ const processor = unified().use(remarkMdxJsxNormalize);
228
+ processor.runSync(astToProcess);
229
+ // Extract the result back
230
+ if (parentType !== 'root') {
231
+ // Get the children from the parent node we created
232
+ const processedParent = astToProcess.children[0];
233
+ results = processedParent.children;
234
+ }
235
+ else {
236
+ // Get children directly from root
237
+ results = astToProcess.children;
238
+ }
239
+ }
240
+ return results;
241
+ }
242
+ // Export a wrapper that always returns an array for consistency
243
+ // Note: htmlToMdxAst now already returns an array, so this is just an alias
244
+ export function parseHtmlToMdxAst(options) {
245
+ return htmlToMdxAst(options);
246
+ }
247
+ //# sourceMappingURL=html-to-mdx-ast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-to-mdx-ast.js","sourceRoot":"","sources":["../../src/html/html-to-mdx-ast.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=html-to-mdx-ast.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-to-mdx-ast.test.d.ts","sourceRoot":"","sources":["../../src/html/html-to-mdx-ast.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,411 @@
1
+ import { test, expect, describe } from 'vitest';
2
+ import React from 'react';
3
+ import { renderToStaticMarkup } from 'react-dom/server';
4
+ import { parseHtmlToMdxAst, htmlToMdxAst, } from './html-to-mdx-ast.js';
5
+ import { unified } from 'unified';
6
+ import remarkMdx from 'remark-mdx';
7
+ import remarkStringify from 'remark-stringify';
8
+ import remarkParse from 'remark-parse';
9
+ import { mdxParse } from '../parse.js';
10
+ import { MdastToJsx } from '../safe-mdx.js';
11
+ // Default components for testing
12
+ const components = {
13
+ Heading({ level, children, ...props }) {
14
+ return React.createElement('h1', props, children);
15
+ },
16
+ Cards({ level, children, ...props }) {
17
+ return React.createElement('div', props, children);
18
+ },
19
+ };
20
+ // Helper to convert HTML to MDX string and rendered HTML
21
+ async function htmlToMdxString({ html, withProcessor = false, onError, convertTagName, convertAttributeValue, }) {
22
+ // If withProcessor is true, create a textToMdast that parses markdown
23
+ const textToMdast = withProcessor
24
+ ? ({ text }) => {
25
+ const markdownProcessor = unified()
26
+ .use(remarkParse)
27
+ .use(remarkMdx);
28
+ const mdast = markdownProcessor.parse(text);
29
+ markdownProcessor.runSync(mdast);
30
+ // Return the children of the root node
31
+ return mdast.children || [];
32
+ }
33
+ : undefined;
34
+ const mdxAst = parseHtmlToMdxAst({
35
+ html,
36
+ textToMdast,
37
+ onError,
38
+ convertTagName,
39
+ convertAttributeValue,
40
+ });
41
+ // Generate MDX string
42
+ const processor = unified().use(remarkMdx).use(remarkStringify, {
43
+ // bullet: '-',
44
+ // fence: '`',
45
+ // fences: true
46
+ });
47
+ // Create a root node with the content
48
+ const root = {
49
+ type: 'root',
50
+ children: mdxAst,
51
+ };
52
+ const mdx = processor.stringify(root);
53
+ // Generate HTML using MdastToJsx like in safe-mdx.test.tsx
54
+ // First parse the MDX to get the full AST
55
+ const mdast = mdxParse(mdx);
56
+ const visitor = new MdastToJsx({ markdown: mdx, mdast, components });
57
+ const jsx = visitor.run();
58
+ const renderedHtml = renderToStaticMarkup(jsx);
59
+ return { mdx, html: renderedHtml };
60
+ }
61
+ describe('parseHtmlToMdxAst', () => {
62
+ test('parses simple HTML element', () => {
63
+ const result = parseHtmlToMdxAst({ html: '<div>Hello</div>' });
64
+ expect(result).toMatchInlineSnapshot(`
65
+ [
66
+ {
67
+ "attributes": [],
68
+ "children": [
69
+ {
70
+ "type": "text",
71
+ "value": "Hello",
72
+ },
73
+ ],
74
+ "name": "div",
75
+ "type": "mdxJsxTextElement",
76
+ },
77
+ ]
78
+ `);
79
+ });
80
+ test('filters out non-HTML elements when convertTagName returns empty string', () => {
81
+ const result = parseHtmlToMdxAst({
82
+ html: '<custom-element>Hello <span>world</span></custom-element>',
83
+ convertTagName: ({ tagName }) => {
84
+ // Only keep span, filter out custom-element
85
+ if (tagName === 'span')
86
+ return 'span';
87
+ return tagName;
88
+ },
89
+ });
90
+ expect(result).toMatchInlineSnapshot(`
91
+ [
92
+ {
93
+ "attributes": [],
94
+ "children": [
95
+ {
96
+ "type": "text",
97
+ "value": "Hello ",
98
+ },
99
+ {
100
+ "attributes": [],
101
+ "children": [
102
+ {
103
+ "type": "text",
104
+ "value": "world",
105
+ },
106
+ ],
107
+ "name": "span",
108
+ "type": "mdxJsxTextElement",
109
+ },
110
+ ],
111
+ "name": "custom-element",
112
+ "type": "mdxJsxTextElement",
113
+ },
114
+ ]
115
+ `);
116
+ });
117
+ test('handles self-closing tags', () => {
118
+ const result = parseHtmlToMdxAst({
119
+ html: '<img src="https://example.com/img.jpg" />',
120
+ });
121
+ expect(result).toMatchInlineSnapshot(`
122
+ [
123
+ {
124
+ "attributes": [
125
+ {
126
+ "name": "src",
127
+ "type": "mdxJsxAttribute",
128
+ "value": "https://example.com/img.jpg",
129
+ },
130
+ ],
131
+ "children": [],
132
+ "name": "img",
133
+ "type": "mdxJsxTextElement",
134
+ },
135
+ ]
136
+ `);
137
+ });
138
+ test('handles span with attributes', () => {
139
+ const result = parseHtmlToMdxAst({
140
+ html: '<span color="blue">colored text</span>',
141
+ });
142
+ expect(result).toMatchInlineSnapshot(`
143
+ [
144
+ {
145
+ "attributes": [
146
+ {
147
+ "name": "color",
148
+ "type": "mdxJsxAttribute",
149
+ "value": "blue",
150
+ },
151
+ ],
152
+ "children": [
153
+ {
154
+ "type": "text",
155
+ "value": "colored text",
156
+ },
157
+ ],
158
+ "name": "span",
159
+ "type": "mdxJsxTextElement",
160
+ },
161
+ ]
162
+ `);
163
+ });
164
+ test('handles mixed content', () => {
165
+ const result = parseHtmlToMdxAst({
166
+ html: 'Some text <strong>bold</strong> more text',
167
+ });
168
+ expect(result).toMatchInlineSnapshot(`
169
+ [
170
+ {
171
+ "type": "text",
172
+ "value": "Some text ",
173
+ },
174
+ {
175
+ "attributes": [],
176
+ "children": [
177
+ {
178
+ "type": "text",
179
+ "value": "bold",
180
+ },
181
+ ],
182
+ "name": "strong",
183
+ "type": "mdxJsxTextElement",
184
+ },
185
+ {
186
+ "type": "text",
187
+ "value": " more text",
188
+ },
189
+ ]
190
+ `);
191
+ });
192
+ test('handles comments', () => {
193
+ const result = parseHtmlToMdxAst({ html: '<!-- This is a comment -->' });
194
+ expect(result).toMatchInlineSnapshot(`[]`);
195
+ });
196
+ test('handles table with attributes', () => {
197
+ const result = parseHtmlToMdxAst({
198
+ html: '<table class="data-table"><tr><td>Cell</td></tr></table>',
199
+ });
200
+ expect(result).toMatchInlineSnapshot(`
201
+ [
202
+ {
203
+ "attributes": [
204
+ {
205
+ "name": "className",
206
+ "type": "mdxJsxAttribute",
207
+ "value": "data-table",
208
+ },
209
+ ],
210
+ "children": [
211
+ {
212
+ "attributes": [],
213
+ "children": [
214
+ {
215
+ "attributes": [],
216
+ "children": [
217
+ {
218
+ "type": "text",
219
+ "value": "Cell",
220
+ },
221
+ ],
222
+ "name": "td",
223
+ "type": "mdxJsxTextElement",
224
+ },
225
+ ],
226
+ "name": "tr",
227
+ "type": "mdxJsxTextElement",
228
+ },
229
+ ],
230
+ "name": "table",
231
+ "type": "mdxJsxTextElement",
232
+ },
233
+ ]
234
+ `);
235
+ });
236
+ });
237
+ describe('parseHtmlToMdxAst with markdown processor', () => {
238
+ test('parses markdown inside HTML tags', async () => {
239
+ const htmlToConvert = '<div>This is **bold** text</div>';
240
+ const result = await htmlToMdxString({
241
+ html: htmlToConvert,
242
+ withProcessor: true,
243
+ onError: (e) => {
244
+ throw e;
245
+ },
246
+ });
247
+ expect(result).toMatchInlineSnapshot(`
248
+ {
249
+ "html": "<div>This is <strong>bold</strong> text</div>",
250
+ "mdx": "<div>This is **bold** text</div>
251
+ ",
252
+ }
253
+ `);
254
+ });
255
+ test('handles mixed markdown and HTML inside tags', async () => {
256
+ const htmlToConvert = '<div>**Bold text:** <a href="#">link</a></div>';
257
+ const result = await htmlToMdxString({
258
+ html: htmlToConvert,
259
+ withProcessor: true,
260
+ onError: (e) => {
261
+ throw e;
262
+ },
263
+ });
264
+ expect(result).toMatchInlineSnapshot(`
265
+ {
266
+ "html": "<div><strong>Bold text:</strong><a href="#">link</a></div>",
267
+ "mdx": "<div>**Bold text:**<a href="#">link</a></div>
268
+ ",
269
+ }
270
+ `);
271
+ });
272
+ test('handles markdown inside table cells', async () => {
273
+ const htmlToConvert = '<table><tr><td>**Bold** text and [link](http://example.com)</td></tr></table>';
274
+ const result = await htmlToMdxString({
275
+ html: htmlToConvert,
276
+ withProcessor: true,
277
+ onError: (e) => {
278
+ throw e;
279
+ },
280
+ });
281
+ expect(result).toMatchInlineSnapshot(`
282
+ {
283
+ "html": "<table><tr><td><strong>Bold</strong> text and <a href="http://example.com" title="">link</a></td></tr></table>",
284
+ "mdx": "<table><tr><td>**Bold** text and [link](http://example.com)</td></tr></table>
285
+ ",
286
+ }
287
+ `);
288
+ });
289
+ test('preserves plain text when no markdown', async () => {
290
+ const htmlToConvert = '<div>Plain text without markdown</div>';
291
+ const result = await htmlToMdxString({
292
+ html: htmlToConvert,
293
+ withProcessor: true,
294
+ onError: (e) => {
295
+ throw e;
296
+ },
297
+ });
298
+ expect(result).toMatchInlineSnapshot(`
299
+ {
300
+ "html": "<div>Plain text without markdown</div>",
301
+ "mdx": "<div>Plain text without markdown</div>
302
+ ",
303
+ }
304
+ `);
305
+ });
306
+ test('handles nested HTML tags with markdown', async () => {
307
+ const htmlToConvert = '<div><span>**Bold** and <a href="#">link</a></span></div>';
308
+ const result = await htmlToMdxString({
309
+ html: htmlToConvert,
310
+ withProcessor: true,
311
+ onError: (e) => {
312
+ throw e;
313
+ },
314
+ });
315
+ expect(result).toMatchInlineSnapshot(`
316
+ {
317
+ "html": "<div><span><strong>Bold</strong> and<a href="#">link</a></span></div>",
318
+ "mdx": "<div><span>**Bold** and<a href="#">link</a></span></div>
319
+ ",
320
+ }
321
+ `);
322
+ });
323
+ test('normalize plugin without parentType does not apply', () => {
324
+ // Without parentType, normalization should not happen
325
+ const withoutParent = htmlToMdxAst({
326
+ html: '<span>Text</span>'
327
+ });
328
+ // Without normalization, elements remain as initially created (text elements)
329
+ expect(withoutParent).toHaveLength(1);
330
+ expect(withoutParent[0]).toMatchObject({
331
+ type: 'mdxJsxTextElement',
332
+ name: 'span'
333
+ });
334
+ });
335
+ test('applies normalize plugin with parentType', () => {
336
+ // Test with a block element inside a paragraph (phrasing context)
337
+ const blockInParagraph = htmlToMdxAst({
338
+ html: '<div>Block in paragraph</div>',
339
+ parentType: 'paragraph'
340
+ });
341
+ // Even though div is a block element, it should remain mdxJsxFlowElement
342
+ // because block-level tags have priority
343
+ expect(blockInParagraph).toHaveLength(1);
344
+ expect(blockInParagraph[0]).toMatchObject({
345
+ type: 'mdxJsxFlowElement',
346
+ name: 'div'
347
+ });
348
+ // Test with inline element in paragraph (should be text element)
349
+ const inlineInParagraph = htmlToMdxAst({
350
+ html: '<span>Inline in paragraph</span>',
351
+ parentType: 'paragraph'
352
+ });
353
+ expect(inlineInParagraph).toHaveLength(1);
354
+ expect(inlineInParagraph[0]).toMatchObject({
355
+ type: 'mdxJsxTextElement',
356
+ name: 'span'
357
+ });
358
+ // Test with inline element in root (should be flow element)
359
+ const inlineInRoot = htmlToMdxAst({
360
+ html: '<span>Inline in root</span>',
361
+ parentType: 'root'
362
+ });
363
+ expect(inlineInRoot).toHaveLength(1);
364
+ expect(inlineInRoot[0]).toMatchObject({
365
+ type: 'mdxJsxFlowElement',
366
+ name: 'span'
367
+ });
368
+ // Test with multiple elements
369
+ const multipleElements = htmlToMdxAst({
370
+ html: '<span>First</span><div>Second</div>',
371
+ parentType: 'paragraph'
372
+ });
373
+ expect(multipleElements).toHaveLength(2);
374
+ expect(multipleElements[0]).toMatchObject({
375
+ type: 'mdxJsxTextElement',
376
+ name: 'span'
377
+ });
378
+ expect(multipleElements[1]).toMatchObject({
379
+ type: 'mdxJsxFlowElement',
380
+ name: 'div'
381
+ });
382
+ // Test with nested elements - the normalize plugin should handle nested context correctly
383
+ const nestedElements = htmlToMdxAst({
384
+ html: '<div><span>Nested span</span><p><em>Emphasis</em></p></div>',
385
+ parentType: 'root'
386
+ });
387
+ // The plugin normalizes based on the entire tree structure
388
+ expect(nestedElements).toHaveLength(1);
389
+ expect(nestedElements[0]).toMatchObject({
390
+ type: 'mdxJsxFlowElement',
391
+ name: 'div',
392
+ children: expect.arrayContaining([
393
+ expect.objectContaining({
394
+ type: 'mdxJsxFlowElement',
395
+ name: 'span'
396
+ }),
397
+ expect.objectContaining({
398
+ type: 'mdxJsxFlowElement',
399
+ name: 'p',
400
+ children: expect.arrayContaining([
401
+ expect.objectContaining({
402
+ type: 'mdxJsxTextElement',
403
+ name: 'em'
404
+ })
405
+ ])
406
+ })
407
+ ])
408
+ });
409
+ });
410
+ });
411
+ //# sourceMappingURL=html-to-mdx-ast.test.js.map