@emailens/engine 0.6.0 → 0.7.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/README.md +92 -6
- package/dist/compile/index.d.cts +2 -2
- package/dist/compile/index.d.ts +2 -2
- package/dist/index.cjs +448 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +445 -9
- package/dist/index.js.map +1 -1
- package/dist/{react-email-BQljgXbo.d.cts → react-email-CYOtHJch.d.cts} +48 -1
- package/dist/{react-email-BQljgXbo.d.ts → react-email-CYOtHJch.d.ts} +48 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2747,6 +2747,32 @@ var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
|
|
|
2747
2747
|
"tap here",
|
|
2748
2748
|
"this"
|
|
2749
2749
|
]);
|
|
2750
|
+
var GMAIL_CLIP_THRESHOLD = 102 * 1024;
|
|
2751
|
+
var GMAIL_CLIP_WARNING_THRESHOLD = 90 * 1024;
|
|
2752
|
+
var CLIENT_DISPLAY_LIMITS = [
|
|
2753
|
+
{ client: "Gmail (Web)", subjectLimit: 70, preheaderLimit: 90 },
|
|
2754
|
+
{ client: "Gmail (Mobile)", subjectLimit: 40, preheaderLimit: 90 },
|
|
2755
|
+
{ client: "Outlook (Web)", subjectLimit: 60, preheaderLimit: 35 },
|
|
2756
|
+
{ client: "Outlook (Desktop)", subjectLimit: 55, preheaderLimit: 35 },
|
|
2757
|
+
{ client: "Apple Mail (macOS)", subjectLimit: 78, preheaderLimit: 140 },
|
|
2758
|
+
{ client: "Apple Mail (iOS)", subjectLimit: 35, preheaderLimit: 90 },
|
|
2759
|
+
{ client: "Yahoo Mail", subjectLimit: 46, preheaderLimit: 100 },
|
|
2760
|
+
{ client: "Samsung Email", subjectLimit: 40, preheaderLimit: 70 }
|
|
2761
|
+
];
|
|
2762
|
+
var TEMPLATE_VARIABLE_PATTERNS = [
|
|
2763
|
+
[/\{\{[\s\S]*?\}\}/g, "Handlebars/Mustache"],
|
|
2764
|
+
// {{var}}
|
|
2765
|
+
[/\$\{[^}]+\}/g, "ES template literal"],
|
|
2766
|
+
// ${var}
|
|
2767
|
+
[/<%=?\s*[^%]+%>/g, "ERB/EJS"],
|
|
2768
|
+
// <% %> / <%= %>
|
|
2769
|
+
[/\*\|[A-Z_][A-Z0-9_]*\|\*/g, "Mailchimp merge tag"],
|
|
2770
|
+
// *|TAG|*
|
|
2771
|
+
[/%%[A-Za-z_][A-Za-z0-9_]*%%/g, "Salesforce AMPscript"],
|
|
2772
|
+
// %%tag%%
|
|
2773
|
+
[/\{[A-Za-z_][A-Za-z0-9_.]{2,}\}/g, "Single-brace merge field"]
|
|
2774
|
+
// {merge_field} (3+ char names)
|
|
2775
|
+
];
|
|
2750
2776
|
var EMPTY_SPAM = { score: 100, level: "low", issues: [] };
|
|
2751
2777
|
var EMPTY_LINKS = {
|
|
2752
2778
|
totalLinks: 0,
|
|
@@ -2755,6 +2781,9 @@ var EMPTY_LINKS = {
|
|
|
2755
2781
|
};
|
|
2756
2782
|
var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
|
|
2757
2783
|
var EMPTY_IMAGES = { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
|
|
2784
|
+
var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, preheaderLength: 0, truncation: [], issues: [] };
|
|
2785
|
+
var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
|
|
2786
|
+
var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
|
|
2758
2787
|
|
|
2759
2788
|
// src/transform.ts
|
|
2760
2789
|
function inlineStyles($) {
|
|
@@ -4417,7 +4446,9 @@ var WEIGHTS = {
|
|
|
4417
4446
|
"image-only": 20,
|
|
4418
4447
|
"high-image-ratio": 10,
|
|
4419
4448
|
"deceptive-link": 15,
|
|
4420
|
-
"all-caps-subject": 10
|
|
4449
|
+
"all-caps-subject": 10,
|
|
4450
|
+
"missing-physical-address": 8,
|
|
4451
|
+
"missing-one-click-unsubscribe": 5
|
|
4421
4452
|
};
|
|
4422
4453
|
function extractVisibleText($) {
|
|
4423
4454
|
const clone = $.root().clone();
|
|
@@ -4634,6 +4665,47 @@ Href: ${href}`
|
|
|
4634
4665
|
});
|
|
4635
4666
|
return issues;
|
|
4636
4667
|
}
|
|
4668
|
+
var STREET_ADDRESS_PATTERN = /\b\d{1,5}\s+[A-Za-z]+\s+(St(reet)?|Ave(nue)?|Blvd|Boulevard|Dr(ive)?|Rd|Road|Ln|Lane|Way|Ct|Court|Pl(ace)?|Pkwy|Parkway|Cir(cle)?|Terr(ace)?|Loop)\b/i;
|
|
4669
|
+
var PO_BOX_PATTERN = /\bP\.?\s*O\.?\s*Box\s+\d+/i;
|
|
4670
|
+
var ZIP_CODE_PATTERN = /\b\d{5}(-\d{4})?\b/;
|
|
4671
|
+
var ADDRESS_CLASS_PATTERN = /address|mailing|postal|footer-address|physical-address/i;
|
|
4672
|
+
function checkPhysicalAddress($, text, options) {
|
|
4673
|
+
if ((options == null ? void 0 : options.emailType) === "transactional") return null;
|
|
4674
|
+
if (STREET_ADDRESS_PATTERN.test(text) && ZIP_CODE_PATTERN.test(text)) return null;
|
|
4675
|
+
if (PO_BOX_PATTERN.test(text) && ZIP_CODE_PATTERN.test(text)) return null;
|
|
4676
|
+
let foundByClass = false;
|
|
4677
|
+
$("[class]").each((_, el) => {
|
|
4678
|
+
const cls = $(el).attr("class") || "";
|
|
4679
|
+
if (ADDRESS_CLASS_PATTERN.test(cls)) {
|
|
4680
|
+
const elText = $(el).text().trim();
|
|
4681
|
+
if (elText.length > 5) {
|
|
4682
|
+
foundByClass = true;
|
|
4683
|
+
return false;
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
});
|
|
4687
|
+
if (foundByClass) return null;
|
|
4688
|
+
if ($("address").length > 0 && $("address").text().trim().length > 5) return null;
|
|
4689
|
+
return {
|
|
4690
|
+
rule: "missing-physical-address",
|
|
4691
|
+
severity: "warning",
|
|
4692
|
+
message: "No physical mailing address detected \u2014 required by CAN-SPAM for marketing emails.",
|
|
4693
|
+
detail: "Include a street address or P.O. Box in the email footer."
|
|
4694
|
+
};
|
|
4695
|
+
}
|
|
4696
|
+
function checkOneClickUnsubscribe(options) {
|
|
4697
|
+
var _a, _b;
|
|
4698
|
+
if (!((_a = options == null ? void 0 : options.listUnsubscribeHeader) == null ? void 0 : _a.trim())) return null;
|
|
4699
|
+
if (!((_b = options == null ? void 0 : options.listUnsubscribePostHeader) == null ? void 0 : _b.trim())) {
|
|
4700
|
+
return {
|
|
4701
|
+
rule: "missing-one-click-unsubscribe",
|
|
4702
|
+
severity: "warning",
|
|
4703
|
+
message: "List-Unsubscribe header present but missing List-Unsubscribe-Post header (RFC 8058). Gmail and Yahoo require one-click unsubscribe.",
|
|
4704
|
+
detail: 'Add "List-Unsubscribe-Post: List-Unsubscribe=One-Click" header.'
|
|
4705
|
+
};
|
|
4706
|
+
}
|
|
4707
|
+
return null;
|
|
4708
|
+
}
|
|
4637
4709
|
function checkAllCapsTitle($) {
|
|
4638
4710
|
const title = $("title").text().trim();
|
|
4639
4711
|
if (title.length > 5 && title === title.toUpperCase() && /[A-Z]/.test(title)) {
|
|
@@ -4663,6 +4735,10 @@ function analyzeSpamFromDom($, options) {
|
|
|
4663
4735
|
issues.push(...checkDeceptiveLinks($));
|
|
4664
4736
|
const capsTitle = checkAllCapsTitle($);
|
|
4665
4737
|
if (capsTitle) issues.push(capsTitle);
|
|
4738
|
+
const addressIssue = checkPhysicalAddress($, text, options);
|
|
4739
|
+
if (addressIssue) issues.push(addressIssue);
|
|
4740
|
+
const oneClickIssue = checkOneClickUnsubscribe(options);
|
|
4741
|
+
if (oneClickIssue) issues.push(oneClickIssue);
|
|
4666
4742
|
let penalty = 0;
|
|
4667
4743
|
const seenRules = /* @__PURE__ */ new Map();
|
|
4668
4744
|
for (const issue of issues) {
|
|
@@ -4851,6 +4927,23 @@ function validateLinksFromDom($) {
|
|
|
4851
4927
|
});
|
|
4852
4928
|
}
|
|
4853
4929
|
});
|
|
4930
|
+
links.each((_, el) => {
|
|
4931
|
+
const href = $(el).attr("href") || "";
|
|
4932
|
+
const trimmed = href.trim();
|
|
4933
|
+
if (trimmed.startsWith("#") && trimmed.length > 1) {
|
|
4934
|
+
const targetId = trimmed.slice(1);
|
|
4935
|
+
const target = $(`[id="${targetId}"]`);
|
|
4936
|
+
if (target.length === 0) {
|
|
4937
|
+
issues.push({
|
|
4938
|
+
severity: "error",
|
|
4939
|
+
rule: "broken-anchor",
|
|
4940
|
+
message: `Anchor link "${trimmed}" points to an element that does not exist`,
|
|
4941
|
+
href: trimmed,
|
|
4942
|
+
text: $(el).text().trim().slice(0, 80) || "(no text)"
|
|
4943
|
+
});
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
});
|
|
4854
4947
|
for (const [href, count] of hrefCounts) {
|
|
4855
4948
|
if (count > 5) {
|
|
4856
4949
|
issues.push({
|
|
@@ -5104,6 +5197,21 @@ function checkTextSizeAndContrast($) {
|
|
|
5104
5197
|
}
|
|
5105
5198
|
return issues;
|
|
5106
5199
|
}
|
|
5200
|
+
function checkCharsetDeclaration($) {
|
|
5201
|
+
const metaCharset = $("meta[charset]");
|
|
5202
|
+
if (metaCharset.length > 0) return null;
|
|
5203
|
+
const httpEquiv = $('meta[http-equiv="Content-Type"]');
|
|
5204
|
+
if (httpEquiv.length > 0) {
|
|
5205
|
+
const content = httpEquiv.attr("content") || "";
|
|
5206
|
+
if (/charset\s*=/i.test(content)) return null;
|
|
5207
|
+
}
|
|
5208
|
+
return {
|
|
5209
|
+
severity: "warning",
|
|
5210
|
+
rule: "missing-charset",
|
|
5211
|
+
message: "Missing charset declaration",
|
|
5212
|
+
details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
|
|
5213
|
+
};
|
|
5214
|
+
}
|
|
5107
5215
|
function checkSemanticStructure($) {
|
|
5108
5216
|
const issues = [];
|
|
5109
5217
|
const headings = [];
|
|
@@ -5136,6 +5244,8 @@ function checkAccessibilityFromDom($) {
|
|
|
5136
5244
|
issues.push(...checkTableAccessibility($));
|
|
5137
5245
|
issues.push(...checkTextSizeAndContrast($));
|
|
5138
5246
|
issues.push(...checkSemanticStructure($));
|
|
5247
|
+
const charsetIssue = checkCharsetDeclaration($);
|
|
5248
|
+
if (charsetIssue) issues.push(charsetIssue);
|
|
5139
5249
|
let penalty = 0;
|
|
5140
5250
|
const seenRules = /* @__PURE__ */ new Map();
|
|
5141
5251
|
for (const issue of issues) {
|
|
@@ -5338,8 +5448,307 @@ function analyzeImages(html) {
|
|
|
5338
5448
|
return analyzeImagesFromDom($);
|
|
5339
5449
|
}
|
|
5340
5450
|
|
|
5341
|
-
// src/
|
|
5451
|
+
// src/inbox-preview.ts
|
|
5342
5452
|
import * as cheerio8 from "cheerio";
|
|
5453
|
+
var MAX_SUBJECT_LENGTH = 60;
|
|
5454
|
+
var MAX_PREHEADER_LENGTH = 100;
|
|
5455
|
+
var MIN_PREHEADER_LENGTH = 30;
|
|
5456
|
+
var EMOJI_PATTERN = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{2B50}-\u{2B55}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{E0020}-\u{E007F}]/u;
|
|
5457
|
+
var ZWNJ_PADDING_PATTERN = /(\u200C\s*( |\u00A0)\s*){2,}|(‌\s* \s*){2,}/;
|
|
5458
|
+
function extractInboxPreview(html) {
|
|
5459
|
+
if (!html || !html.trim()) {
|
|
5460
|
+
return {
|
|
5461
|
+
subject: null,
|
|
5462
|
+
preheader: null,
|
|
5463
|
+
subjectLength: 0,
|
|
5464
|
+
preheaderLength: 0,
|
|
5465
|
+
truncation: [],
|
|
5466
|
+
issues: [{ rule: "missing-subject", severity: "warning", message: "No <title> tag found. Most email clients use this as the subject line." }]
|
|
5467
|
+
};
|
|
5468
|
+
}
|
|
5469
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
5470
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
5471
|
+
}
|
|
5472
|
+
const $ = cheerio8.load(html);
|
|
5473
|
+
return extractInboxPreviewFromDom($);
|
|
5474
|
+
}
|
|
5475
|
+
function extractInboxPreviewFromDom($) {
|
|
5476
|
+
var _a, _b;
|
|
5477
|
+
const issues = [];
|
|
5478
|
+
const titleEl = $("title");
|
|
5479
|
+
const subject = titleEl.length > 0 ? titleEl.first().text().trim() || null : null;
|
|
5480
|
+
const subjectLength = (_a = subject == null ? void 0 : subject.length) != null ? _a : 0;
|
|
5481
|
+
if (!subject) {
|
|
5482
|
+
issues.push({
|
|
5483
|
+
rule: "missing-subject",
|
|
5484
|
+
severity: "warning",
|
|
5485
|
+
message: "No <title> tag found. Most email clients use this as the subject line."
|
|
5486
|
+
});
|
|
5487
|
+
} else if (subjectLength > MAX_SUBJECT_LENGTH) {
|
|
5488
|
+
issues.push({
|
|
5489
|
+
rule: "subject-too-long",
|
|
5490
|
+
severity: "warning",
|
|
5491
|
+
message: `Subject is ${subjectLength} characters \u2014 may be truncated in inboxes (recommended: ${MAX_SUBJECT_LENGTH} or fewer).`
|
|
5492
|
+
});
|
|
5493
|
+
}
|
|
5494
|
+
const preheader = extractPreheaderText($);
|
|
5495
|
+
const preheaderLength = (_b = preheader == null ? void 0 : preheader.length) != null ? _b : 0;
|
|
5496
|
+
if (!preheader) {
|
|
5497
|
+
issues.push({
|
|
5498
|
+
rule: "missing-preheader",
|
|
5499
|
+
severity: "info",
|
|
5500
|
+
message: "No preheader text detected. Adding preview text improves open rates in crowded inboxes."
|
|
5501
|
+
});
|
|
5502
|
+
} else if (preheaderLength < MIN_PREHEADER_LENGTH) {
|
|
5503
|
+
issues.push({
|
|
5504
|
+
rule: "preheader-too-short",
|
|
5505
|
+
severity: "warning",
|
|
5506
|
+
message: `Preheader is only ${preheaderLength} characters \u2014 email clients may backfill with body text (recommended: ${MIN_PREHEADER_LENGTH}+).`
|
|
5507
|
+
});
|
|
5508
|
+
} else if (preheaderLength > MAX_PREHEADER_LENGTH) {
|
|
5509
|
+
issues.push({
|
|
5510
|
+
rule: "preheader-too-long",
|
|
5511
|
+
severity: "info",
|
|
5512
|
+
message: `Preheader is ${preheaderLength} characters \u2014 most clients show 40\u2013100 characters. Text beyond that is hidden.`
|
|
5513
|
+
});
|
|
5514
|
+
}
|
|
5515
|
+
if (hasZwnjPadding($)) {
|
|
5516
|
+
issues.push({
|
|
5517
|
+
rule: "zwnj-padding",
|
|
5518
|
+
severity: "info",
|
|
5519
|
+
message: "Preheader uses ‌ padding hack \u2014 works in most clients but may show garbled text in some."
|
|
5520
|
+
});
|
|
5521
|
+
}
|
|
5522
|
+
if (subject && EMOJI_PATTERN.test(subject)) {
|
|
5523
|
+
issues.push({
|
|
5524
|
+
rule: "emoji-in-subject",
|
|
5525
|
+
severity: "info",
|
|
5526
|
+
message: "Subject line contains emoji \u2014 renders inconsistently across email clients and may trigger spam filters."
|
|
5527
|
+
});
|
|
5528
|
+
}
|
|
5529
|
+
const truncation = computeTruncation(subject, preheader);
|
|
5530
|
+
return {
|
|
5531
|
+
subject,
|
|
5532
|
+
preheader,
|
|
5533
|
+
subjectLength,
|
|
5534
|
+
preheaderLength,
|
|
5535
|
+
truncation,
|
|
5536
|
+
issues
|
|
5537
|
+
};
|
|
5538
|
+
}
|
|
5539
|
+
function computeTruncation(subject, preheader) {
|
|
5540
|
+
return CLIENT_DISPLAY_LIMITS.map((limit) => {
|
|
5541
|
+
const truncatedSubject = subject && subject.length > limit.subjectLimit ? subject.slice(0, limit.subjectLimit - 1) + "\u2026" : subject;
|
|
5542
|
+
const truncatedPreheader = preheader && preheader.length > limit.preheaderLimit ? preheader.slice(0, limit.preheaderLimit - 1) + "\u2026" : preheader;
|
|
5543
|
+
return {
|
|
5544
|
+
client: limit.client,
|
|
5545
|
+
subjectLimit: limit.subjectLimit,
|
|
5546
|
+
preheaderLimit: limit.preheaderLimit,
|
|
5547
|
+
truncatedSubject: truncatedSubject != null ? truncatedSubject : null,
|
|
5548
|
+
truncatedPreheader: truncatedPreheader != null ? truncatedPreheader : null,
|
|
5549
|
+
subjectTruncated: !!subject && subject.length > limit.subjectLimit,
|
|
5550
|
+
preheaderTruncated: !!preheader && preheader.length > limit.preheaderLimit
|
|
5551
|
+
};
|
|
5552
|
+
});
|
|
5553
|
+
}
|
|
5554
|
+
function hasZwnjPadding($) {
|
|
5555
|
+
const body = $("body");
|
|
5556
|
+
if (!body.length) return false;
|
|
5557
|
+
const hiddenSelectors = [
|
|
5558
|
+
'div[style*="display:none"]',
|
|
5559
|
+
'div[style*="display: none"]',
|
|
5560
|
+
'span[style*="display:none"]',
|
|
5561
|
+
'span[style*="display: none"]',
|
|
5562
|
+
'div[style*="max-height:0"]',
|
|
5563
|
+
'div[style*="max-height: 0"]',
|
|
5564
|
+
'span[style*="max-height:0"]',
|
|
5565
|
+
'span[style*="max-height: 0"]',
|
|
5566
|
+
'[class*="preheader"]',
|
|
5567
|
+
'[class*="preview-text"]',
|
|
5568
|
+
'[class*="previewText"]'
|
|
5569
|
+
];
|
|
5570
|
+
for (const sel of hiddenSelectors) {
|
|
5571
|
+
const el = body.find(sel).first();
|
|
5572
|
+
if (el.length) {
|
|
5573
|
+
const rawHtml = el.html() || "";
|
|
5574
|
+
if (ZWNJ_PADDING_PATTERN.test(rawHtml)) return true;
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
return false;
|
|
5578
|
+
}
|
|
5579
|
+
function extractPreheaderText($) {
|
|
5580
|
+
const body = $("body");
|
|
5581
|
+
if (!body.length) return null;
|
|
5582
|
+
const hiddenSelectors = [
|
|
5583
|
+
'div[style*="display:none"]',
|
|
5584
|
+
'div[style*="display: none"]',
|
|
5585
|
+
'span[style*="display:none"]',
|
|
5586
|
+
'span[style*="display: none"]',
|
|
5587
|
+
'div[style*="visibility:hidden"]',
|
|
5588
|
+
'div[style*="visibility: hidden"]',
|
|
5589
|
+
'span[style*="visibility:hidden"]',
|
|
5590
|
+
'span[style*="visibility: hidden"]',
|
|
5591
|
+
'div[style*="max-height:0"]',
|
|
5592
|
+
'div[style*="max-height: 0"]',
|
|
5593
|
+
'span[style*="max-height:0"]',
|
|
5594
|
+
'span[style*="max-height: 0"]',
|
|
5595
|
+
'div[style*="mso-hide:all"]',
|
|
5596
|
+
'div[style*="mso-hide: all"]'
|
|
5597
|
+
];
|
|
5598
|
+
for (const sel of hiddenSelectors) {
|
|
5599
|
+
const el = body.find(sel).first();
|
|
5600
|
+
if (el.length) {
|
|
5601
|
+
const text = el.text().trim();
|
|
5602
|
+
if (text) return text;
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
const preheaderClasses = body.find(
|
|
5606
|
+
'[class*="preheader"], [class*="preview-text"], [class*="previewText"]'
|
|
5607
|
+
).first();
|
|
5608
|
+
if (preheaderClasses.length) {
|
|
5609
|
+
const text = preheaderClasses.text().trim();
|
|
5610
|
+
if (text) return text;
|
|
5611
|
+
}
|
|
5612
|
+
const firstText = getFirstVisibleText($, body);
|
|
5613
|
+
return firstText || null;
|
|
5614
|
+
}
|
|
5615
|
+
function getFirstVisibleText($, body) {
|
|
5616
|
+
const skipTags = /* @__PURE__ */ new Set(["style", "script", "head", "title"]);
|
|
5617
|
+
let result = null;
|
|
5618
|
+
body.find("*").each((_, el) => {
|
|
5619
|
+
var _a;
|
|
5620
|
+
if (result) return false;
|
|
5621
|
+
const tagName = (_a = el.tagName) == null ? void 0 : _a.toLowerCase();
|
|
5622
|
+
if (skipTags.has(tagName)) return;
|
|
5623
|
+
const style = $(el).attr("style") || "";
|
|
5624
|
+
if (style.includes("display:none") || style.includes("display: none") || style.includes("visibility:hidden") || style.includes("visibility: hidden")) {
|
|
5625
|
+
return;
|
|
5626
|
+
}
|
|
5627
|
+
const directText = $(el).contents().filter((_2, node) => node.type === "text").text().trim();
|
|
5628
|
+
if (directText && directText.length > 1) {
|
|
5629
|
+
const cleaned = directText.replace(/[\u200B\u00A0\s]+/g, " ").trim();
|
|
5630
|
+
if (cleaned.length > 1) {
|
|
5631
|
+
result = cleaned;
|
|
5632
|
+
return false;
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
});
|
|
5636
|
+
return result;
|
|
5637
|
+
}
|
|
5638
|
+
|
|
5639
|
+
// src/size-checker.ts
|
|
5640
|
+
import * as cheerio9 from "cheerio";
|
|
5641
|
+
function humanizeBytes(bytes) {
|
|
5642
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
5643
|
+
const kb = bytes / 1024;
|
|
5644
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
5645
|
+
const mb = kb / 1024;
|
|
5646
|
+
return `${mb.toFixed(2)} MB`;
|
|
5647
|
+
}
|
|
5648
|
+
function checkSizeFromDom(_$, html) {
|
|
5649
|
+
const htmlBytes = new TextEncoder().encode(html).length;
|
|
5650
|
+
const humanSize = humanizeBytes(htmlBytes);
|
|
5651
|
+
const issues = [];
|
|
5652
|
+
let clipped = false;
|
|
5653
|
+
if (htmlBytes > GMAIL_CLIP_THRESHOLD) {
|
|
5654
|
+
clipped = true;
|
|
5655
|
+
issues.push({
|
|
5656
|
+
rule: "gmail-clipped",
|
|
5657
|
+
severity: "error",
|
|
5658
|
+
message: `Email is ${humanSize} \u2014 Gmail will clip it at ~102 KB. Recipients see a "View entire message" link instead of your content.`,
|
|
5659
|
+
detail: `${htmlBytes} bytes exceeds the ${GMAIL_CLIP_THRESHOLD} byte threshold.`
|
|
5660
|
+
});
|
|
5661
|
+
} else if (htmlBytes > GMAIL_CLIP_WARNING_THRESHOLD) {
|
|
5662
|
+
issues.push({
|
|
5663
|
+
rule: "gmail-clip-warning",
|
|
5664
|
+
severity: "warning",
|
|
5665
|
+
message: `Email is ${humanSize} \u2014 approaching Gmail's ~102 KB clip threshold. Consider trimming.`,
|
|
5666
|
+
detail: `${htmlBytes} bytes is within ${GMAIL_CLIP_THRESHOLD - htmlBytes} bytes of the clip threshold.`
|
|
5667
|
+
});
|
|
5668
|
+
}
|
|
5669
|
+
return { htmlBytes, humanSize, clipped, issues };
|
|
5670
|
+
}
|
|
5671
|
+
function checkSize(html) {
|
|
5672
|
+
if (!html || !html.trim()) {
|
|
5673
|
+
return { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
|
|
5674
|
+
}
|
|
5675
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
5676
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
5677
|
+
}
|
|
5678
|
+
const $ = cheerio9.load(html);
|
|
5679
|
+
return checkSizeFromDom($, html);
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5682
|
+
// src/template-checker.ts
|
|
5683
|
+
import * as cheerio10 from "cheerio";
|
|
5684
|
+
function checkTemplateVariablesFromDom($) {
|
|
5685
|
+
const issues = [];
|
|
5686
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5687
|
+
const textContent = extractTextContent($);
|
|
5688
|
+
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
5689
|
+
pattern.lastIndex = 0;
|
|
5690
|
+
let match;
|
|
5691
|
+
while ((match = pattern.exec(textContent)) !== null) {
|
|
5692
|
+
const variable = match[0];
|
|
5693
|
+
const key = `text:${variable}`;
|
|
5694
|
+
if (seen.has(key)) continue;
|
|
5695
|
+
seen.add(key);
|
|
5696
|
+
issues.push({
|
|
5697
|
+
rule: "unresolved-variable",
|
|
5698
|
+
severity: "error",
|
|
5699
|
+
message: `Unresolved ${label} variable "${variable}" found in text content.`,
|
|
5700
|
+
variable,
|
|
5701
|
+
location: "text"
|
|
5702
|
+
});
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
const attrSelectors = ["[href]", "[src]", "[alt]"];
|
|
5706
|
+
for (const sel of attrSelectors) {
|
|
5707
|
+
$(sel).each((_, el) => {
|
|
5708
|
+
const attrs = ["href", "src", "alt"];
|
|
5709
|
+
for (const attr of attrs) {
|
|
5710
|
+
const value = $(el).attr(attr);
|
|
5711
|
+
if (!value) continue;
|
|
5712
|
+
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
5713
|
+
pattern.lastIndex = 0;
|
|
5714
|
+
let match;
|
|
5715
|
+
while ((match = pattern.exec(value)) !== null) {
|
|
5716
|
+
const variable = match[0];
|
|
5717
|
+
const key = `attr:${attr}:${variable}`;
|
|
5718
|
+
if (seen.has(key)) continue;
|
|
5719
|
+
seen.add(key);
|
|
5720
|
+
issues.push({
|
|
5721
|
+
rule: "unresolved-variable",
|
|
5722
|
+
severity: "error",
|
|
5723
|
+
message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
|
|
5724
|
+
variable,
|
|
5725
|
+
location: "attribute"
|
|
5726
|
+
});
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
});
|
|
5731
|
+
}
|
|
5732
|
+
return { unresolvedCount: issues.length, issues };
|
|
5733
|
+
}
|
|
5734
|
+
function extractTextContent($) {
|
|
5735
|
+
const clone = $.root().clone();
|
|
5736
|
+
clone.find("style, script, head").remove();
|
|
5737
|
+
return clone.text();
|
|
5738
|
+
}
|
|
5739
|
+
function checkTemplateVariables(html) {
|
|
5740
|
+
if (!html || !html.trim()) {
|
|
5741
|
+
return { unresolvedCount: 0, issues: [] };
|
|
5742
|
+
}
|
|
5743
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
5744
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
5745
|
+
}
|
|
5746
|
+
const $ = cheerio10.load(html);
|
|
5747
|
+
return checkTemplateVariablesFromDom($);
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
// src/audit.ts
|
|
5751
|
+
import * as cheerio11 from "cheerio";
|
|
5343
5752
|
function auditEmail(html, options) {
|
|
5344
5753
|
var _a;
|
|
5345
5754
|
if (!html || !html.trim()) {
|
|
@@ -5348,7 +5757,10 @@ function auditEmail(html, options) {
|
|
|
5348
5757
|
spam: EMPTY_SPAM,
|
|
5349
5758
|
links: EMPTY_LINKS,
|
|
5350
5759
|
accessibility: EMPTY_ACCESSIBILITY,
|
|
5351
|
-
images: EMPTY_IMAGES
|
|
5760
|
+
images: EMPTY_IMAGES,
|
|
5761
|
+
inboxPreview: EMPTY_INBOX_PREVIEW,
|
|
5762
|
+
size: EMPTY_SIZE,
|
|
5763
|
+
templateVariables: EMPTY_TEMPLATE
|
|
5352
5764
|
};
|
|
5353
5765
|
}
|
|
5354
5766
|
if (html.length > MAX_HTML_SIZE) {
|
|
@@ -5356,18 +5768,21 @@ function auditEmail(html, options) {
|
|
|
5356
5768
|
}
|
|
5357
5769
|
const framework = options == null ? void 0 : options.framework;
|
|
5358
5770
|
const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
|
|
5359
|
-
const $ =
|
|
5771
|
+
const $ = cheerio11.load(html);
|
|
5360
5772
|
const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
|
|
5361
5773
|
const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
|
|
5362
5774
|
const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
|
|
5363
5775
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
5364
5776
|
const accessibility = skip.has("accessibility") ? EMPTY_ACCESSIBILITY : checkAccessibilityFromDom($);
|
|
5365
5777
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
5366
|
-
|
|
5778
|
+
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
5779
|
+
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
5780
|
+
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($);
|
|
5781
|
+
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables };
|
|
5367
5782
|
}
|
|
5368
5783
|
|
|
5369
5784
|
// src/session.ts
|
|
5370
|
-
import * as
|
|
5785
|
+
import * as cheerio12 from "cheerio";
|
|
5371
5786
|
function createSession(html, options) {
|
|
5372
5787
|
if (!html || !html.trim()) {
|
|
5373
5788
|
const fw = options == null ? void 0 : options.framework;
|
|
@@ -5379,7 +5794,10 @@ function createSession(html, options) {
|
|
|
5379
5794
|
spam: EMPTY_SPAM,
|
|
5380
5795
|
links: EMPTY_LINKS,
|
|
5381
5796
|
accessibility: EMPTY_ACCESSIBILITY,
|
|
5382
|
-
images: EMPTY_IMAGES
|
|
5797
|
+
images: EMPTY_IMAGES,
|
|
5798
|
+
inboxPreview: EMPTY_INBOX_PREVIEW,
|
|
5799
|
+
size: EMPTY_SIZE,
|
|
5800
|
+
templateVariables: EMPTY_TEMPLATE
|
|
5383
5801
|
}),
|
|
5384
5802
|
analyze: () => [],
|
|
5385
5803
|
score: () => ({}),
|
|
@@ -5387,6 +5805,9 @@ function createSession(html, options) {
|
|
|
5387
5805
|
validateLinks: () => EMPTY_LINKS,
|
|
5388
5806
|
checkAccessibility: () => EMPTY_ACCESSIBILITY,
|
|
5389
5807
|
analyzeImages: () => EMPTY_IMAGES,
|
|
5808
|
+
extractInboxPreview: () => EMPTY_INBOX_PREVIEW,
|
|
5809
|
+
checkSize: () => EMPTY_SIZE,
|
|
5810
|
+
checkTemplateVariables: () => EMPTY_TEMPLATE,
|
|
5390
5811
|
transformForClient: (clientId) => ({ clientId, html: html || "", warnings: [] }),
|
|
5391
5812
|
transformForAllClients: () => [],
|
|
5392
5813
|
simulateDarkMode: (clientId) => ({ html: html || "", warnings: [] })
|
|
@@ -5395,7 +5816,7 @@ function createSession(html, options) {
|
|
|
5395
5816
|
if (html.length > MAX_HTML_SIZE) {
|
|
5396
5817
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
5397
5818
|
}
|
|
5398
|
-
const $ =
|
|
5819
|
+
const $ = cheerio12.load(html);
|
|
5399
5820
|
const framework = options == null ? void 0 : options.framework;
|
|
5400
5821
|
return {
|
|
5401
5822
|
html,
|
|
@@ -5409,7 +5830,10 @@ function createSession(html, options) {
|
|
|
5409
5830
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
5410
5831
|
const accessibility = skip.has("accessibility") ? EMPTY_ACCESSIBILITY : checkAccessibilityFromDom($);
|
|
5411
5832
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
5412
|
-
|
|
5833
|
+
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
5834
|
+
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
5835
|
+
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($);
|
|
5836
|
+
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables };
|
|
5413
5837
|
},
|
|
5414
5838
|
analyze() {
|
|
5415
5839
|
return analyzeEmailFromDom($, framework);
|
|
@@ -5429,6 +5853,15 @@ function createSession(html, options) {
|
|
|
5429
5853
|
analyzeImages() {
|
|
5430
5854
|
return analyzeImagesFromDom($);
|
|
5431
5855
|
},
|
|
5856
|
+
extractInboxPreview() {
|
|
5857
|
+
return extractInboxPreviewFromDom($);
|
|
5858
|
+
},
|
|
5859
|
+
checkSize() {
|
|
5860
|
+
return checkSizeFromDom($, html);
|
|
5861
|
+
},
|
|
5862
|
+
checkTemplateVariables() {
|
|
5863
|
+
return checkTemplateVariablesFromDom($);
|
|
5864
|
+
},
|
|
5432
5865
|
// Transforms create isolated copies since they mutate the DOM
|
|
5433
5866
|
transformForClient(clientId) {
|
|
5434
5867
|
return transformForClient(html, clientId, framework);
|
|
@@ -5454,11 +5887,14 @@ export {
|
|
|
5454
5887
|
analyzeSpam,
|
|
5455
5888
|
auditEmail,
|
|
5456
5889
|
checkAccessibility,
|
|
5890
|
+
checkSize,
|
|
5891
|
+
checkTemplateVariables,
|
|
5457
5892
|
contrastRatio,
|
|
5458
5893
|
createSession,
|
|
5459
5894
|
diffResults,
|
|
5460
5895
|
errorWarnings,
|
|
5461
5896
|
estimateAiFixTokens,
|
|
5897
|
+
extractInboxPreview,
|
|
5462
5898
|
generateAiFix,
|
|
5463
5899
|
generateCompatibilityScore,
|
|
5464
5900
|
generateFixPrompt,
|