safe-mdx 1.3.7 → 1.3.9

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.
@@ -0,0 +1,869 @@
1
+ import { test, expect, describe } from 'vitest';
2
+ import remarkMdx from 'remark-mdx';
3
+ import remarkStringify from 'remark-stringify';
4
+ import { parseHtmlToMdxAst } from 'safe-mdx/parse';
5
+ import { remark } from 'remark';
6
+ import { visit } from 'unist-util-visit';
7
+ /** Template literal for auto formatting with dedent */
8
+ function html(strings, ...expressions) {
9
+ // Join all string parts
10
+ let raw = strings[0] ?? '';
11
+ for (let i = 1, l = strings.length; i < l; i++) {
12
+ raw += expressions[i - 1];
13
+ raw += strings[i];
14
+ }
15
+ // dedent: remove common leading whitespace from all non-empty lines
16
+ const lines = raw.split('\n');
17
+ // Ignore empty lines and lines with only whitespace
18
+ const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
19
+ const indentLengths = nonEmptyLines.map((line) => line.match(/^(\s*)/)[0].length);
20
+ const minIndent = indentLengths.length > 0 ? Math.min(...indentLengths) : 0;
21
+ // Remove the common indent from all lines
22
+ const dedented = lines.map((line) => line.slice(minIndent)).join('\n');
23
+ // Trim leading/trailing newlines
24
+ return dedented.trim();
25
+ }
26
+ // Helper to convert HTML to MDX string
27
+ async function htmlToMdxString({ markdown, onError, }) {
28
+ const remarkHtmlBlocks = function () {
29
+ return (tree) => {
30
+ visit(tree, (node, index, parent) => {
31
+ if (node.type === 'html' &&
32
+ parent &&
33
+ typeof index === 'number') {
34
+ const htmlValue = node.value;
35
+ // Parse HTML to MDX AST with processor for markdown parsing
36
+ const mdxNodes = parseHtmlToMdxAst({
37
+ html: htmlValue,
38
+ onError,
39
+ textToMdast: ({ text: x }) => {
40
+ const processor = remark().use(() => {
41
+ return (tree, file) => {
42
+ file.data.ast = tree;
43
+ };
44
+ });
45
+ const mdast = processor.parse(x);
46
+ processor.runSync(mdast);
47
+ return mdast;
48
+ },
49
+ parentType: parent.type,
50
+ });
51
+ // Replace the HTML node with the MDX nodes
52
+ if (mdxNodes.length === 1) {
53
+ parent.children[index] = mdxNodes[0];
54
+ }
55
+ else if (mdxNodes.length > 1) {
56
+ parent.children.splice(index, 1, ...mdxNodes);
57
+ }
58
+ else {
59
+ // Remove the node if no content
60
+ parent.children.splice(index, 1);
61
+ }
62
+ }
63
+ });
64
+ };
65
+ };
66
+ const processor = remark().use(remarkHtmlBlocks).use(remarkStringify, {});
67
+ const mdast = processor.parse(markdown);
68
+ processor.runSync(mdast);
69
+ return remark().use(remarkMdx).use(remarkStringify, {}).stringify(mdast);
70
+ }
71
+ describe('Notion-specific HTML to MDX', () => {
72
+ test('converts page element to MDX with surrounding markdown', async () => {
73
+ const htmlContent = html `
74
+ <page url="{{https://notion.so/test}}">
75
+ Test Page
76
+ </page>
77
+ `;
78
+ const markdown = `
79
+ # My Document
80
+
81
+ ${htmlContent}
82
+
83
+ Some text after the page element.
84
+ `;
85
+ const mdxString = await htmlToMdxString({
86
+ markdown,
87
+ onError: (e) => {
88
+ throw e;
89
+ },
90
+ });
91
+ expect(mdxString).toMatchInlineSnapshot(`
92
+ "# My Document
93
+
94
+ <page url="{{https://notion.so/test}}">
95
+ Test Page
96
+ </page>
97
+
98
+ Some text after the page element.
99
+ "
100
+ `);
101
+ });
102
+ test('converts callout element to MDX with surrounding content', async () => {
103
+ const htmlContent = html `
104
+ <callout icon="📎" color="pink_bg">
105
+ Important note
106
+ </callout>
107
+ `;
108
+ const markdown = `
109
+ Here's an important message:
110
+
111
+ ${htmlContent}
112
+
113
+ **Bold text** after the callout.
114
+ `;
115
+ const mdxString = await htmlToMdxString({
116
+ markdown,
117
+ onError: (e) => {
118
+ throw e;
119
+ },
120
+ });
121
+ expect(mdxString).toMatchInlineSnapshot(`
122
+ "Here's an important message:
123
+
124
+ <callout icon="📎" color="pink_bg">
125
+ Important note
126
+ </callout>
127
+
128
+ **Bold text** after the callout.
129
+ "
130
+ `);
131
+ });
132
+ test('converts mention-page element to MDX with mixed content', async () => {
133
+ const htmlContent = html `
134
+ <mention-page url="{{https://notion.so/test}}" />
135
+ `;
136
+ const markdown = `Check out this page: ${htmlContent} for more information.
137
+
138
+ - First item
139
+ - Second item`;
140
+ const mdxString = await htmlToMdxString({
141
+ markdown,
142
+ onError: (e) => {
143
+ throw e;
144
+ },
145
+ });
146
+ expect(mdxString).toMatchInlineSnapshot(`
147
+ "Check out this page: <mention-page url="{{https://notion.so/test}}" /> for more information.
148
+
149
+ * First item
150
+ * Second item
151
+ "
152
+ `);
153
+ });
154
+ test('converts nested Notion elements to MDX', async () => {
155
+ const htmlContent = html `
156
+ <columns>
157
+ <column>
158
+ <page url="{{https://notion.so/page1}}">Page 1</page>
159
+ Some text
160
+ </column>
161
+ <column>
162
+ <callout icon="💡" color="yellow_bg">
163
+ Important callout
164
+ </callout>
165
+ </column>
166
+ </columns>
167
+ `;
168
+ const mdxString = await htmlToMdxString({
169
+ markdown: htmlContent,
170
+ onError: (e) => {
171
+ throw e;
172
+ },
173
+ });
174
+ expect(mdxString).toMatchInlineSnapshot(`
175
+ "<columns>
176
+ <column>
177
+ <page url="{{https://notion.so/page1}}">
178
+ Page 1
179
+ </page>
180
+
181
+ Some text
182
+ </column>
183
+
184
+ <column>
185
+ <callout icon="💡" color="yellow_bg">
186
+ Important callout
187
+ </callout>
188
+ </column>
189
+ </columns>
190
+ "
191
+ `);
192
+ });
193
+ test('handles mixed HTML and Notion elements with surrounding markdown', async () => {
194
+ const htmlContent = html `
195
+ <div>
196
+ <h1>Title</h1>
197
+ <page url="{{https://notion.so/test}}">Test Page</page>
198
+ <p>Regular paragraph</p>
199
+ <mention-page url="{{https://notion.so/another}}" />
200
+ </div>
201
+ `;
202
+ const markdown = `## Section Header
203
+
204
+ ${htmlContent}
205
+
206
+ And here's a [link](https://example.com) after the HTML block.`;
207
+ const mdxString = await htmlToMdxString({
208
+ markdown,
209
+ onError: (e) => {
210
+ throw e;
211
+ },
212
+ });
213
+ expect(mdxString).toMatchInlineSnapshot(`
214
+ "## Section Header
215
+
216
+ <div>
217
+ <h1>
218
+ Title
219
+ </h1>
220
+
221
+ <page url="{{https://notion.so/test}}">
222
+ Test Page
223
+ </page>
224
+
225
+ <p>
226
+ Regular paragraph
227
+ </p>
228
+
229
+ <mention-page url="{{https://notion.so/another}}" />
230
+ </div>
231
+
232
+ And here's a [link](https://example.com) after the HTML block.
233
+ "
234
+ `);
235
+ });
236
+ test('converts span with color attribute', async () => {
237
+ const htmlContent = html `
238
+ <span color="blue">
239
+ Blue text
240
+ </span>
241
+ `;
242
+ const mdxString = await htmlToMdxString({
243
+ markdown: htmlContent,
244
+ onError: (e) => {
245
+ throw e;
246
+ },
247
+ });
248
+ expect(mdxString).toMatchInlineSnapshot(`
249
+ "<span color="blue">
250
+ Blue text
251
+ </span>
252
+ "
253
+ `);
254
+ });
255
+ test('handles table element conversion', async () => {
256
+ const htmlContent = html `
257
+ <table header-row="true">
258
+ <tr>
259
+ <td>Cell 1</td>
260
+ <td>Cell 2</td>
261
+ </tr>
262
+ </table>
263
+ `;
264
+ const mdxString = await htmlToMdxString({
265
+ markdown: htmlContent,
266
+ onError: (e) => {
267
+ throw e;
268
+ },
269
+ });
270
+ expect(mdxString).toMatchInlineSnapshot(`
271
+ "<table header-row="true">
272
+ <tr>
273
+ <td>
274
+ Cell 1
275
+ </td>
276
+
277
+ <td>
278
+ Cell 2
279
+ </td>
280
+ </tr>
281
+ </table>
282
+ "
283
+ `);
284
+ });
285
+ test('handles image element conversion', async () => {
286
+ const htmlContent = html `
287
+ <image
288
+ source="{{https://example.com/image.jpg}}"
289
+ alt="Test image"
290
+ />
291
+ `;
292
+ const mdxString = await htmlToMdxString({
293
+ markdown: htmlContent,
294
+ onError: (e) => {
295
+ throw e;
296
+ },
297
+ });
298
+ expect(mdxString).toMatchInlineSnapshot(`
299
+ "<image source="{{https://example.com/image.jpg}}" alt="Test image" />
300
+ "
301
+ `);
302
+ });
303
+ test('handles unknown element conversion', async () => {
304
+ const htmlContent = html `
305
+ <unknown
306
+ url="{{https://notion.so/embed}}"
307
+ alt="embed"
308
+ />
309
+ `;
310
+ const mdxString = await htmlToMdxString({
311
+ markdown: htmlContent,
312
+ onError: (e) => {
313
+ throw e;
314
+ },
315
+ });
316
+ expect(mdxString).toMatchInlineSnapshot(`
317
+ "<unknown url="{{https://notion.so/embed}}" alt="embed" />
318
+ "
319
+ `);
320
+ });
321
+ test('handles columns with content and surrounding markdown', async () => {
322
+ const htmlContent = html `
323
+ <columns>
324
+ <column>
325
+ <h2>Section 1</h2>
326
+ <page url="{{https://notion.so/page}}">Page Link</page>
327
+ </column>
328
+ <column>
329
+ <callout icon="⚠️" color="yellow_bg">
330
+ <strong>Warning:</strong> Important information
331
+ </callout>
332
+ </column>
333
+ </columns>
334
+ `;
335
+ const markdown = `# Main Title
336
+
337
+ Here's some introductory text before the columns.
338
+
339
+ ${htmlContent}
340
+
341
+ ---
342
+
343
+ Footer text with **bold** and *italic*.`;
344
+ const mdxString = await htmlToMdxString({
345
+ markdown,
346
+ onError: (e) => {
347
+ throw e;
348
+ },
349
+ });
350
+ expect(mdxString).toMatchInlineSnapshot(`
351
+ "# Main Title
352
+
353
+ Here's some introductory text before the columns.
354
+
355
+ <columns>
356
+ <column>
357
+ <h2>
358
+ Section 1
359
+ </h2>
360
+
361
+ <page url="{{https://notion.so/page}}">
362
+ Page Link
363
+ </page>
364
+ </column>
365
+
366
+ <column>
367
+ <callout icon="⚠️" color="yellow_bg">
368
+ <strong>
369
+ Warning:
370
+ </strong>
371
+
372
+ Important information
373
+ </callout>
374
+ </column>
375
+ </columns>
376
+
377
+ ***
378
+
379
+ Footer text with **bold** and *italic*.
380
+ "
381
+ `);
382
+ });
383
+ test('handles HTML wrappers around markdown content', async () => {
384
+ // TODO if ypu do not add a new line after <selfClosingTag /> it gets all parsed as html!
385
+ const markdown = html `
386
+ <table-of-contents color="gray" />
387
+
388
+ ## GitHub/GitLab: Update issues with pull request actions
389
+ The GitHub and GitLab integrations move issues from *In Progress* to *Done* automatically so you never have to update issues manually. It takes less than a minute to connect GitHub to the workspace and then go to team settings to configure the automatic updates. Read more in the detailed [documentation]({{/60b0cf80dbe0420faa1264a58da48bd2}}).
390
+ <unknown url="{{https://www.notion.so/f050b7b5625b40c1a67ec00d8523dca8#68722a306eb646ffac1a6bf590b654f6}}" alt="tweet" />
391
+ ### ✨ProTip: Set personal GitHub preferences
392
+ Configure these settings in Preferences under Account Settings.
393
+ `;
394
+ const mdxString = await htmlToMdxString({
395
+ markdown,
396
+ onError: (e) => {
397
+ throw e;
398
+ },
399
+ });
400
+ expect(mdxString).toMatchInlineSnapshot(`
401
+ "<table-of-contents color="gray" />
402
+
403
+ ## GitHub/GitLab: Update issues with pull request actions
404
+
405
+ The GitHub and GitLab integrations move issues from *In Progress* to *Done* automatically so you never have to update issues manually. It takes less than a minute to connect GitHub to the workspace and then go to team settings to configure the automatic updates. Read more in the detailed [documentation](\\{\\{/60b0cf80dbe0420faa1264a58da48bd2}}).
406
+ <unknown url="{{https://www.notion.so/f050b7b5625b40c1a67ec00d8523dca8#68722a306eb646ffac1a6bf590b654f6}}" alt="tweet" />
407
+
408
+ ### ✨ProTip: Set personal GitHub preferences
409
+
410
+ Configure these settings in Preferences under Account Settings.
411
+ "
412
+ `);
413
+ });
414
+ });
415
+ describe('parseHtmlToMdxAst', () => {
416
+ test('parses simple HTML element', () => {
417
+ const result = parseHtmlToMdxAst({ html: '<div>Hello</div>' });
418
+ expect(result).toMatchInlineSnapshot(`
419
+ [
420
+ {
421
+ "attributes": [],
422
+ "children": [
423
+ {
424
+ "type": "text",
425
+ "value": "Hello",
426
+ },
427
+ ],
428
+ "name": "div",
429
+ "type": "mdxJsxTextElement",
430
+ },
431
+ ]
432
+ `);
433
+ });
434
+ test('parses element without transforms (generic)', () => {
435
+ const result = parseHtmlToMdxAst({
436
+ html: '<page url="{{https://notion.so/test}}">Test Page</page>',
437
+ });
438
+ expect(result).toMatchInlineSnapshot(`
439
+ [
440
+ {
441
+ "attributes": [
442
+ {
443
+ "name": "url",
444
+ "type": "mdxJsxAttribute",
445
+ "value": "{{https://notion.so/test}}",
446
+ },
447
+ ],
448
+ "children": [
449
+ {
450
+ "type": "text",
451
+ "value": "Test Page",
452
+ },
453
+ ],
454
+ "name": "page",
455
+ "type": "mdxJsxTextElement",
456
+ },
457
+ ]
458
+ `);
459
+ });
460
+ test('parses Notion page element', () => {
461
+ const result = parseHtmlToMdxAst({
462
+ html: '<page url="{{https://www.notion.so/test}}">Test Page</page>',
463
+ });
464
+ expect(result).toMatchInlineSnapshot(`
465
+ [
466
+ {
467
+ "attributes": [
468
+ {
469
+ "name": "url",
470
+ "type": "mdxJsxAttribute",
471
+ "value": "{{https://www.notion.so/test}}",
472
+ },
473
+ ],
474
+ "children": [
475
+ {
476
+ "type": "text",
477
+ "value": "Test Page",
478
+ },
479
+ ],
480
+ "name": "page",
481
+ "type": "mdxJsxTextElement",
482
+ },
483
+ ]
484
+ `);
485
+ });
486
+ test('handles partial HTML - opening tag only', () => {
487
+ const result = parseHtmlToMdxAst({ html: '<div>' });
488
+ expect(result).toMatchInlineSnapshot(`
489
+ [
490
+ {
491
+ "attributes": [],
492
+ "children": [],
493
+ "name": "div",
494
+ "type": "mdxJsxTextElement",
495
+ },
496
+ ]
497
+ `);
498
+ });
499
+ test('handles partial HTML - closing tag only', () => {
500
+ const result = parseHtmlToMdxAst({ html: '</div>' });
501
+ expect(result).toMatchInlineSnapshot(`
502
+ []
503
+ `);
504
+ });
505
+ test('handles self-closing tags', () => {
506
+ const result = parseHtmlToMdxAst({
507
+ html: '<img source="{{https://example.com/img.jpg}}" />',
508
+ });
509
+ expect(result).toMatchInlineSnapshot(`
510
+ [
511
+ {
512
+ "attributes": [
513
+ {
514
+ "name": "source",
515
+ "type": "mdxJsxAttribute",
516
+ "value": "{{https://example.com/img.jpg}}",
517
+ },
518
+ ],
519
+ "children": [],
520
+ "name": "img",
521
+ "type": "mdxJsxTextElement",
522
+ },
523
+ ]
524
+ `);
525
+ });
526
+ test('handles mention-page element', () => {
527
+ const result = parseHtmlToMdxAst({
528
+ html: '<mention-page url="{{https://www.notion.so/test}}" />',
529
+ });
530
+ expect(result).toMatchInlineSnapshot(`
531
+ [
532
+ {
533
+ "attributes": [
534
+ {
535
+ "name": "url",
536
+ "type": "mdxJsxAttribute",
537
+ "value": "{{https://www.notion.so/test}}",
538
+ },
539
+ ],
540
+ "children": [],
541
+ "name": "mention-page",
542
+ "type": "mdxJsxTextElement",
543
+ },
544
+ ]
545
+ `);
546
+ });
547
+ test('handles callout with attributes', () => {
548
+ const result = parseHtmlToMdxAst({
549
+ html: '<callout icon="📎" color="pink_bg">Some text</callout>',
550
+ });
551
+ expect(result).toMatchInlineSnapshot(`
552
+ [
553
+ {
554
+ "attributes": [
555
+ {
556
+ "name": "icon",
557
+ "type": "mdxJsxAttribute",
558
+ "value": "📎",
559
+ },
560
+ {
561
+ "name": "color",
562
+ "type": "mdxJsxAttribute",
563
+ "value": "pink_bg",
564
+ },
565
+ ],
566
+ "children": [
567
+ {
568
+ "type": "text",
569
+ "value": "Some text",
570
+ },
571
+ ],
572
+ "name": "callout",
573
+ "type": "mdxJsxTextElement",
574
+ },
575
+ ]
576
+ `);
577
+ });
578
+ test('handles span with color', () => {
579
+ const result = parseHtmlToMdxAst({
580
+ html: '<span color="blue">colored text</span>',
581
+ });
582
+ expect(result).toMatchInlineSnapshot(`
583
+ [
584
+ {
585
+ "attributes": [
586
+ {
587
+ "name": "color",
588
+ "type": "mdxJsxAttribute",
589
+ "value": "blue",
590
+ },
591
+ ],
592
+ "children": [
593
+ {
594
+ "type": "text",
595
+ "value": "colored text",
596
+ },
597
+ ],
598
+ "name": "span",
599
+ "type": "mdxJsxTextElement",
600
+ },
601
+ ]
602
+ `);
603
+ });
604
+ test('handles mixed content', () => {
605
+ const result = parseHtmlToMdxAst({
606
+ html: 'Some text <page url="{{https://notion.so/test}}">Page</page> more text',
607
+ });
608
+ expect(result).toMatchInlineSnapshot(`
609
+ [
610
+ {
611
+ "type": "text",
612
+ "value": "Some text",
613
+ },
614
+ {
615
+ "attributes": [
616
+ {
617
+ "name": "url",
618
+ "type": "mdxJsxAttribute",
619
+ "value": "{{https://notion.so/test}}",
620
+ },
621
+ ],
622
+ "children": [
623
+ {
624
+ "type": "text",
625
+ "value": "Page",
626
+ },
627
+ ],
628
+ "name": "page",
629
+ "type": "mdxJsxTextElement",
630
+ },
631
+ {
632
+ "type": "text",
633
+ "value": "more text",
634
+ },
635
+ ]
636
+ `);
637
+ });
638
+ test('handles comments', () => {
639
+ const result = parseHtmlToMdxAst({ html: '<!-- This is a comment -->' });
640
+ expect(result).toMatchInlineSnapshot(`[]`);
641
+ });
642
+ test('handles table with attributes', () => {
643
+ const result = parseHtmlToMdxAst({
644
+ html: '<table header-row="true"><tr><td>Cell</td></tr></table>',
645
+ });
646
+ expect(result).toMatchInlineSnapshot(`
647
+ [
648
+ {
649
+ "attributes": [
650
+ {
651
+ "name": "header-row",
652
+ "type": "mdxJsxAttribute",
653
+ "value": "true",
654
+ },
655
+ ],
656
+ "children": [
657
+ {
658
+ "attributes": [],
659
+ "children": [
660
+ {
661
+ "attributes": [],
662
+ "children": [
663
+ {
664
+ "type": "text",
665
+ "value": "Cell",
666
+ },
667
+ ],
668
+ "name": "td",
669
+ "type": "mdxJsxTextElement",
670
+ },
671
+ ],
672
+ "name": "tr",
673
+ "type": "mdxJsxTextElement",
674
+ },
675
+ ],
676
+ "name": "table",
677
+ "type": "mdxJsxTextElement",
678
+ },
679
+ ]
680
+ `);
681
+ });
682
+ });
683
+ describe('parseHtmlToMdxAst without transforms (generic)', () => {
684
+ test('preserves tag names without transform', () => {
685
+ const result = parseHtmlToMdxAst({ html: '<page>Content</page>' });
686
+ expect(result[0]).toHaveProperty('name', 'page');
687
+ });
688
+ test('preserves curly brace URLs without transform', () => {
689
+ const result = parseHtmlToMdxAst({
690
+ html: '<a href="{{https://example.com}}">Link</a>',
691
+ });
692
+ expect(result[0]).toHaveProperty('attributes');
693
+ const attrs = result[0].attributes;
694
+ expect(attrs[0]).toHaveProperty('value', '{{https://example.com}}');
695
+ });
696
+ });
697
+ describe('parseHtmlToMdxAst with markdown processor', () => {
698
+ test('parses markdown inside HTML tags', async () => {
699
+ const htmlContent = html `
700
+ <callout>
701
+ This is **bold** text
702
+ </callout>
703
+ `;
704
+ const mdxString = await htmlToMdxString({
705
+ markdown: htmlContent,
706
+ onError: (e) => {
707
+ throw e;
708
+ },
709
+ });
710
+ expect(mdxString).toMatchInlineSnapshot(`
711
+ "<callout>
712
+ This is **bold** text
713
+ </callout>
714
+ "
715
+ `);
716
+ });
717
+ test('parses markdown links inside HTML', async () => {
718
+ const htmlContent = html `
719
+ <span color="orange">
720
+ [link](http://google.com)
721
+ </span>
722
+ `;
723
+ const mdxString = await htmlToMdxString({
724
+ markdown: htmlContent,
725
+ onError: (e) => {
726
+ throw e;
727
+ },
728
+ });
729
+ expect(mdxString).toMatchInlineSnapshot(`
730
+ "<span color="orange">
731
+ [link](http://google.com)
732
+ </span>
733
+ "
734
+ `);
735
+ });
736
+ test('parses mixed markdown and HTML inside tags', async () => {
737
+ const htmlContent = html `
738
+ <callout>
739
+ **Read next:** <mention-page url="https://notion.so/page"/>
740
+ </callout>
741
+ `;
742
+ const mdxString = await htmlToMdxString({
743
+ markdown: htmlContent,
744
+ onError: (e) => {
745
+ throw e;
746
+ },
747
+ });
748
+ expect(mdxString).toMatchInlineSnapshot(`
749
+ "<callout>
750
+ **Read next:**
751
+
752
+ <mention-page url="https://notion.so/page" />
753
+ </callout>
754
+ "
755
+ `);
756
+ });
757
+ test('handles bold inside span with underline', async () => {
758
+ const htmlContent = html `
759
+ <span underline="true">
760
+ **sdf dsf**
761
+ </span>
762
+ `;
763
+ const mdxString = await htmlToMdxString({
764
+ markdown: htmlContent,
765
+ onError: (e) => {
766
+ throw e;
767
+ },
768
+ });
769
+ expect(mdxString).toMatchInlineSnapshot(`
770
+ "<span underline="true">
771
+ **sdf dsf**
772
+ </span>
773
+ "
774
+ `);
775
+ });
776
+ test('converts markdown inside callout to MDX string', async () => {
777
+ const htmlContent = html `
778
+ <callout icon="👉" color="orange_bg">
779
+ **Read next:** Some page
780
+ </callout>
781
+ `;
782
+ const mdxString = await htmlToMdxString({
783
+ markdown: htmlContent,
784
+ onError: (e) => {
785
+ throw e;
786
+ },
787
+ });
788
+ expect(mdxString).toMatchInlineSnapshot(`
789
+ "<callout icon="👉" color="orange_bg">
790
+ **Read next:** Some page
791
+ </callout>
792
+ "
793
+ `);
794
+ });
795
+ test('handles markdown inside table cells', async () => {
796
+ const htmlContent = html `
797
+ <table>
798
+ <tr>
799
+ <td>
800
+ **Bold** text and [link](http://example.com)
801
+ </td>
802
+ </tr>
803
+ </table>
804
+ `;
805
+ const mdxString = await htmlToMdxString({
806
+ markdown: htmlContent,
807
+ onError: (e) => {
808
+ throw e;
809
+ },
810
+ });
811
+ expect(mdxString).toMatchInlineSnapshot(`
812
+ "<table>
813
+ <tr>
814
+ <td>
815
+ **Bold** text and [link](http://example.com)
816
+ </td>
817
+ </tr>
818
+ </table>
819
+ "
820
+ `);
821
+ });
822
+ test('preserves plain text when no markdown', async () => {
823
+ const htmlContent = html `
824
+ <div>
825
+ Plain text without markdown
826
+ </div>
827
+ `;
828
+ const mdxString = await htmlToMdxString({
829
+ markdown: htmlContent,
830
+ onError: (e) => {
831
+ throw e;
832
+ },
833
+ });
834
+ expect(mdxString).toMatchInlineSnapshot(`
835
+ "<div>
836
+ Plain text without markdown
837
+ </div>
838
+ "
839
+ `);
840
+ });
841
+ test('handles nested HTML tags with markdown', async () => {
842
+ const htmlContent = html `
843
+ <div>
844
+ <span>
845
+ **Bold** and <a href="#">link</a>
846
+ </span>
847
+ </div>
848
+ `;
849
+ const mdxString = await htmlToMdxString({
850
+ markdown: htmlContent,
851
+ onError: (e) => {
852
+ throw e;
853
+ },
854
+ });
855
+ expect(mdxString).toMatchInlineSnapshot(`
856
+ "<div>
857
+ <span>
858
+ **Bold** and
859
+
860
+ <a href="#">
861
+ link
862
+ </a>
863
+ </span>
864
+ </div>
865
+ "
866
+ `);
867
+ });
868
+ });
869
+ //# sourceMappingURL=html-and-md.test.js.map