@link-assistant/web-capture 1.1.2

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 (56) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +294 -0
  4. package/.jscpd.json +20 -0
  5. package/.lenv +8 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/ARCHITECTURE.md +982 -0
  9. package/CHANGELOG.md +22 -0
  10. package/Dockerfile +45 -0
  11. package/LICENSE +24 -0
  12. package/README.md +268 -0
  13. package/apply.sh +166 -0
  14. package/babel.config.cjs +13 -0
  15. package/bin/web-capture.js +285 -0
  16. package/docker-compose.yml +9 -0
  17. package/docs/market-analysis-2025-05-13.md +64 -0
  18. package/eslint.config.js +112 -0
  19. package/examples/js/engine_comparison.js +39 -0
  20. package/examples/js/html_download.js +23 -0
  21. package/examples/js/image_download.js +36 -0
  22. package/examples/js/markdown_download.js +23 -0
  23. package/examples/js/playwright_example.js +23 -0
  24. package/examples/python/html_download.py +15 -0
  25. package/examples/python/image_download.py +20 -0
  26. package/examples/python/markdown_download.py +15 -0
  27. package/examples/python/playwright_example.py +22 -0
  28. package/jest.config.mjs +18 -0
  29. package/package.json +83 -0
  30. package/scripts/changeset-version.mjs +38 -0
  31. package/scripts/create-github-release.mjs +93 -0
  32. package/scripts/create-manual-changeset.mjs +88 -0
  33. package/scripts/format-github-release.mjs +83 -0
  34. package/scripts/format-release-notes.mjs +218 -0
  35. package/scripts/instant-version-bump.mjs +121 -0
  36. package/scripts/publish-to-npm.mjs +128 -0
  37. package/scripts/setup-npm.mjs +37 -0
  38. package/scripts/validate-changeset.mjs +106 -0
  39. package/scripts/version-and-commit.mjs +237 -0
  40. package/src/browser.js +207 -0
  41. package/src/fetch.js +38 -0
  42. package/src/html.js +64 -0
  43. package/src/image.js +41 -0
  44. package/src/index.js +55 -0
  45. package/src/lib.js +330 -0
  46. package/src/markdown.js +17 -0
  47. package/src/stream.js +50 -0
  48. package/tests/e2e/docker.test.js +148 -0
  49. package/tests/e2e/process.test.js +117 -0
  50. package/tests/integration/browser-engines.test.js +128 -0
  51. package/tests/jest.setup.mjs +7 -0
  52. package/tests/mock/index.test.js +223 -0
  53. package/tests/unit/browser.test.js +136 -0
  54. package/tests/unit/cli.test.js +151 -0
  55. package/tests/unit/convertRelativeUrls.test.js +34 -0
  56. package/tests/unit/html2md.test.js +610 -0
@@ -0,0 +1,610 @@
1
+ import { convertHtmlToMarkdown } from '../../src/lib.js';
2
+
3
+ describe('convertHtmlToMarkdown', () => {
4
+ it('removes empty links and anchor headings', () => {
5
+ const html = `
6
+ <a href="/http-api/authorization"></a>
7
+ <a href="/webhooks/authorization"></a>
8
+ <a href="/api-reference/http-api"></a>
9
+ <h3></h3>
10
+ <a href="#quick-start"></a>
11
+ <h3>Quick start</h3>
12
+ `;
13
+ const md = convertHtmlToMarkdown(html);
14
+ // Should not contain empty []() links or [](#anchor) or empty headings
15
+ expect(md).not.toMatch(/\[\s*\]\([^)]*\)/);
16
+ expect(md).not.toMatch(/#+\s*$/m);
17
+ expect(md).not.toMatch(/\[\s*\]\(#.*\)/);
18
+ // Should contain the real heading
19
+ expect(md).toMatch(/### Quick start/);
20
+ });
21
+
22
+ it('removes empty links in complex content', () => {
23
+ const html = `
24
+ <div>
25
+ <a href="/foo"></a>
26
+ <a href="/bar">Bar</a>
27
+ <a href="#anchor"></a>
28
+ <h2></h2>
29
+ <h2>Title</h2>
30
+ </div>
31
+ `;
32
+ const md = convertHtmlToMarkdown(html);
33
+ expect(md).not.toMatch(/\[\s*\]\([^)]*\)/);
34
+ expect(md).toMatch(/\[Bar\]\(\/bar\)/);
35
+ expect(md).toMatch(/## Title/);
36
+ });
37
+
38
+ it('removes empty links with only whitespace', () => {
39
+ const html = `
40
+ <a href="/foo"> </a>
41
+ <a href="/bar">\n\t</a>
42
+ <a href="#anchor"> </a>
43
+ `;
44
+ const md = convertHtmlToMarkdown(html);
45
+ expect(md).not.toMatch(/\[\s*\]\([^)]*\)/);
46
+ expect(md).not.toMatch(/\[\s*\]\(#.*\)/);
47
+ });
48
+
49
+ it('removes empty links with only child elements', () => {
50
+ const html = `
51
+ <a href="/foo"><span></span></a>
52
+ <a href="/bar"><div></div></a>
53
+ <a href="#anchor"><span> </span></a>
54
+ <a href="/baz"><img src='x.png'></a>
55
+ <a href="/qux"><span>\n</span></a>
56
+ <a href="/keep">Text<span>child</span></a>
57
+ `;
58
+ const md = convertHtmlToMarkdown(html);
59
+ expect(md).not.toMatch(/\[\s*\]\([^)]*\)/);
60
+ expect(md).not.toMatch(/\[\s*\]\(#.*\)/);
61
+ expect(md).toMatch(/\[Textchild\]\(\/keep\)/);
62
+ });
63
+
64
+ it('removes headings with only whitespace', () => {
65
+ const html = `
66
+ <h2> </h2>
67
+ <h3>\n\t</h3>
68
+ <h4>\u00A0</h4>
69
+ <h2>Valid Heading</h2>
70
+ `;
71
+ const md = convertHtmlToMarkdown(html);
72
+ expect(md).not.toMatch(/#+\s*$/m);
73
+ expect(md).toMatch(/## Valid Heading/);
74
+ });
75
+
76
+ it('converts relative links to absolute if baseUrl is provided', () => {
77
+ const html = `
78
+ <a href="/foo">Foo</a>
79
+ <img src="/bar.png" alt="Bar">
80
+ <a href="https://external.com">External</a>
81
+ `;
82
+ const md = convertHtmlToMarkdown(html, 'https://example.com/base/');
83
+ expect(md).toMatch(/\[Foo\]\(https:\/\/example.com\/foo\)/);
84
+ expect(md).toMatch(/!\[Bar\]\(https:\/\/example.com\/bar.png\)/);
85
+ expect(md).toMatch(/\[External\]\(https:\/\/external.com\/?\)/);
86
+ });
87
+
88
+ it('does not change links if no baseUrl is provided', () => {
89
+ const html = `
90
+ <a href="/foo">Foo</a>
91
+ <img src="/bar.png" alt="Bar">
92
+ `;
93
+ const md = convertHtmlToMarkdown(html);
94
+ expect(md).toMatch(/\[Foo\]\(\/foo\)/);
95
+ expect(md).toMatch(/!\[Bar\]\(\/bar.png\)/);
96
+ });
97
+
98
+ it('converts ARIA role table to Markdown table', () => {
99
+ const html = `
100
+ <div
101
+ role="table"
102
+ aria-label="Semantic Elements"
103
+ aria-describedby="semantic_elements_table_desc"
104
+ aria-rowcount="81">
105
+ <div id="semantic_elements_table_desc">
106
+ Semantic Elements to use instead of ARIA's roles
107
+ </div>
108
+ <div role="rowgroup">
109
+ <div role="row">
110
+ <span role="columnheader" aria-sort="none">ARIA Role</span>
111
+ <span role="columnheader" aria-sort="none">Semantic Element</span>
112
+ </div>
113
+ </div>
114
+ <div role="rowgroup">
115
+ <div role="row" aria-rowindex="11">
116
+ <span role="cell">header</span>
117
+ <span role="cell">h1</span>
118
+ </div>
119
+ <div role="row" aria-rowindex="16">
120
+ <span role="cell">header</span>
121
+ <span role="cell">h6</span>
122
+ </div>
123
+ <div role="row" aria-rowindex="18">
124
+ <span role="cell">rowgroup</span>
125
+ <span role="cell">thead</span>
126
+ </div>
127
+ <div role="row" aria-rowindex="24">
128
+ <span role="cell">term</span>
129
+ <span role="cell">dt</span>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ `;
134
+ const md = convertHtmlToMarkdown(html);
135
+ // Should contain a Markdown table header
136
+ expect(md).toMatch(/\|\s*ARIA Role\s*\|\s*Semantic Element\s*\|/);
137
+ // Should contain the separator row
138
+ expect(md).toMatch(/\|\s*-+\s*\|\s*-+\s*\|/);
139
+ // Should contain all data rows
140
+ expect(md).toMatch(/\|\s*header\s*\|\s*h1\s*\|/);
141
+ expect(md).toMatch(/\|\s*header\s*\|\s*h6\s*\|/);
142
+ expect(md).toMatch(/\|\s*rowgroup\s*\|\s*thead\s*\|/);
143
+ expect(md).toMatch(/\|\s*term\s*\|\s*dt\s*\|/);
144
+ });
145
+
146
+ it('converts a regular HTML table to Markdown table', () => {
147
+ const html = `
148
+ <table>
149
+ <caption>Sample Table</caption>
150
+ <thead>
151
+ <tr>
152
+ <th>Header 1</th>
153
+ <th>Header 2</th>
154
+ </tr>
155
+ </thead>
156
+ <tbody>
157
+ <tr>
158
+ <td>Cell 1</td>
159
+ <td>Cell 2</td>
160
+ </tr>
161
+ <tr>
162
+ <td>Cell 3</td>
163
+ <td>Cell 4</td>
164
+ </tr>
165
+ </tbody>
166
+ </table>
167
+ `;
168
+ const md = convertHtmlToMarkdown(html);
169
+ // Should contain a Markdown table header
170
+ expect(md).toMatch(/\|\s*Header 1\s*\|\s*Header 2\s*\|/);
171
+ // Should contain the separator row
172
+ expect(md).toMatch(/\|\s*-+\s*\|\s*-+\s*\|/);
173
+ // Should contain all data rows
174
+ expect(md).toMatch(/\|\s*Cell 1\s*\|\s*Cell 2\s*\|/);
175
+ expect(md).toMatch(/\|\s*Cell 3\s*\|\s*Cell 4\s*\|/);
176
+ });
177
+
178
+ it('handles empty tables gracefully', () => {
179
+ const html = `
180
+ <table>
181
+ <thead>
182
+ <tr>
183
+ <th></th>
184
+ <th></th>
185
+ </tr>
186
+ </thead>
187
+ <tbody>
188
+ <tr>
189
+ <td></td>
190
+ <td></td>
191
+ </tr>
192
+ </tbody>
193
+ </table>
194
+ `;
195
+ const md = convertHtmlToMarkdown(html);
196
+ // Should not throw an error
197
+ // Accept the actual output for an empty table (2 columns, empty header and row)
198
+ expect(md.trim()).toBe('| | |\n| --- | --- |\n| | |');
199
+ });
200
+
201
+ it('handles specific ARIA table example', () => {
202
+ const html = `
203
+ <div
204
+ class="mx-auto w-full decoration-primary/6 max-w-3xl page-api-block:ml-0 table_tableWrapper__zr7LC"
205
+ >
206
+ <div role="table" class="flex flex-col">
207
+ <div
208
+ role="rowgroup"
209
+ class="w-full table_rowGroup__IKtSP straight-corners:rounded-none"
210
+ >
211
+ <div role="row" class="flex w-full">
212
+ <div
213
+ role="columnheader"
214
+ class="table_columnHeader__PGmsy text-left"
215
+ title="Attribute"
216
+ style="width: 181px; min-width: 181px"
217
+ >
218
+ Attribute
219
+ </div>
220
+ <div
221
+ role="columnheader"
222
+ class="table_columnHeader__PGmsy text-left"
223
+ title="Type"
224
+ style="width: 110px; min-width: 110px"
225
+ >
226
+ Type
227
+ </div>
228
+ <div
229
+ role="columnheader"
230
+ class="table_columnHeader__PGmsy text-left"
231
+ title="Required"
232
+ style="width: 104px; min-width: 104px"
233
+ >
234
+ Required
235
+ </div>
236
+ <div
237
+ role="columnheader"
238
+ class="table_columnHeader__PGmsy text-left"
239
+ title="Description"
240
+ style="width: clamp(100px, 100% - 395px, 100%); min-width: 100px"
241
+ >
242
+ Description
243
+ </div>
244
+ </div>
245
+ </div>
246
+ <div role="rowgroup" class="flex flex-col w-full [&amp;>*+*]:border-t">
247
+ <div class="table_row__LpfCG" role="row">
248
+ <div
249
+ role="cell"
250
+ class="table_cell__X_gFM"
251
+ style="width: 181px; min-width: 181px"
252
+ >
253
+ <div
254
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
255
+ >
256
+ <p class="mx-auto decoration-primary/6 w-full max-w-[unset]">
257
+ amount
258
+ </p>
259
+ </div>
260
+ </div>
261
+ <div
262
+ role="cell"
263
+ class="table_cell__X_gFM"
264
+ style="width: 110px; min-width: 110px"
265
+ >
266
+ <div
267
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
268
+ >
269
+ <p class="mx-auto decoration-primary/6 w-full max-w-[unset]">
270
+ decimal
271
+ </p>
272
+ </div>
273
+ </div>
274
+ <div
275
+ role="cell"
276
+ class="table_cell__X_gFM"
277
+ style="width: 104px; min-width: 104px"
278
+ >
279
+ <div
280
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
281
+ >
282
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">yes</p>
283
+ </div>
284
+ </div>
285
+ <div
286
+ role="cell"
287
+ class="table_cell__X_gFM"
288
+ style="
289
+ width: clamp(100px, 100% - 395px, 100%);
290
+ min-width: clamp(100px, 100% - 395px, 100%);
291
+ "
292
+ >
293
+ <div
294
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
295
+ >
296
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
297
+ order amount
298
+ </p>
299
+ </div>
300
+ </div>
301
+ </div>
302
+ <div class="table_row__LpfCG" role="row">
303
+ <div
304
+ role="cell"
305
+ class="table_cell__X_gFM"
306
+ style="width: 181px; min-width: 181px"
307
+ >
308
+ <div
309
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
310
+ >
311
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
312
+ currency_id
313
+ </p>
314
+ </div>
315
+ </div>
316
+ <div
317
+ role="cell"
318
+ class="table_cell__X_gFM"
319
+ style="width: 110px; min-width: 110px"
320
+ >
321
+ <div
322
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
323
+ >
324
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
325
+ string
326
+ </p>
327
+ </div>
328
+ </div>
329
+ <div
330
+ role="cell"
331
+ class="table_cell__X_gFM"
332
+ style="width: 104px; min-width: 104px"
333
+ >
334
+ <div
335
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
336
+ >
337
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">yes</p>
338
+ </div>
339
+ </div>
340
+ <div
341
+ role="cell"
342
+ class="table_cell__X_gFM"
343
+ style="
344
+ width: clamp(100px, 100% - 395px, 100%);
345
+ min-width: clamp(100px, 100% - 395px, 100%);
346
+ "
347
+ >
348
+ <div
349
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
350
+ >
351
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
352
+ order currency id
353
+ </p>
354
+ </div>
355
+ </div>
356
+ </div>
357
+ <div class="table_row__LpfCG" role="row">
358
+ <div
359
+ role="cell"
360
+ class="table_cell__X_gFM"
361
+ style="width: 181px; min-width: 181px"
362
+ >
363
+ <div
364
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
365
+ >
366
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
367
+ network
368
+ </p>
369
+ </div>
370
+ </div>
371
+ <div
372
+ role="cell"
373
+ class="table_cell__X_gFM"
374
+ style="width: 110px; min-width: 110px"
375
+ >
376
+ <div
377
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
378
+ >
379
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
380
+ string
381
+ </p>
382
+ </div>
383
+ </div>
384
+ <div
385
+ role="cell"
386
+ class="table_cell__X_gFM"
387
+ style="width: 104px; min-width: 104px"
388
+ >
389
+ <div
390
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
391
+ >
392
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">yes</p>
393
+ </div>
394
+ </div>
395
+ <div
396
+ role="cell"
397
+ class="table_cell__X_gFM"
398
+ style="
399
+ width: clamp(100px, 100% - 395px, 100%);
400
+ min-width: clamp(100px, 100% - 395px, 100%);
401
+ "
402
+ >
403
+ <div
404
+ class="blocks w-full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
405
+ >
406
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
407
+ currency network
408
+ </p>
409
+ </div>
410
+ </div>
411
+ </div>
412
+ <div class="table_row__LpfCG" role="row">
413
+ <div
414
+ role="cell"
415
+ class="table_cell__X_gFM"
416
+ style="width: 181px; min-width: 181px"
417
+ >
418
+ <div
419
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
420
+ >
421
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
422
+ external_order_id
423
+ </p>
424
+ </div>
425
+ </div>
426
+ <div
427
+ role="cell"
428
+ class="table_cell__X_gFM"
429
+ style="width: 110px; min-width: 110px"
430
+ >
431
+ <div
432
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
433
+ >
434
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
435
+ string
436
+ </p>
437
+ </div>
438
+ </div>
439
+ <div
440
+ role="cell"
441
+ class="table_cell__X_gFM"
442
+ style="width: 104px; min-width: 104px"
443
+ >
444
+ <div
445
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
446
+ >
447
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">no</p>
448
+ </div>
449
+ </div>
450
+ <div
451
+ role="cell"
452
+ class="table_cell__X_gFM"
453
+ style="
454
+ width: clamp(100px, 100% - 395px, 100%);
455
+ min-width: clamp(100px, 100% - 395px, 100%);
456
+ "
457
+ >
458
+ <div
459
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
460
+ >
461
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
462
+ additional external invoice id
463
+ </p>
464
+ </div>
465
+ </div>
466
+ </div>
467
+ <div class="table_row__LpfCG" role="row">
468
+ <div
469
+ role="cell"
470
+ class="table_cell__X_gFM"
471
+ style="width: 181px; min-width: 181px"
472
+ >
473
+ <div
474
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
475
+ >
476
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
477
+ email
478
+ </p>
479
+ </div>
480
+ </div>
481
+ <div
482
+ role="cell"
483
+ class="table_cell__X_gFM"
484
+ style="width: 110px; min-width: 110px"
485
+ >
486
+ <div
487
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
488
+ >
489
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
490
+ string
491
+ </p>
492
+ </div>
493
+ </div>
494
+ <div
495
+ role="cell"
496
+ class="table_cell__X_gFM"
497
+ style="width: 104px; min-width: 104px"
498
+ >
499
+ <div
500
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
501
+ >
502
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">no</p>
503
+ </div>
504
+ </div>
505
+ <div
506
+ role="cell"
507
+ class="table_cell__X_gFM"
508
+ style="
509
+ width: clamp(100px, 100% - 395px, 100%);
510
+ min-width: clamp(100px, 100% - 395px, 100%);
511
+ "
512
+ >
513
+ <div
514
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
515
+ >
516
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
517
+ end users email
518
+ </p>
519
+ </div>
520
+ </div>
521
+ </div>
522
+ <div class="table_row__LpfCG" role="row">
523
+ <div
524
+ role="cell"
525
+ class="table_cell__X_gFM"
526
+ style="width: 181px; min-width: 181px"
527
+ >
528
+ <div
529
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
530
+ >
531
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
532
+ description
533
+ </p>
534
+ </div>
535
+ </div>
536
+ <div
537
+ role="cell"
538
+ class="table_cell__X_gFM"
539
+ style="width: 110px; min-width: 110px"
540
+ >
541
+ <div
542
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
543
+ >
544
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
545
+ string
546
+ </p>
547
+ </div>
548
+ </div>
549
+ <div
550
+ role="cell"
551
+ class="table_cell__X_gFM"
552
+ style="width: 104px; min-width: 104px"
553
+ >
554
+ <div
555
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
556
+ >
557
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">no</p>
558
+ </div>
559
+ </div>
560
+ <div
561
+ role="cell"
562
+ class="table_cell__X_gFM"
563
+ style="
564
+ width: clamp(100px, 100% - 395px, 100%);
565
+ min-width: clamp(100px, 100% - 395px, 100%);
566
+ "
567
+ >
568
+ <div
569
+ class="blocks w_full space-y-2 lg:space-y-3 leading-normal self-center [&amp;_*]:text-left text-left"
570
+ >
571
+ <p class="mx-auto decoration-primary/6 w_full max-w-[unset]">
572
+ order description
573
+ </p>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ </div>
578
+ </div>
579
+ </div>
580
+ `;
581
+
582
+ const md = convertHtmlToMarkdown(html);
583
+ // Should contain a Markdown table header
584
+ expect(md).toMatch(
585
+ /\|\s*Attribute\s*\|\s*Type\s*\|\s*Required\s*\|\s*Description\s*\|/
586
+ );
587
+ // Should contain the separator row
588
+ expect(md).toMatch(/\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|/);
589
+ // Should contain all data rows
590
+ // Accept any number of backslashes before underscores (Turndown may escape as \\_)
591
+ expect(md).toMatch(
592
+ /\|\s*amount\s*\|\s*decimal\s*\|\s*yes\s*\|\s*order amount\s*\|/
593
+ );
594
+ expect(md).toMatch(
595
+ /\|\s*currency(?:\\*)_id\s*\|\s*string\s*\|\s*yes\s*\|\s*order currency id\s*\|/
596
+ );
597
+ expect(md).toMatch(
598
+ /\|\s*network\s*\|\s*string\s*\|\s*yes\s*\|\s*currency network\s*\|/
599
+ );
600
+ expect(md).toMatch(
601
+ /\|\s*external(?:\\*)_order(?:\\*)_id\s*\|\s*string\s*\|\s*no\s*\|\s*additional external invoice id\s*\|/
602
+ );
603
+ expect(md).toMatch(
604
+ /\|\s*email\s*\|\s*string\s*\|\s*no\s*\|\s*end users email\s*\|/
605
+ );
606
+ expect(md).toMatch(
607
+ /\|\s*description\s*\|\s*string\s*\|\s*no\s*\|\s*order description\s*\|/
608
+ );
609
+ });
610
+ });