@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +7 -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 +67 -10
  50. package/.github/VULNERABILITY_REPORT.yml +0 -75
  51. package/.github/workflows/release.yaml +0 -38
  52. package/.github/workflows/test.yaml +0 -35
  53. package/.ncurc.js +0 -28
  54. package/.prettierignore +0 -14
  55. package/.prettierrc.js +0 -10
  56. package/.release-please-manifest.json +0 -3
  57. package/CLAUDE.md +0 -105
  58. package/benchmark/benchmark.js +0 -348
  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 -257
  63. package/docs/PERFORMANCE.md +0 -262
  64. package/eslint.config.js +0 -71
  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
@@ -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,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -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,4 @@
1
+ /** The worker script that runs juice off the main thread, the one of the same build */
2
+ export declare const JUICE_WORKER_PATH: string;
3
+ /** The top level domain list of the tlds package, which only ships it as index.json */
4
+ export declare const TLDS: string[];
@@ -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
- } catch (_err) {
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 {Date|String|Number} value Date to format
34
- * @param {Object} [options]
35
- * @param {String} [options.locale] BCP 47 language tag, defaults to 'en'
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
- } catch (err) {
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;
@@ -0,0 +1,3 @@
1
+ export declare const MAX_NESTING_DEPTH = 1000;
2
+ export declare function exceedsNestingDepth(html: string, maxDepth: number): boolean;
3
+ export declare function fallbackText(html: string): string;