@matthesketh/utopia-email 0.4.0 → 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 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(/\/\*[\s\S]*?\*\//g, "");
64
+ const cleaned = css.replace(CSS_COMMENT_RE, "");
46
65
  let i = 0;
47
66
  while (i < cleaned.length) {
48
- while (i < cleaned.length && /\s/.test(cleaned[i])) i++;
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(/\s*>\s*/g, " ").trim();
90
- const idMatches = parts.match(/#[a-zA-Z_-][\w-]*/g);
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(/\.[a-zA-Z_-][\w-]*/g);
111
+ const classMatches = parts.match(CLASS_SELECTOR_RE);
93
112
  if (classMatches) classes += classMatches.length;
94
- const attrMatches = parts.match(/\[[^\]]+\]/g);
113
+ const attrMatches = parts.match(ATTR_SELECTOR_RE);
95
114
  if (attrMatches) classes += attrMatches.length;
96
- const segments = parts.split(/[\s>+~]+/);
115
+ const segments = parts.split(COMBINATOR_SPLIT_RE);
97
116
  for (const seg of segments) {
98
- const stripped = seg.replace(/#[a-zA-Z_-][\w-]*/g, "").replace(/\.[a-zA-Z_-][\w-]*/g, "").replace(/\[[^\]]+\]/g, "").replace(/:[\w-]+(\([^)]*\))?/g, "").trim();
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(/^([a-zA-Z][\w-]*)/);
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(/^#([a-zA-Z_-][\w-]*)/);
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(/^\.([a-zA-Z_-][\w-]*)/);
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(/[~|^$*]$/, "").trim();
137
- const attrValue = attrExpr.slice(eqIdx + 1).replace(/^["']|["']$/g, "").trim();
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(/^:[\w-]+(\([^)]*\))?/);
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(/\s*>\s*/);
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(/\s+/);
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
- const allTagsRegex = /<\/?([a-zA-Z][\w-]*)(\s[^>]*?)?\s*\/?>/g;
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 = allTagsRegex.exec(html)) !== null) {
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
- const attrRegex = /([a-zA-Z_:][\w:.-]*)\s*(?:=\s*"([^"]*)")?/g;
305
+ ATTR_PARSE_RE.lastIndex = 0;
288
306
  let attrMatch;
289
- while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
307
+ while ((attrMatch = ATTR_PARSE_RE.exec(attrsStr)) !== null) {
290
308
  attrs[attrMatch[1]] = attrMatch[2] ?? "";
291
309
  }
292
- const classes = (attrs["class"] || "").split(/\s+/).filter(Boolean);
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(/style="[^"]*"/, `style="${mergedStyle}"`);
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
  "&amp;": "&",
351
386
  "&lt;": "<",
@@ -364,45 +399,45 @@ var ENTITY_MAP = {
364
399
  };
365
400
  function htmlToText(html) {
366
401
  let text = html;
367
- text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
368
- text = text.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, "");
369
- text = text.replace(/<!--[\s\S]*?-->/g, "");
370
- text = text.replace(/<a\s[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, content) => {
371
- const linkText = content.replace(/<[^>]+>/g, "").trim();
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(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, (_, content) => {
378
- const headingText = content.replace(/<[^>]+>/g, "").trim();
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(/<br\s*\/?>/gi, "\n");
386
- text = text.replace(/<hr\s*\/?>/gi, "\n---\n");
387
- text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, content) => {
388
- const itemText = content.replace(/<[^>]+>/g, "").trim();
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(/<\/(p|div|tr|table|blockquote)>/gi, "\n\n");
393
- text = text.replace(/<\/(td|th)>/gi, " ");
394
- text = text.replace(/<[^>]+>/g, "");
395
- text = text.replace(/&[a-zA-Z0-9#]+;/g, (entity) => {
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(/^&#(\d+);$/);
432
+ const numMatch = entity.match(NUMERIC_ENTITY_RE);
398
433
  if (numMatch) return String.fromCharCode(parseInt(numMatch[1], 10));
399
- const hexMatch = entity.match(/^&#x([a-fA-F0-9]+);$/);
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(/\t/g, " ");
404
- text = text.replace(/[^\S\n]+/g, " ");
405
- text = text.replace(/\n{3,}/g, "\n\n");
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(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
491
+ return str.replace(AMPERSAND_RE, "&amp;").replace(LESS_THAN_RE, "&lt;").replace(GREATER_THAN_RE, "&gt;").replace(DOUBLE_QUOTE_RE, "&quot;");
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(/\/\*[\s\S]*?\*\//g, "");
26
+ const cleaned = css.replace(CSS_COMMENT_RE, "");
8
27
  let i = 0;
9
28
  while (i < cleaned.length) {
10
- while (i < cleaned.length && /\s/.test(cleaned[i])) i++;
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(/\s*>\s*/g, " ").trim();
52
- const idMatches = parts.match(/#[a-zA-Z_-][\w-]*/g);
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(/\.[a-zA-Z_-][\w-]*/g);
73
+ const classMatches = parts.match(CLASS_SELECTOR_RE);
55
74
  if (classMatches) classes += classMatches.length;
56
- const attrMatches = parts.match(/\[[^\]]+\]/g);
75
+ const attrMatches = parts.match(ATTR_SELECTOR_RE);
57
76
  if (attrMatches) classes += attrMatches.length;
58
- const segments = parts.split(/[\s>+~]+/);
77
+ const segments = parts.split(COMBINATOR_SPLIT_RE);
59
78
  for (const seg of segments) {
60
- const stripped = seg.replace(/#[a-zA-Z_-][\w-]*/g, "").replace(/\.[a-zA-Z_-][\w-]*/g, "").replace(/\[[^\]]+\]/g, "").replace(/:[\w-]+(\([^)]*\))?/g, "").trim();
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(/^([a-zA-Z][\w-]*)/);
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(/^#([a-zA-Z_-][\w-]*)/);
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(/^\.([a-zA-Z_-][\w-]*)/);
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(/[~|^$*]$/, "").trim();
99
- const attrValue = attrExpr.slice(eqIdx + 1).replace(/^["']|["']$/g, "").trim();
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(/^:[\w-]+(\([^)]*\))?/);
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(/\s*>\s*/);
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(/\s+/);
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
- const allTagsRegex = /<\/?([a-zA-Z][\w-]*)(\s[^>]*?)?\s*\/?>/g;
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 = allTagsRegex.exec(html)) !== null) {
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
- const attrRegex = /([a-zA-Z_:][\w:.-]*)\s*(?:=\s*"([^"]*)")?/g;
267
+ ATTR_PARSE_RE.lastIndex = 0;
250
268
  let attrMatch;
251
- while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
269
+ while ((attrMatch = ATTR_PARSE_RE.exec(attrsStr)) !== null) {
252
270
  attrs[attrMatch[1]] = attrMatch[2] ?? "";
253
271
  }
254
- const classes = (attrs["class"] || "").split(/\s+/).filter(Boolean);
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(/style="[^"]*"/, `style="${mergedStyle}"`);
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
  "&amp;": "&",
313
348
  "&lt;": "<",
@@ -326,45 +361,45 @@ var ENTITY_MAP = {
326
361
  };
327
362
  function htmlToText(html) {
328
363
  let text = html;
329
- text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
330
- text = text.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, "");
331
- text = text.replace(/<!--[\s\S]*?-->/g, "");
332
- text = text.replace(/<a\s[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, content) => {
333
- const linkText = content.replace(/<[^>]+>/g, "").trim();
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(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, (_, content) => {
340
- const headingText = content.replace(/<[^>]+>/g, "").trim();
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(/<br\s*\/?>/gi, "\n");
348
- text = text.replace(/<hr\s*\/?>/gi, "\n---\n");
349
- text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, content) => {
350
- const itemText = content.replace(/<[^>]+>/g, "").trim();
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(/<\/(p|div|tr|table|blockquote)>/gi, "\n\n");
355
- text = text.replace(/<\/(td|th)>/gi, " ");
356
- text = text.replace(/<[^>]+>/g, "");
357
- text = text.replace(/&[a-zA-Z0-9#]+;/g, (entity) => {
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(/^&#(\d+);$/);
394
+ const numMatch = entity.match(NUMERIC_ENTITY_RE);
360
395
  if (numMatch) return String.fromCharCode(parseInt(numMatch[1], 10));
361
- const hexMatch = entity.match(/^&#x([a-fA-F0-9]+);$/);
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(/\t/g, " ");
366
- text = text.replace(/[^\S\n]+/g, " ");
367
- text = text.replace(/\n{3,}/g, "\n\n");
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(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
453
+ return str.replace(AMPERSAND_RE, "&amp;").replace(LESS_THAN_RE, "&lt;").replace(GREATER_THAN_RE, "&gt;").replace(DOUBLE_QUOTE_RE, "&quot;");
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.4.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.4.0",
59
- "@matthesketh/utopia-server": "0.4.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",