@emailens/engine 0.11.3 → 0.11.6
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 +1 -1
- package/dist/index.cjs +128 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +128 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10571,9 +10571,12 @@ function transformForClient(html, clientId, framework) {
|
|
|
10571
10571
|
]
|
|
10572
10572
|
};
|
|
10573
10573
|
}
|
|
10574
|
-
const
|
|
10574
|
+
const wordEngine = clientId === "outlook-windows-legacy" && /<!--\[if/i.test(html);
|
|
10575
|
+
const source = wordEngine ? resolveMsoBranch(html) : html;
|
|
10575
10576
|
const downleveled = downlevelCSS(source);
|
|
10576
|
-
|
|
10577
|
+
const result = applyTransform(downleveled, config, framework);
|
|
10578
|
+
if (wordEngine) result.html = vmlToCss(result.html);
|
|
10579
|
+
return result;
|
|
10577
10580
|
}
|
|
10578
10581
|
function transformForAllClients(html, framework) {
|
|
10579
10582
|
if (!html || !html.trim()) {
|
|
@@ -10587,14 +10590,18 @@ function transformForAllClients(html, framework) {
|
|
|
10587
10590
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
10588
10591
|
}
|
|
10589
10592
|
const downleveled = downlevelCSS(html);
|
|
10590
|
-
const
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10593
|
+
const hasOutlookBranch = /<!--\[if/i.test(html);
|
|
10594
|
+
const outlookSource = hasOutlookBranch ? downlevelCSS(resolveMsoBranch(html)) : downleveled;
|
|
10595
|
+
return Object.keys(CLIENT_CONFIGS).map((clientId) => {
|
|
10596
|
+
const wordEngine = clientId === "outlook-windows-legacy" && hasOutlookBranch;
|
|
10597
|
+
const result = applyTransform(
|
|
10598
|
+
wordEngine ? outlookSource : downleveled,
|
|
10594
10599
|
CLIENT_CONFIGS[clientId],
|
|
10595
10600
|
framework
|
|
10596
|
-
)
|
|
10597
|
-
|
|
10601
|
+
);
|
|
10602
|
+
if (wordEngine) result.html = vmlToCss(result.html);
|
|
10603
|
+
return result;
|
|
10604
|
+
});
|
|
10598
10605
|
}
|
|
10599
10606
|
|
|
10600
10607
|
// src/analyze.ts
|
|
@@ -11494,6 +11501,7 @@ function fromHtml(html, empty, fn, options) {
|
|
|
11494
11501
|
}
|
|
11495
11502
|
|
|
11496
11503
|
// src/analyze.ts
|
|
11504
|
+
var WORD_ENGINE_CLIENT = "outlook-windows-legacy";
|
|
11497
11505
|
var HTML_ELEMENT_SELECTORS = {
|
|
11498
11506
|
"<style>": "style",
|
|
11499
11507
|
"<link>": "link[rel='stylesheet']",
|
|
@@ -11834,7 +11842,32 @@ function analyzeEmail(html, framework, options) {
|
|
|
11834
11842
|
throw new Error(`HTML input exceeds ${MAX_HTML_SIZE / 1024}KB limit.`);
|
|
11835
11843
|
}
|
|
11836
11844
|
const $ = loadHtml(html, options);
|
|
11837
|
-
return
|
|
11845
|
+
return analyzeAllBranches($, html, framework, (options == null ? void 0 : options.positions) ? html : void 0);
|
|
11846
|
+
}
|
|
11847
|
+
function analyzeAllBranches($, html, framework, source) {
|
|
11848
|
+
return withOutlookBranch(html, analyzeEmailFromDom($, framework, source), framework);
|
|
11849
|
+
}
|
|
11850
|
+
function withOutlookBranch(html, warnings, framework) {
|
|
11851
|
+
if (!/<!--\[if/i.test(html)) return warnings;
|
|
11852
|
+
let branchWarnings;
|
|
11853
|
+
try {
|
|
11854
|
+
const resolved = resolveMsoBranch(html);
|
|
11855
|
+
if (resolved === html) return warnings;
|
|
11856
|
+
branchWarnings = analyzeEmailFromDom(loadHtml(resolved), framework).filter((w) => w.client === WORD_ENGINE_CLIENT);
|
|
11857
|
+
} catch (e) {
|
|
11858
|
+
return warnings;
|
|
11859
|
+
}
|
|
11860
|
+
const kept = warnings.filter((w) => w.client !== WORD_ENGINE_CLIENT);
|
|
11861
|
+
const firstPassWord = warnings.filter((w) => w.client === WORD_ENGINE_CLIENT);
|
|
11862
|
+
const byKey = new Map(firstPassWord.map((w) => [`${w.property}:${w.severity}`, w]));
|
|
11863
|
+
const merged = branchWarnings.map((w) => {
|
|
11864
|
+
var _a;
|
|
11865
|
+
return (_a = byKey.get(`${w.property}:${w.severity}`)) != null ? _a : w;
|
|
11866
|
+
});
|
|
11867
|
+
for (const w of firstPassWord) {
|
|
11868
|
+
if (!merged.some((m) => m.property === w.property && m.severity === w.severity)) merged.push(w);
|
|
11869
|
+
}
|
|
11870
|
+
return [...kept, ...merged];
|
|
11838
11871
|
}
|
|
11839
11872
|
function getFixType(prop) {
|
|
11840
11873
|
return STRUCTURAL_FIX_PROPERTIES.has(prop) ? "structural" : "css";
|
|
@@ -14300,6 +14333,7 @@ function checkTemplateVariables(html, options) {
|
|
|
14300
14333
|
|
|
14301
14334
|
// src/overflow-checker.ts
|
|
14302
14335
|
import * as csstree7 from "css-tree";
|
|
14336
|
+
var RESPONSIVE_MIN_WIDTH = 320;
|
|
14303
14337
|
function fixedPxWidth($el) {
|
|
14304
14338
|
const style = $el.attr("style") || "";
|
|
14305
14339
|
const styleMatch = style.match(/(?:^|[;\s])width\s*:\s*(\d+)px/i);
|
|
@@ -14434,6 +14468,30 @@ function checkOverflowFromDom($, source) {
|
|
|
14434
14468
|
}, loc ? { loc, locs: [loc] } : {}));
|
|
14435
14469
|
}
|
|
14436
14470
|
}
|
|
14471
|
+
if (!/@media/i.test($.html())) {
|
|
14472
|
+
let widest = 0;
|
|
14473
|
+
let widestLabel = "";
|
|
14474
|
+
let widestLoc;
|
|
14475
|
+
$("table, td, div").each((_, el) => {
|
|
14476
|
+
const $el = $(el);
|
|
14477
|
+
const width = fixedPxWidth($el);
|
|
14478
|
+
if (width === null || width < RESPONSIVE_MIN_WIDTH) return;
|
|
14479
|
+
if (isFluid($el.attr("style") || "")) return;
|
|
14480
|
+
if (width <= widest) return;
|
|
14481
|
+
widest = width;
|
|
14482
|
+
widestLabel = `<${(el.tagName || "element").toLowerCase()}>`;
|
|
14483
|
+
const fromStyle = /(?:^|[;\s])width\s*:\s*\d+px/i.test($el.attr("style") || "");
|
|
14484
|
+
widestLoc = locOfAttr(el, fromStyle ? "style" : "width");
|
|
14485
|
+
});
|
|
14486
|
+
if (widest > 0) {
|
|
14487
|
+
issues.push(__spreadValues({
|
|
14488
|
+
rule: "no-responsive-rules",
|
|
14489
|
+
severity: "warning",
|
|
14490
|
+
message: `${widestLabel} is a fixed ${widest}px wide and the email has no @media rules, so nothing tells a narrow screen what to do.`,
|
|
14491
|
+
detail: `Each mobile client then decides for itself: one scales the layout to fit, another leaves it to scroll, a third stretches the container while any child with a px max-width stays put and leaves gutters. Give the container width:100% with max-width:${widest}px, and add a breakpoint for the parts that need to stack.`
|
|
14492
|
+
}, widestLoc ? { loc: widestLoc, locs: [widestLoc] } : {}));
|
|
14493
|
+
}
|
|
14494
|
+
}
|
|
14437
14495
|
return { hasOverflow: issues.length > 0, issues };
|
|
14438
14496
|
}
|
|
14439
14497
|
function checkOverflow(html, options) {
|
|
@@ -14540,13 +14598,70 @@ function checkLooseText(issues, seen, open, inner, source) {
|
|
|
14540
14598
|
detail: `Verified in Outlook Classic: the shape renders its fill and shape correctly and shows no text at all, so the reader gets a blank coloured block where the label should be. Nothing errors and the HTML fallback still reads correctly in every other client, so this ships unnoticed. Wrap the text in <center> (what the bulletproof-button pattern uses) or in <v:textbox>.`
|
|
14541
14599
|
}, locAt(source, open.offset, open.length)));
|
|
14542
14600
|
}
|
|
14601
|
+
var GHOST_TAGS = /* @__PURE__ */ new Set(["table", "tr", "td", "th", "tbody", "thead", "tfoot"]);
|
|
14602
|
+
function ghostTags(blocks) {
|
|
14603
|
+
const tags = [];
|
|
14604
|
+
const re = /<(\/?)(table|tr|td|th|tbody|thead|tfoot)\b((?:[^>"']|"[^"]*"|'[^']*')*?)(\/?)>/gi;
|
|
14605
|
+
for (const block of blocks) {
|
|
14606
|
+
let m;
|
|
14607
|
+
re.lastIndex = 0;
|
|
14608
|
+
while ((m = re.exec(block.inner)) !== null) {
|
|
14609
|
+
tags.push({
|
|
14610
|
+
name: m[2].toLowerCase(),
|
|
14611
|
+
closing: m[1] === "/",
|
|
14612
|
+
selfClosing: m[4] === "/",
|
|
14613
|
+
attrs: m[3],
|
|
14614
|
+
offset: block.offset + m.index,
|
|
14615
|
+
length: m[0].length
|
|
14616
|
+
});
|
|
14617
|
+
}
|
|
14618
|
+
}
|
|
14619
|
+
return tags;
|
|
14620
|
+
}
|
|
14621
|
+
function checkGhostTables(issues, seen, blocks, source) {
|
|
14622
|
+
var _a;
|
|
14623
|
+
const tags = ghostTags(blocks);
|
|
14624
|
+
if (tags.length === 0) return;
|
|
14625
|
+
const opens = /* @__PURE__ */ new Map();
|
|
14626
|
+
const strays = [];
|
|
14627
|
+
for (const tag of tags) {
|
|
14628
|
+
if (tag.selfClosing) continue;
|
|
14629
|
+
const list = (_a = opens.get(tag.name)) != null ? _a : [];
|
|
14630
|
+
if (tag.closing) {
|
|
14631
|
+
if (list.length === 0) strays.push(tag);
|
|
14632
|
+
else list.pop();
|
|
14633
|
+
} else list.push(tag);
|
|
14634
|
+
opens.set(tag.name, list);
|
|
14635
|
+
}
|
|
14636
|
+
for (const [name, list] of opens) {
|
|
14637
|
+
if (!GHOST_TAGS.has(name)) continue;
|
|
14638
|
+
for (const tag of list.slice(0, 1)) {
|
|
14639
|
+
add(issues, seen, `ghost-open:${name}`, withLoc({
|
|
14640
|
+
rule: "ghost-table-unbalanced",
|
|
14641
|
+
severity: "error",
|
|
14642
|
+
message: `<${name}> is opened inside an Outlook conditional comment and never closed.`,
|
|
14643
|
+
detail: `The closing half of a ghost wrapper lives in its own <!--[if mso]> block, usually much further down the file, and is easy to lose in an edit. Outlook is left with a table that never ends, so it swallows the rest of the email; every other client sees only comment nodes and renders normally, which is why this survives review. Add the matching </${name}> in a closing conditional block.`
|
|
14644
|
+
}, locAt(source, tag.offset, tag.length)));
|
|
14645
|
+
}
|
|
14646
|
+
}
|
|
14647
|
+
for (const tag of strays.slice(0, 1)) {
|
|
14648
|
+
add(issues, seen, `ghost-stray:${tag.name}`, withLoc({
|
|
14649
|
+
rule: "ghost-table-unbalanced",
|
|
14650
|
+
severity: "error",
|
|
14651
|
+
message: `Closing </${tag.name}> inside an Outlook conditional comment has no matching opening tag.`,
|
|
14652
|
+
detail: `The opening half of the ghost wrapper is missing or was removed. Outlook receives a stray closing tag, which ends a table it never started and can truncate the layout from that point.`
|
|
14653
|
+
}, locAt(source, tag.offset, tag.length)));
|
|
14654
|
+
}
|
|
14655
|
+
}
|
|
14543
14656
|
function checkVml(html, options) {
|
|
14544
14657
|
if (!html || !html.trim()) return EMPTY_VML;
|
|
14545
14658
|
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
14546
|
-
const
|
|
14547
|
-
|
|
14659
|
+
const blocks = msoBlocks(html);
|
|
14660
|
+
const tags = vmlTags(blocks);
|
|
14548
14661
|
const issues = [];
|
|
14549
14662
|
const seen = /* @__PURE__ */ new Map();
|
|
14663
|
+
checkGhostTables(issues, seen, blocks, source);
|
|
14664
|
+
if (tags.length === 0) return { hasVml: false, issues };
|
|
14550
14665
|
const openShapes = [];
|
|
14551
14666
|
let groupDepth = 0;
|
|
14552
14667
|
for (const tag of tags) {
|
|
@@ -14979,7 +15094,7 @@ function runAudit($, html, framework, options) {
|
|
|
14979
15094
|
var _a;
|
|
14980
15095
|
const skip = new Set((_a = options == null ? void 0 : options.skip) != null ? _a : []);
|
|
14981
15096
|
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
14982
|
-
const warnings = skip.has("compatibility") ? [] :
|
|
15097
|
+
const warnings = skip.has("compatibility") ? [] : analyzeAllBranches($, html, framework, source);
|
|
14983
15098
|
const scores = skip.has("compatibility") ? {} : generateCompatibilityScore(warnings);
|
|
14984
15099
|
const spam = skip.has("spam") ? EMPTY_SPAM : analyzeSpamFromDom($, options == null ? void 0 : options.spam);
|
|
14985
15100
|
const links = skip.has("links") ? EMPTY_LINKS : validateLinksFromDom($);
|
|
@@ -15144,7 +15259,7 @@ function createSession(html, options) {
|
|
|
15144
15259
|
return runAudit($, html, framework, __spreadProps(__spreadValues({}, opts), { positions: options == null ? void 0 : options.positions }));
|
|
15145
15260
|
},
|
|
15146
15261
|
analyze() {
|
|
15147
|
-
return
|
|
15262
|
+
return analyzeAllBranches($, html, framework, source);
|
|
15148
15263
|
},
|
|
15149
15264
|
score(warnings) {
|
|
15150
15265
|
return generateCompatibilityScore(warnings);
|