@postalsys/email-text-tools 2.4.26 → 2.6.0

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 (88) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +12 -0
  3. package/dist/cjs/index.d.ts +20 -0
  4. package/dist/cjs/index.js +31 -0
  5. package/dist/cjs/lib/commonjs.cjs +17 -0
  6. package/dist/cjs/lib/commonjs.d.cts +4 -0
  7. package/dist/cjs/lib/format-date.d.ts +15 -0
  8. package/dist/cjs/lib/format-date.js +60 -0
  9. package/dist/cjs/lib/html-guard.d.ts +3 -0
  10. package/dist/cjs/lib/html-guard.js +119 -0
  11. package/dist/cjs/lib/html-to-text.d.ts +1 -0
  12. package/dist/cjs/lib/html-to-text.js +49 -0
  13. package/dist/cjs/lib/inline-html.d.ts +19 -0
  14. package/dist/cjs/lib/inline-html.js +150 -0
  15. package/dist/cjs/lib/inline-style.d.ts +2 -0
  16. package/dist/cjs/lib/inline-style.js +42 -0
  17. package/dist/cjs/lib/inline-text.d.ts +3 -0
  18. package/dist/cjs/lib/inline-text.js +111 -0
  19. package/dist/cjs/lib/juice-worker.d.ts +1 -0
  20. package/dist/cjs/lib/juice-worker.js +19 -0
  21. package/dist/cjs/lib/mime-html.d.ts +34 -0
  22. package/dist/cjs/lib/mime-html.js +496 -0
  23. package/dist/cjs/lib/text-to-html.d.ts +1 -0
  24. package/dist/cjs/lib/text-to-html.js +182 -0
  25. package/dist/cjs/lib/worker-pool.d.ts +61 -0
  26. package/dist/cjs/lib/worker-pool.js +229 -0
  27. package/dist/cjs/package.json +3 -0
  28. package/dist/esm/index.d.ts +20 -0
  29. package/dist/esm/index.js +13 -0
  30. package/dist/esm/lib/commonjs.cjs +17 -0
  31. package/dist/esm/lib/commonjs.d.cts +4 -0
  32. package/dist/esm/lib/format-date.d.ts +15 -0
  33. package/{lib → dist/esm/lib}/format-date.js +8 -16
  34. package/dist/esm/lib/html-guard.d.ts +3 -0
  35. package/{lib → dist/esm/lib}/html-guard.js +8 -24
  36. package/dist/esm/lib/html-to-text.d.ts +1 -0
  37. package/{lib → dist/esm/lib}/html-to-text.js +2 -14
  38. package/dist/esm/lib/inline-html.d.ts +19 -0
  39. package/{lib → dist/esm/lib}/inline-html.js +17 -55
  40. package/dist/esm/lib/inline-style.d.ts +2 -0
  41. package/dist/esm/lib/inline-style.js +39 -0
  42. package/dist/esm/lib/inline-text.d.ts +3 -0
  43. package/{lib → dist/esm/lib}/inline-text.js +8 -31
  44. package/dist/esm/lib/juice-worker.d.ts +1 -0
  45. package/{lib → dist/esm/lib}/juice-worker.js +7 -8
  46. package/dist/esm/lib/mime-html.d.ts +34 -0
  47. package/{lib → dist/esm/lib}/mime-html.js +80 -161
  48. package/dist/esm/lib/text-to-html.d.ts +1 -0
  49. package/{lib → dist/esm/lib}/text-to-html.js +35 -63
  50. package/dist/esm/lib/worker-pool.d.ts +61 -0
  51. package/{lib → dist/esm/lib}/worker-pool.js +18 -64
  52. package/dist/esm/package.json +3 -0
  53. package/package.json +67 -10
  54. package/.github/VULNERABILITY_REPORT.yml +0 -75
  55. package/.github/workflows/release.yaml +0 -38
  56. package/.github/workflows/test.yaml +0 -35
  57. package/.ncurc.js +0 -28
  58. package/.prettierignore +0 -14
  59. package/.prettierrc.js +0 -10
  60. package/.release-please-manifest.json +0 -3
  61. package/CLAUDE.md +0 -105
  62. package/benchmark/benchmark.js +0 -348
  63. package/benchmark/concurrency-benchmark.js +0 -176
  64. package/benchmark/quick-benchmark.js +0 -200
  65. package/benchmark/quick-concurrency.js +0 -64
  66. package/benchmark/real-world-benchmark.js +0 -257
  67. package/docs/PERFORMANCE.md +0 -262
  68. package/eslint.config.js +0 -71
  69. package/examples/failing.html +0 -617
  70. package/examples/fixtures/message-1.json +0 -74
  71. package/examples/inline-html.js +0 -24
  72. package/examples/inline-text.js +0 -24
  73. package/examples/mime-html.js +0 -284
  74. package/index.js +0 -22
  75. package/release-please-config.json +0 -10
  76. package/test/comprehensive.test.js +0 -358
  77. package/test/containment.test.js +0 -128
  78. package/test/edge-cases.test.js +0 -62
  79. package/test/hardening.test.js +0 -504
  80. package/test/html-to-text.test.js +0 -253
  81. package/test/inline-html.test.js +0 -230
  82. package/test/inline-text.test.js +0 -204
  83. package/test/mime-html-extra.test.js +0 -39
  84. package/test/mime-html.test.js +0 -302
  85. package/test/text-to-html-extra.test.js +0 -42
  86. package/test/worker-pool-extra.test.js +0 -132
  87. package/test/worker-pool-regression.test.js +0 -128
  88. package/test/worker-pool.test.js +0 -282
@@ -1,358 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it, before: _before, after } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const mimeHtml = require('../lib/mime-html');
6
- const textToHtml = require('../lib/text-to-html');
7
- const _fs = require('fs');
8
- const path = require('path');
9
- const { Worker } = require('worker_threads');
10
-
11
- describe('Comprehensive Test Suite', () => {
12
- describe('text-to-html conversion', () => {
13
- it('should convert plain text to HTML', () => {
14
- const text = 'Hello World\nThis is a test';
15
- const html = textToHtml(text);
16
-
17
- assert.ok(html.includes('Hello World'));
18
- assert.ok(html.includes('<br'));
19
- assert.ok(html.includes('This is a test'));
20
- });
21
-
22
- it('should handle quoted text', () => {
23
- const text = 'Normal text\n> Quoted text\n> More quoted\nNormal again';
24
- const html = textToHtml(text);
25
-
26
- assert.ok(html.includes('blockquote'));
27
- assert.ok(html.includes('Quoted text'));
28
- assert.ok(html.includes('Normal again'));
29
- });
30
-
31
- it('should linkify URLs', () => {
32
- const text = 'Visit https://example.com for more info';
33
- const html = textToHtml(text);
34
-
35
- assert.ok(html.includes('<a'));
36
- assert.ok(html.includes('href='));
37
- assert.ok(html.includes('https://example.com'));
38
- });
39
-
40
- it('should linkify bare domains without a scheme', () => {
41
- // linkify-it 6 stopped doing this by default, which silently dropped
42
- // autolinking for plain text emails, so keep it covered
43
- const html = textToHtml('Visit example.com and www.example.org today');
44
-
45
- assert.ok(html.includes('<a href="http://example.com">example.com</a>'));
46
- assert.ok(html.includes('<a href="http://www.example.org">www.example.org</a>'));
47
- });
48
-
49
- it('should handle email addresses', () => {
50
- const text = 'Contact me at test@example.com';
51
- const html = textToHtml(text);
52
-
53
- assert.ok(html.includes('mailto:'));
54
- assert.ok(html.includes('test@example.com'));
55
- });
56
-
57
- it('should escape HTML entities', () => {
58
- const text = 'Test <script>alert("xss")</script> & special chars';
59
- const html = textToHtml(text);
60
-
61
- assert.ok(!html.includes('<script>'));
62
- assert.ok(html.includes('&lt;script'));
63
- assert.ok(html.includes('&amp;'));
64
- });
65
-
66
- it('should handle empty text', () => {
67
- const html = textToHtml('');
68
- assert.ok(typeof html === 'string');
69
- });
70
-
71
- it('should handle null/undefined', () => {
72
- const html1 = textToHtml(null);
73
- const html2 = textToHtml(undefined);
74
- const html3 = textToHtml('');
75
-
76
- // textToHtml returns minimal HTML for empty input
77
- assert.ok(typeof html1 === 'string');
78
- assert.ok(typeof html2 === 'string');
79
- assert.ok(typeof html3 === 'string');
80
- // Should return consistent output for all empty inputs
81
- assert.equal(html1, html2);
82
- assert.equal(html2, html3);
83
- });
84
- });
85
-
86
- describe('juice-worker.js', () => {
87
- it('should process HTML in worker thread', async () => {
88
- const worker = new Worker(path.join(__dirname, '../lib/juice-worker.js'));
89
-
90
- const html = '<style>p { color: red; }</style><p>Test</p>';
91
-
92
- const result = await new Promise((resolve, reject) => {
93
- worker.on('message', msg => {
94
- worker.terminate();
95
- if (msg.success) {
96
- resolve(msg.result);
97
- } else {
98
- reject(new Error(msg.error));
99
- }
100
- });
101
-
102
- worker.on('error', reject);
103
- worker.postMessage(html);
104
- });
105
-
106
- assert.ok(result.includes('style='));
107
- assert.ok(result.includes('Test'));
108
- });
109
-
110
- it('should handle worker errors gracefully', async () => {
111
- const worker = new Worker(path.join(__dirname, '../lib/juice-worker.js'));
112
-
113
- // Send invalid data that should cause an error
114
- const invalidData = null;
115
-
116
- try {
117
- await new Promise((resolve, reject) => {
118
- worker.on('message', msg => {
119
- worker.terminate();
120
- if (msg.success) {
121
- resolve(msg.result);
122
- } else {
123
- reject(new Error(msg.error));
124
- }
125
- });
126
-
127
- worker.on('error', reject);
128
- worker.postMessage(invalidData);
129
- });
130
-
131
- // If we get here without error, that's also OK
132
- assert.ok(true);
133
- } catch (err) {
134
- // Expected to error
135
- assert.ok(err);
136
- }
137
- });
138
- });
139
-
140
- describe('mime-html with text input', () => {
141
- it('should properly convert text to HTML with default styles', () => {
142
- const input = {
143
- text: 'Hello\nWorld\n\nThis is a test'
144
- };
145
-
146
- const result = mimeHtml.sync(input);
147
-
148
- assert.ok(result.includes('Hello'));
149
- assert.ok(result.includes('World'));
150
- assert.ok(result.includes('font-family')); // Default styles applied
151
- });
152
-
153
- it('should handle quoted text in text mode', () => {
154
- const input = {
155
- text: 'Reply:\n> Original message\n> Second line\n\nMy response'
156
- };
157
-
158
- const result = mimeHtml.sync(input);
159
-
160
- assert.ok(result.includes('blockquote'));
161
- assert.ok(result.includes('Original message'));
162
- });
163
- });
164
-
165
- describe('error recovery and edge cases', () => {
166
- it('should handle circular references in DOM', () => {
167
- // This tests the DOM walking logic
168
- const input = {
169
- html: '<div><p>Test</p></div>'
170
- };
171
-
172
- const result = mimeHtml.sync(input);
173
- assert.ok(result.includes('Test'));
174
- });
175
-
176
- it('should handle very long attribute values', () => {
177
- const longValue = 'x'.repeat(10000);
178
- const input = {
179
- html: `<div data-test="${longValue}">Content</div>`
180
- };
181
-
182
- const result = mimeHtml.sync(input);
183
- assert.ok(result.includes('Content'));
184
- });
185
-
186
- it('should handle style tags in body', () => {
187
- const input = {
188
- html: `
189
- <body>
190
- <style>p { color: red; }</style>
191
- <p>Test</p>
192
- <style>div { color: blue; }</style>
193
- <div>More</div>
194
- </body>
195
- `
196
- };
197
-
198
- const result = mimeHtml.sync(input);
199
- assert.ok(result.includes('Test'));
200
- assert.ok(result.includes('More'));
201
- // Style tags in body should be removed
202
- assert.ok(!result.includes('<style'));
203
- });
204
-
205
- it('should handle CDATA sections', () => {
206
- const input = {
207
- html: '<div><![CDATA[Some text]]></div>'
208
- };
209
-
210
- const result = mimeHtml.sync(input);
211
- assert.ok(typeof result === 'string');
212
- });
213
-
214
- it('should handle XML namespaces', () => {
215
- const input = {
216
- html: '<html xmlns="http://www.w3.org/1999/xhtml"><body><p>Test</p></body></html>'
217
- };
218
-
219
- const result = mimeHtml.sync(input);
220
- assert.ok(result.includes('Test'));
221
- });
222
- });
223
-
224
- describe('async version configuration', () => {
225
- it('should respect timeout configuration', async () => {
226
- const input = {
227
- html: '<p>Quick test</p>'
228
- };
229
-
230
- const start = Date.now();
231
- const result = await mimeHtml.async(input, {
232
- timeout: 100,
233
- useWorkerPool: false
234
- });
235
- const elapsed = Date.now() - start;
236
-
237
- assert.ok(result.includes('Quick test'));
238
- // Should complete without timing out
239
- assert.ok(elapsed < 200);
240
- });
241
-
242
- it('should respect logErrors configuration', async () => {
243
- const input = {
244
- html: null
245
- };
246
-
247
- // Capture console.warn
248
- const originalWarn = console.warn;
249
- let warnCalled = false;
250
- console.warn = () => {
251
- warnCalled = true;
252
- };
253
-
254
- await mimeHtml.async(input, {
255
- logErrors: false,
256
- fallbackOnError: true
257
- });
258
-
259
- console.warn = originalWarn;
260
- assert.ok(!warnCalled, 'Should not log errors when logErrors is false');
261
- });
262
-
263
- it('should respect fallbackOnError configuration', async () => {
264
- const input = {
265
- html: '<p>Test</p>'
266
- };
267
-
268
- // With fallback
269
- const result1 = await mimeHtml.async(input, {
270
- fallbackOnError: true,
271
- logErrors: false
272
- });
273
- assert.ok(typeof result1 === 'string');
274
- });
275
- });
276
-
277
- describe('worker pool configuration', () => {
278
- it('should respect min/max worker settings', async () => {
279
- const input = {
280
- // juice, and with it the pool, only runs when there is a style sheet to inline
281
- html: '<style>p { color: red; }</style><p>Pool config test</p>'
282
- };
283
-
284
- // First close any existing pool
285
- await mimeHtml.closeWorkerPool();
286
-
287
- // Process with custom pool settings
288
- await mimeHtml.async(input, {
289
- useWorkerPool: true,
290
- minWorkers: 1,
291
- maxWorkers: 2
292
- });
293
-
294
- const stats = mimeHtml.getWorkerPoolStats();
295
- assert.ok(stats);
296
- assert.equal(stats.minWorkers, 1);
297
- assert.equal(stats.maxWorkers, 2);
298
- });
299
- });
300
-
301
- describe('concurrent stress test', () => {
302
- it('should handle rapid concurrent requests without deadlock', async () => {
303
- const promises = [];
304
-
305
- // Fire off 50 concurrent requests rapidly
306
- for (let i = 0; i < 50; i++) {
307
- promises.push(
308
- mimeHtml.async(
309
- {
310
- html: `<p>Stress test ${i}</p>`
311
- },
312
- {
313
- useWorkerPool: true,
314
- timeout: 5000
315
- }
316
- )
317
- );
318
- }
319
-
320
- const results = await Promise.all(promises);
321
-
322
- assert.equal(results.length, 50);
323
- results.forEach((result, i) => {
324
- assert.ok(result.includes(`Stress test ${i}`));
325
- });
326
- });
327
- });
328
-
329
- describe('memory leak prevention', () => {
330
- it('should clean up workers after timeout', async () => {
331
- const input = {
332
- // a style sheet, so the juice worker actually runs and times out
333
- html: '<style>p { color: red; }</style><p>Timeout test</p>'
334
- };
335
-
336
- try {
337
- // This should timeout and clean up the worker
338
- await mimeHtml.async(input, {
339
- timeout: 1,
340
- useWorkerPool: false,
341
- fallbackOnError: false,
342
- logErrors: false
343
- });
344
- } catch (err) {
345
- assert.ok(err.message.includes('timed out'));
346
- }
347
-
348
- // Worker should be cleaned up, no leak
349
- const memAfter = process.memoryUsage();
350
- assert.ok(memAfter.heapUsed < 500 * 1024 * 1024); // Less than 500MB
351
- });
352
- });
353
-
354
- // Clean up
355
- after(async () => {
356
- await mimeHtml.closeWorkerPool();
357
- });
358
- });
@@ -1,128 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it } = require('node:test');
4
- const assert = require('node:assert');
5
- const { mimeHtml } = require('..');
6
-
7
- // The message is shown inside a page that is not the sender's. These tests pin what keeps it there:
8
- // a clipping wrapper the sender cannot style, links that cannot reach back, no foreign content.
9
-
10
- describe('container', () => {
11
- it('wraps a message with no body styles in a bare inner element', () => {
12
- assert.equal(mimeHtml({ html: '<p>x</p>' }), '<div style="overflow: auto; contain: paint;"><div><p>x</p></div></div>');
13
- });
14
-
15
- it('keeps the body styles off the clipping wrapper', () => {
16
- const html =
17
- '<html><body style="display: contents; margin: -500px; overflow: visible; overflow-x: visible; box-shadow: 0 0 0 9999px red; outline: 9999px solid red; color: red">' +
18
- '<div style="transform: translateY(-300px)">x</div></body></html>';
19
- const out = mimeHtml({ html });
20
- assert.ok(out.endsWith('</div></div>'), out);
21
- const inner = /^<div style="overflow: auto; contain: paint;"><div style="([^"]*)">/.exec(out)[1];
22
- assert.match(inner, /display: contents/);
23
- assert.match(inner, /margin: -500px/);
24
- assert.match(inner, /color: red/);
25
- assert.match(out, /transform: translateY\(-300px\)/);
26
- });
27
-
28
- it('puts a body rule from the stylesheet on the inner element too', () => {
29
- const html = '<html><head><style>body { display: contents; margin: -200px; } p { color: red; }</style></head><body><p>x</p></body></html>';
30
- const out = mimeHtml({ html });
31
- assert.ok(out.startsWith('<div style="overflow: auto; contain: paint;"><div style="'), out);
32
- assert.match(out, /display: contents/);
33
- assert.match(out, /color: red/);
34
- });
35
-
36
- it('drops the dimensions of the body and keeps its other styles', () => {
37
- const out = mimeHtml({
38
- html: '<html><body style="width: 600px; min-height: 100vh; max-width: 50%; background-color: #fff; color: red">x</body></html>'
39
- });
40
- assert.doesNotMatch(out, /width|height/);
41
- assert.match(out, /background-color: (?:#fff|rgb\(255, 255, 255\))/);
42
- assert.match(out, /color: red/);
43
- });
44
-
45
- it('drops a body width the CSSOM could not parse', () => {
46
- // cssstyle parses nothing from this attribute, a browser reads the width
47
- const out = mimeHtml({ html: '<html><body style="background:url(https://example.com/x.gif) no-repeat; width: 5000px">x</body></html>' });
48
- assert.equal(out, '<div style="overflow: auto; contain: paint;"><div>x</div></div>');
49
- });
50
-
51
- it('keeps body styles the CSSOM could not parse when none of them is forbidden', () => {
52
- // the old wrapper lost every body style here, because setting its overflow rewrote the
53
- // attribute from a CSSOM that held nothing
54
- const out = mimeHtml({ html: '<html><body style="background:url(https://example.com/x.gif) no-repeat; color:red; font:12px serif">x</body></html>' });
55
- assert.ok(out.includes('<div style="background:url(https://example.com/x.gif) no-repeat; color:red; font:12px serif">x</div>'), out);
56
- });
57
-
58
- it('still removes position everywhere, including the body', () => {
59
- const out = mimeHtml({ html: '<html><body style="position: fixed; inset: 0; color: red"><div style="position: sticky; top: 0">x</div></body></html>' });
60
- assert.doesNotMatch(out, /position/);
61
- assert.match(out, /color: red/);
62
- assert.match(out, /top: 0/);
63
- });
64
-
65
- it('marks skipped inlining on the outer wrapper', () => {
66
- let css = '';
67
- for (let i = 0; i <= 5000; i++) {
68
- css += `.c${i} { color: red; }\n`;
69
- }
70
- const out = mimeHtml({ html: `<html><head><style>${css}</style></head><body style="color: blue"><p class="c1">x</p></body></html>` });
71
- assert.ok(out.startsWith('<div style="overflow: auto; contain: paint;" data-css-inlining="skipped"><div style="color: blue">'), out.slice(0, 120));
72
- });
73
- });
74
-
75
- describe('links', () => {
76
- it('opens every link in a new context that gets no opener and no referrer', () => {
77
- const out = mimeHtml({ html: '<a href="https://example.com/" rel="opener">x</a><a href="https://example.com/" target="_top" rel="nofollow">y</a>' });
78
- assert.equal((out.match(/<a /g) || []).length, 2);
79
- assert.doesNotMatch(out, /rel="opener"|rel="nofollow"|_top/);
80
- assert.equal((out.match(/target="_blank"/g) || []).length, 2);
81
- assert.equal((out.match(/rel="noopener noreferrer"/g) || []).length, 2);
82
- });
83
-
84
- it('treats an image map area as a link', () => {
85
- const out = mimeHtml({
86
- html: '<img src="https://example.com/i.png" usemap="#m"><map name="m"><area shape="rect" coords="0,0,9,9" href="https://example.com/"></map>'
87
- });
88
- assert.match(out, /<area [^>]*target="_blank"/);
89
- assert.match(out, /<area [^>]*rel="noopener noreferrer"/);
90
- });
91
- });
92
-
93
- describe('foreign content', () => {
94
- it('removes SVG and MathML', () => {
95
- const out = mimeHtml({
96
- html:
97
- '<p>a</p><svg width="20" height="20"><circle cx="10" cy="10" r="8"/></svg><math><mi>x</mi></math>' +
98
- '<math><mtext><table><mglyph><style><!--</style><img title="--><img src=1 onerror=alert(1)>"><p>b</p>'
99
- });
100
- assert.doesNotMatch(out, /<svg|<math|<circle|<mi|onerror/);
101
- assert.ok(out.includes('<p>a</p>'), out);
102
- });
103
- });
104
-
105
- describe('single sanitization pass for markup without a stylesheet', () => {
106
- it('sanitizes the same way', () => {
107
- const out = mimeHtml({
108
- html: '<p onclick="x()">a</p><script>alert(1)</script><a href="javascript:alert(1)">b</a><iframe src="https://example.com/"></iframe>'
109
- });
110
- assert.equal(out, '<div style="overflow: auto; contain: paint;"><div><p>a</p><a target="_blank" rel="noopener noreferrer">b</a></div></div>');
111
- });
112
-
113
- for (const [label, tag, close] of [
114
- ['upper case', '<STYLE>', '</STYLE>'],
115
- ['whitespace after the name', '<style\n>', '</style>'],
116
- ['a self-closing slash', '<style/>', '</style>']
117
- ]) {
118
- it(`still inlines a stylesheet whose tag uses ${label}`, () => {
119
- const out = mimeHtml({ html: `<html><head>${tag}p { color: red; }${close}</head><body><p>x</p></body></html>` });
120
- assert.match(out, /color: red/, out);
121
- });
122
- }
123
-
124
- it('leaves a style tag that is only text alone', () => {
125
- const out = mimeHtml({ html: '<p>write &lt;style&gt; to add rules</p>' });
126
- assert.equal(out, '<div style="overflow: auto; contain: paint;"><div><p>write &lt;style&gt; to add rules</p></div></div>');
127
- });
128
- });
@@ -1,62 +0,0 @@
1
- 'use strict';
2
-
3
- const { describe, it, after } = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const mimeHtml = require('../lib/mime-html');
6
- const fs = require('fs');
7
- const path = require('path');
8
-
9
- describe('Edge Cases', () => {
10
- describe('large HTML', () => {
11
- it('should handle large HTML', async () => {
12
- const largeContent = '<p>' + 'x'.repeat(10000) + '</p>';
13
- const result = await mimeHtml.async({ html: largeContent });
14
- assert.ok(result.length > 0);
15
- });
16
- });
17
-
18
- describe('special characters', () => {
19
- it('should handle UTF-8', () => {
20
- const result = mimeHtml.sync({ html: '<p>Hello 世界</p>' });
21
- assert.ok(result.includes('Hello'));
22
- });
23
-
24
- it('should handle entities', () => {
25
- const result = mimeHtml.sync({ html: '<p>&lt;&gt;&amp;</p>' });
26
- assert.ok(result.includes('&lt;') || result.includes('<'));
27
- });
28
- });
29
-
30
- describe('problematic CSS', () => {
31
- it('should handle modern selectors', () => {
32
- const html = `
33
- <style>
34
- .test:is(.a) { color: red; }
35
- </style>
36
- <p class="test a">Test</p>
37
- `;
38
- const result = mimeHtml.sync({ html });
39
- assert.ok(result.includes('Test'));
40
- });
41
- });
42
-
43
- describe('real-world HTML', () => {
44
- it('should process failing.html quickly', async () => {
45
- const failingPath = path.join(__dirname, '../examples/failing.html');
46
- if (fs.existsSync(failingPath)) {
47
- const html = fs.readFileSync(failingPath, 'utf8');
48
-
49
- const start = Date.now();
50
- const result = await mimeHtml.async({ html });
51
- const elapsed = Date.now() - start;
52
-
53
- assert.ok(result.length > 0);
54
- assert.ok(elapsed < 2000, `Took ${elapsed}ms`);
55
- }
56
- });
57
- });
58
-
59
- after(async () => {
60
- await mimeHtml.closeWorkerPool();
61
- });
62
- });