@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.
- package/CHANGELOG.md +14 -0
- package/README.md +12 -0
- package/dist/cjs/index.d.ts +20 -0
- package/dist/cjs/index.js +31 -0
- package/dist/cjs/lib/commonjs.cjs +17 -0
- package/dist/cjs/lib/commonjs.d.cts +4 -0
- package/dist/cjs/lib/format-date.d.ts +15 -0
- package/dist/cjs/lib/format-date.js +60 -0
- package/dist/cjs/lib/html-guard.d.ts +3 -0
- package/dist/cjs/lib/html-guard.js +119 -0
- package/dist/cjs/lib/html-to-text.d.ts +1 -0
- package/dist/cjs/lib/html-to-text.js +49 -0
- package/dist/cjs/lib/inline-html.d.ts +19 -0
- package/dist/cjs/lib/inline-html.js +150 -0
- package/dist/cjs/lib/inline-style.d.ts +2 -0
- package/dist/cjs/lib/inline-style.js +42 -0
- package/dist/cjs/lib/inline-text.d.ts +3 -0
- package/dist/cjs/lib/inline-text.js +111 -0
- package/dist/cjs/lib/juice-worker.d.ts +1 -0
- package/dist/cjs/lib/juice-worker.js +19 -0
- package/dist/cjs/lib/mime-html.d.ts +34 -0
- package/dist/cjs/lib/mime-html.js +496 -0
- package/dist/cjs/lib/text-to-html.d.ts +1 -0
- package/dist/cjs/lib/text-to-html.js +182 -0
- package/dist/cjs/lib/worker-pool.d.ts +61 -0
- package/dist/cjs/lib/worker-pool.js +229 -0
- package/dist/cjs/package.json +3 -0
- package/dist/esm/index.d.ts +20 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/lib/commonjs.cjs +17 -0
- package/dist/esm/lib/commonjs.d.cts +4 -0
- package/dist/esm/lib/format-date.d.ts +15 -0
- package/{lib → dist/esm/lib}/format-date.js +8 -16
- package/dist/esm/lib/html-guard.d.ts +3 -0
- package/{lib → dist/esm/lib}/html-guard.js +8 -24
- package/dist/esm/lib/html-to-text.d.ts +1 -0
- package/{lib → dist/esm/lib}/html-to-text.js +2 -14
- package/dist/esm/lib/inline-html.d.ts +19 -0
- package/{lib → dist/esm/lib}/inline-html.js +17 -55
- package/dist/esm/lib/inline-style.d.ts +2 -0
- package/dist/esm/lib/inline-style.js +39 -0
- package/dist/esm/lib/inline-text.d.ts +3 -0
- package/{lib → dist/esm/lib}/inline-text.js +8 -31
- package/dist/esm/lib/juice-worker.d.ts +1 -0
- package/{lib → dist/esm/lib}/juice-worker.js +7 -8
- package/dist/esm/lib/mime-html.d.ts +34 -0
- package/{lib → dist/esm/lib}/mime-html.js +80 -161
- package/dist/esm/lib/text-to-html.d.ts +1 -0
- package/{lib → dist/esm/lib}/text-to-html.js +35 -63
- package/dist/esm/lib/worker-pool.d.ts +61 -0
- package/{lib → dist/esm/lib}/worker-pool.js +18 -64
- package/dist/esm/package.json +3 -0
- package/package.json +67 -10
- package/.github/VULNERABILITY_REPORT.yml +0 -75
- package/.github/workflows/release.yaml +0 -38
- package/.github/workflows/test.yaml +0 -35
- package/.ncurc.js +0 -28
- package/.prettierignore +0 -14
- package/.prettierrc.js +0 -10
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -105
- package/benchmark/benchmark.js +0 -348
- package/benchmark/concurrency-benchmark.js +0 -176
- package/benchmark/quick-benchmark.js +0 -200
- package/benchmark/quick-concurrency.js +0 -64
- package/benchmark/real-world-benchmark.js +0 -257
- package/docs/PERFORMANCE.md +0 -262
- package/eslint.config.js +0 -71
- package/examples/failing.html +0 -617
- package/examples/fixtures/message-1.json +0 -74
- package/examples/inline-html.js +0 -24
- package/examples/inline-text.js +0 -24
- package/examples/mime-html.js +0 -284
- package/index.js +0 -22
- package/release-please-config.json +0 -10
- package/test/comprehensive.test.js +0 -358
- package/test/containment.test.js +0 -128
- package/test/edge-cases.test.js +0 -62
- package/test/hardening.test.js +0 -504
- package/test/html-to-text.test.js +0 -253
- package/test/inline-html.test.js +0 -230
- package/test/inline-text.test.js +0 -204
- package/test/mime-html-extra.test.js +0 -39
- package/test/mime-html.test.js +0 -302
- package/test/text-to-html-extra.test.js +0 -42
- package/test/worker-pool-extra.test.js +0 -132
- package/test/worker-pool-regression.test.js +0 -128
- 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('<script'));
|
|
63
|
-
assert.ok(html.includes('&'));
|
|
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
|
-
});
|
package/test/containment.test.js
DELETED
|
@@ -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 <style> to add rules</p>' });
|
|
126
|
-
assert.equal(out, '<div style="overflow: auto; contain: paint;"><div><p>write <style> to add rules</p></div></div>');
|
|
127
|
-
});
|
|
128
|
-
});
|
package/test/edge-cases.test.js
DELETED
|
@@ -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><>&</p>' });
|
|
26
|
-
assert.ok(result.includes('<') || 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
|
-
});
|