@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,18 +1,14 @@
1
- 'use strict';
2
-
3
- const createDOMPurify = require('dompurify');
4
- const { JSDOM } = require('jsdom');
1
+ import createDOMPurify from 'dompurify';
2
+ import { JSDOM } from 'jsdom';
5
3
  // Pinned to juice 11.x: juice 12+ is pure ESM (requires Node >=22.12) and breaks pkg builds.
6
- // The `.default` fallback keeps this working if juice ever ships a CJS-compatible build again.
7
- const juice = require('juice').default || require('juice');
8
- const he = require('he');
9
- const { Worker } = require('worker_threads');
10
- const path = require('path');
11
- const WorkerPool = require('./worker-pool');
12
- const { MAX_NESTING_DEPTH, exceedsNestingDepth, fallbackText } = require('./html-guard');
13
-
14
- const textToHtml = require('./text-to-html');
15
-
4
+ import juice from 'juice';
5
+ import he from 'he';
6
+ import { Worker } from 'node:worker_threads';
7
+ import WorkerPool from './worker-pool.js';
8
+ import { MAX_NESTING_DEPTH, exceedsNestingDepth, fallbackText } from './html-guard.js';
9
+ import { INLINE_STYLE_BLOCK } from './inline-style.js';
10
+ import { JUICE_WORKER_PATH } from './commonjs.cjs';
11
+ import textToHtml from './text-to-html.js';
16
12
  const FORBID_TAGS = [
17
13
  'title',
18
14
  'link',
@@ -40,10 +36,8 @@ const FORBID_TAGS = [
40
36
  'select',
41
37
  'textarea'
42
38
  ];
43
-
44
39
  // form submission targets and popovers, which let sender markup act on or overlay the viewer
45
40
  const FORBID_ATTR = ['action', 'formaction', 'popover', 'popovertarget'];
46
-
47
41
  const FORBID_STYLES = [
48
42
  'all',
49
43
  'position',
@@ -71,16 +65,10 @@ const FORBID_STYLES = [
71
65
  'scrollbar-width',
72
66
  'zoom'
73
67
  ];
74
-
75
- // Properties to remove from style attributes: the list, a cheap test for style text that could
76
- // name one of them, and a test for one of them in declaration position (at the start, or after any
77
- // character that cannot be part of a longer property name, so "background-position" is not one)
78
68
  function styleRule(properties, hint) {
79
69
  return { properties, hint, declaration: new RegExp('(?:^|[^a-z0-9-])(?:' + properties.join('|') + ')\\s*:', 'i') };
80
70
  }
81
-
82
71
  const FORBID_STYLES_RULE = styleRule(FORBID_STYLES, /position|float|clip|zoom|anim|offset|scrollbar|paint|clear|\ball\b/i);
83
-
84
72
  // Resolves CSS escape sequences (\6F, \o) the way a browser does before it matches a property
85
73
  // name, so that a forbidden property cannot hide behind an escaped character
86
74
  function decodeCssEscapes(value) {
@@ -92,33 +80,26 @@ function decodeCssEscapes(value) {
92
80
  return chr;
93
81
  });
94
82
  }
95
-
96
83
  // keep the style at first
97
84
  const FORBID_TAGS_ALL = ['style'].concat(FORBID_TAGS);
98
-
99
85
  // HTML only. The default profiles admit SVG and MathML too, and the namespace confusion between
100
86
  // the three is where most sanitizer bypasses have come from: markup one parser closes inside an
101
87
  // <svg> or <mglyph> and another reads as HTML. Mail has no use for either, the major webmail
102
88
  // clients strip inline SVG and MathML never reached a mail client, so both go.
103
89
  const USE_PROFILES = { html: true };
104
-
105
90
  // A start tag the HTML parser reads as a style element: the name followed by whitespace, "/" or
106
91
  // ">". Any other character inside the name makes it some other element, and a "<style" at the
107
92
  // very end of the input is dropped by the parser, so markup this does not match has no stylesheet.
108
93
  const STYLE_TAG = /<style[\s/>]/i;
109
-
110
94
  // The dimensions of the body. In mail they are page-layout leftovers ("width: 100%; height: 100%",
111
95
  // "min-height: 100vh") that would only pad or scroll the box the message is shown in, and they
112
96
  // were removed when the body was the outermost element, which consumers may rely on.
113
97
  const BODY_STYLES_RULE = styleRule(['width', 'min-width', 'max-width', 'height', 'min-height', 'max-height'], /width|height/i);
114
-
115
98
  // One sanitizer for the module. Every sanitize() call passes its full config, so calls do not
116
99
  // leak settings into each other.
117
100
  const DOMPurify = createDOMPurify(new JSDOM('').window);
118
-
119
101
  // Global worker pool instance (lazy initialization)
120
102
  let globalWorkerPool = null;
121
-
122
103
  function getWorkerPool(options = {}) {
123
104
  if (!globalWorkerPool) {
124
105
  globalWorkerPool = new WorkerPool({
@@ -130,97 +111,55 @@ function getWorkerPool(options = {}) {
130
111
  }
131
112
  return globalWorkerPool;
132
113
  }
133
-
134
114
  // define styles for plaintext emails
135
115
  // Wrapper for juice that runs in a worker thread with timeout protection
136
116
  async function juiceWithTimeout(html, options = {}) {
137
117
  const timeout = options.timeout || 5000;
138
118
  const usePool = options.useWorkerPool !== false; // Default to true
139
-
140
119
  if (usePool) {
141
120
  try {
142
121
  const pool = getWorkerPool(options);
143
122
  return await pool.process(html, { timeout });
144
- } catch (err) {
123
+ }
124
+ catch (err) {
145
125
  // Fall back to single worker if pool fails
146
126
  if (options.fallbackOnError === false) {
147
127
  throw err;
148
128
  }
149
129
  }
150
130
  }
151
-
152
131
  // Single worker implementation (fallback or when pool disabled)
153
132
  const timeoutMs = timeout;
154
133
  return new Promise((resolve, reject) => {
155
- const worker = new Worker(path.join(__dirname, 'juice-worker.js'));
134
+ const worker = new Worker(JUICE_WORKER_PATH);
156
135
  let timedOut = false;
157
-
158
136
  const timeout = setTimeout(() => {
159
137
  timedOut = true;
160
- worker.terminate();
138
+ void worker.terminate();
161
139
  reject(new Error('Juice processing timed out - likely due to unsupported CSS selectors'));
162
140
  }, timeoutMs);
163
-
164
- worker.on('message', msg => {
141
+ worker.on('message', (msg) => {
165
142
  clearTimeout(timeout);
166
- worker.terminate();
143
+ void worker.terminate();
167
144
  if (msg.success) {
168
145
  resolve(msg.result);
169
- } else {
146
+ }
147
+ else {
170
148
  reject(new Error(msg.error));
171
149
  }
172
150
  });
173
-
174
- worker.on('error', err => {
151
+ worker.on('error', (err) => {
175
152
  clearTimeout(timeout);
176
153
  if (!timedOut) {
177
154
  reject(err);
178
155
  }
179
156
  });
180
-
181
157
  worker.postMessage(html);
182
158
  });
183
159
  }
184
-
185
- const INLINE_STYLE_BLOCK = `<style>
186
-
187
- body, td, th, p {
188
- font-family: sans-serif;
189
- font-size: 12px;
190
- }
191
-
192
- blockquote {
193
- border-left-width: 2px;
194
- border-left-style: solid;
195
-
196
- border-left-color: darkblue;
197
- color: darkblue;
198
-
199
- margin: 1rem 0;
200
- padding-left: 1rem;
201
- }
202
-
203
- blockquote blockquote{
204
- border-left-color: royalblue;
205
- color: royalblue;
206
- }
207
-
208
- blockquote blockquote blockquote{
209
- border-left-color: dodgerblue;
210
- color: dodgerblue;
211
- }
212
-
213
- blockquote blockquote blockquote blockquote{
214
- border-left-color: darkblue;
215
- color: darkblue;
216
- }
217
-
218
- </style>`;
219
-
220
160
  // Only this much <style> text is kept per message, the rest is dropped. juice and the selector
221
161
  // filter below scale with it, and no legitimate email needs more.
222
162
  const MAX_STYLE_TEXT = 256 * 1024;
223
-
224
163
  // juice matches every selector against every element, so its cost is the product of the two
225
164
  // counts: 1,000 rules on 10,000 paragraphs took 2.7 s and 10,000 on 10,000 took 25 s, on the
226
165
  // calling thread, and the style text cap alone leaves the body unbounded. Each bound sits well
@@ -232,7 +171,6 @@ const MAX_STYLE_TEXT = 256 * 1024;
232
171
  const MAX_INLINE_SELECTORS = 5000;
233
172
  const MAX_INLINE_ELEMENTS = 50000;
234
173
  const MAX_INLINE_WORK = 5000000;
235
-
236
174
  // Selectors juice would match against the document: every selector of every rule at the top level
237
175
  // of the style text. A rule nested in an at-rule is not inlined (juice keeps media queries as rules),
238
176
  // so it only costs its parse and is not counted. One linear pass
@@ -248,20 +186,22 @@ function countSelectors(css) {
248
186
  commas = 0;
249
187
  }
250
188
  depth++;
251
- } else if (c === '}') {
189
+ }
190
+ else if (c === '}') {
252
191
  if (depth > 0) {
253
192
  depth--;
254
193
  }
255
194
  commas = 0;
256
- } else if (c === ';' && depth === 0) {
195
+ }
196
+ else if (c === ';' && depth === 0) {
257
197
  commas = 0;
258
- } else if (c === ',' && depth === 0) {
198
+ }
199
+ else if (c === ',' && depth === 0) {
259
200
  commas++;
260
201
  }
261
202
  }
262
203
  return count;
263
204
  }
264
-
265
205
  // Rendered in place of the sanitized HTML when the pipeline cannot process the input at all, for
266
206
  // example when a hostile nesting depth overflows the stack of the parser or the serializer.
267
207
  function fallbackHtml(message) {
@@ -271,7 +211,6 @@ function fallbackHtml(message) {
271
211
  }
272
212
  return `<div style="overflow: auto;"><div style="white-space: pre-wrap;">${he.encode((text || '').toString(), { useNamedReferences: true })}</div></div>`;
273
213
  }
274
-
275
214
  // True when a :not( argument contains another colon. Such selectors, like :is(), :where() and
276
215
  // :has(), can make juice hang. A single pass with a paren counter keeps this linear, the regex
277
216
  // it replaces backtracked catastrophically on a few hundred bytes of repeated ":not(a:b)".
@@ -286,14 +225,16 @@ function hasNotWithPseudo(selector) {
286
225
  if (notDepth) {
287
226
  return true;
288
227
  }
289
- if (selector.substr(i + 1, 4).toLowerCase() === 'not(') {
228
+ if (selector.slice(i + 1, i + 5).toLowerCase() === 'not(') {
290
229
  depth++;
291
230
  notDepth = depth;
292
231
  i += 4;
293
232
  }
294
- } else if (c === '(') {
233
+ }
234
+ else if (c === '(') {
295
235
  depth++;
296
- } else if (c === ')') {
236
+ }
237
+ else if (c === ')') {
297
238
  if (depth === notDepth) {
298
239
  notDepth = 0;
299
240
  }
@@ -304,12 +245,10 @@ function hasNotWithPseudo(selector) {
304
245
  }
305
246
  return false;
306
247
  }
307
-
308
248
  function isProblematicSelector(selector) {
309
249
  const lower = selector.toLowerCase();
310
250
  return lower.includes(':is(') || lower.includes(':where(') || lower.includes(':has(') || hasNotWithPseudo(selector);
311
251
  }
312
-
313
252
  // Drops every rule whose selector could make juice hang. The text is split into rules on "}" and
314
253
  // the selector of each rule is the text between the last two "{" in it, so a rule nested in an
315
254
  // at-rule is dropped without its enclosing block.
@@ -329,39 +268,39 @@ function filterCss(css) {
329
268
  }
330
269
  return output.join('');
331
270
  }
332
-
333
271
  // Sanitizes the whole document and strips the CSS juice is known to choke on. Returns the HTML
334
272
  // to run juice on and whether any style text is left for juice to inline.
335
273
  function prepareHtml(message) {
336
- let html = message.html;
337
- if (!html && message.text) {
338
- html = textToHtml(message.text) || '';
339
- if (html) {
340
- html = INLINE_STYLE_BLOCK + html;
274
+ let source = message.html;
275
+ if (!source && message.text) {
276
+ source = textToHtml(message.text) || '';
277
+ if (source) {
278
+ source = INLINE_STYLE_BLOCK + source;
341
279
  }
342
280
  }
343
-
344
- html = (html || '').toString().replace(/^"data:[^"]+"/g, '');
345
-
281
+ const html = (source || '').toString().replace(/^"data:[^"]+"/g, '');
346
282
  if (exceedsNestingDepth(html, MAX_NESTING_DEPTH)) {
347
283
  throw new Error('HTML is nested too deep to process');
348
284
  }
349
-
350
285
  if (!STYLE_TAG.test(html)) {
351
286
  // No style element can come out of this markup, so there is nothing to inline. This pass
352
287
  // exists to find and filter the stylesheets before juice sees them, and without one it
353
288
  // would only add a parse and a serialization to the pass finishHtml() makes anyway.
354
289
  return { html, hasStyle: false, inliningSkipped: false };
355
290
  }
356
-
357
291
  // first pass, returns the <html> element of the fixed HTML page
358
- const root = DOMPurify.sanitize(html, { WHOLE_DOCUMENT: true, FORCE_BODY: false, USE_PROFILES, FORBID_TAGS, FORBID_ATTR, RETURN_DOM: true });
359
-
292
+ const root = DOMPurify.sanitize(html, {
293
+ WHOLE_DOCUMENT: true,
294
+ FORCE_BODY: false,
295
+ USE_PROFILES,
296
+ FORBID_TAGS,
297
+ FORBID_ATTR,
298
+ RETURN_DOM: true
299
+ });
360
300
  // Remove any style tags from within body content
361
301
  const body = root.querySelector('body');
362
302
  const bodyStyleTags = body ? body.querySelectorAll('style') : [];
363
303
  bodyStyleTags.forEach(tag => tag.remove());
364
-
365
304
  // Process remaining style tags to remove known problematic selectors
366
305
  let styleBudget = MAX_STYLE_TEXT;
367
306
  let hasStyle = false;
@@ -380,7 +319,6 @@ function prepareHtml(message) {
380
319
  selectors += countSelectors(cssContent);
381
320
  }
382
321
  });
383
-
384
322
  let inliningSkipped = false;
385
323
  if (hasStyle) {
386
324
  const elements = root.getElementsByTagName('*').length;
@@ -389,10 +327,8 @@ function prepareHtml(message) {
389
327
  inliningSkipped = true;
390
328
  }
391
329
  }
392
-
393
330
  return { html: root.outerHTML, hasStyle, inliningSkipped };
394
331
  }
395
-
396
332
  // Removes the properties of a style rule from an element the way a browser will read them, by the
397
333
  // CSSOM first and then by the attribute text the CSSOM did not parse.
398
334
  function removeStyleProperties(node, rule) {
@@ -413,14 +349,15 @@ function removeStyleProperties(node, rule) {
413
349
  const parsed = node.style.cssText;
414
350
  if (parsed) {
415
351
  node.setAttribute('style', parsed);
416
- } else {
352
+ }
353
+ else {
417
354
  node.removeAttribute('style');
418
355
  }
419
- } else if (!remaining.trim()) {
356
+ }
357
+ else if (!remaining.trim()) {
420
358
  node.removeAttribute('style');
421
359
  }
422
360
  }
423
-
424
361
  // The sanitization pass that produces the output (the second one when there was a stylesheet to
425
362
  // inline, the only one otherwise) and the post-processing of its result.
426
363
  function finishHtml(html, inliningSkipped) {
@@ -433,7 +370,6 @@ function finishHtml(html, inliningSkipped) {
433
370
  FORBID_TAGS: FORBID_TAGS_ALL,
434
371
  FORBID_ATTR
435
372
  });
436
-
437
373
  // Every link opens a new browsing context that gets neither an opener nor a referrer. Browsers
438
374
  // imply noopener for target="_blank", but rel="opener" opts back in and the sender writes rel,
439
375
  // and an opener can navigate the page the message is shown in. An <area> is a link too.
@@ -441,7 +377,6 @@ function finishHtml(html, inliningSkipped) {
441
377
  link.setAttribute('target', '_blank');
442
378
  link.setAttribute('rel', 'noopener noreferrer');
443
379
  }
444
-
445
380
  // remove specific style properties, touching the CSSOM only where the style text could name one
446
381
  for (const node of [dom, ...dom.querySelectorAll('[style]')]) {
447
382
  if (!node.style) {
@@ -457,9 +392,7 @@ function finishHtml(html, inliningSkipped) {
457
392
  removeStyleProperties(node, FORBID_STYLES_RULE);
458
393
  }
459
394
  removeStyleProperties(dom, BODY_STYLES_RULE);
460
-
461
395
  const bodyStyles = (dom.getAttribute('style') || '').trim();
462
-
463
396
  // Two wrappers. The outer one is ours alone and clips everything inside it; the inner one is
464
397
  // the message body with the styles it came with. While the body's styles sat on the clipping
465
398
  // element, a sender could undo the clipping (display: contents or inline, an overflow-x:
@@ -473,11 +406,10 @@ function finishHtml(html, inliningSkipped) {
473
406
  const body = bodyStyles ? `<div style="${he.encode(bodyStyles)}">` : '<div>';
474
407
  return `<div style="overflow: auto; contain: paint;"${marker}>${body}${dom.innerHTML.trim()}</div></div>`;
475
408
  }
476
-
477
409
  // Runs the pipeline and renders the plain text fallback when it fails, for example when a
478
410
  // hostile nesting depth overflows the stack of the parser or the serializer.
479
411
  function guarded(fn, message, rethrow) {
480
- const onError = err => {
412
+ const onError = (err) => {
481
413
  if (rethrow) {
482
414
  throw err;
483
415
  }
@@ -485,54 +417,46 @@ function guarded(fn, message, rethrow) {
485
417
  };
486
418
  try {
487
419
  const result = fn();
488
- return result && typeof result.then === 'function' ? result.catch(onError) : result;
489
- } catch (err) {
420
+ return (typeof result === 'string' ? result : result.catch(onError));
421
+ }
422
+ catch (err) {
490
423
  return onError(err);
491
424
  }
492
425
  }
493
-
494
426
  function mimeHtmlSync(message) {
495
- return guarded(
496
- () => {
497
- let { html, hasStyle, inliningSkipped } = prepareHtml(message);
498
- if (hasStyle) {
499
- try {
500
- // Run juice synchronously - risky but backwards compatible
501
- html = juice(html);
502
- } catch (_err) {
503
- // not so important, so we'll ignore if style inlining fails
504
- }
427
+ return guarded(() => {
428
+ let { html, hasStyle, inliningSkipped } = prepareHtml(message);
429
+ if (hasStyle) {
430
+ try {
431
+ // Run juice synchronously - risky but backwards compatible
432
+ html = juice(html);
433
+ }
434
+ catch (_err) {
435
+ // not so important, so we'll ignore if style inlining fails
505
436
  }
506
- return finishHtml(html, inliningSkipped);
507
- },
508
- message,
509
- false
510
- );
437
+ }
438
+ return finishHtml(html, inliningSkipped);
439
+ }, message, false);
511
440
  }
512
-
513
441
  async function mimeHtmlAsync(message, options = {}) {
514
- return guarded(
515
- async () => {
516
- let { html, hasStyle, inliningSkipped } = prepareHtml(message);
517
- if (hasStyle) {
518
- try {
519
- // Use worker thread with timeout for juice - provides protection against ANY
520
- // unexpected CSS that might cause hangs (beyond our known problematic selectors)
521
- html = await juiceWithTimeout(html, options);
522
- } catch (err) {
523
- // If juice fails or times out, continue without inlining styles
524
- if (options.fallbackOnError === false) {
525
- throw err;
526
- }
442
+ return guarded(async () => {
443
+ let { html, hasStyle, inliningSkipped } = prepareHtml(message);
444
+ if (hasStyle) {
445
+ try {
446
+ // Use worker thread with timeout for juice - provides protection against ANY
447
+ // unexpected CSS that might cause hangs (beyond our known problematic selectors)
448
+ html = await juiceWithTimeout(html, options);
449
+ }
450
+ catch (err) {
451
+ // If juice fails or times out, continue without inlining styles
452
+ if (options.fallbackOnError === false) {
453
+ throw err;
527
454
  }
528
455
  }
529
- return finishHtml(html, inliningSkipped);
530
- },
531
- message,
532
- options.fallbackOnError === false
533
- );
456
+ }
457
+ return finishHtml(html, inliningSkipped);
458
+ }, message, options.fallbackOnError === false);
534
459
  }
535
-
536
460
  // Main export - maintains backward compatibility with sync API
537
461
  // Architecture:
538
462
  // - Both sync and async versions pre-filter known problematic CSS selectors
@@ -543,14 +467,11 @@ function mimeHtml(message) {
543
467
  // Use the sync version for backward compatibility
544
468
  return mimeHtmlSync(message);
545
469
  }
546
-
547
470
  // Export both versions
548
471
  mimeHtml.async = mimeHtmlAsync;
549
472
  mimeHtml.sync = mimeHtmlSync;
550
-
551
473
  // Exposed for tests only, not part of the public API
552
474
  mimeHtml._internal = { filterCss, hasNotWithPseudo };
553
-
554
475
  // Export function to manually close worker pool
555
476
  mimeHtml.closeWorkerPool = async () => {
556
477
  if (globalWorkerPool) {
@@ -558,7 +479,6 @@ mimeHtml.closeWorkerPool = async () => {
558
479
  globalWorkerPool = null;
559
480
  }
560
481
  };
561
-
562
482
  // Export function to get worker pool stats
563
483
  mimeHtml.getWorkerPoolStats = () => {
564
484
  if (globalWorkerPool) {
@@ -566,5 +486,4 @@ mimeHtml.getWorkerPoolStats = () => {
566
486
  }
567
487
  return null;
568
488
  };
569
-
570
- module.exports = mimeHtml;
489
+ export default mimeHtml;
@@ -0,0 +1 @@
1
+ export default function textToHtml(text?: string | Buffer | null): string | undefined;