@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.cjs
CHANGED
|
@@ -71,11 +71,14 @@ __export(index_exports, {
|
|
|
71
71
|
analyzeSpam: () => analyzeSpam,
|
|
72
72
|
auditEmail: () => auditEmail,
|
|
73
73
|
checkAccessibility: () => checkAccessibility,
|
|
74
|
+
checkSize: () => checkSize,
|
|
75
|
+
checkTemplateVariables: () => checkTemplateVariables,
|
|
74
76
|
contrastRatio: () => contrastRatio,
|
|
75
77
|
createSession: () => createSession,
|
|
76
78
|
diffResults: () => diffResults,
|
|
77
79
|
errorWarnings: () => errorWarnings,
|
|
78
80
|
estimateAiFixTokens: () => estimateAiFixTokens,
|
|
81
|
+
extractInboxPreview: () => extractInboxPreview,
|
|
79
82
|
generateAiFix: () => generateAiFix,
|
|
80
83
|
generateCompatibilityScore: () => generateCompatibilityScore,
|
|
81
84
|
generateFixPrompt: () => generateFixPrompt,
|
|
@@ -2837,6 +2840,32 @@ var GENERIC_LINK_TEXT = /* @__PURE__ */ new Set([
|
|
|
2837
2840
|
"tap here",
|
|
2838
2841
|
"this"
|
|
2839
2842
|
]);
|
|
2843
|
+
var GMAIL_CLIP_THRESHOLD = 102 * 1024;
|
|
2844
|
+
var GMAIL_CLIP_WARNING_THRESHOLD = 90 * 1024;
|
|
2845
|
+
var CLIENT_DISPLAY_LIMITS = [
|
|
2846
|
+
{ client: "Gmail (Web)", subjectLimit: 70, preheaderLimit: 90 },
|
|
2847
|
+
{ client: "Gmail (Mobile)", subjectLimit: 40, preheaderLimit: 90 },
|
|
2848
|
+
{ client: "Outlook (Web)", subjectLimit: 60, preheaderLimit: 35 },
|
|
2849
|
+
{ client: "Outlook (Desktop)", subjectLimit: 55, preheaderLimit: 35 },
|
|
2850
|
+
{ client: "Apple Mail (macOS)", subjectLimit: 78, preheaderLimit: 140 },
|
|
2851
|
+
{ client: "Apple Mail (iOS)", subjectLimit: 35, preheaderLimit: 90 },
|
|
2852
|
+
{ client: "Yahoo Mail", subjectLimit: 46, preheaderLimit: 100 },
|
|
2853
|
+
{ client: "Samsung Email", subjectLimit: 40, preheaderLimit: 70 }
|
|
2854
|
+
];
|
|
2855
|
+
var TEMPLATE_VARIABLE_PATTERNS = [
|
|
2856
|
+
[/\{\{[\s\S]*?\}\}/g, "Handlebars/Mustache"],
|
|
2857
|
+
// {{var}}
|
|
2858
|
+
[/\$\{[^}]+\}/g, "ES template literal"],
|
|
2859
|
+
// ${var}
|
|
2860
|
+
[/<%=?\s*[^%]+%>/g, "ERB/EJS"],
|
|
2861
|
+
// <% %> / <%= %>
|
|
2862
|
+
[/\*\|[A-Z_][A-Z0-9_]*\|\*/g, "Mailchimp merge tag"],
|
|
2863
|
+
// *|TAG|*
|
|
2864
|
+
[/%%[A-Za-z_][A-Za-z0-9_]*%%/g, "Salesforce AMPscript"],
|
|
2865
|
+
// %%tag%%
|
|
2866
|
+
[/\{[A-Za-z_][A-Za-z0-9_.]{2,}\}/g, "Single-brace merge field"]
|
|
2867
|
+
// {merge_field} (3+ char names)
|
|
2868
|
+
];
|
|
2840
2869
|
var EMPTY_SPAM = { score: 100, level: "low", issues: [] };
|
|
2841
2870
|
var EMPTY_LINKS = {
|
|
2842
2871
|
totalLinks: 0,
|
|
@@ -2845,6 +2874,9 @@ var EMPTY_LINKS = {
|
|
|
2845
2874
|
};
|
|
2846
2875
|
var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
|
|
2847
2876
|
var EMPTY_IMAGES = { total: 0, totalDataUriBytes: 0, issues: [], images: [] };
|
|
2877
|
+
var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, preheaderLength: 0, truncation: [], issues: [] };
|
|
2878
|
+
var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
|
|
2879
|
+
var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
|
|
2848
2880
|
|
|
2849
2881
|
// src/transform.ts
|
|
2850
2882
|
function inlineStyles($) {
|
|
@@ -4507,7 +4539,9 @@ var WEIGHTS = {
|
|
|
4507
4539
|
"image-only": 20,
|
|
4508
4540
|
"high-image-ratio": 10,
|
|
4509
4541
|
"deceptive-link": 15,
|
|
4510
|
-
"all-caps-subject": 10
|
|
4542
|
+
"all-caps-subject": 10,
|
|
4543
|
+
"missing-physical-address": 8,
|
|
4544
|
+
"missing-one-click-unsubscribe": 5
|
|
4511
4545
|
};
|
|
4512
4546
|
function extractVisibleText($) {
|
|
4513
4547
|
const clone = $.root().clone();
|
|
@@ -4724,6 +4758,47 @@ Href: ${href}`
|
|
|
4724
4758
|
});
|
|
4725
4759
|
return issues;
|
|
4726
4760
|
}
|
|
4761
|
+
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;
|
|
4762
|
+
var PO_BOX_PATTERN = /\bP\.?\s*O\.?\s*Box\s+\d+/i;
|
|
4763
|
+
var ZIP_CODE_PATTERN = /\b\d{5}(-\d{4})?\b/;
|
|
4764
|
+
var ADDRESS_CLASS_PATTERN = /address|mailing|postal|footer-address|physical-address/i;
|
|
4765
|
+
function checkPhysicalAddress($, text, options) {
|
|
4766
|
+
if ((options == null ? void 0 : options.emailType) === "transactional") return null;
|
|
4767
|
+
if (STREET_ADDRESS_PATTERN.test(text) && ZIP_CODE_PATTERN.test(text)) return null;
|
|
4768
|
+
if (PO_BOX_PATTERN.test(text) && ZIP_CODE_PATTERN.test(text)) return null;
|
|
4769
|
+
let foundByClass = false;
|
|
4770
|
+
$("[class]").each((_, el) => {
|
|
4771
|
+
const cls = $(el).attr("class") || "";
|
|
4772
|
+
if (ADDRESS_CLASS_PATTERN.test(cls)) {
|
|
4773
|
+
const elText = $(el).text().trim();
|
|
4774
|
+
if (elText.length > 5) {
|
|
4775
|
+
foundByClass = true;
|
|
4776
|
+
return false;
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
});
|
|
4780
|
+
if (foundByClass) return null;
|
|
4781
|
+
if ($("address").length > 0 && $("address").text().trim().length > 5) return null;
|
|
4782
|
+
return {
|
|
4783
|
+
rule: "missing-physical-address",
|
|
4784
|
+
severity: "warning",
|
|
4785
|
+
message: "No physical mailing address detected \u2014 required by CAN-SPAM for marketing emails.",
|
|
4786
|
+
detail: "Include a street address or P.O. Box in the email footer."
|
|
4787
|
+
};
|
|
4788
|
+
}
|
|
4789
|
+
function checkOneClickUnsubscribe(options) {
|
|
4790
|
+
var _a, _b;
|
|
4791
|
+
if (!((_a = options == null ? void 0 : options.listUnsubscribeHeader) == null ? void 0 : _a.trim())) return null;
|
|
4792
|
+
if (!((_b = options == null ? void 0 : options.listUnsubscribePostHeader) == null ? void 0 : _b.trim())) {
|
|
4793
|
+
return {
|
|
4794
|
+
rule: "missing-one-click-unsubscribe",
|
|
4795
|
+
severity: "warning",
|
|
4796
|
+
message: "List-Unsubscribe header present but missing List-Unsubscribe-Post header (RFC 8058). Gmail and Yahoo require one-click unsubscribe.",
|
|
4797
|
+
detail: 'Add "List-Unsubscribe-Post: List-Unsubscribe=One-Click" header.'
|
|
4798
|
+
};
|
|
4799
|
+
}
|
|
4800
|
+
return null;
|
|
4801
|
+
}
|
|
4727
4802
|
function checkAllCapsTitle($) {
|
|
4728
4803
|
const title = $("title").text().trim();
|
|
4729
4804
|
if (title.length > 5 && title === title.toUpperCase() && /[A-Z]/.test(title)) {
|
|
@@ -4753,6 +4828,10 @@ function analyzeSpamFromDom($, options) {
|
|
|
4753
4828
|
issues.push(...checkDeceptiveLinks($));
|
|
4754
4829
|
const capsTitle = checkAllCapsTitle($);
|
|
4755
4830
|
if (capsTitle) issues.push(capsTitle);
|
|
4831
|
+
const addressIssue = checkPhysicalAddress($, text, options);
|
|
4832
|
+
if (addressIssue) issues.push(addressIssue);
|
|
4833
|
+
const oneClickIssue = checkOneClickUnsubscribe(options);
|
|
4834
|
+
if (oneClickIssue) issues.push(oneClickIssue);
|
|
4756
4835
|
let penalty = 0;
|
|
4757
4836
|
const seenRules = /* @__PURE__ */ new Map();
|
|
4758
4837
|
for (const issue of issues) {
|
|
@@ -4941,6 +5020,23 @@ function validateLinksFromDom($) {
|
|
|
4941
5020
|
});
|
|
4942
5021
|
}
|
|
4943
5022
|
});
|
|
5023
|
+
links.each((_, el) => {
|
|
5024
|
+
const href = $(el).attr("href") || "";
|
|
5025
|
+
const trimmed = href.trim();
|
|
5026
|
+
if (trimmed.startsWith("#") && trimmed.length > 1) {
|
|
5027
|
+
const targetId = trimmed.slice(1);
|
|
5028
|
+
const target = $(`[id="${targetId}"]`);
|
|
5029
|
+
if (target.length === 0) {
|
|
5030
|
+
issues.push({
|
|
5031
|
+
severity: "error",
|
|
5032
|
+
rule: "broken-anchor",
|
|
5033
|
+
message: `Anchor link "${trimmed}" points to an element that does not exist`,
|
|
5034
|
+
href: trimmed,
|
|
5035
|
+
text: $(el).text().trim().slice(0, 80) || "(no text)"
|
|
5036
|
+
});
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
});
|
|
4944
5040
|
for (const [href, count] of hrefCounts) {
|
|
4945
5041
|
if (count > 5) {
|
|
4946
5042
|
issues.push({
|
|
@@ -5194,6 +5290,21 @@ function checkTextSizeAndContrast($) {
|
|
|
5194
5290
|
}
|
|
5195
5291
|
return issues;
|
|
5196
5292
|
}
|
|
5293
|
+
function checkCharsetDeclaration($) {
|
|
5294
|
+
const metaCharset = $("meta[charset]");
|
|
5295
|
+
if (metaCharset.length > 0) return null;
|
|
5296
|
+
const httpEquiv = $('meta[http-equiv="Content-Type"]');
|
|
5297
|
+
if (httpEquiv.length > 0) {
|
|
5298
|
+
const content = httpEquiv.attr("content") || "";
|
|
5299
|
+
if (/charset\s*=/i.test(content)) return null;
|
|
5300
|
+
}
|
|
5301
|
+
return {
|
|
5302
|
+
severity: "warning",
|
|
5303
|
+
rule: "missing-charset",
|
|
5304
|
+
message: "Missing charset declaration",
|
|
5305
|
+
details: 'Add <meta charset="utf-8"> in <head> to prevent encoding issues across email clients.'
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5197
5308
|
function checkSemanticStructure($) {
|
|
5198
5309
|
const issues = [];
|
|
5199
5310
|
const headings = [];
|
|
@@ -5226,6 +5337,8 @@ function checkAccessibilityFromDom($) {
|
|
|
5226
5337
|
issues.push(...checkTableAccessibility($));
|
|
5227
5338
|
issues.push(...checkTextSizeAndContrast($));
|
|
5228
5339
|
issues.push(...checkSemanticStructure($));
|
|
5340
|
+
const charsetIssue = checkCharsetDeclaration($);
|
|
5341
|
+
if (charsetIssue) issues.push(charsetIssue);
|
|
5229
5342
|
let penalty = 0;
|
|
5230
5343
|
const seenRules = /* @__PURE__ */ new Map();
|
|
5231
5344
|
for (const issue of issues) {
|
|
@@ -5428,8 +5541,307 @@ function analyzeImages(html) {
|
|
|
5428
5541
|
return analyzeImagesFromDom($);
|
|
5429
5542
|
}
|
|
5430
5543
|
|
|
5431
|
-
// src/
|
|
5544
|
+
// src/inbox-preview.ts
|
|
5432
5545
|
var cheerio8 = __toESM(require("cheerio"), 1);
|
|
5546
|
+
var MAX_SUBJECT_LENGTH = 60;
|
|
5547
|
+
var MAX_PREHEADER_LENGTH = 100;
|
|
5548
|
+
var MIN_PREHEADER_LENGTH = 30;
|
|
5549
|
+
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;
|
|
5550
|
+
var ZWNJ_PADDING_PATTERN = /(\u200C\s*( |\u00A0)\s*){2,}|(‌\s* \s*){2,}/;
|
|
5551
|
+
function extractInboxPreview(html) {
|
|
5552
|
+
if (!html || !html.trim()) {
|
|
5553
|
+
return {
|
|
5554
|
+
subject: null,
|
|
5555
|
+
preheader: null,
|
|
5556
|
+
subjectLength: 0,
|
|
5557
|
+
preheaderLength: 0,
|
|
5558
|
+
truncation: [],
|
|
5559
|
+
issues: [{ rule: "missing-subject", severity: "warning", message: "No <title> tag found. Most email clients use this as the subject line." }]
|
|
5560
|
+
};
|
|
5561
|
+
}
|
|
5562
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
5563
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
5564
|
+
}
|
|
5565
|
+
const $ = cheerio8.load(html);
|
|
5566
|
+
return extractInboxPreviewFromDom($);
|
|
5567
|
+
}
|
|
5568
|
+
function extractInboxPreviewFromDom($) {
|
|
5569
|
+
var _a, _b;
|
|
5570
|
+
const issues = [];
|
|
5571
|
+
const titleEl = $("title");
|
|
5572
|
+
const subject = titleEl.length > 0 ? titleEl.first().text().trim() || null : null;
|
|
5573
|
+
const subjectLength = (_a = subject == null ? void 0 : subject.length) != null ? _a : 0;
|
|
5574
|
+
if (!subject) {
|
|
5575
|
+
issues.push({
|
|
5576
|
+
rule: "missing-subject",
|
|
5577
|
+
severity: "warning",
|
|
5578
|
+
message: "No <title> tag found. Most email clients use this as the subject line."
|
|
5579
|
+
});
|
|
5580
|
+
} else if (subjectLength > MAX_SUBJECT_LENGTH) {
|
|
5581
|
+
issues.push({
|
|
5582
|
+
rule: "subject-too-long",
|
|
5583
|
+
severity: "warning",
|
|
5584
|
+
message: `Subject is ${subjectLength} characters \u2014 may be truncated in inboxes (recommended: ${MAX_SUBJECT_LENGTH} or fewer).`
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
5587
|
+
const preheader = extractPreheaderText($);
|
|
5588
|
+
const preheaderLength = (_b = preheader == null ? void 0 : preheader.length) != null ? _b : 0;
|
|
5589
|
+
if (!preheader) {
|
|
5590
|
+
issues.push({
|
|
5591
|
+
rule: "missing-preheader",
|
|
5592
|
+
severity: "info",
|
|
5593
|
+
message: "No preheader text detected. Adding preview text improves open rates in crowded inboxes."
|
|
5594
|
+
});
|
|
5595
|
+
} else if (preheaderLength < MIN_PREHEADER_LENGTH) {
|
|
5596
|
+
issues.push({
|
|
5597
|
+
rule: "preheader-too-short",
|
|
5598
|
+
severity: "warning",
|
|
5599
|
+
message: `Preheader is only ${preheaderLength} characters \u2014 email clients may backfill with body text (recommended: ${MIN_PREHEADER_LENGTH}+).`
|
|
5600
|
+
});
|
|
5601
|
+
} else if (preheaderLength > MAX_PREHEADER_LENGTH) {
|
|
5602
|
+
issues.push({
|
|
5603
|
+
rule: "preheader-too-long",
|
|
5604
|
+
severity: "info",
|
|
5605
|
+
message: `Preheader is ${preheaderLength} characters \u2014 most clients show 40\u2013100 characters. Text beyond that is hidden.`
|
|
5606
|
+
});
|
|
5607
|
+
}
|
|
5608
|
+
if (hasZwnjPadding($)) {
|
|
5609
|
+
issues.push({
|
|
5610
|
+
rule: "zwnj-padding",
|
|
5611
|
+
severity: "info",
|
|
5612
|
+
message: "Preheader uses ‌ padding hack \u2014 works in most clients but may show garbled text in some."
|
|
5613
|
+
});
|
|
5614
|
+
}
|
|
5615
|
+
if (subject && EMOJI_PATTERN.test(subject)) {
|
|
5616
|
+
issues.push({
|
|
5617
|
+
rule: "emoji-in-subject",
|
|
5618
|
+
severity: "info",
|
|
5619
|
+
message: "Subject line contains emoji \u2014 renders inconsistently across email clients and may trigger spam filters."
|
|
5620
|
+
});
|
|
5621
|
+
}
|
|
5622
|
+
const truncation = computeTruncation(subject, preheader);
|
|
5623
|
+
return {
|
|
5624
|
+
subject,
|
|
5625
|
+
preheader,
|
|
5626
|
+
subjectLength,
|
|
5627
|
+
preheaderLength,
|
|
5628
|
+
truncation,
|
|
5629
|
+
issues
|
|
5630
|
+
};
|
|
5631
|
+
}
|
|
5632
|
+
function computeTruncation(subject, preheader) {
|
|
5633
|
+
return CLIENT_DISPLAY_LIMITS.map((limit) => {
|
|
5634
|
+
const truncatedSubject = subject && subject.length > limit.subjectLimit ? subject.slice(0, limit.subjectLimit - 1) + "\u2026" : subject;
|
|
5635
|
+
const truncatedPreheader = preheader && preheader.length > limit.preheaderLimit ? preheader.slice(0, limit.preheaderLimit - 1) + "\u2026" : preheader;
|
|
5636
|
+
return {
|
|
5637
|
+
client: limit.client,
|
|
5638
|
+
subjectLimit: limit.subjectLimit,
|
|
5639
|
+
preheaderLimit: limit.preheaderLimit,
|
|
5640
|
+
truncatedSubject: truncatedSubject != null ? truncatedSubject : null,
|
|
5641
|
+
truncatedPreheader: truncatedPreheader != null ? truncatedPreheader : null,
|
|
5642
|
+
subjectTruncated: !!subject && subject.length > limit.subjectLimit,
|
|
5643
|
+
preheaderTruncated: !!preheader && preheader.length > limit.preheaderLimit
|
|
5644
|
+
};
|
|
5645
|
+
});
|
|
5646
|
+
}
|
|
5647
|
+
function hasZwnjPadding($) {
|
|
5648
|
+
const body = $("body");
|
|
5649
|
+
if (!body.length) return false;
|
|
5650
|
+
const hiddenSelectors = [
|
|
5651
|
+
'div[style*="display:none"]',
|
|
5652
|
+
'div[style*="display: none"]',
|
|
5653
|
+
'span[style*="display:none"]',
|
|
5654
|
+
'span[style*="display: none"]',
|
|
5655
|
+
'div[style*="max-height:0"]',
|
|
5656
|
+
'div[style*="max-height: 0"]',
|
|
5657
|
+
'span[style*="max-height:0"]',
|
|
5658
|
+
'span[style*="max-height: 0"]',
|
|
5659
|
+
'[class*="preheader"]',
|
|
5660
|
+
'[class*="preview-text"]',
|
|
5661
|
+
'[class*="previewText"]'
|
|
5662
|
+
];
|
|
5663
|
+
for (const sel of hiddenSelectors) {
|
|
5664
|
+
const el = body.find(sel).first();
|
|
5665
|
+
if (el.length) {
|
|
5666
|
+
const rawHtml = el.html() || "";
|
|
5667
|
+
if (ZWNJ_PADDING_PATTERN.test(rawHtml)) return true;
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
return false;
|
|
5671
|
+
}
|
|
5672
|
+
function extractPreheaderText($) {
|
|
5673
|
+
const body = $("body");
|
|
5674
|
+
if (!body.length) return null;
|
|
5675
|
+
const hiddenSelectors = [
|
|
5676
|
+
'div[style*="display:none"]',
|
|
5677
|
+
'div[style*="display: none"]',
|
|
5678
|
+
'span[style*="display:none"]',
|
|
5679
|
+
'span[style*="display: none"]',
|
|
5680
|
+
'div[style*="visibility:hidden"]',
|
|
5681
|
+
'div[style*="visibility: hidden"]',
|
|
5682
|
+
'span[style*="visibility:hidden"]',
|
|
5683
|
+
'span[style*="visibility: hidden"]',
|
|
5684
|
+
'div[style*="max-height:0"]',
|
|
5685
|
+
'div[style*="max-height: 0"]',
|
|
5686
|
+
'span[style*="max-height:0"]',
|
|
5687
|
+
'span[style*="max-height: 0"]',
|
|
5688
|
+
'div[style*="mso-hide:all"]',
|
|
5689
|
+
'div[style*="mso-hide: all"]'
|
|
5690
|
+
];
|
|
5691
|
+
for (const sel of hiddenSelectors) {
|
|
5692
|
+
const el = body.find(sel).first();
|
|
5693
|
+
if (el.length) {
|
|
5694
|
+
const text = el.text().trim();
|
|
5695
|
+
if (text) return text;
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
const preheaderClasses = body.find(
|
|
5699
|
+
'[class*="preheader"], [class*="preview-text"], [class*="previewText"]'
|
|
5700
|
+
).first();
|
|
5701
|
+
if (preheaderClasses.length) {
|
|
5702
|
+
const text = preheaderClasses.text().trim();
|
|
5703
|
+
if (text) return text;
|
|
5704
|
+
}
|
|
5705
|
+
const firstText = getFirstVisibleText($, body);
|
|
5706
|
+
return firstText || null;
|
|
5707
|
+
}
|
|
5708
|
+
function getFirstVisibleText($, body) {
|
|
5709
|
+
const skipTags = /* @__PURE__ */ new Set(["style", "script", "head", "title"]);
|
|
5710
|
+
let result = null;
|
|
5711
|
+
body.find("*").each((_, el) => {
|
|
5712
|
+
var _a;
|
|
5713
|
+
if (result) return false;
|
|
5714
|
+
const tagName = (_a = el.tagName) == null ? void 0 : _a.toLowerCase();
|
|
5715
|
+
if (skipTags.has(tagName)) return;
|
|
5716
|
+
const style = $(el).attr("style") || "";
|
|
5717
|
+
if (style.includes("display:none") || style.includes("display: none") || style.includes("visibility:hidden") || style.includes("visibility: hidden")) {
|
|
5718
|
+
return;
|
|
5719
|
+
}
|
|
5720
|
+
const directText = $(el).contents().filter((_2, node) => node.type === "text").text().trim();
|
|
5721
|
+
if (directText && directText.length > 1) {
|
|
5722
|
+
const cleaned = directText.replace(/[\u200B\u00A0\s]+/g, " ").trim();
|
|
5723
|
+
if (cleaned.length > 1) {
|
|
5724
|
+
result = cleaned;
|
|
5725
|
+
return false;
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
});
|
|
5729
|
+
return result;
|
|
5730
|
+
}
|
|
5731
|
+
|
|
5732
|
+
// src/size-checker.ts
|
|
5733
|
+
var cheerio9 = __toESM(require("cheerio"), 1);
|
|
5734
|
+
function humanizeBytes(bytes) {
|
|
5735
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
5736
|
+
const kb = bytes / 1024;
|
|
5737
|
+
if (kb < 1024) return `${kb.toFixed(1)} KB`;
|
|
5738
|
+
const mb = kb / 1024;
|
|
5739
|
+
return `${mb.toFixed(2)} MB`;
|
|
5740
|
+
}
|
|
5741
|
+
function checkSizeFromDom(_$, html) {
|
|
5742
|
+
const htmlBytes = new TextEncoder().encode(html).length;
|
|
5743
|
+
const humanSize = humanizeBytes(htmlBytes);
|
|
5744
|
+
const issues = [];
|
|
5745
|
+
let clipped = false;
|
|
5746
|
+
if (htmlBytes > GMAIL_CLIP_THRESHOLD) {
|
|
5747
|
+
clipped = true;
|
|
5748
|
+
issues.push({
|
|
5749
|
+
rule: "gmail-clipped",
|
|
5750
|
+
severity: "error",
|
|
5751
|
+
message: `Email is ${humanSize} \u2014 Gmail will clip it at ~102 KB. Recipients see a "View entire message" link instead of your content.`,
|
|
5752
|
+
detail: `${htmlBytes} bytes exceeds the ${GMAIL_CLIP_THRESHOLD} byte threshold.`
|
|
5753
|
+
});
|
|
5754
|
+
} else if (htmlBytes > GMAIL_CLIP_WARNING_THRESHOLD) {
|
|
5755
|
+
issues.push({
|
|
5756
|
+
rule: "gmail-clip-warning",
|
|
5757
|
+
severity: "warning",
|
|
5758
|
+
message: `Email is ${humanSize} \u2014 approaching Gmail's ~102 KB clip threshold. Consider trimming.`,
|
|
5759
|
+
detail: `${htmlBytes} bytes is within ${GMAIL_CLIP_THRESHOLD - htmlBytes} bytes of the clip threshold.`
|
|
5760
|
+
});
|
|
5761
|
+
}
|
|
5762
|
+
return { htmlBytes, humanSize, clipped, issues };
|
|
5763
|
+
}
|
|
5764
|
+
function checkSize(html) {
|
|
5765
|
+
if (!html || !html.trim()) {
|
|
5766
|
+
return { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
|
|
5767
|
+
}
|
|
5768
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
5769
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
5770
|
+
}
|
|
5771
|
+
const $ = cheerio9.load(html);
|
|
5772
|
+
return checkSizeFromDom($, html);
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
// src/template-checker.ts
|
|
5776
|
+
var cheerio10 = __toESM(require("cheerio"), 1);
|
|
5777
|
+
function checkTemplateVariablesFromDom($) {
|
|
5778
|
+
const issues = [];
|
|
5779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5780
|
+
const textContent = extractTextContent($);
|
|
5781
|
+
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
5782
|
+
pattern.lastIndex = 0;
|
|
5783
|
+
let match;
|
|
5784
|
+
while ((match = pattern.exec(textContent)) !== null) {
|
|
5785
|
+
const variable = match[0];
|
|
5786
|
+
const key = `text:${variable}`;
|
|
5787
|
+
if (seen.has(key)) continue;
|
|
5788
|
+
seen.add(key);
|
|
5789
|
+
issues.push({
|
|
5790
|
+
rule: "unresolved-variable",
|
|
5791
|
+
severity: "error",
|
|
5792
|
+
message: `Unresolved ${label} variable "${variable}" found in text content.`,
|
|
5793
|
+
variable,
|
|
5794
|
+
location: "text"
|
|
5795
|
+
});
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
const attrSelectors = ["[href]", "[src]", "[alt]"];
|
|
5799
|
+
for (const sel of attrSelectors) {
|
|
5800
|
+
$(sel).each((_, el) => {
|
|
5801
|
+
const attrs = ["href", "src", "alt"];
|
|
5802
|
+
for (const attr of attrs) {
|
|
5803
|
+
const value = $(el).attr(attr);
|
|
5804
|
+
if (!value) continue;
|
|
5805
|
+
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
5806
|
+
pattern.lastIndex = 0;
|
|
5807
|
+
let match;
|
|
5808
|
+
while ((match = pattern.exec(value)) !== null) {
|
|
5809
|
+
const variable = match[0];
|
|
5810
|
+
const key = `attr:${attr}:${variable}`;
|
|
5811
|
+
if (seen.has(key)) continue;
|
|
5812
|
+
seen.add(key);
|
|
5813
|
+
issues.push({
|
|
5814
|
+
rule: "unresolved-variable",
|
|
5815
|
+
severity: "error",
|
|
5816
|
+
message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
|
|
5817
|
+
variable,
|
|
5818
|
+
location: "attribute"
|
|
5819
|
+
});
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
}
|
|
5823
|
+
});
|
|
5824
|
+
}
|
|
5825
|
+
return { unresolvedCount: issues.length, issues };
|
|
5826
|
+
}
|
|
5827
|
+
function extractTextContent($) {
|
|
5828
|
+
const clone = $.root().clone();
|
|
5829
|
+
clone.find("style, script, head").remove();
|
|
5830
|
+
return clone.text();
|
|
5831
|
+
}
|
|
5832
|
+
function checkTemplateVariables(html) {
|
|
5833
|
+
if (!html || !html.trim()) {
|
|
5834
|
+
return { unresolvedCount: 0, issues: [] };
|
|
5835
|
+
}
|
|
5836
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
5837
|
+
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
5838
|
+
}
|
|
5839
|
+
const $ = cheerio10.load(html);
|
|
5840
|
+
return checkTemplateVariablesFromDom($);
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
// src/audit.ts
|
|
5844
|
+
var cheerio11 = __toESM(require("cheerio"), 1);
|
|
5433
5845
|
function auditEmail(html, options) {
|
|
5434
5846
|
var _a;
|
|
5435
5847
|
if (!html || !html.trim()) {
|
|
@@ -5438,7 +5850,10 @@ function auditEmail(html, options) {
|
|
|
5438
5850
|
spam: EMPTY_SPAM,
|
|
5439
5851
|
links: EMPTY_LINKS,
|
|
5440
5852
|
accessibility: EMPTY_ACCESSIBILITY,
|
|
5441
|
-
images: EMPTY_IMAGES
|
|
5853
|
+
images: EMPTY_IMAGES,
|
|
5854
|
+
inboxPreview: EMPTY_INBOX_PREVIEW,
|
|
5855
|
+
size: EMPTY_SIZE,
|
|
5856
|
+
templateVariables: EMPTY_TEMPLATE
|
|
5442
5857
|
};
|
|
5443
5858
|
}
|
|
5444
5859
|
if (html.length > MAX_HTML_SIZE) {
|
|
@@ -5446,18 +5861,21 @@ function auditEmail(html, options) {
|
|
|
5446
5861
|
}
|
|
5447
5862
|
const framework = options == null ? void 0 : options.framework;
|
|
5448
5863
|
const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
|
|
5449
|
-
const $ =
|
|
5864
|
+
const $ = cheerio11.load(html);
|
|
5450
5865
|
const warnings = skip.has("compatibility") ? [] : analyzeEmailFromDom($, framework);
|
|
5451
5866
|
const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
|
|
5452
5867
|
const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
|
|
5453
5868
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
5454
5869
|
const accessibility = skip.has("accessibility") ? EMPTY_ACCESSIBILITY : checkAccessibilityFromDom($);
|
|
5455
5870
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
5456
|
-
|
|
5871
|
+
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
5872
|
+
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
5873
|
+
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($);
|
|
5874
|
+
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables };
|
|
5457
5875
|
}
|
|
5458
5876
|
|
|
5459
5877
|
// src/session.ts
|
|
5460
|
-
var
|
|
5878
|
+
var cheerio12 = __toESM(require("cheerio"), 1);
|
|
5461
5879
|
function createSession(html, options) {
|
|
5462
5880
|
if (!html || !html.trim()) {
|
|
5463
5881
|
const fw = options == null ? void 0 : options.framework;
|
|
@@ -5469,7 +5887,10 @@ function createSession(html, options) {
|
|
|
5469
5887
|
spam: EMPTY_SPAM,
|
|
5470
5888
|
links: EMPTY_LINKS,
|
|
5471
5889
|
accessibility: EMPTY_ACCESSIBILITY,
|
|
5472
|
-
images: EMPTY_IMAGES
|
|
5890
|
+
images: EMPTY_IMAGES,
|
|
5891
|
+
inboxPreview: EMPTY_INBOX_PREVIEW,
|
|
5892
|
+
size: EMPTY_SIZE,
|
|
5893
|
+
templateVariables: EMPTY_TEMPLATE
|
|
5473
5894
|
}),
|
|
5474
5895
|
analyze: () => [],
|
|
5475
5896
|
score: () => ({}),
|
|
@@ -5477,6 +5898,9 @@ function createSession(html, options) {
|
|
|
5477
5898
|
validateLinks: () => EMPTY_LINKS,
|
|
5478
5899
|
checkAccessibility: () => EMPTY_ACCESSIBILITY,
|
|
5479
5900
|
analyzeImages: () => EMPTY_IMAGES,
|
|
5901
|
+
extractInboxPreview: () => EMPTY_INBOX_PREVIEW,
|
|
5902
|
+
checkSize: () => EMPTY_SIZE,
|
|
5903
|
+
checkTemplateVariables: () => EMPTY_TEMPLATE,
|
|
5480
5904
|
transformForClient: (clientId) => ({ clientId, html: html || "", warnings: [] }),
|
|
5481
5905
|
transformForAllClients: () => [],
|
|
5482
5906
|
simulateDarkMode: (clientId) => ({ html: html || "", warnings: [] })
|
|
@@ -5485,7 +5909,7 @@ function createSession(html, options) {
|
|
|
5485
5909
|
if (html.length > MAX_HTML_SIZE) {
|
|
5486
5910
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
5487
5911
|
}
|
|
5488
|
-
const $ =
|
|
5912
|
+
const $ = cheerio12.load(html);
|
|
5489
5913
|
const framework = options == null ? void 0 : options.framework;
|
|
5490
5914
|
return {
|
|
5491
5915
|
html,
|
|
@@ -5499,7 +5923,10 @@ function createSession(html, options) {
|
|
|
5499
5923
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
5500
5924
|
const accessibility = skip.has("accessibility") ? EMPTY_ACCESSIBILITY : checkAccessibilityFromDom($);
|
|
5501
5925
|
const images = skip.has("images") ? EMPTY_IMAGES : analyzeImagesFromDom($);
|
|
5502
|
-
|
|
5926
|
+
const inboxPreview = skip.has("inboxPreview") ? EMPTY_INBOX_PREVIEW : extractInboxPreviewFromDom($);
|
|
5927
|
+
const size = skip.has("size") ? EMPTY_SIZE : checkSizeFromDom($, html);
|
|
5928
|
+
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($);
|
|
5929
|
+
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables };
|
|
5503
5930
|
},
|
|
5504
5931
|
analyze() {
|
|
5505
5932
|
return analyzeEmailFromDom($, framework);
|
|
@@ -5519,6 +5946,15 @@ function createSession(html, options) {
|
|
|
5519
5946
|
analyzeImages() {
|
|
5520
5947
|
return analyzeImagesFromDom($);
|
|
5521
5948
|
},
|
|
5949
|
+
extractInboxPreview() {
|
|
5950
|
+
return extractInboxPreviewFromDom($);
|
|
5951
|
+
},
|
|
5952
|
+
checkSize() {
|
|
5953
|
+
return checkSizeFromDom($, html);
|
|
5954
|
+
},
|
|
5955
|
+
checkTemplateVariables() {
|
|
5956
|
+
return checkTemplateVariablesFromDom($);
|
|
5957
|
+
},
|
|
5522
5958
|
// Transforms create isolated copies since they mutate the DOM
|
|
5523
5959
|
transformForClient(clientId) {
|
|
5524
5960
|
return transformForClient(html, clientId, framework);
|
|
@@ -5555,11 +5991,14 @@ var CompileError = class extends Error {
|
|
|
5555
5991
|
analyzeSpam,
|
|
5556
5992
|
auditEmail,
|
|
5557
5993
|
checkAccessibility,
|
|
5994
|
+
checkSize,
|
|
5995
|
+
checkTemplateVariables,
|
|
5558
5996
|
contrastRatio,
|
|
5559
5997
|
createSession,
|
|
5560
5998
|
diffResults,
|
|
5561
5999
|
errorWarnings,
|
|
5562
6000
|
estimateAiFixTokens,
|
|
6001
|
+
extractInboxPreview,
|
|
5563
6002
|
generateAiFix,
|
|
5564
6003
|
generateCompatibilityScore,
|
|
5565
6004
|
generateFixPrompt,
|