@postalsys/email-text-tools 2.4.26 → 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.
- package/CHANGELOG.md +7 -0
- package/README.md +10 -0
- package/dist/cjs/index.d.ts +18 -0
- package/dist/cjs/index.js +28 -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-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 +529 -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 +18 -0
- package/dist/esm/index.js +12 -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-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 +79 -127
- 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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = textToHtml;
|
|
7
|
+
const linkify_it_1 = __importDefault(require("linkify-it"));
|
|
8
|
+
const he_1 = __importDefault(require("he"));
|
|
9
|
+
const commonjs_cjs_1 = require("./commonjs.cjs");
|
|
10
|
+
const linkify = new linkify_it_1.default()
|
|
11
|
+
.tlds(commonjs_cjs_1.TLDS) // Reload with full tlds list
|
|
12
|
+
.tlds('onion', true) // Add unofficial `.onion` domain
|
|
13
|
+
.add('git:', 'http:') // Add `git:` protocol as "alias"
|
|
14
|
+
.set({ fuzzyIP: true });
|
|
15
|
+
// Quote levels deeper than this are kept as literal "> " text. Every level costs a few stack
|
|
16
|
+
// frames here and a nested <blockquote> later, so hostile input with thousands of leading ">"
|
|
17
|
+
// used to overflow the stack.
|
|
18
|
+
const MAX_QUOTE_DEPTH = 100;
|
|
19
|
+
function generateQuotedTree(text) {
|
|
20
|
+
let lines = text.split(/\r?\n/);
|
|
21
|
+
let tree = {
|
|
22
|
+
type: 'text',
|
|
23
|
+
children: []
|
|
24
|
+
};
|
|
25
|
+
let createNode = (parent, type) => {
|
|
26
|
+
let node = {
|
|
27
|
+
parent,
|
|
28
|
+
type,
|
|
29
|
+
lines: [],
|
|
30
|
+
children: []
|
|
31
|
+
};
|
|
32
|
+
parent.children.push(node);
|
|
33
|
+
return node;
|
|
34
|
+
};
|
|
35
|
+
let walkNode = (curNode, lines, depth) => {
|
|
36
|
+
for (let line of lines) {
|
|
37
|
+
if (depth < MAX_QUOTE_DEPTH && /^>/.test(line)) {
|
|
38
|
+
if (curNode.type !== 'quote') {
|
|
39
|
+
curNode = createNode(curNode.parent, 'quote');
|
|
40
|
+
}
|
|
41
|
+
curNode.lines.push(line.replace(/^> ?/, ''));
|
|
42
|
+
}
|
|
43
|
+
else if (curNode.type === 'quote') {
|
|
44
|
+
// process child
|
|
45
|
+
let quoteNode = curNode;
|
|
46
|
+
let childNode = createNode(curNode, 'text');
|
|
47
|
+
walkNode(childNode, quoteNode.lines, depth + 1);
|
|
48
|
+
curNode = createNode(curNode.parent, 'text');
|
|
49
|
+
curNode.lines.push(line);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
curNode.lines.push(line);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (curNode.type === 'quote') {
|
|
56
|
+
// process child
|
|
57
|
+
let quoteNode = curNode;
|
|
58
|
+
let childNode = createNode(curNode, 'text');
|
|
59
|
+
walkNode(childNode, quoteNode.lines, depth + 1);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
let rootNode = createNode(tree, 'text');
|
|
63
|
+
walkNode(rootNode, lines, 0);
|
|
64
|
+
return tree;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Return HTML escaped text with clickable links
|
|
68
|
+
* @param text Text content to HTML escape
|
|
69
|
+
* @returns HTML escaped content
|
|
70
|
+
*/
|
|
71
|
+
function linkifyAndHtmlEscape(text) {
|
|
72
|
+
try {
|
|
73
|
+
let links = linkify.match(text);
|
|
74
|
+
if (links && links.length) {
|
|
75
|
+
let parts = [];
|
|
76
|
+
let cursor = 0;
|
|
77
|
+
for (let link of links) {
|
|
78
|
+
if (cursor < link.index) {
|
|
79
|
+
parts.push({
|
|
80
|
+
type: 'text',
|
|
81
|
+
content: text.substring(cursor, link.index)
|
|
82
|
+
});
|
|
83
|
+
cursor = link.index;
|
|
84
|
+
}
|
|
85
|
+
parts.push(Object.assign({ type: 'link' }, link));
|
|
86
|
+
cursor = link.lastIndex;
|
|
87
|
+
}
|
|
88
|
+
if (cursor < text.length) {
|
|
89
|
+
parts.push({
|
|
90
|
+
type: 'text',
|
|
91
|
+
content: text.slice(cursor)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return parts
|
|
95
|
+
.map(part => {
|
|
96
|
+
switch (part.type) {
|
|
97
|
+
case 'text': {
|
|
98
|
+
// normal text, escape HTML
|
|
99
|
+
return he_1.default.encode(part.content, { useNamedReferences: true });
|
|
100
|
+
}
|
|
101
|
+
case 'link':
|
|
102
|
+
// URL with html escaped text content and URL
|
|
103
|
+
return `<a href="${he_1.default.encode(part.url, { useNamedReferences: true })}">${he_1.default.encode(part.text, {
|
|
104
|
+
useNamedReferences: true
|
|
105
|
+
})}</a>`;
|
|
106
|
+
}
|
|
107
|
+
return '';
|
|
108
|
+
})
|
|
109
|
+
.join('');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (_err) {
|
|
113
|
+
// ignore?
|
|
114
|
+
}
|
|
115
|
+
// No links or exception, so HTML escape everything
|
|
116
|
+
return he_1.default.encode(text, { useNamedReferences: true });
|
|
117
|
+
}
|
|
118
|
+
function textLinesToHtml(lines) {
|
|
119
|
+
let textparts = ['<p>'];
|
|
120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
121
|
+
let line = lines[i];
|
|
122
|
+
if (!line.trim()) {
|
|
123
|
+
if (i && lines[i - 1].trim()) {
|
|
124
|
+
textparts.push('</p><p>');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (i < lines.length - 1 && lines[i + 1].trim()) {
|
|
128
|
+
let escaped = linkifyAndHtmlEscape(line);
|
|
129
|
+
if (/^[ \t]*[-_]{2,}[ \t]*$/.test(escaped)) {
|
|
130
|
+
textparts.push(`<hr />`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
textparts.push(`${escaped}<br />`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
let escaped = linkifyAndHtmlEscape(line);
|
|
138
|
+
if (/^[ \t]*[-_]{2,}[ \t]*$/.test(escaped)) {
|
|
139
|
+
escaped = `<hr />`;
|
|
140
|
+
}
|
|
141
|
+
textparts.push(escaped);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return textparts.join('\n') + '</p>';
|
|
145
|
+
}
|
|
146
|
+
function textToHtml(text) {
|
|
147
|
+
text = (text || '').toString();
|
|
148
|
+
let tree = generateQuotedTree(text);
|
|
149
|
+
let blockQuoteCounter = 0;
|
|
150
|
+
function processQuotedNode(curNode, level) {
|
|
151
|
+
let entries = [];
|
|
152
|
+
level = (level || 0) + 1;
|
|
153
|
+
if (curNode.type === 'text') {
|
|
154
|
+
for (let child of curNode.children) {
|
|
155
|
+
let content = processQuotedNode(child);
|
|
156
|
+
if (typeof content === 'string') {
|
|
157
|
+
entries.push(content);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (curNode.lines && curNode.lines.length) {
|
|
161
|
+
entries.push(textLinesToHtml(curNode.lines));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (curNode.type === 'quote') {
|
|
165
|
+
for (let child of curNode.children) {
|
|
166
|
+
let content = processQuotedNode(child, level);
|
|
167
|
+
if (typeof content === 'string') {
|
|
168
|
+
let id = ++blockQuoteCounter;
|
|
169
|
+
entries.push(`<blockquote type="cite" class="ee-block-${level}"><!-- blocquote ${id} start-->${content}</blockquote><!-- blocquote ${id} end-->`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (entries.length) {
|
|
174
|
+
return entries.join('\n');
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
let generatedHtml = processQuotedNode(tree);
|
|
179
|
+
return generatedHtml;
|
|
180
|
+
}
|
|
181
|
+
module.exports = exports.default;
|
|
182
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { Worker } from 'node:worker_threads';
|
|
3
|
+
export interface WorkerPoolOptions {
|
|
4
|
+
/** Worker script, defaults to the juice worker */
|
|
5
|
+
workerPath?: string | URL | undefined;
|
|
6
|
+
minWorkers?: number | undefined;
|
|
7
|
+
maxWorkers?: number | undefined;
|
|
8
|
+
maxQueueSize?: number | undefined;
|
|
9
|
+
/** Default task timeout in milliseconds */
|
|
10
|
+
workerTimeout?: number | undefined;
|
|
11
|
+
/** How long a worker above the minimum may stay idle, in milliseconds */
|
|
12
|
+
idleTimeout?: number | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface WorkerPoolStats {
|
|
15
|
+
totalWorkers: number;
|
|
16
|
+
busyWorkers: number;
|
|
17
|
+
freeWorkers: number;
|
|
18
|
+
queuedTasks: number;
|
|
19
|
+
minWorkers: number;
|
|
20
|
+
maxWorkers: number;
|
|
21
|
+
}
|
|
22
|
+
interface Task {
|
|
23
|
+
data: unknown;
|
|
24
|
+
resolve: (result: any) => void;
|
|
25
|
+
reject: (err: Error) => void;
|
|
26
|
+
timeout: number;
|
|
27
|
+
}
|
|
28
|
+
interface WorkerInfo {
|
|
29
|
+
worker: Worker;
|
|
30
|
+
busy: boolean;
|
|
31
|
+
lastUsed: number;
|
|
32
|
+
timeoutHandle: NodeJS.Timeout | null;
|
|
33
|
+
currentTask: (Task & {
|
|
34
|
+
timeoutHandle: NodeJS.Timeout;
|
|
35
|
+
}) | null;
|
|
36
|
+
}
|
|
37
|
+
export default class WorkerPool extends EventEmitter {
|
|
38
|
+
workerPath: string | URL;
|
|
39
|
+
minWorkers: number;
|
|
40
|
+
maxWorkers: number;
|
|
41
|
+
maxQueueSize: number;
|
|
42
|
+
workerTimeout: number;
|
|
43
|
+
idleTimeout: number;
|
|
44
|
+
workers: WorkerInfo[];
|
|
45
|
+
freeWorkers: WorkerInfo[];
|
|
46
|
+
queue: Task[];
|
|
47
|
+
closing: boolean;
|
|
48
|
+
constructor(options?: WorkerPoolOptions);
|
|
49
|
+
_initWorkers(): void;
|
|
50
|
+
_createWorker(): WorkerInfo | null;
|
|
51
|
+
_setupIdleTimeout(workerInfo: WorkerInfo): void;
|
|
52
|
+
_removeWorker(workerInfo: WorkerInfo): void;
|
|
53
|
+
process(data: unknown, options?: {
|
|
54
|
+
timeout?: number | undefined;
|
|
55
|
+
}): Promise<any>;
|
|
56
|
+
_executeTask(workerInfo: WorkerInfo, task: Task): void;
|
|
57
|
+
_processQueue(): void;
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
getStats(): WorkerPoolStats;
|
|
60
|
+
}
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_events_1 = require("node:events");
|
|
4
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
5
|
+
const commonjs_cjs_1 = require("./commonjs.cjs");
|
|
6
|
+
class WorkerPool extends node_events_1.EventEmitter {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
super();
|
|
9
|
+
this.workerPath = options.workerPath || commonjs_cjs_1.JUICE_WORKER_PATH;
|
|
10
|
+
this.minWorkers = options.minWorkers || 2;
|
|
11
|
+
this.maxWorkers = options.maxWorkers || 4;
|
|
12
|
+
this.maxQueueSize = options.maxQueueSize || 100;
|
|
13
|
+
this.workerTimeout = options.workerTimeout || 10000;
|
|
14
|
+
this.idleTimeout = options.idleTimeout || 30000;
|
|
15
|
+
this.workers = [];
|
|
16
|
+
this.freeWorkers = [];
|
|
17
|
+
this.queue = [];
|
|
18
|
+
this.closing = false;
|
|
19
|
+
// Initialize minimum number of workers
|
|
20
|
+
this._initWorkers();
|
|
21
|
+
}
|
|
22
|
+
_initWorkers() {
|
|
23
|
+
for (let i = 0; i < this.minWorkers; i++) {
|
|
24
|
+
this._createWorker();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
_createWorker() {
|
|
28
|
+
if (this.closing)
|
|
29
|
+
return null;
|
|
30
|
+
const worker = new node_worker_threads_1.Worker(this.workerPath);
|
|
31
|
+
const workerInfo = {
|
|
32
|
+
worker,
|
|
33
|
+
busy: false,
|
|
34
|
+
lastUsed: Date.now(),
|
|
35
|
+
timeoutHandle: null,
|
|
36
|
+
currentTask: null
|
|
37
|
+
};
|
|
38
|
+
worker.on('message', (msg) => {
|
|
39
|
+
if (workerInfo.currentTask) {
|
|
40
|
+
const { resolve, reject, timeoutHandle } = workerInfo.currentTask;
|
|
41
|
+
clearTimeout(timeoutHandle);
|
|
42
|
+
if (msg.success) {
|
|
43
|
+
resolve(msg.result);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
reject(new Error(msg.error));
|
|
47
|
+
}
|
|
48
|
+
workerInfo.currentTask = null;
|
|
49
|
+
workerInfo.busy = false;
|
|
50
|
+
workerInfo.lastUsed = Date.now();
|
|
51
|
+
// Add worker back to free list if not already there
|
|
52
|
+
if (!this.freeWorkers.includes(workerInfo)) {
|
|
53
|
+
this.freeWorkers.push(workerInfo);
|
|
54
|
+
}
|
|
55
|
+
// Process next item in queue
|
|
56
|
+
this._processQueue();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
worker.on('error', (err) => {
|
|
60
|
+
if (workerInfo.currentTask) {
|
|
61
|
+
const { reject, timeoutHandle } = workerInfo.currentTask;
|
|
62
|
+
clearTimeout(timeoutHandle);
|
|
63
|
+
reject(err);
|
|
64
|
+
workerInfo.currentTask = null;
|
|
65
|
+
}
|
|
66
|
+
// Remove failed worker
|
|
67
|
+
const index = this.workers.indexOf(workerInfo);
|
|
68
|
+
if (index > -1) {
|
|
69
|
+
this.workers.splice(index, 1);
|
|
70
|
+
}
|
|
71
|
+
const freeIndex = this.freeWorkers.indexOf(workerInfo);
|
|
72
|
+
if (freeIndex > -1) {
|
|
73
|
+
this.freeWorkers.splice(freeIndex, 1);
|
|
74
|
+
}
|
|
75
|
+
// Create replacement if below minimum
|
|
76
|
+
if (this.workers.length < this.minWorkers && !this.closing) {
|
|
77
|
+
this._createWorker();
|
|
78
|
+
}
|
|
79
|
+
// Process queue with remaining workers
|
|
80
|
+
this._processQueue();
|
|
81
|
+
});
|
|
82
|
+
worker.on('exit', () => {
|
|
83
|
+
// Clean up worker references
|
|
84
|
+
const index = this.workers.indexOf(workerInfo);
|
|
85
|
+
if (index > -1) {
|
|
86
|
+
this.workers.splice(index, 1);
|
|
87
|
+
}
|
|
88
|
+
const freeIndex = this.freeWorkers.indexOf(workerInfo);
|
|
89
|
+
if (freeIndex > -1) {
|
|
90
|
+
this.freeWorkers.splice(freeIndex, 1);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
this.workers.push(workerInfo);
|
|
94
|
+
this.freeWorkers.push(workerInfo);
|
|
95
|
+
// Set up idle timeout for workers above minimum
|
|
96
|
+
if (this.workers.length > this.minWorkers) {
|
|
97
|
+
this._setupIdleTimeout(workerInfo);
|
|
98
|
+
}
|
|
99
|
+
return workerInfo;
|
|
100
|
+
}
|
|
101
|
+
_setupIdleTimeout(workerInfo) {
|
|
102
|
+
if (workerInfo.timeoutHandle) {
|
|
103
|
+
clearTimeout(workerInfo.timeoutHandle);
|
|
104
|
+
}
|
|
105
|
+
workerInfo.timeoutHandle = setTimeout(() => {
|
|
106
|
+
if (!workerInfo.busy && this.workers.length > this.minWorkers) {
|
|
107
|
+
this._removeWorker(workerInfo);
|
|
108
|
+
}
|
|
109
|
+
}, this.idleTimeout);
|
|
110
|
+
}
|
|
111
|
+
_removeWorker(workerInfo) {
|
|
112
|
+
const index = this.workers.indexOf(workerInfo);
|
|
113
|
+
if (index > -1) {
|
|
114
|
+
this.workers.splice(index, 1);
|
|
115
|
+
}
|
|
116
|
+
const freeIndex = this.freeWorkers.indexOf(workerInfo);
|
|
117
|
+
if (freeIndex > -1) {
|
|
118
|
+
this.freeWorkers.splice(freeIndex, 1);
|
|
119
|
+
}
|
|
120
|
+
if (workerInfo.timeoutHandle) {
|
|
121
|
+
clearTimeout(workerInfo.timeoutHandle);
|
|
122
|
+
}
|
|
123
|
+
void workerInfo.worker.terminate();
|
|
124
|
+
}
|
|
125
|
+
async process(data, options = {}) {
|
|
126
|
+
if (this.closing) {
|
|
127
|
+
throw new Error('Worker pool is closing');
|
|
128
|
+
}
|
|
129
|
+
const timeout = options.timeout || this.workerTimeout;
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const task = { data, resolve, reject, timeout };
|
|
132
|
+
// Try to get a free worker
|
|
133
|
+
const workerInfo = this.freeWorkers.shift();
|
|
134
|
+
if (workerInfo) {
|
|
135
|
+
this._executeTask(workerInfo, task);
|
|
136
|
+
}
|
|
137
|
+
else if (this.workers.length < this.maxWorkers) {
|
|
138
|
+
// Create new worker if below max
|
|
139
|
+
const newWorker = this._createWorker();
|
|
140
|
+
if (newWorker) {
|
|
141
|
+
this.freeWorkers.shift(); // Remove from free list
|
|
142
|
+
this._executeTask(newWorker, task);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (this.queue.length < this.maxQueueSize) {
|
|
146
|
+
// Queue the task
|
|
147
|
+
this.queue.push(task);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
reject(new Error('Worker pool queue is full'));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
_executeTask(workerInfo, task) {
|
|
155
|
+
workerInfo.busy = true;
|
|
156
|
+
workerInfo.lastUsed = Date.now();
|
|
157
|
+
// Clear idle timeout while working
|
|
158
|
+
if (workerInfo.timeoutHandle) {
|
|
159
|
+
clearTimeout(workerInfo.timeoutHandle);
|
|
160
|
+
workerInfo.timeoutHandle = null;
|
|
161
|
+
}
|
|
162
|
+
// Set up task timeout
|
|
163
|
+
const timeoutHandle = setTimeout(() => {
|
|
164
|
+
task.reject(new Error(`Worker task timed out after ${task.timeout}ms`));
|
|
165
|
+
workerInfo.currentTask = null;
|
|
166
|
+
workerInfo.busy = false;
|
|
167
|
+
// Terminate and replace the worker
|
|
168
|
+
this._removeWorker(workerInfo);
|
|
169
|
+
if (this.workers.length < this.minWorkers && !this.closing) {
|
|
170
|
+
this._createWorker();
|
|
171
|
+
}
|
|
172
|
+
this._processQueue();
|
|
173
|
+
}, task.timeout);
|
|
174
|
+
workerInfo.currentTask = { ...task, timeoutHandle };
|
|
175
|
+
workerInfo.worker.postMessage(task.data);
|
|
176
|
+
}
|
|
177
|
+
_processQueue() {
|
|
178
|
+
if (this.queue.length === 0) {
|
|
179
|
+
// Mark free workers and set up idle timeouts
|
|
180
|
+
this.workers.forEach(workerInfo => {
|
|
181
|
+
if (!workerInfo.busy && !this.freeWorkers.includes(workerInfo)) {
|
|
182
|
+
this.freeWorkers.push(workerInfo);
|
|
183
|
+
if (this.workers.length > this.minWorkers) {
|
|
184
|
+
this._setupIdleTimeout(workerInfo);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const workerInfo = this.freeWorkers.shift();
|
|
191
|
+
if (workerInfo) {
|
|
192
|
+
const task = this.queue.shift();
|
|
193
|
+
if (task) {
|
|
194
|
+
this._executeTask(workerInfo, task);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async close() {
|
|
199
|
+
this.closing = true;
|
|
200
|
+
// Clear the queue
|
|
201
|
+
while (this.queue.length > 0) {
|
|
202
|
+
const task = this.queue.shift();
|
|
203
|
+
task.reject(new Error('Worker pool is closing'));
|
|
204
|
+
}
|
|
205
|
+
// Terminate all workers
|
|
206
|
+
const terminatePromises = this.workers.map(workerInfo => {
|
|
207
|
+
if (workerInfo.timeoutHandle) {
|
|
208
|
+
clearTimeout(workerInfo.timeoutHandle);
|
|
209
|
+
}
|
|
210
|
+
return workerInfo.worker.terminate();
|
|
211
|
+
});
|
|
212
|
+
await Promise.all(terminatePromises);
|
|
213
|
+
this.workers = [];
|
|
214
|
+
this.freeWorkers = [];
|
|
215
|
+
}
|
|
216
|
+
getStats() {
|
|
217
|
+
return {
|
|
218
|
+
totalWorkers: this.workers.length,
|
|
219
|
+
busyWorkers: this.workers.filter(w => w.busy).length,
|
|
220
|
+
freeWorkers: this.freeWorkers.length,
|
|
221
|
+
queuedTasks: this.queue.length,
|
|
222
|
+
minWorkers: this.minWorkers,
|
|
223
|
+
maxWorkers: this.maxWorkers
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
exports.default = WorkerPool;
|
|
228
|
+
module.exports = exports.default;
|
|
229
|
+
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import textToHtml from './lib/text-to-html.js';
|
|
2
|
+
import htmlToText from './lib/html-to-text.js';
|
|
3
|
+
import inlineHtml from './lib/inline-html.js';
|
|
4
|
+
import inlineText from './lib/inline-text.js';
|
|
5
|
+
import mimeHtml from './lib/mime-html.js';
|
|
6
|
+
export type { FormatDateOptions } from './lib/format-date.js';
|
|
7
|
+
export type { InlineAction, MessageAddress, QuotedMessage } from './lib/inline-html.js';
|
|
8
|
+
export type { MimeHtmlAsyncOptions, MimeHtmlMessage } from './lib/mime-html.js';
|
|
9
|
+
export type { WorkerPoolOptions, WorkerPoolStats } from './lib/worker-pool.js';
|
|
10
|
+
export { textToHtml, htmlToText, inlineHtml, inlineText, mimeHtml };
|
|
11
|
+
declare const textTools: {
|
|
12
|
+
textToHtml: typeof textToHtml;
|
|
13
|
+
htmlToText: typeof htmlToText;
|
|
14
|
+
inlineHtml: typeof inlineHtml;
|
|
15
|
+
inlineText: typeof inlineText;
|
|
16
|
+
mimeHtml: typeof mimeHtml;
|
|
17
|
+
};
|
|
18
|
+
export default textTools;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The ES module entry point and the source of the type declarations of both builds. The
|
|
2
|
+
// CommonJS entry point is index.cts: it has the same exports but loads mimeHtml, and with it
|
|
3
|
+
// jsdom, only on first access. An ES module can not defer a synchronous import, so here
|
|
4
|
+
// mimeHtml is loaded with the rest.
|
|
5
|
+
import textToHtml from './lib/text-to-html.js';
|
|
6
|
+
import htmlToText from './lib/html-to-text.js';
|
|
7
|
+
import inlineHtml from './lib/inline-html.js';
|
|
8
|
+
import inlineText from './lib/inline-text.js';
|
|
9
|
+
import mimeHtml from './lib/mime-html.js';
|
|
10
|
+
export { textToHtml, htmlToText, inlineHtml, inlineText, mimeHtml };
|
|
11
|
+
const textTools = { textToHtml, htmlToText, inlineHtml, inlineText, mimeHtml };
|
|
12
|
+
export default textTools;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// What an ES module can not do the same way in both builds. This file compiles to
|
|
3
|
+
// commonjs.cjs in dist/cjs and in dist/esm alike, so __dirname is the directory of the
|
|
4
|
+
// build that loaded it and require() is available to both.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TLDS = exports.JUICE_WORKER_PATH = void 0;
|
|
7
|
+
// A plain require() keeps the call below in the literal path.join(__dirname, '...') form, the
|
|
8
|
+
// one form @yao-pkg/pkg recognizes when it collects the files a binary needs (a default import
|
|
9
|
+
// compiles to path_1.default.join, which it does not). Without it the worker script is left out
|
|
10
|
+
// of an EmailEngine binary
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
/** The worker script that runs juice off the main thread, the one of the same build */
|
|
14
|
+
exports.JUICE_WORKER_PATH = path.join(__dirname, 'juice-worker.js');
|
|
15
|
+
/** The top level domain list of the tlds package, which only ships it as index.json */
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
|
+
exports.TLDS = require('tlds');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface FormatDateOptions {
|
|
2
|
+
/** BCP 47 language tag, defaults to 'en' */
|
|
3
|
+
locale?: string | undefined;
|
|
4
|
+
/** IANA time zone, defaults to the local time zone */
|
|
5
|
+
tz?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Formats the date of a quoted message for the reply/forward header, for example
|
|
9
|
+
* "Sat, Jun 15, 2024, 2:30 PM" in English.
|
|
10
|
+
*
|
|
11
|
+
* @param value Date to format
|
|
12
|
+
* @param options Locale and time zone
|
|
13
|
+
* @returns Formatted date, or the input as text when it is not a valid date
|
|
14
|
+
*/
|
|
15
|
+
export default function formatDate(value: Date | string | number, options?: FormatDateOptions | null): string;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
const DATE_FORMAT = {
|
|
4
2
|
weekday: 'short',
|
|
5
3
|
year: 'numeric',
|
|
@@ -8,7 +6,6 @@ const DATE_FORMAT = {
|
|
|
8
6
|
hour: 'numeric',
|
|
9
7
|
minute: '2-digit'
|
|
10
8
|
};
|
|
11
|
-
|
|
12
9
|
// An unsupported locale would silently fall back to the host's default locale, and a Node build
|
|
13
10
|
// with only English ICU data (pkg binaries use small-icu) renders other locales as broken
|
|
14
11
|
// placeholders such as "M09 28, Mon", so anything not actually supported becomes 'en'.
|
|
@@ -19,35 +16,33 @@ function resolveLocale(locale) {
|
|
|
19
16
|
if (supported.length) {
|
|
20
17
|
return supported[0];
|
|
21
18
|
}
|
|
22
|
-
}
|
|
19
|
+
}
|
|
20
|
+
catch (_err) {
|
|
23
21
|
// structurally invalid language tag
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
24
|
return 'en';
|
|
27
25
|
}
|
|
28
|
-
|
|
29
26
|
/**
|
|
30
27
|
* Formats the date of a quoted message for the reply/forward header, for example
|
|
31
28
|
* "Sat, Jun 15, 2024, 2:30 PM" in English.
|
|
32
29
|
*
|
|
33
|
-
* @param
|
|
34
|
-
* @param
|
|
35
|
-
* @
|
|
36
|
-
* @param {String} [options.tz] IANA time zone, defaults to the local time zone
|
|
37
|
-
* @returns {String} Formatted date, or the input as text when it is not a valid date
|
|
30
|
+
* @param value Date to format
|
|
31
|
+
* @param options Locale and time zone
|
|
32
|
+
* @returns Formatted date, or the input as text when it is not a valid date
|
|
38
33
|
*/
|
|
39
|
-
function formatDate(value, options) {
|
|
34
|
+
export default function formatDate(value, options) {
|
|
40
35
|
options = options || {};
|
|
41
36
|
const date = value instanceof Date ? value : new Date(value);
|
|
42
37
|
if (isNaN(date.getTime())) {
|
|
43
38
|
return String(value);
|
|
44
39
|
}
|
|
45
|
-
|
|
46
40
|
const locale = resolveLocale(options.locale);
|
|
47
41
|
let formatter;
|
|
48
42
|
try {
|
|
49
43
|
formatter = new Intl.DateTimeFormat(locale, Object.assign({ timeZone: options.tz || undefined }, DATE_FORMAT));
|
|
50
|
-
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
51
46
|
if (!(err instanceof RangeError)) {
|
|
52
47
|
throw err;
|
|
53
48
|
}
|
|
@@ -55,9 +50,6 @@ function formatDate(value, options) {
|
|
|
55
50
|
formatter = new Intl.DateTimeFormat(locale, Object.assign({ timeZone: 'UTC' }, DATE_FORMAT));
|
|
56
51
|
}
|
|
57
52
|
const formatted = formatter.format(date);
|
|
58
|
-
|
|
59
53
|
// ICU puts a narrow no-break space before "PM", which older mail clients show as a box
|
|
60
54
|
return formatted.replace(/[\u00a0\u202f]/g, ' ');
|
|
61
55
|
}
|
|
62
|
-
|
|
63
|
-
module.exports = formatDate;
|