@postalsys/email-text-tools 2.4.25 → 2.5.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 (84) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +10 -0
  3. package/dist/cjs/index.d.ts +18 -0
  4. package/dist/cjs/index.js +28 -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-text.d.ts +3 -0
  16. package/dist/cjs/lib/inline-text.js +111 -0
  17. package/dist/cjs/lib/juice-worker.d.ts +1 -0
  18. package/dist/cjs/lib/juice-worker.js +19 -0
  19. package/dist/cjs/lib/mime-html.d.ts +34 -0
  20. package/dist/cjs/lib/mime-html.js +529 -0
  21. package/dist/cjs/lib/text-to-html.d.ts +1 -0
  22. package/dist/cjs/lib/text-to-html.js +182 -0
  23. package/dist/cjs/lib/worker-pool.d.ts +61 -0
  24. package/dist/cjs/lib/worker-pool.js +229 -0
  25. package/dist/cjs/package.json +3 -0
  26. package/dist/esm/index.d.ts +18 -0
  27. package/dist/esm/index.js +12 -0
  28. package/dist/esm/lib/commonjs.cjs +17 -0
  29. package/dist/esm/lib/commonjs.d.cts +4 -0
  30. package/dist/esm/lib/format-date.d.ts +15 -0
  31. package/{lib → dist/esm/lib}/format-date.js +8 -16
  32. package/dist/esm/lib/html-guard.d.ts +3 -0
  33. package/{lib → dist/esm/lib}/html-guard.js +8 -24
  34. package/dist/esm/lib/html-to-text.d.ts +1 -0
  35. package/{lib → dist/esm/lib}/html-to-text.js +2 -14
  36. package/dist/esm/lib/inline-html.d.ts +19 -0
  37. package/{lib → dist/esm/lib}/inline-html.js +17 -55
  38. package/dist/esm/lib/inline-text.d.ts +3 -0
  39. package/{lib → dist/esm/lib}/inline-text.js +8 -31
  40. package/dist/esm/lib/juice-worker.d.ts +1 -0
  41. package/{lib → dist/esm/lib}/juice-worker.js +7 -8
  42. package/dist/esm/lib/mime-html.d.ts +34 -0
  43. package/{lib → dist/esm/lib}/mime-html.js +79 -127
  44. package/dist/esm/lib/text-to-html.d.ts +1 -0
  45. package/{lib → dist/esm/lib}/text-to-html.js +35 -63
  46. package/dist/esm/lib/worker-pool.d.ts +61 -0
  47. package/{lib → dist/esm/lib}/worker-pool.js +18 -64
  48. package/dist/esm/package.json +3 -0
  49. package/package.json +72 -10
  50. package/.eslintrc +0 -15
  51. package/.github/VULNERABILITY_REPORT.yml +0 -75
  52. package/.github/workflows/release.yaml +0 -37
  53. package/.github/workflows/test.yaml +0 -31
  54. package/.ncurc.js +0 -22
  55. package/.prettierignore +0 -14
  56. package/.prettierrc.js +0 -8
  57. package/.release-please-manifest.json +0 -3
  58. package/benchmark/benchmark.js +0 -358
  59. package/benchmark/concurrency-benchmark.js +0 -176
  60. package/benchmark/quick-benchmark.js +0 -200
  61. package/benchmark/quick-concurrency.js +0 -64
  62. package/benchmark/real-world-benchmark.js +0 -267
  63. package/docs/PERFORMANCE.md +0 -262
  64. package/eslint.config.js +0 -52
  65. package/examples/failing.html +0 -617
  66. package/examples/fixtures/message-1.json +0 -74
  67. package/examples/inline-html.js +0 -24
  68. package/examples/inline-text.js +0 -24
  69. package/examples/mime-html.js +0 -284
  70. package/index.js +0 -22
  71. package/release-please-config.json +0 -10
  72. package/test/comprehensive.test.js +0 -358
  73. package/test/containment.test.js +0 -128
  74. package/test/edge-cases.test.js +0 -62
  75. package/test/hardening.test.js +0 -504
  76. package/test/html-to-text.test.js +0 -253
  77. package/test/inline-html.test.js +0 -230
  78. package/test/inline-text.test.js +0 -204
  79. package/test/mime-html-extra.test.js +0 -39
  80. package/test/mime-html.test.js +0 -302
  81. package/test/text-to-html-extra.test.js +0 -42
  82. package/test/worker-pool-extra.test.js +0 -132
  83. package/test/worker-pool-regression.test.js +0 -128
  84. package/test/worker-pool.test.js +0 -282
@@ -1,176 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const mimeHtml = require('../lib/mime-html');
7
-
8
- async function concurrencyBenchmark() {
9
- console.log('Concurrency Benchmark - Real-World HTML');
10
- console.log('='.repeat(80));
11
- console.log('Testing concurrent processing with problematic HTML\n');
12
-
13
- // Load the real-world HTML
14
- const realWorldHtml = fs.readFileSync(path.join(__dirname, '../examples/failing.html'), 'utf8');
15
-
16
- console.log(`HTML size: ${(realWorldHtml.length / 1024).toFixed(1)} KB`);
17
- console.log('Testing different concurrency levels...\n');
18
-
19
- const concurrencyLevels = [1, 5, 10, 20, 50];
20
- const totalRequests = 100;
21
-
22
- const results = [];
23
-
24
- for (const concurrency of concurrencyLevels) {
25
- console.log(`Testing with concurrency level: ${concurrency}`);
26
- console.log('-'.repeat(40));
27
-
28
- // Test sync (sequential only)
29
- if (concurrency === 1) {
30
- console.log(' Sync version (sequential)...');
31
- const syncStart = process.hrtime.bigint();
32
- for (let i = 0; i < totalRequests; i++) {
33
- mimeHtml.sync({ html: realWorldHtml });
34
- if ((i + 1) % 20 === 0) {
35
- process.stdout.write(` Progress: ${i + 1}/${totalRequests}\r`);
36
- }
37
- }
38
- const syncEnd = process.hrtime.bigint();
39
- const syncTime = Number(syncEnd - syncStart) / 1000000;
40
- process.stdout.write(` Progress: ${totalRequests}/${totalRequests} - Done!\n`);
41
-
42
- results.push({
43
- method: 'Sync (sequential)',
44
- concurrency: 1,
45
- totalRequests,
46
- totalTime: syncTime.toFixed(2),
47
- avgTime: (syncTime / totalRequests).toFixed(2),
48
- reqPerSec: (totalRequests / (syncTime / 1000)).toFixed(0)
49
- });
50
- }
51
-
52
- // Test async with pool
53
- console.log(' Async with pool...');
54
- const asyncPoolStart = process.hrtime.bigint();
55
- const poolPromises = [];
56
-
57
- for (let i = 0; i < totalRequests; i++) {
58
- poolPromises.push(
59
- mimeHtml.async(
60
- { html: realWorldHtml },
61
- {
62
- useWorkerPool: true,
63
- timeout: 5000,
64
- minWorkers: 2,
65
- maxWorkers: 4,
66
- logErrors: false
67
- }
68
- )
69
- );
70
-
71
- // Control concurrency
72
- if (poolPromises.length >= concurrency) {
73
- await Promise.race(poolPromises).then(() => {
74
- const index = poolPromises.findIndex(
75
- p => p.constructor.name !== 'Promise' || Promise.race([p, Promise.resolve('done')]).then(v => v === 'done')
76
- );
77
- if (index > -1) poolPromises.splice(index, 1);
78
- });
79
- }
80
-
81
- if ((i + 1) % 20 === 0) {
82
- process.stdout.write(` Progress: ${i + 1}/${totalRequests}\r`);
83
- }
84
- }
85
-
86
- // Wait for remaining promises
87
- await Promise.all(poolPromises);
88
- const asyncPoolEnd = process.hrtime.bigint();
89
- const asyncPoolTime = Number(asyncPoolEnd - asyncPoolStart) / 1000000;
90
- process.stdout.write(` Progress: ${totalRequests}/${totalRequests} - Done!\n`);
91
-
92
- results.push({
93
- method: 'Async (pool)',
94
- concurrency,
95
- totalRequests,
96
- totalTime: asyncPoolTime.toFixed(2),
97
- avgTime: (asyncPoolTime / totalRequests).toFixed(2),
98
- reqPerSec: (totalRequests / (asyncPoolTime / 1000)).toFixed(0)
99
- });
100
-
101
- // Show worker pool stats at peak concurrency
102
- if (concurrency === 20) {
103
- const stats = mimeHtml.getWorkerPoolStats();
104
- console.log(` Worker pool stats: ${stats.totalWorkers} workers, ${stats.busyWorkers} busy`);
105
- }
106
-
107
- console.log();
108
- }
109
-
110
- // Display results table
111
- console.log('='.repeat(80));
112
- console.log('RESULTS');
113
- console.log('='.repeat(80) + '\n');
114
-
115
- console.table(
116
- results.map(r => ({
117
- Method: r.method,
118
- Concurrency: r.concurrency,
119
- 'Total Time': `${r.totalTime}ms`,
120
- 'Avg per Request': `${r.avgTime}ms`,
121
- Throughput: `${r.reqPerSec} req/s`
122
- }))
123
- );
124
-
125
- // Analysis
126
- console.log('Performance Analysis:');
127
- console.log('-'.repeat(40));
128
-
129
- const syncResult = results.find(r => r.method === 'Sync (sequential)');
130
- const bestAsync = results.filter(r => r.method === 'Async (pool)').sort((a, b) => parseFloat(a.totalTime) - parseFloat(b.totalTime))[0];
131
-
132
- if (syncResult && bestAsync) {
133
- const speedup = (parseFloat(syncResult.totalTime) / parseFloat(bestAsync.totalTime)).toFixed(2);
134
- console.log(`• Best async configuration: Concurrency ${bestAsync.concurrency}`);
135
- console.log(`• Async is ${speedup}x faster than sync for batch processing`);
136
- console.log(`• Sync throughput: ${syncResult.reqPerSec} req/s`);
137
- console.log(`• Best async throughput: ${bestAsync.reqPerSec} req/s`);
138
- }
139
-
140
- // Throughput graph
141
- console.log('\nThroughput by Concurrency Level:');
142
- console.log('-'.repeat(40));
143
-
144
- const asyncResults = results.filter(r => r.method === 'Async (pool)');
145
- const maxThroughput = Math.max(...asyncResults.map(r => parseInt(r.reqPerSec)));
146
-
147
- asyncResults.forEach(r => {
148
- const barLength = Math.round((parseInt(r.reqPerSec) / maxThroughput) * 30);
149
- const bar = '█'.repeat(barLength);
150
- console.log(` ${String(r.concurrency).padStart(2)}: ${bar} ${r.reqPerSec} req/s`);
151
- });
152
-
153
- // Memory usage
154
- const memUsage = process.memoryUsage();
155
- console.log('\nFinal Memory Usage:');
156
- console.log('-'.repeat(40));
157
- console.log(`• RSS: ${(memUsage.rss / 1024 / 1024).toFixed(1)} MB`);
158
- console.log(`• Heap Used: ${(memUsage.heapUsed / 1024 / 1024).toFixed(1)} MB`);
159
-
160
- // Cleanup
161
- await mimeHtml.closeWorkerPool();
162
-
163
- console.log('\n' + '='.repeat(80));
164
- console.log('Summary:');
165
- console.log('• Async with worker pool provides significant speedup for concurrent processing');
166
- console.log('• Pre-filtering prevents timeouts even under high load');
167
- console.log('• Worker pool efficiently handles concurrent requests');
168
- console.log('• Optimal concurrency depends on CPU cores and workload');
169
- console.log('='.repeat(80));
170
- }
171
-
172
- // Run benchmark
173
- concurrencyBenchmark().catch(err => {
174
- console.error('Benchmark failed:', err);
175
- process.exit(1);
176
- });
@@ -1,200 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const mimeHtml = require('../lib/mime-html');
5
-
6
- // Quick performance comparison
7
- async function quickBenchmark() {
8
- const testHtml = `
9
- <!DOCTYPE html>
10
- <html>
11
- <head>
12
- <style>
13
- body { font-family: Arial, sans-serif; }
14
- .container { max-width: 600px; margin: 0 auto; padding: 20px; }
15
- h1 { color: #333; }
16
- p { line-height: 1.6; }
17
- .button { background: #007bff; color: white; padding: 10px 20px; }
18
- </style>
19
- </head>
20
- <body>
21
- <div class="container">
22
- <h1>Test Email</h1>
23
- <p>This is a test email with moderate complexity.</p>
24
- <a href="#" class="button">Click Here</a>
25
- </div>
26
- </body>
27
- </html>
28
- `;
29
-
30
- console.log('Quick Performance Comparison');
31
- console.log('============================\n');
32
-
33
- const iterations = 50;
34
-
35
- // Test Sync
36
- console.log('Testing Sync version...');
37
- const syncTimes = [];
38
- for (let i = 0; i < iterations; i++) {
39
- const start = process.hrtime.bigint();
40
- mimeHtml.sync({ html: testHtml });
41
- const end = process.hrtime.bigint();
42
- syncTimes.push(Number(end - start) / 1000000);
43
- }
44
-
45
- // Test Async with Pool
46
- console.log('Testing Async with worker pool...');
47
- const asyncPoolTimes = [];
48
- for (let i = 0; i < iterations; i++) {
49
- const start = process.hrtime.bigint();
50
- await mimeHtml.async(
51
- { html: testHtml },
52
- {
53
- useWorkerPool: true,
54
- timeout: 5000
55
- }
56
- );
57
- const end = process.hrtime.bigint();
58
- asyncPoolTimes.push(Number(end - start) / 1000000);
59
- }
60
-
61
- // Test Async without Pool (just first 5 to avoid long wait)
62
- console.log('Testing Async without pool (5 iterations only)...');
63
- const asyncNoPoolTimes = [];
64
- for (let i = 0; i < 5; i++) {
65
- const start = process.hrtime.bigint();
66
- await mimeHtml.async(
67
- { html: testHtml },
68
- {
69
- useWorkerPool: false,
70
- timeout: 5000
71
- }
72
- );
73
- const end = process.hrtime.bigint();
74
- asyncNoPoolTimes.push(Number(end - start) / 1000000);
75
- }
76
-
77
- // Calculate stats
78
- const getStats = times => {
79
- const avg = times.reduce((a, b) => a + b, 0) / times.length;
80
- times.sort((a, b) => a - b);
81
- const median = times[Math.floor(times.length / 2)];
82
- const min = times[0];
83
- const max = times[times.length - 1];
84
- return { avg, median, min, max };
85
- };
86
-
87
- const syncStats = getStats(syncTimes);
88
- const asyncPoolStats = getStats(asyncPoolTimes);
89
- const asyncNoPoolStats = getStats(asyncNoPoolTimes);
90
-
91
- // Display results
92
- console.log('\nResults (all times in milliseconds):');
93
- console.log('=====================================\n');
94
-
95
- console.log('Sync Version:');
96
- console.log(` Average: ${syncStats.avg.toFixed(2)} ms`);
97
- console.log(` Median: ${syncStats.median.toFixed(2)} ms`);
98
- console.log(` Min: ${syncStats.min.toFixed(2)} ms`);
99
- console.log(` Max: ${syncStats.max.toFixed(2)} ms`);
100
-
101
- console.log('\nAsync with Worker Pool:');
102
- console.log(` Average: ${asyncPoolStats.avg.toFixed(2)} ms`);
103
- console.log(` Median: ${asyncPoolStats.median.toFixed(2)} ms`);
104
- console.log(` Min: ${asyncPoolStats.min.toFixed(2)} ms`);
105
- console.log(` Max: ${asyncPoolStats.max.toFixed(2)} ms`);
106
-
107
- console.log('\nAsync without Pool (single worker per call):');
108
- console.log(` Average: ${asyncNoPoolStats.avg.toFixed(2)} ms`);
109
- console.log(` Median: ${asyncNoPoolStats.median.toFixed(2)} ms`);
110
- console.log(` Min: ${asyncNoPoolStats.min.toFixed(2)} ms`);
111
- console.log(` Max: ${asyncNoPoolStats.max.toFixed(2)} ms`);
112
-
113
- console.log('\nPerformance Summary:');
114
- console.log('====================');
115
-
116
- if (asyncPoolStats.avg < syncStats.avg) {
117
- const speedup = (syncStats.avg / asyncPoolStats.avg).toFixed(2);
118
- console.log(`✓ Async with pool is ${speedup}x faster than sync`);
119
- } else {
120
- const slowdown = (asyncPoolStats.avg / syncStats.avg).toFixed(2);
121
- console.log(`✗ Async with pool is ${slowdown}x slower than sync`);
122
- }
123
-
124
- const poolVsNoPool = (asyncNoPoolStats.avg / asyncPoolStats.avg).toFixed(1);
125
- console.log(`✓ Worker pool is ${poolVsNoPool}x faster than single workers`);
126
-
127
- // Worker pool stats
128
- const poolStats = mimeHtml.getWorkerPoolStats();
129
- if (poolStats) {
130
- console.log('\nWorker Pool Statistics:');
131
- console.log('======================');
132
- console.log(` Total Workers: ${poolStats.totalWorkers}`);
133
- console.log(` Busy Workers: ${poolStats.busyWorkers}`);
134
- console.log(` Free Workers: ${poolStats.freeWorkers}`);
135
- console.log(` Queued Tasks: ${poolStats.queuedTasks}`);
136
- console.log(` Min Workers: ${poolStats.minWorkers}`);
137
- console.log(` Max Workers: ${poolStats.maxWorkers}`);
138
- }
139
-
140
- // Test with problematic CSS
141
- console.log('\nTesting Problematic CSS Handling:');
142
- console.log('=================================');
143
-
144
- const problematicHtml = `
145
- <!DOCTYPE html>
146
- <html>
147
- <head>
148
- <style>
149
- /* These selectors would cause juice to hang */
150
- .item:is(.active, .selected) { background: yellow; }
151
- .element:where(.primary) { color: blue; }
152
- .parent:has(> .child) { border: 1px solid red; }
153
- </style>
154
- </head>
155
- <body>
156
- <div class="item active">Active Item</div>
157
- <div class="parent"><div class="child">Child</div></div>
158
- </body>
159
- </html>
160
- `;
161
-
162
- try {
163
- console.log(' Sync version... ');
164
- const syncStart = process.hrtime.bigint();
165
- const _syncResult = mimeHtml.sync({ html: problematicHtml });
166
- const syncEnd = process.hrtime.bigint();
167
- const syncTime = Number(syncEnd - syncStart) / 1000000;
168
- console.log(` ✓ Success (${syncTime.toFixed(2)} ms) - problematic selectors removed`);
169
- } catch (err) {
170
- console.log(` ✗ Failed: ${err.message}`);
171
- }
172
-
173
- try {
174
- console.log(' Async version...');
175
- const asyncStart = process.hrtime.bigint();
176
- const _asyncResult = await mimeHtml.async(
177
- { html: problematicHtml },
178
- {
179
- timeout: 2000,
180
- logErrors: false
181
- }
182
- );
183
- const asyncEnd = process.hrtime.bigint();
184
- const asyncTime = Number(asyncEnd - asyncStart) / 1000000;
185
- console.log(` ✓ Success (${asyncTime.toFixed(2)} ms) - handled with timeout protection`);
186
- } catch (err) {
187
- console.log(` ✗ Failed: ${err.message}`);
188
- }
189
-
190
- // Clean up
191
- await mimeHtml.closeWorkerPool();
192
-
193
- console.log('\n✓ Benchmark complete!');
194
- }
195
-
196
- // Run the benchmark
197
- quickBenchmark().catch(err => {
198
- console.error('Benchmark failed:', err);
199
- process.exit(1);
200
- });
@@ -1,64 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const mimeHtml = require('../lib/mime-html');
7
-
8
- async function quickConcurrencyTest() {
9
- console.log('Quick Concurrency Test - Real-World HTML');
10
- console.log('='.repeat(60));
11
-
12
- const realWorldHtml = fs.readFileSync(path.join(__dirname, '../examples/failing.html'), 'utf8');
13
-
14
- console.log(`Testing with ${(realWorldHtml.length / 1024).toFixed(1)} KB of problematic HTML\n`);
15
-
16
- const iterations = 30;
17
-
18
- // Test 1: Sequential Sync
19
- console.log(`Test 1: Sequential Sync (${iterations} requests)`);
20
- const syncStart = process.hrtime.bigint();
21
- for (let i = 0; i < iterations; i++) {
22
- mimeHtml.sync({ html: realWorldHtml });
23
- }
24
- const syncTime = Number(process.hrtime.bigint() - syncStart) / 1000000;
25
- console.log(` Time: ${syncTime.toFixed(2)}ms`);
26
- console.log(` Throughput: ${(iterations / (syncTime / 1000)).toFixed(0)} req/s\n`);
27
-
28
- // Test 2: Concurrent Async with Pool (concurrency = 10)
29
- console.log(`Test 2: Concurrent Async with Pool (${iterations} requests, concurrency=10)`);
30
- const asyncStart = process.hrtime.bigint();
31
- const promises = [];
32
- for (let i = 0; i < iterations; i++) {
33
- promises.push(
34
- mimeHtml.async(
35
- { html: realWorldHtml },
36
- {
37
- useWorkerPool: true,
38
- timeout: 5000,
39
- logErrors: false
40
- }
41
- )
42
- );
43
- }
44
- await Promise.all(promises);
45
- const asyncTime = Number(process.hrtime.bigint() - asyncStart) / 1000000;
46
- console.log(` Time: ${asyncTime.toFixed(2)}ms`);
47
- console.log(` Throughput: ${(iterations / (asyncTime / 1000)).toFixed(0)} req/s\n`);
48
-
49
- // Show speedup
50
- const speedup = (syncTime / asyncTime).toFixed(2);
51
- console.log('='.repeat(60));
52
- console.log('Results:');
53
- console.log(`• Concurrent processing is ${speedup}x faster than sequential`);
54
- console.log(`• No timeouts occurred (pre-filtering working correctly)`);
55
-
56
- const stats = mimeHtml.getWorkerPoolStats();
57
- if (stats) {
58
- console.log(`• Worker pool used ${stats.totalWorkers} workers efficiently`);
59
- }
60
-
61
- await mimeHtml.closeWorkerPool();
62
- }
63
-
64
- quickConcurrencyTest().catch(console.error);