@matthesketh/utopia-email 0.3.1 → 0.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/dist/index.cjs +84 -45
- package/dist/index.js +84 -45
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -40,12 +40,31 @@ module.exports = __toCommonJS(index_exports);
|
|
|
40
40
|
var import_utopia_server = require("@matthesketh/utopia-server");
|
|
41
41
|
|
|
42
42
|
// src/css-inliner.ts
|
|
43
|
+
var CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
44
|
+
var WHITESPACE_CHAR_RE = /\s/;
|
|
45
|
+
var CHILD_COMBINATOR_RE = /\s*>\s*/g;
|
|
46
|
+
var ID_SELECTOR_RE = /#[a-zA-Z_-][\w-]*/g;
|
|
47
|
+
var CLASS_SELECTOR_RE = /\.[a-zA-Z_-][\w-]*/g;
|
|
48
|
+
var ATTR_SELECTOR_RE = /\[[^\]]+\]/g;
|
|
49
|
+
var PSEUDO_CLASS_RE = /:[\w-]+(\([^)]*\))?/g;
|
|
50
|
+
var COMBINATOR_SPLIT_RE = /[\s>+~]+/;
|
|
51
|
+
var LEADING_TAG_RE = /^([a-zA-Z][\w-]*)/;
|
|
52
|
+
var LEADING_ID_RE = /^#([a-zA-Z_-][\w-]*)/;
|
|
53
|
+
var LEADING_CLASS_RE = /^\.([a-zA-Z_-][\w-]*)/;
|
|
54
|
+
var LEADING_ATTR_RE = /^\[([^\]]+)\]/;
|
|
55
|
+
var QUOTE_WRAP_RE = /^["']|["']$/g;
|
|
56
|
+
var LEADING_PSEUDO_RE = /^:[\w-]+(\([^)]*\))?/;
|
|
57
|
+
var WHITESPACE_RUN_RE = /\s+/;
|
|
58
|
+
var ALL_TAGS_RE = /<\/?([a-zA-Z][\w-]*)(\s[^>]*?)?\s*\/?>/g;
|
|
59
|
+
var ATTR_PARSE_RE = /([a-zA-Z_:][\w:.-]*)\s*(?:=\s*"([^"]*)")?/g;
|
|
60
|
+
var STYLE_ATTR_RE = /style="[^"]*"/;
|
|
61
|
+
var ATTR_OPERATOR_SUFFIX_RE = /[~|^$*]$/;
|
|
43
62
|
function parseCSS(css) {
|
|
44
63
|
const rules = [];
|
|
45
|
-
const cleaned = css.replace(
|
|
64
|
+
const cleaned = css.replace(CSS_COMMENT_RE, "");
|
|
46
65
|
let i = 0;
|
|
47
66
|
while (i < cleaned.length) {
|
|
48
|
-
while (i < cleaned.length &&
|
|
67
|
+
while (i < cleaned.length && WHITESPACE_CHAR_RE.test(cleaned[i])) i++;
|
|
49
68
|
if (i >= cleaned.length) break;
|
|
50
69
|
if (cleaned[i] === "@") {
|
|
51
70
|
let depth = 0;
|
|
@@ -86,16 +105,16 @@ function calculateSpecificity(selector) {
|
|
|
86
105
|
let ids = 0;
|
|
87
106
|
let classes = 0;
|
|
88
107
|
let types = 0;
|
|
89
|
-
const parts = selector.replace(
|
|
90
|
-
const idMatches = parts.match(
|
|
108
|
+
const parts = selector.replace(CHILD_COMBINATOR_RE, " ").trim();
|
|
109
|
+
const idMatches = parts.match(ID_SELECTOR_RE);
|
|
91
110
|
if (idMatches) ids = idMatches.length;
|
|
92
|
-
const classMatches = parts.match(
|
|
111
|
+
const classMatches = parts.match(CLASS_SELECTOR_RE);
|
|
93
112
|
if (classMatches) classes += classMatches.length;
|
|
94
|
-
const attrMatches = parts.match(
|
|
113
|
+
const attrMatches = parts.match(ATTR_SELECTOR_RE);
|
|
95
114
|
if (attrMatches) classes += attrMatches.length;
|
|
96
|
-
const segments = parts.split(
|
|
115
|
+
const segments = parts.split(COMBINATOR_SPLIT_RE);
|
|
97
116
|
for (const seg of segments) {
|
|
98
|
-
const stripped = seg.replace(
|
|
117
|
+
const stripped = seg.replace(ID_SELECTOR_RE, "").replace(CLASS_SELECTOR_RE, "").replace(ATTR_SELECTOR_RE, "").replace(PSEUDO_CLASS_RE, "").trim();
|
|
99
118
|
if (stripped && stripped !== "*") {
|
|
100
119
|
types++;
|
|
101
120
|
}
|
|
@@ -109,37 +128,37 @@ function compareSpecificity(a, b) {
|
|
|
109
128
|
}
|
|
110
129
|
function matchesSimpleSelector(selector, tag, classes, id, attrs) {
|
|
111
130
|
let remaining = selector;
|
|
112
|
-
const tagMatch = remaining.match(
|
|
131
|
+
const tagMatch = remaining.match(LEADING_TAG_RE);
|
|
113
132
|
if (tagMatch) {
|
|
114
133
|
if (tag.toLowerCase() !== tagMatch[1].toLowerCase()) return false;
|
|
115
134
|
remaining = remaining.slice(tagMatch[1].length);
|
|
116
135
|
}
|
|
117
136
|
while (remaining.length > 0) {
|
|
118
137
|
if (remaining[0] === "#") {
|
|
119
|
-
const idMatch = remaining.match(
|
|
138
|
+
const idMatch = remaining.match(LEADING_ID_RE);
|
|
120
139
|
if (!idMatch) return false;
|
|
121
140
|
if (id !== idMatch[1]) return false;
|
|
122
141
|
remaining = remaining.slice(idMatch[0].length);
|
|
123
142
|
} else if (remaining[0] === ".") {
|
|
124
|
-
const classMatch = remaining.match(
|
|
143
|
+
const classMatch = remaining.match(LEADING_CLASS_RE);
|
|
125
144
|
if (!classMatch) return false;
|
|
126
145
|
if (!classes.includes(classMatch[1])) return false;
|
|
127
146
|
remaining = remaining.slice(classMatch[0].length);
|
|
128
147
|
} else if (remaining[0] === "[") {
|
|
129
|
-
const attrMatch = remaining.match(
|
|
148
|
+
const attrMatch = remaining.match(LEADING_ATTR_RE);
|
|
130
149
|
if (!attrMatch) return false;
|
|
131
150
|
const attrExpr = attrMatch[1];
|
|
132
151
|
const eqIdx = attrExpr.indexOf("=");
|
|
133
152
|
if (eqIdx === -1) {
|
|
134
153
|
if (!(attrExpr.trim() in attrs)) return false;
|
|
135
154
|
} else {
|
|
136
|
-
const attrName = attrExpr.slice(0, eqIdx).replace(
|
|
137
|
-
const attrValue = attrExpr.slice(eqIdx + 1).replace(
|
|
155
|
+
const attrName = attrExpr.slice(0, eqIdx).replace(ATTR_OPERATOR_SUFFIX_RE, "").trim();
|
|
156
|
+
const attrValue = attrExpr.slice(eqIdx + 1).replace(QUOTE_WRAP_RE, "").trim();
|
|
138
157
|
if (attrs[attrName] !== attrValue) return false;
|
|
139
158
|
}
|
|
140
159
|
remaining = remaining.slice(attrMatch[0].length);
|
|
141
160
|
} else if (remaining[0] === ":") {
|
|
142
|
-
const pseudoMatch = remaining.match(
|
|
161
|
+
const pseudoMatch = remaining.match(LEADING_PSEUDO_RE);
|
|
143
162
|
if (!pseudoMatch) return false;
|
|
144
163
|
remaining = remaining.slice(pseudoMatch[0].length);
|
|
145
164
|
} else if (remaining[0] === "*") {
|
|
@@ -152,7 +171,7 @@ function matchesSimpleSelector(selector, tag, classes, id, attrs) {
|
|
|
152
171
|
}
|
|
153
172
|
function selectorMatches(selector, element) {
|
|
154
173
|
if (selector.includes(">")) {
|
|
155
|
-
const parts2 = selector.split(
|
|
174
|
+
const parts2 = selector.split(CHILD_COMBINATOR_RE);
|
|
156
175
|
const targetSelector2 = parts2[parts2.length - 1].trim();
|
|
157
176
|
if (!matchesSimpleSelector(
|
|
158
177
|
targetSelector2,
|
|
@@ -175,7 +194,7 @@ function selectorMatches(selector, element) {
|
|
|
175
194
|
}
|
|
176
195
|
return true;
|
|
177
196
|
}
|
|
178
|
-
const parts = selector.split(
|
|
197
|
+
const parts = selector.split(WHITESPACE_RUN_RE);
|
|
179
198
|
if (parts.length === 1) {
|
|
180
199
|
return matchesSimpleSelector(parts[0], element.tag, element.classes, element.id, element.attrs);
|
|
181
200
|
}
|
|
@@ -248,10 +267,9 @@ function inlineCSS(html, css) {
|
|
|
248
267
|
if (!css.trim()) return html;
|
|
249
268
|
const rules = parseCSS(css);
|
|
250
269
|
if (rules.length === 0) return html;
|
|
251
|
-
const tagRegex = /<([a-zA-Z][\w-]*)(\s[^>]*?)?\s*\/?>/g;
|
|
252
270
|
const elements = [];
|
|
253
271
|
const ancestorStack = [];
|
|
254
|
-
|
|
272
|
+
ALL_TAGS_RE.lastIndex = 0;
|
|
255
273
|
const voidElements = /* @__PURE__ */ new Set([
|
|
256
274
|
"area",
|
|
257
275
|
"base",
|
|
@@ -269,7 +287,7 @@ function inlineCSS(html, css) {
|
|
|
269
287
|
"wbr"
|
|
270
288
|
]);
|
|
271
289
|
let match;
|
|
272
|
-
while ((match =
|
|
290
|
+
while ((match = ALL_TAGS_RE.exec(html)) !== null) {
|
|
273
291
|
const fullTag = match[0];
|
|
274
292
|
const isClosing = fullTag[1] === "/";
|
|
275
293
|
const tagName = match[1].toLowerCase();
|
|
@@ -284,12 +302,12 @@ function inlineCSS(html, css) {
|
|
|
284
302
|
continue;
|
|
285
303
|
}
|
|
286
304
|
const attrs = {};
|
|
287
|
-
|
|
305
|
+
ATTR_PARSE_RE.lastIndex = 0;
|
|
288
306
|
let attrMatch;
|
|
289
|
-
while ((attrMatch =
|
|
307
|
+
while ((attrMatch = ATTR_PARSE_RE.exec(attrsStr)) !== null) {
|
|
290
308
|
attrs[attrMatch[1]] = attrMatch[2] ?? "";
|
|
291
309
|
}
|
|
292
|
-
const classes = (attrs["class"] || "").split(
|
|
310
|
+
const classes = (attrs["class"] || "").split(WHITESPACE_RUN_RE).filter(Boolean);
|
|
293
311
|
const id = attrs["id"] || "";
|
|
294
312
|
const existingStyle = attrs["style"] || "";
|
|
295
313
|
const element = {
|
|
@@ -335,7 +353,7 @@ function inlineCSS(html, css) {
|
|
|
335
353
|
const originalTag = element.fullTag;
|
|
336
354
|
let newTag;
|
|
337
355
|
if (element.existingStyle) {
|
|
338
|
-
newTag = originalTag.replace(
|
|
356
|
+
newTag = originalTag.replace(STYLE_ATTR_RE, `style="${mergedStyle}"`);
|
|
339
357
|
} else {
|
|
340
358
|
const insertPos = originalTag.endsWith("/>") ? originalTag.length - 2 : originalTag.length - 1;
|
|
341
359
|
newTag = originalTag.slice(0, insertPos) + ` style="${mergedStyle}"` + originalTag.slice(insertPos);
|
|
@@ -346,6 +364,23 @@ function inlineCSS(html, css) {
|
|
|
346
364
|
}
|
|
347
365
|
|
|
348
366
|
// src/html-to-text.ts
|
|
367
|
+
var STYLE_BLOCK_RE = /<style[^>]*>[\s\S]*?<\/style>/gi;
|
|
368
|
+
var HEAD_BLOCK_RE = /<head[^>]*>[\s\S]*?<\/head>/gi;
|
|
369
|
+
var HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
370
|
+
var ANCHOR_TAG_RE = /<a\s[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
371
|
+
var HTML_TAG_RE = /<[^>]+>/g;
|
|
372
|
+
var HEADING_TAG_RE = /<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi;
|
|
373
|
+
var BR_TAG_RE = /<br\s*\/?>/gi;
|
|
374
|
+
var HR_TAG_RE = /<hr\s*\/?>/gi;
|
|
375
|
+
var LIST_ITEM_RE = /<li[^>]*>([\s\S]*?)<\/li>/gi;
|
|
376
|
+
var BLOCK_CLOSE_TAG_RE = /<\/(p|div|tr|table|blockquote)>/gi;
|
|
377
|
+
var TABLE_CELL_CLOSE_TAG_RE = /<\/(td|th)>/gi;
|
|
378
|
+
var HTML_ENTITY_RE = /&[a-zA-Z0-9#]+;/g;
|
|
379
|
+
var NUMERIC_ENTITY_RE = /^&#(\d+);$/;
|
|
380
|
+
var HEX_ENTITY_RE = /^&#x([a-fA-F0-9]+);$/;
|
|
381
|
+
var TAB_CHAR_RE = /\t/g;
|
|
382
|
+
var NON_NEWLINE_WHITESPACE_RE = /[^\S\n]+/g;
|
|
383
|
+
var EXCESSIVE_NEWLINES_RE = /\n{3,}/g;
|
|
349
384
|
var ENTITY_MAP = {
|
|
350
385
|
"&": "&",
|
|
351
386
|
"<": "<",
|
|
@@ -364,45 +399,45 @@ var ENTITY_MAP = {
|
|
|
364
399
|
};
|
|
365
400
|
function htmlToText(html) {
|
|
366
401
|
let text = html;
|
|
367
|
-
text = text.replace(
|
|
368
|
-
text = text.replace(
|
|
369
|
-
text = text.replace(
|
|
370
|
-
text = text.replace(
|
|
371
|
-
const linkText = content.replace(
|
|
402
|
+
text = text.replace(STYLE_BLOCK_RE, "");
|
|
403
|
+
text = text.replace(HEAD_BLOCK_RE, "");
|
|
404
|
+
text = text.replace(HTML_COMMENT_RE, "");
|
|
405
|
+
text = text.replace(ANCHOR_TAG_RE, (_, href, content) => {
|
|
406
|
+
const linkText = content.replace(HTML_TAG_RE, "").trim();
|
|
372
407
|
if (linkText && href && linkText !== href) {
|
|
373
408
|
return `${linkText} (${href})`;
|
|
374
409
|
}
|
|
375
410
|
return linkText || href;
|
|
376
411
|
});
|
|
377
|
-
text = text.replace(
|
|
378
|
-
const headingText = content.replace(
|
|
412
|
+
text = text.replace(HEADING_TAG_RE, (_, content) => {
|
|
413
|
+
const headingText = content.replace(HTML_TAG_RE, "").trim();
|
|
379
414
|
return `
|
|
380
415
|
|
|
381
416
|
${headingText.toUpperCase()}
|
|
382
417
|
|
|
383
418
|
`;
|
|
384
419
|
});
|
|
385
|
-
text = text.replace(
|
|
386
|
-
text = text.replace(
|
|
387
|
-
text = text.replace(
|
|
388
|
-
const itemText = content.replace(
|
|
420
|
+
text = text.replace(BR_TAG_RE, "\n");
|
|
421
|
+
text = text.replace(HR_TAG_RE, "\n---\n");
|
|
422
|
+
text = text.replace(LIST_ITEM_RE, (_, content) => {
|
|
423
|
+
const itemText = content.replace(HTML_TAG_RE, "").trim();
|
|
389
424
|
return `
|
|
390
425
|
- ${itemText}`;
|
|
391
426
|
});
|
|
392
|
-
text = text.replace(
|
|
393
|
-
text = text.replace(
|
|
394
|
-
text = text.replace(
|
|
395
|
-
text = text.replace(
|
|
427
|
+
text = text.replace(BLOCK_CLOSE_TAG_RE, "\n\n");
|
|
428
|
+
text = text.replace(TABLE_CELL_CLOSE_TAG_RE, " ");
|
|
429
|
+
text = text.replace(HTML_TAG_RE, "");
|
|
430
|
+
text = text.replace(HTML_ENTITY_RE, (entity) => {
|
|
396
431
|
if (ENTITY_MAP[entity]) return ENTITY_MAP[entity];
|
|
397
|
-
const numMatch = entity.match(
|
|
432
|
+
const numMatch = entity.match(NUMERIC_ENTITY_RE);
|
|
398
433
|
if (numMatch) return String.fromCharCode(parseInt(numMatch[1], 10));
|
|
399
|
-
const hexMatch = entity.match(
|
|
434
|
+
const hexMatch = entity.match(HEX_ENTITY_RE);
|
|
400
435
|
if (hexMatch) return String.fromCharCode(parseInt(hexMatch[1], 16));
|
|
401
436
|
return entity;
|
|
402
437
|
});
|
|
403
|
-
text = text.replace(
|
|
404
|
-
text = text.replace(
|
|
405
|
-
text = text.replace(
|
|
438
|
+
text = text.replace(TAB_CHAR_RE, " ");
|
|
439
|
+
text = text.replace(NON_NEWLINE_WHITESPACE_RE, " ");
|
|
440
|
+
text = text.replace(EXCESSIVE_NEWLINES_RE, "\n\n");
|
|
406
441
|
text = text.split("\n").map((line) => line.trim()).join("\n");
|
|
407
442
|
text = text.trim();
|
|
408
443
|
return text;
|
|
@@ -448,8 +483,12 @@ ${css}
|
|
|
448
483
|
</body>
|
|
449
484
|
</html>`;
|
|
450
485
|
}
|
|
486
|
+
var AMPERSAND_RE = /&/g;
|
|
487
|
+
var LESS_THAN_RE = /</g;
|
|
488
|
+
var GREATER_THAN_RE = />/g;
|
|
489
|
+
var DOUBLE_QUOTE_RE = /"/g;
|
|
451
490
|
function escapeHtml(str) {
|
|
452
|
-
return str.replace(
|
|
491
|
+
return str.replace(AMPERSAND_RE, "&").replace(LESS_THAN_RE, "<").replace(GREATER_THAN_RE, ">").replace(DOUBLE_QUOTE_RE, """);
|
|
453
492
|
}
|
|
454
493
|
|
|
455
494
|
// src/render-email.ts
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,31 @@
|
|
|
2
2
|
import { renderToString } from "@matthesketh/utopia-server";
|
|
3
3
|
|
|
4
4
|
// src/css-inliner.ts
|
|
5
|
+
var CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
6
|
+
var WHITESPACE_CHAR_RE = /\s/;
|
|
7
|
+
var CHILD_COMBINATOR_RE = /\s*>\s*/g;
|
|
8
|
+
var ID_SELECTOR_RE = /#[a-zA-Z_-][\w-]*/g;
|
|
9
|
+
var CLASS_SELECTOR_RE = /\.[a-zA-Z_-][\w-]*/g;
|
|
10
|
+
var ATTR_SELECTOR_RE = /\[[^\]]+\]/g;
|
|
11
|
+
var PSEUDO_CLASS_RE = /:[\w-]+(\([^)]*\))?/g;
|
|
12
|
+
var COMBINATOR_SPLIT_RE = /[\s>+~]+/;
|
|
13
|
+
var LEADING_TAG_RE = /^([a-zA-Z][\w-]*)/;
|
|
14
|
+
var LEADING_ID_RE = /^#([a-zA-Z_-][\w-]*)/;
|
|
15
|
+
var LEADING_CLASS_RE = /^\.([a-zA-Z_-][\w-]*)/;
|
|
16
|
+
var LEADING_ATTR_RE = /^\[([^\]]+)\]/;
|
|
17
|
+
var QUOTE_WRAP_RE = /^["']|["']$/g;
|
|
18
|
+
var LEADING_PSEUDO_RE = /^:[\w-]+(\([^)]*\))?/;
|
|
19
|
+
var WHITESPACE_RUN_RE = /\s+/;
|
|
20
|
+
var ALL_TAGS_RE = /<\/?([a-zA-Z][\w-]*)(\s[^>]*?)?\s*\/?>/g;
|
|
21
|
+
var ATTR_PARSE_RE = /([a-zA-Z_:][\w:.-]*)\s*(?:=\s*"([^"]*)")?/g;
|
|
22
|
+
var STYLE_ATTR_RE = /style="[^"]*"/;
|
|
23
|
+
var ATTR_OPERATOR_SUFFIX_RE = /[~|^$*]$/;
|
|
5
24
|
function parseCSS(css) {
|
|
6
25
|
const rules = [];
|
|
7
|
-
const cleaned = css.replace(
|
|
26
|
+
const cleaned = css.replace(CSS_COMMENT_RE, "");
|
|
8
27
|
let i = 0;
|
|
9
28
|
while (i < cleaned.length) {
|
|
10
|
-
while (i < cleaned.length &&
|
|
29
|
+
while (i < cleaned.length && WHITESPACE_CHAR_RE.test(cleaned[i])) i++;
|
|
11
30
|
if (i >= cleaned.length) break;
|
|
12
31
|
if (cleaned[i] === "@") {
|
|
13
32
|
let depth = 0;
|
|
@@ -48,16 +67,16 @@ function calculateSpecificity(selector) {
|
|
|
48
67
|
let ids = 0;
|
|
49
68
|
let classes = 0;
|
|
50
69
|
let types = 0;
|
|
51
|
-
const parts = selector.replace(
|
|
52
|
-
const idMatches = parts.match(
|
|
70
|
+
const parts = selector.replace(CHILD_COMBINATOR_RE, " ").trim();
|
|
71
|
+
const idMatches = parts.match(ID_SELECTOR_RE);
|
|
53
72
|
if (idMatches) ids = idMatches.length;
|
|
54
|
-
const classMatches = parts.match(
|
|
73
|
+
const classMatches = parts.match(CLASS_SELECTOR_RE);
|
|
55
74
|
if (classMatches) classes += classMatches.length;
|
|
56
|
-
const attrMatches = parts.match(
|
|
75
|
+
const attrMatches = parts.match(ATTR_SELECTOR_RE);
|
|
57
76
|
if (attrMatches) classes += attrMatches.length;
|
|
58
|
-
const segments = parts.split(
|
|
77
|
+
const segments = parts.split(COMBINATOR_SPLIT_RE);
|
|
59
78
|
for (const seg of segments) {
|
|
60
|
-
const stripped = seg.replace(
|
|
79
|
+
const stripped = seg.replace(ID_SELECTOR_RE, "").replace(CLASS_SELECTOR_RE, "").replace(ATTR_SELECTOR_RE, "").replace(PSEUDO_CLASS_RE, "").trim();
|
|
61
80
|
if (stripped && stripped !== "*") {
|
|
62
81
|
types++;
|
|
63
82
|
}
|
|
@@ -71,37 +90,37 @@ function compareSpecificity(a, b) {
|
|
|
71
90
|
}
|
|
72
91
|
function matchesSimpleSelector(selector, tag, classes, id, attrs) {
|
|
73
92
|
let remaining = selector;
|
|
74
|
-
const tagMatch = remaining.match(
|
|
93
|
+
const tagMatch = remaining.match(LEADING_TAG_RE);
|
|
75
94
|
if (tagMatch) {
|
|
76
95
|
if (tag.toLowerCase() !== tagMatch[1].toLowerCase()) return false;
|
|
77
96
|
remaining = remaining.slice(tagMatch[1].length);
|
|
78
97
|
}
|
|
79
98
|
while (remaining.length > 0) {
|
|
80
99
|
if (remaining[0] === "#") {
|
|
81
|
-
const idMatch = remaining.match(
|
|
100
|
+
const idMatch = remaining.match(LEADING_ID_RE);
|
|
82
101
|
if (!idMatch) return false;
|
|
83
102
|
if (id !== idMatch[1]) return false;
|
|
84
103
|
remaining = remaining.slice(idMatch[0].length);
|
|
85
104
|
} else if (remaining[0] === ".") {
|
|
86
|
-
const classMatch = remaining.match(
|
|
105
|
+
const classMatch = remaining.match(LEADING_CLASS_RE);
|
|
87
106
|
if (!classMatch) return false;
|
|
88
107
|
if (!classes.includes(classMatch[1])) return false;
|
|
89
108
|
remaining = remaining.slice(classMatch[0].length);
|
|
90
109
|
} else if (remaining[0] === "[") {
|
|
91
|
-
const attrMatch = remaining.match(
|
|
110
|
+
const attrMatch = remaining.match(LEADING_ATTR_RE);
|
|
92
111
|
if (!attrMatch) return false;
|
|
93
112
|
const attrExpr = attrMatch[1];
|
|
94
113
|
const eqIdx = attrExpr.indexOf("=");
|
|
95
114
|
if (eqIdx === -1) {
|
|
96
115
|
if (!(attrExpr.trim() in attrs)) return false;
|
|
97
116
|
} else {
|
|
98
|
-
const attrName = attrExpr.slice(0, eqIdx).replace(
|
|
99
|
-
const attrValue = attrExpr.slice(eqIdx + 1).replace(
|
|
117
|
+
const attrName = attrExpr.slice(0, eqIdx).replace(ATTR_OPERATOR_SUFFIX_RE, "").trim();
|
|
118
|
+
const attrValue = attrExpr.slice(eqIdx + 1).replace(QUOTE_WRAP_RE, "").trim();
|
|
100
119
|
if (attrs[attrName] !== attrValue) return false;
|
|
101
120
|
}
|
|
102
121
|
remaining = remaining.slice(attrMatch[0].length);
|
|
103
122
|
} else if (remaining[0] === ":") {
|
|
104
|
-
const pseudoMatch = remaining.match(
|
|
123
|
+
const pseudoMatch = remaining.match(LEADING_PSEUDO_RE);
|
|
105
124
|
if (!pseudoMatch) return false;
|
|
106
125
|
remaining = remaining.slice(pseudoMatch[0].length);
|
|
107
126
|
} else if (remaining[0] === "*") {
|
|
@@ -114,7 +133,7 @@ function matchesSimpleSelector(selector, tag, classes, id, attrs) {
|
|
|
114
133
|
}
|
|
115
134
|
function selectorMatches(selector, element) {
|
|
116
135
|
if (selector.includes(">")) {
|
|
117
|
-
const parts2 = selector.split(
|
|
136
|
+
const parts2 = selector.split(CHILD_COMBINATOR_RE);
|
|
118
137
|
const targetSelector2 = parts2[parts2.length - 1].trim();
|
|
119
138
|
if (!matchesSimpleSelector(
|
|
120
139
|
targetSelector2,
|
|
@@ -137,7 +156,7 @@ function selectorMatches(selector, element) {
|
|
|
137
156
|
}
|
|
138
157
|
return true;
|
|
139
158
|
}
|
|
140
|
-
const parts = selector.split(
|
|
159
|
+
const parts = selector.split(WHITESPACE_RUN_RE);
|
|
141
160
|
if (parts.length === 1) {
|
|
142
161
|
return matchesSimpleSelector(parts[0], element.tag, element.classes, element.id, element.attrs);
|
|
143
162
|
}
|
|
@@ -210,10 +229,9 @@ function inlineCSS(html, css) {
|
|
|
210
229
|
if (!css.trim()) return html;
|
|
211
230
|
const rules = parseCSS(css);
|
|
212
231
|
if (rules.length === 0) return html;
|
|
213
|
-
const tagRegex = /<([a-zA-Z][\w-]*)(\s[^>]*?)?\s*\/?>/g;
|
|
214
232
|
const elements = [];
|
|
215
233
|
const ancestorStack = [];
|
|
216
|
-
|
|
234
|
+
ALL_TAGS_RE.lastIndex = 0;
|
|
217
235
|
const voidElements = /* @__PURE__ */ new Set([
|
|
218
236
|
"area",
|
|
219
237
|
"base",
|
|
@@ -231,7 +249,7 @@ function inlineCSS(html, css) {
|
|
|
231
249
|
"wbr"
|
|
232
250
|
]);
|
|
233
251
|
let match;
|
|
234
|
-
while ((match =
|
|
252
|
+
while ((match = ALL_TAGS_RE.exec(html)) !== null) {
|
|
235
253
|
const fullTag = match[0];
|
|
236
254
|
const isClosing = fullTag[1] === "/";
|
|
237
255
|
const tagName = match[1].toLowerCase();
|
|
@@ -246,12 +264,12 @@ function inlineCSS(html, css) {
|
|
|
246
264
|
continue;
|
|
247
265
|
}
|
|
248
266
|
const attrs = {};
|
|
249
|
-
|
|
267
|
+
ATTR_PARSE_RE.lastIndex = 0;
|
|
250
268
|
let attrMatch;
|
|
251
|
-
while ((attrMatch =
|
|
269
|
+
while ((attrMatch = ATTR_PARSE_RE.exec(attrsStr)) !== null) {
|
|
252
270
|
attrs[attrMatch[1]] = attrMatch[2] ?? "";
|
|
253
271
|
}
|
|
254
|
-
const classes = (attrs["class"] || "").split(
|
|
272
|
+
const classes = (attrs["class"] || "").split(WHITESPACE_RUN_RE).filter(Boolean);
|
|
255
273
|
const id = attrs["id"] || "";
|
|
256
274
|
const existingStyle = attrs["style"] || "";
|
|
257
275
|
const element = {
|
|
@@ -297,7 +315,7 @@ function inlineCSS(html, css) {
|
|
|
297
315
|
const originalTag = element.fullTag;
|
|
298
316
|
let newTag;
|
|
299
317
|
if (element.existingStyle) {
|
|
300
|
-
newTag = originalTag.replace(
|
|
318
|
+
newTag = originalTag.replace(STYLE_ATTR_RE, `style="${mergedStyle}"`);
|
|
301
319
|
} else {
|
|
302
320
|
const insertPos = originalTag.endsWith("/>") ? originalTag.length - 2 : originalTag.length - 1;
|
|
303
321
|
newTag = originalTag.slice(0, insertPos) + ` style="${mergedStyle}"` + originalTag.slice(insertPos);
|
|
@@ -308,6 +326,23 @@ function inlineCSS(html, css) {
|
|
|
308
326
|
}
|
|
309
327
|
|
|
310
328
|
// src/html-to-text.ts
|
|
329
|
+
var STYLE_BLOCK_RE = /<style[^>]*>[\s\S]*?<\/style>/gi;
|
|
330
|
+
var HEAD_BLOCK_RE = /<head[^>]*>[\s\S]*?<\/head>/gi;
|
|
331
|
+
var HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
332
|
+
var ANCHOR_TAG_RE = /<a\s[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
333
|
+
var HTML_TAG_RE = /<[^>]+>/g;
|
|
334
|
+
var HEADING_TAG_RE = /<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi;
|
|
335
|
+
var BR_TAG_RE = /<br\s*\/?>/gi;
|
|
336
|
+
var HR_TAG_RE = /<hr\s*\/?>/gi;
|
|
337
|
+
var LIST_ITEM_RE = /<li[^>]*>([\s\S]*?)<\/li>/gi;
|
|
338
|
+
var BLOCK_CLOSE_TAG_RE = /<\/(p|div|tr|table|blockquote)>/gi;
|
|
339
|
+
var TABLE_CELL_CLOSE_TAG_RE = /<\/(td|th)>/gi;
|
|
340
|
+
var HTML_ENTITY_RE = /&[a-zA-Z0-9#]+;/g;
|
|
341
|
+
var NUMERIC_ENTITY_RE = /^&#(\d+);$/;
|
|
342
|
+
var HEX_ENTITY_RE = /^&#x([a-fA-F0-9]+);$/;
|
|
343
|
+
var TAB_CHAR_RE = /\t/g;
|
|
344
|
+
var NON_NEWLINE_WHITESPACE_RE = /[^\S\n]+/g;
|
|
345
|
+
var EXCESSIVE_NEWLINES_RE = /\n{3,}/g;
|
|
311
346
|
var ENTITY_MAP = {
|
|
312
347
|
"&": "&",
|
|
313
348
|
"<": "<",
|
|
@@ -326,45 +361,45 @@ var ENTITY_MAP = {
|
|
|
326
361
|
};
|
|
327
362
|
function htmlToText(html) {
|
|
328
363
|
let text = html;
|
|
329
|
-
text = text.replace(
|
|
330
|
-
text = text.replace(
|
|
331
|
-
text = text.replace(
|
|
332
|
-
text = text.replace(
|
|
333
|
-
const linkText = content.replace(
|
|
364
|
+
text = text.replace(STYLE_BLOCK_RE, "");
|
|
365
|
+
text = text.replace(HEAD_BLOCK_RE, "");
|
|
366
|
+
text = text.replace(HTML_COMMENT_RE, "");
|
|
367
|
+
text = text.replace(ANCHOR_TAG_RE, (_, href, content) => {
|
|
368
|
+
const linkText = content.replace(HTML_TAG_RE, "").trim();
|
|
334
369
|
if (linkText && href && linkText !== href) {
|
|
335
370
|
return `${linkText} (${href})`;
|
|
336
371
|
}
|
|
337
372
|
return linkText || href;
|
|
338
373
|
});
|
|
339
|
-
text = text.replace(
|
|
340
|
-
const headingText = content.replace(
|
|
374
|
+
text = text.replace(HEADING_TAG_RE, (_, content) => {
|
|
375
|
+
const headingText = content.replace(HTML_TAG_RE, "").trim();
|
|
341
376
|
return `
|
|
342
377
|
|
|
343
378
|
${headingText.toUpperCase()}
|
|
344
379
|
|
|
345
380
|
`;
|
|
346
381
|
});
|
|
347
|
-
text = text.replace(
|
|
348
|
-
text = text.replace(
|
|
349
|
-
text = text.replace(
|
|
350
|
-
const itemText = content.replace(
|
|
382
|
+
text = text.replace(BR_TAG_RE, "\n");
|
|
383
|
+
text = text.replace(HR_TAG_RE, "\n---\n");
|
|
384
|
+
text = text.replace(LIST_ITEM_RE, (_, content) => {
|
|
385
|
+
const itemText = content.replace(HTML_TAG_RE, "").trim();
|
|
351
386
|
return `
|
|
352
387
|
- ${itemText}`;
|
|
353
388
|
});
|
|
354
|
-
text = text.replace(
|
|
355
|
-
text = text.replace(
|
|
356
|
-
text = text.replace(
|
|
357
|
-
text = text.replace(
|
|
389
|
+
text = text.replace(BLOCK_CLOSE_TAG_RE, "\n\n");
|
|
390
|
+
text = text.replace(TABLE_CELL_CLOSE_TAG_RE, " ");
|
|
391
|
+
text = text.replace(HTML_TAG_RE, "");
|
|
392
|
+
text = text.replace(HTML_ENTITY_RE, (entity) => {
|
|
358
393
|
if (ENTITY_MAP[entity]) return ENTITY_MAP[entity];
|
|
359
|
-
const numMatch = entity.match(
|
|
394
|
+
const numMatch = entity.match(NUMERIC_ENTITY_RE);
|
|
360
395
|
if (numMatch) return String.fromCharCode(parseInt(numMatch[1], 10));
|
|
361
|
-
const hexMatch = entity.match(
|
|
396
|
+
const hexMatch = entity.match(HEX_ENTITY_RE);
|
|
362
397
|
if (hexMatch) return String.fromCharCode(parseInt(hexMatch[1], 16));
|
|
363
398
|
return entity;
|
|
364
399
|
});
|
|
365
|
-
text = text.replace(
|
|
366
|
-
text = text.replace(
|
|
367
|
-
text = text.replace(
|
|
400
|
+
text = text.replace(TAB_CHAR_RE, " ");
|
|
401
|
+
text = text.replace(NON_NEWLINE_WHITESPACE_RE, " ");
|
|
402
|
+
text = text.replace(EXCESSIVE_NEWLINES_RE, "\n\n");
|
|
368
403
|
text = text.split("\n").map((line) => line.trim()).join("\n");
|
|
369
404
|
text = text.trim();
|
|
370
405
|
return text;
|
|
@@ -410,8 +445,12 @@ ${css}
|
|
|
410
445
|
</body>
|
|
411
446
|
</html>`;
|
|
412
447
|
}
|
|
448
|
+
var AMPERSAND_RE = /&/g;
|
|
449
|
+
var LESS_THAN_RE = /</g;
|
|
450
|
+
var GREATER_THAN_RE = />/g;
|
|
451
|
+
var DOUBLE_QUOTE_RE = /"/g;
|
|
413
452
|
function escapeHtml(str) {
|
|
414
|
-
return str.replace(
|
|
453
|
+
return str.replace(AMPERSAND_RE, "&").replace(LESS_THAN_RE, "<").replace(GREATER_THAN_RE, ">").replace(DOUBLE_QUOTE_RE, """);
|
|
415
454
|
}
|
|
416
455
|
|
|
417
456
|
// src/render-email.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthesketh/utopia-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Template-based email rendering for UtopiaJS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"dist"
|
|
56
56
|
],
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@matthesketh/utopia-core": "0.
|
|
59
|
-
"@matthesketh/utopia-server": "0.
|
|
58
|
+
"@matthesketh/utopia-core": "0.5.0",
|
|
59
|
+
"@matthesketh/utopia-server": "0.5.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"@sendgrid/mail": "^7.0.0 || ^8.0.0",
|