@postalsys/email-text-tools 2.3.3 → 2.3.5

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/.ncurc.js CHANGED
@@ -2,6 +2,7 @@ module.exports = {
2
2
  upgrade: true,
3
3
  reject: [
4
4
  // Block package upgrades that moved to ESM
5
- 'nanoid'
5
+ 'nanoid',
6
+ 'jsdom' // parse5 dependency in 27.0.1 is ESM only
6
7
  ]
7
8
  };
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "2.3.3"
2
+ ".": "2.3.5"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.5](https://github.com/postalsys/email-text-tools/compare/email-text-tools-v2.3.4...email-text-tools-v2.3.5) (2025-10-29)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **htmlToText:** Improve HTML-to-text conversion for better newline preservation ([6606d5c](https://github.com/postalsys/email-text-tools/commit/6606d5c5b9d3b05f41d54c434f2b68760141dacb))
9
+
10
+ ## [2.3.4](https://github.com/postalsys/email-text-tools/compare/email-text-tools-v2.3.3...email-text-tools-v2.3.4) (2025-10-23)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * Downgraded jsdom to 27.0.0 ([a2969e9](https://github.com/postalsys/email-text-tools/commit/a2969e9399ed6c9d15a85d7d8c11342bc5bf00ce))
16
+
3
17
  ## [2.3.3](https://github.com/postalsys/email-text-tools/compare/email-text-tools-v2.3.2...email-text-tools-v2.3.3) (2025-10-23)
4
18
 
5
19
 
@@ -1,16 +1,54 @@
1
1
  'use strict';
2
2
  const { compile } = require('html-to-text');
3
3
 
4
- const htmlToText = compile({
4
+ // Post-process function to clean up excessive newlines
5
+ function cleanupNewlines(text) {
6
+ // Replace 3 or more consecutive newlines with just 2
7
+ // This handles cases where HTML has many nested empty divs
8
+ return text.replace(/\n{3,}/g, '\n\n');
9
+ }
10
+
11
+ const htmlToTextCompiled = compile({
5
12
  wordwrap: 160,
6
13
  selectors: [
14
+ // Block elements that should preserve line breaks
7
15
  { selector: 'p', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
16
+ // Divs get minimal breaks - many HTML emails wrap each line in a div
17
+ { selector: 'div', options: { leadingLineBreaks: 1, trailingLineBreaks: 0 } },
8
18
  { selector: 'pre', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
9
- { selector: 'table.class#id', format: 'dataTable' },
10
- // ignore images
19
+ { selector: 'blockquote', options: { leadingLineBreaks: 1, trailingLineBreaks: 1, trimEmptyLines: true } },
20
+
21
+ // Headers should have spacing
22
+ { selector: 'h1', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
23
+ { selector: 'h2', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
24
+ { selector: 'h3', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
25
+ { selector: 'h4', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
26
+ { selector: 'h5', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
27
+ { selector: 'h6', options: { leadingLineBreaks: 2, trailingLineBreaks: 1, uppercase: false } },
28
+
29
+ // Line breaks
30
+ { selector: 'br', options: { leadingLineBreaks: 1, trailingLineBreaks: 0 } },
31
+ { selector: 'hr', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
32
+
33
+ // Lists should preserve structure
34
+ { selector: 'ul', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
35
+ { selector: 'ol', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
36
+
37
+ // Tables
38
+ { selector: 'table', format: 'dataTable' },
39
+
40
+ // Ignore images
11
41
  { selector: 'img', format: 'skip' }
12
42
  ],
13
- hideLinkHrefIfSameAsText: true
43
+ hideLinkHrefIfSameAsText: true,
44
+ // Preserve whitespace better for content
45
+ preserveNewlines: true
14
46
  });
15
47
 
48
+ // Wrapper function that applies post-processing
49
+ function htmlToText(html) {
50
+ const text = htmlToTextCompiled(html);
51
+ return cleanupNewlines(text);
52
+ }
53
+
16
54
  module.exports = htmlToText;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postalsys/email-text-tools",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "description": "Email text tools",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -34,7 +34,7 @@
34
34
  "dompurify": "3.3.0",
35
35
  "he": "1.2.0",
36
36
  "html-to-text": "9.0.5",
37
- "jsdom": "27.0.1",
37
+ "jsdom": "27.0.0",
38
38
  "juice": "11.0.3",
39
39
  "linkify-it": "5.0.0",
40
40
  "moment-timezone": "0.6.0",
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ const { describe, it } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const htmlToText = require('../lib/html-to-text');
6
+
7
+ describe('html-to-text conversion', () => {
8
+ it('should preserve newlines from br tags', () => {
9
+ const html = 'Line 1<br>Line 2<br>Line 3';
10
+ const text = htmlToText(html);
11
+
12
+ assert.ok(text.includes('Line 1'));
13
+ assert.ok(text.includes('Line 2'));
14
+ assert.ok(text.includes('Line 3'));
15
+
16
+ // Check that lines are separated
17
+ const lines = text.trim().split('\n').filter(l => l.trim());
18
+ assert.equal(lines.length, 3);
19
+ });
20
+
21
+ it('should preserve newlines from div tags', () => {
22
+ const html = '<div>Line 1</div><div>Line 2</div><div>Line 3</div>';
23
+ const text = htmlToText(html);
24
+
25
+ assert.ok(text.includes('Line 1'));
26
+ assert.ok(text.includes('Line 2'));
27
+ assert.ok(text.includes('Line 3'));
28
+
29
+ // Check that lines are separated
30
+ const lines = text.trim().split('\n').filter(l => l.trim());
31
+ assert.equal(lines.length, 3);
32
+ });
33
+
34
+ it('should preserve newlines from paragraph tags', () => {
35
+ const html = '<p>Paragraph 1</p><p>Paragraph 2</p><p>Paragraph 3</p>';
36
+ const text = htmlToText(html);
37
+
38
+ assert.ok(text.includes('Paragraph 1'));
39
+ assert.ok(text.includes('Paragraph 2'));
40
+ assert.ok(text.includes('Paragraph 3'));
41
+
42
+ // Check that paragraphs are separated
43
+ const lines = text.trim().split('\n').filter(l => l.trim());
44
+ assert.equal(lines.length, 3);
45
+ });
46
+
47
+ it('should handle MS Graph style HTML with mixed formatting', () => {
48
+ // Typical MS Graph HTML structure
49
+ const html = `
50
+ <div>Hello there,</div>
51
+ <div><br></div>
52
+ <div>This is a test email with multiple lines.</div>
53
+ <div>Each line should be preserved.</div>
54
+ <div><br></div>
55
+ <div>Thanks,</div>
56
+ <div>User</div>
57
+ `;
58
+ const text = htmlToText(html);
59
+
60
+ assert.ok(text.includes('Hello there'));
61
+ assert.ok(text.includes('This is a test email'));
62
+ assert.ok(text.includes('Each line should be preserved'));
63
+ assert.ok(text.includes('Thanks'));
64
+ assert.ok(text.includes('User'));
65
+
66
+ // Verify structure is maintained
67
+ const lines = text.split('\n').map(l => l.trim()).filter(l => l);
68
+ assert.ok(lines.length >= 5, `Expected at least 5 lines, got ${lines.length}`);
69
+ });
70
+
71
+ it('should handle lists with proper line breaks', () => {
72
+ const html = '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>';
73
+ const text = htmlToText(html);
74
+
75
+ assert.ok(text.includes('Item 1'));
76
+ assert.ok(text.includes('Item 2'));
77
+ assert.ok(text.includes('Item 3'));
78
+
79
+ // Each list item should be on a new line
80
+ const lines = text.trim().split('\n').filter(l => l.trim());
81
+ assert.ok(lines.length >= 3);
82
+ });
83
+
84
+ it('should handle headers with appropriate spacing', () => {
85
+ const html = '<h1>Main Title</h1><p>Some content</p><h2>Subtitle</h2><p>More content</p>';
86
+ const text = htmlToText(html);
87
+
88
+ assert.ok(text.includes('Main Title'));
89
+ assert.ok(text.includes('Some content'));
90
+ assert.ok(text.includes('Subtitle'));
91
+ assert.ok(text.includes('More content'));
92
+
93
+ // Headers should create separation
94
+ const lines = text.trim().split('\n').filter(l => l.trim());
95
+ assert.ok(lines.length >= 4);
96
+ });
97
+
98
+ it('should handle blockquotes with line breaks', () => {
99
+ const html = '<p>Normal text</p><blockquote>Quoted text line 1<br>Quoted text line 2</blockquote><p>More normal text</p>';
100
+ const text = htmlToText(html);
101
+
102
+ assert.ok(text.includes('Normal text'));
103
+ assert.ok(text.includes('Quoted text line 1'));
104
+ assert.ok(text.includes('Quoted text line 2'));
105
+ assert.ok(text.includes('More normal text'));
106
+ });
107
+
108
+ it('should not create excessive blank lines', () => {
109
+ const html = '<p>Text 1</p><p>Text 2</p><p>Text 3</p>';
110
+ const text = htmlToText(html);
111
+
112
+ // Should not have more than 3 consecutive newlines
113
+ assert.ok(!text.includes('\n\n\n\n'), 'Should not have 4 or more consecutive newlines');
114
+ });
115
+
116
+ it('should handle tables with line breaks between rows', () => {
117
+ const html = '<table><tr><td>Cell 1</td><td>Cell 2</td></tr><tr><td>Cell 3</td><td>Cell 4</td></tr></table>';
118
+ const text = htmlToText(html);
119
+
120
+ assert.ok(text.includes('Cell 1'));
121
+ assert.ok(text.includes('Cell 2'));
122
+ assert.ok(text.includes('Cell 3'));
123
+ assert.ok(text.includes('Cell 4'));
124
+
125
+ // Rows should be separated
126
+ const lines = text.trim().split('\n').filter(l => l.trim());
127
+ assert.ok(lines.length >= 2);
128
+ });
129
+
130
+ it('should handle empty elements gracefully', () => {
131
+ const html = '<div>Text</div><div></div><div>More text</div>';
132
+ const text = htmlToText(html);
133
+
134
+ assert.ok(text.includes('Text'));
135
+ assert.ok(text.includes('More text'));
136
+ });
137
+
138
+ it('should preserve newlines in pre tags', () => {
139
+ const html = '<pre>Line 1\nLine 2\nLine 3</pre>';
140
+ const text = htmlToText(html);
141
+
142
+ assert.ok(text.includes('Line 1'));
143
+ assert.ok(text.includes('Line 2'));
144
+ assert.ok(text.includes('Line 3'));
145
+ });
146
+
147
+ it('should handle complex nested structure', () => {
148
+ const html = `
149
+ <div>
150
+ <p>First paragraph</p>
151
+ <div>
152
+ <div>Nested content</div>
153
+ </div>
154
+ <p>Last paragraph</p>
155
+ </div>
156
+ `;
157
+ const text = htmlToText(html);
158
+
159
+ assert.ok(text.includes('First paragraph'));
160
+ assert.ok(text.includes('Nested content'));
161
+ assert.ok(text.includes('Last paragraph'));
162
+
163
+ const lines = text.trim().split('\n').filter(l => l.trim());
164
+ assert.equal(lines.length, 3);
165
+ });
166
+
167
+ it('should handle real-world MS Graph HTML example', () => {
168
+ // Real example of how MS Graph might return HTML
169
+ const html = `
170
+ <html>
171
+ <head></head>
172
+ <body>
173
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
174
+ Hi Team,</div>
175
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
176
+ <br>
177
+ </div>
178
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
179
+ I wanted to update you on the following:</div>
180
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
181
+ <br>
182
+ </div>
183
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
184
+ - Item 1</div>
185
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
186
+ - Item 2</div>
187
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
188
+ - Item 3</div>
189
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
190
+ <br>
191
+ </div>
192
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
193
+ Best regards,</div>
194
+ <div style="font-family:Calibri,Arial,Helvetica,sans-serif; font-size:12pt; color:rgb(0,0,0)">
195
+ John</div>
196
+ </body>
197
+ </html>
198
+ `;
199
+ const text = htmlToText(html);
200
+
201
+ // Check all content is present
202
+ assert.ok(text.includes('Hi Team'));
203
+ assert.ok(text.includes('I wanted to update you'));
204
+ assert.ok(text.includes('- Item 1'));
205
+ assert.ok(text.includes('- Item 2'));
206
+ assert.ok(text.includes('- Item 3'));
207
+ assert.ok(text.includes('Best regards'));
208
+ assert.ok(text.includes('John'));
209
+
210
+ // Check structure is preserved
211
+ const lines = text.trim().split('\n').filter(l => l.trim());
212
+ assert.ok(lines.length >= 7, `Expected at least 7 content lines, got ${lines.length}`);
213
+
214
+ // Items should be on separate lines
215
+ const itemIndex1 = text.indexOf('- Item 1');
216
+ const itemIndex2 = text.indexOf('- Item 2');
217
+ const itemIndex3 = text.indexOf('- Item 3');
218
+
219
+ assert.ok(itemIndex2 > itemIndex1);
220
+ assert.ok(itemIndex3 > itemIndex2);
221
+
222
+ // There should be newlines between items
223
+ assert.ok(text.substring(itemIndex1, itemIndex2).includes('\n'));
224
+ assert.ok(text.substring(itemIndex2, itemIndex3).includes('\n'));
225
+ });
226
+ });