@emailens/engine 0.11.0 → 0.11.2
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 +7 -4
- package/dist/compile/index.d.cts +2 -2
- package/dist/compile/index.d.ts +2 -2
- package/dist/index.cjs +328 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -5
- package/dist/index.d.ts +74 -5
- package/dist/index.js +323 -19
- package/dist/index.js.map +1 -1
- package/dist/{react-email-D24HkW2Z.d.ts → react-email-BROBcC8u.d.ts} +1 -1
- package/dist/{react-email-BWIfwG7U.d.cts → react-email-DuZAgJtw.d.cts} +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/{types-BFhPXh3c.d.cts → types-bvGDId6f.d.cts} +9 -1
- package/dist/{types-BFhPXh3c.d.ts → types-bvGDId6f.d.ts} +9 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9740,6 +9740,114 @@ function downlevelCSS(html) {
|
|
|
9740
9740
|
return $.html();
|
|
9741
9741
|
}
|
|
9742
9742
|
|
|
9743
|
+
// src/vml-render.ts
|
|
9744
|
+
function px(value) {
|
|
9745
|
+
if (!value) return void 0;
|
|
9746
|
+
const m = value.trim().match(/^(-?[\d.]+)(?:px)?$/i);
|
|
9747
|
+
return m ? parseFloat(m[1]) : void 0;
|
|
9748
|
+
}
|
|
9749
|
+
function styleProps(style) {
|
|
9750
|
+
const out = {};
|
|
9751
|
+
for (const decl of style.split(";")) {
|
|
9752
|
+
const i = decl.indexOf(":");
|
|
9753
|
+
if (i > 0) out[decl.slice(0, i).trim().toLowerCase()] = decl.slice(i + 1).trim();
|
|
9754
|
+
}
|
|
9755
|
+
return out;
|
|
9756
|
+
}
|
|
9757
|
+
function attrs(raw) {
|
|
9758
|
+
var _a, _b;
|
|
9759
|
+
const out = {};
|
|
9760
|
+
for (const m of raw.matchAll(/([\w:-]+)\s*=\s*"([^"]*)"|([\w:-]+)\s*=\s*'([^']*)'/g)) {
|
|
9761
|
+
out[((_a = m[1]) != null ? _a : m[3]).toLowerCase()] = (_b = m[2]) != null ? _b : m[4];
|
|
9762
|
+
}
|
|
9763
|
+
return out;
|
|
9764
|
+
}
|
|
9765
|
+
function arcsizeToRadius(arcsize, width, height) {
|
|
9766
|
+
const raw = arcsize.trim();
|
|
9767
|
+
const pct = raw.endsWith("%") ? parseFloat(raw) : parseFloat(raw) * 100;
|
|
9768
|
+
if (!Number.isFinite(pct)) return 0;
|
|
9769
|
+
return Math.min(100, Math.max(0, pct)) / 100 * (Math.min(width, height) / 2);
|
|
9770
|
+
}
|
|
9771
|
+
function resolveMsoBranch(html) {
|
|
9772
|
+
return html.replace(/<!--\[if\s*!\s*(?:mso|vml)[^\]]*\]><!-->([\s\S]*?)<!--<!\[endif\]-->/gi, "").replace(/<!--\[if[^\]]*(?:mso|vml)[^\]]*\]>([\s\S]*?)<!\[endif\]-->/gi, "$1");
|
|
9773
|
+
}
|
|
9774
|
+
function vmlToCss(html) {
|
|
9775
|
+
let out = html;
|
|
9776
|
+
out = out.replace(
|
|
9777
|
+
/<v:roundrect([^>]*)>([\s\S]*?)<\/v:roundrect>/gi,
|
|
9778
|
+
(_m, rawAttrs, inner) => {
|
|
9779
|
+
var _a, _b, _c, _d, _e, _f;
|
|
9780
|
+
const a = attrs(rawAttrs);
|
|
9781
|
+
const s = styleProps((_a = a.style) != null ? _a : "");
|
|
9782
|
+
const w = (_b = px(s.width)) != null ? _b : 0;
|
|
9783
|
+
const h = (_c = px(s.height)) != null ? _c : 0;
|
|
9784
|
+
const radius = a.arcsize ? arcsizeToRadius(a.arcsize, w, h) : 0.2 * (Math.min(w, h) / 2);
|
|
9785
|
+
const fill = (_d = a.fillcolor) != null ? _d : "transparent";
|
|
9786
|
+
const stroked = !(a.stroke === "f" || a.stroke === "false");
|
|
9787
|
+
const middle = /middle/i.test((_e = s["v-text-anchor"]) != null ? _e : "");
|
|
9788
|
+
const label = inner.replace(/<\/?(?:w:anchorlock|center)[^>]*>/gi, "").trim();
|
|
9789
|
+
const box = [
|
|
9790
|
+
w ? `width:${w}px` : "",
|
|
9791
|
+
h ? `height:${h}px` : "",
|
|
9792
|
+
`border-radius:${radius}px`,
|
|
9793
|
+
`background:${fill}`,
|
|
9794
|
+
stroked ? `border:1px solid ${(_f = a.strokecolor) != null ? _f : "#000"}` : "border:none",
|
|
9795
|
+
"display:inline-flex",
|
|
9796
|
+
"justify-content:center",
|
|
9797
|
+
`align-items:${middle ? "center" : "flex-start"}`,
|
|
9798
|
+
"overflow:hidden",
|
|
9799
|
+
"text-align:center"
|
|
9800
|
+
].filter(Boolean).join(";");
|
|
9801
|
+
const href = a.href ? ` data-href="${a.href}"` : "";
|
|
9802
|
+
return `<div data-vml="roundrect"${href} style="${box}">${label}</div>`;
|
|
9803
|
+
}
|
|
9804
|
+
);
|
|
9805
|
+
out = out.replace(
|
|
9806
|
+
/<v:rect([^>]*)>([\s\S]*?)<\/v:rect>/gi,
|
|
9807
|
+
(_m, rawAttrs, inner) => {
|
|
9808
|
+
var _a, _b, _c, _d, _e, _f;
|
|
9809
|
+
const a = attrs(rawAttrs);
|
|
9810
|
+
const s = styleProps((_a = a.style) != null ? _a : "");
|
|
9811
|
+
const w = px(s.width);
|
|
9812
|
+
const h = px(s.height);
|
|
9813
|
+
const fillTag = inner.match(/<v:fill([^>]*?)\/?>/i);
|
|
9814
|
+
const f = fillTag ? attrs(fillTag[1]) : {};
|
|
9815
|
+
const type = ((_b = f.type) != null ? _b : "solid").toLowerCase();
|
|
9816
|
+
let background = "";
|
|
9817
|
+
if (f.src && type === "tile") {
|
|
9818
|
+
background = `background-image:url('${f.src}');background-repeat:repeat;`;
|
|
9819
|
+
} else if (f.src) {
|
|
9820
|
+
background = `background-image:url('${f.src}');background-size:cover;background-position:center;`;
|
|
9821
|
+
} else if (type === "gradient" || type === "gradientradial") {
|
|
9822
|
+
const from = (_d = (_c = f.color) != null ? _c : a.fillcolor) != null ? _d : "transparent";
|
|
9823
|
+
const to = (_e = f.color2) != null ? _e : from;
|
|
9824
|
+
const angle = f.angle ? `${parseFloat(f.angle)}deg` : "180deg";
|
|
9825
|
+
background = type === "gradientradial" ? `background-image:radial-gradient(${from},${to});` : `background-image:linear-gradient(${angle},${from},${to});`;
|
|
9826
|
+
}
|
|
9827
|
+
const solid = (_f = f.color) != null ? _f : a.fillcolor;
|
|
9828
|
+
const tb = inner.match(/<v:textbox([^>]*)>([\s\S]*?)<\/v:textbox>/i);
|
|
9829
|
+
const inset = tb ? attrs(tb[1]).inset : void 0;
|
|
9830
|
+
const padding = inset ? inset.split(",").map((p) => {
|
|
9831
|
+
var _a2;
|
|
9832
|
+
return `${(_a2 = px(p.trim())) != null ? _a2 : 0}px`;
|
|
9833
|
+
}).join(" ") : "0";
|
|
9834
|
+
const content = tb ? tb[2] : inner.replace(/<v:fill[^>]*?\/?>/gi, "");
|
|
9835
|
+
const box = [
|
|
9836
|
+
w !== void 0 ? `width:${w}px` : "",
|
|
9837
|
+
h !== void 0 ? `height:${h}px` : "",
|
|
9838
|
+
solid ? `background-color:${solid}` : "",
|
|
9839
|
+
`padding:${padding}`
|
|
9840
|
+
].filter(Boolean).join(";");
|
|
9841
|
+
return `<div data-vml="rect" style="${background}${box}">${content}</div>`;
|
|
9842
|
+
}
|
|
9843
|
+
);
|
|
9844
|
+
return out.replace(/<\/?v:(?:fill|textbox|stroke|shadow|imagedata|path|formulas|handles)[^>]*?\/?>/gi, "");
|
|
9845
|
+
}
|
|
9846
|
+
function renderOutlookBranch(html) {
|
|
9847
|
+
if (!/<!--\[if/i.test(html)) return html;
|
|
9848
|
+
return vmlToCss(resolveMsoBranch(html));
|
|
9849
|
+
}
|
|
9850
|
+
|
|
9743
9851
|
// src/constants.ts
|
|
9744
9852
|
var MAX_HTML_SIZE = 2 * 1024 * 1024;
|
|
9745
9853
|
var MAX_WARNING_LOCATIONS = 100;
|
|
@@ -9793,6 +9901,7 @@ var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, pr
|
|
|
9793
9901
|
var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
|
|
9794
9902
|
var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
|
|
9795
9903
|
var EMPTY_OVERFLOW = { hasOverflow: false, issues: [] };
|
|
9904
|
+
var EMPTY_VML = { hasVml: false, issues: [] };
|
|
9796
9905
|
var EMPTY_VISUAL = { issues: [] };
|
|
9797
9906
|
var EMAIL_MAX_WIDTH = 600;
|
|
9798
9907
|
var UNBREAKABLE_STRING_LENGTH = 30;
|
|
@@ -10462,7 +10571,8 @@ function transformForClient(html, clientId, framework) {
|
|
|
10462
10571
|
]
|
|
10463
10572
|
};
|
|
10464
10573
|
}
|
|
10465
|
-
const
|
|
10574
|
+
const source = clientId === "outlook-windows-legacy" ? renderOutlookBranch(html) : html;
|
|
10575
|
+
const downleveled = downlevelCSS(source);
|
|
10466
10576
|
return applyTransform(downleveled, config, framework);
|
|
10467
10577
|
}
|
|
10468
10578
|
function transformForAllClients(html, framework) {
|
|
@@ -11001,11 +11111,11 @@ function locOfElement(el) {
|
|
|
11001
11111
|
if (!raw) return void 0;
|
|
11002
11112
|
return toLoc((_a = raw.startTag) != null ? _a : raw);
|
|
11003
11113
|
}
|
|
11004
|
-
function locOfAttr(el,
|
|
11114
|
+
function locOfAttr(el, attr2) {
|
|
11005
11115
|
var _a, _b, _c, _d;
|
|
11006
11116
|
const raw = el == null ? void 0 : el.sourceCodeLocation;
|
|
11007
11117
|
if (!raw) return void 0;
|
|
11008
|
-
const attrLoc = (_d = (_a = raw.attrs) == null ? void 0 : _a[
|
|
11118
|
+
const attrLoc = (_d = (_a = raw.attrs) == null ? void 0 : _a[attr2]) != null ? _d : (_c = (_b = raw.startTag) == null ? void 0 : _b.attrs) == null ? void 0 : _c[attr2];
|
|
11009
11119
|
return attrLoc ? toLoc(attrLoc) : locOfElement(el);
|
|
11010
11120
|
}
|
|
11011
11121
|
function locInAttr(attrLoc, source, property, occurrence = 0) {
|
|
@@ -13200,9 +13310,9 @@ function mediaApplies(prelude, ctx) {
|
|
|
13200
13310
|
const scheme = text.match(/prefers-color-scheme\s*:\s*(dark|light)/);
|
|
13201
13311
|
if (scheme && scheme[1] === "dark" !== ctx.dark) return false;
|
|
13202
13312
|
for (const m of text.matchAll(/(max|min)-width\s*:\s*([^)]+)/g)) {
|
|
13203
|
-
const
|
|
13204
|
-
if (
|
|
13205
|
-
if (m[1] === "max" ? ctx.width >
|
|
13313
|
+
const px2 = mediaPx(m[2]);
|
|
13314
|
+
if (px2 === null) continue;
|
|
13315
|
+
if (m[1] === "max" ? ctx.width > px2 : ctx.width < px2) return false;
|
|
13206
13316
|
}
|
|
13207
13317
|
return true;
|
|
13208
13318
|
}
|
|
@@ -13423,10 +13533,10 @@ function resolveBackground($, el, cascade) {
|
|
|
13423
13533
|
function isLargeText(size, weight) {
|
|
13424
13534
|
const match = size == null ? void 0 : size.match(/^(\d+(?:\.\d+)?)(px|pt)/i);
|
|
13425
13535
|
if (!match) return false;
|
|
13426
|
-
const
|
|
13536
|
+
const px2 = match[2].toLowerCase() === "pt" ? parseFloat(match[1]) * 1.333 : parseFloat(match[1]);
|
|
13427
13537
|
const w = weight == null ? void 0 : weight.trim().toLowerCase();
|
|
13428
13538
|
const bold = w === "bold" || w === "bolder" || !!w && parseInt(w, 10) >= 700;
|
|
13429
|
-
return
|
|
13539
|
+
return px2 >= 18 || px2 >= 14 && bold;
|
|
13430
13540
|
}
|
|
13431
13541
|
function inherited($, el, cascade, prop) {
|
|
13432
13542
|
var _a, _b;
|
|
@@ -14147,23 +14257,23 @@ function checkTemplateVariablesFromDom($, source) {
|
|
|
14147
14257
|
const attrSelectors = ["[href]", "[src]", "[alt]"];
|
|
14148
14258
|
for (const sel of attrSelectors) {
|
|
14149
14259
|
$(sel).each((_, el) => {
|
|
14150
|
-
const
|
|
14151
|
-
for (const
|
|
14152
|
-
const value = $(el).attr(
|
|
14260
|
+
const attrs2 = ["href", "src", "alt"];
|
|
14261
|
+
for (const attr2 of attrs2) {
|
|
14262
|
+
const value = $(el).attr(attr2);
|
|
14153
14263
|
if (!value) continue;
|
|
14154
14264
|
for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
|
|
14155
14265
|
pattern.lastIndex = 0;
|
|
14156
14266
|
let match;
|
|
14157
14267
|
while ((match = pattern.exec(value)) !== null) {
|
|
14158
14268
|
const variable = match[0];
|
|
14159
|
-
const key = `attr:${
|
|
14269
|
+
const key = `attr:${attr2}:${variable}`;
|
|
14160
14270
|
if (seen.has(key)) continue;
|
|
14161
14271
|
seen.add(key);
|
|
14162
|
-
const loc = locOfAttr(el,
|
|
14272
|
+
const loc = locOfAttr(el, attr2);
|
|
14163
14273
|
issues.push(__spreadValues({
|
|
14164
14274
|
rule: "unresolved-variable",
|
|
14165
14275
|
severity: "error",
|
|
14166
|
-
message: `Unresolved ${label} variable "${variable}" found in ${
|
|
14276
|
+
message: `Unresolved ${label} variable "${variable}" found in ${attr2} attribute.`,
|
|
14167
14277
|
variable,
|
|
14168
14278
|
location: "attribute"
|
|
14169
14279
|
}, loc ? { loc } : {}));
|
|
@@ -14189,8 +14299,8 @@ function fixedPxWidth($el) {
|
|
|
14189
14299
|
const style = $el.attr("style") || "";
|
|
14190
14300
|
const styleMatch = style.match(/(?:^|[;\s])width\s*:\s*(\d+)px/i);
|
|
14191
14301
|
if (styleMatch) return parseInt(styleMatch[1], 10);
|
|
14192
|
-
const
|
|
14193
|
-
if (
|
|
14302
|
+
const attr2 = $el.attr("width");
|
|
14303
|
+
if (attr2 && /^\d+$/.test(attr2.trim())) return parseInt(attr2.trim(), 10);
|
|
14194
14304
|
return null;
|
|
14195
14305
|
}
|
|
14196
14306
|
function isFluid(style) {
|
|
@@ -14330,6 +14440,193 @@ function checkOverflow(html, options) {
|
|
|
14330
14440
|
);
|
|
14331
14441
|
}
|
|
14332
14442
|
|
|
14443
|
+
// src/vml-checker.ts
|
|
14444
|
+
var SHAPE_TAGS = /* @__PURE__ */ new Set([
|
|
14445
|
+
"rect",
|
|
14446
|
+
"roundrect",
|
|
14447
|
+
"oval",
|
|
14448
|
+
"line",
|
|
14449
|
+
"polyline",
|
|
14450
|
+
"curve",
|
|
14451
|
+
"arc",
|
|
14452
|
+
"shape",
|
|
14453
|
+
"image",
|
|
14454
|
+
"background"
|
|
14455
|
+
]);
|
|
14456
|
+
var GROUP_TAGS = /* @__PURE__ */ new Set(["group"]);
|
|
14457
|
+
function msoBlocks(html) {
|
|
14458
|
+
const blocks = [];
|
|
14459
|
+
const re = /<!--\[if([^\]]*)\]>([\s\S]*?)<!\[endif\]-->/g;
|
|
14460
|
+
let m;
|
|
14461
|
+
while ((m = re.exec(html)) !== null) {
|
|
14462
|
+
const condition = m[1];
|
|
14463
|
+
if (/!\s*(mso|vml)/i.test(condition)) continue;
|
|
14464
|
+
if (!/mso|vml/i.test(condition)) continue;
|
|
14465
|
+
blocks.push({ inner: m[2], offset: m.index + m[0].length - m[2].length - "<![endif]-->".length });
|
|
14466
|
+
}
|
|
14467
|
+
return blocks;
|
|
14468
|
+
}
|
|
14469
|
+
function vmlTags(blocks) {
|
|
14470
|
+
const tags = [];
|
|
14471
|
+
const re = /<(\/?)v:([a-z]+)((?:[^>"']|"[^"]*"|'[^']*')*?)(\/?)>/gi;
|
|
14472
|
+
for (const block of blocks) {
|
|
14473
|
+
let m;
|
|
14474
|
+
re.lastIndex = 0;
|
|
14475
|
+
while ((m = re.exec(block.inner)) !== null) {
|
|
14476
|
+
tags.push({
|
|
14477
|
+
name: m[2].toLowerCase(),
|
|
14478
|
+
closing: m[1] === "/",
|
|
14479
|
+
selfClosing: m[4] === "/",
|
|
14480
|
+
attrs: m[3],
|
|
14481
|
+
offset: block.offset + m.index,
|
|
14482
|
+
length: m[0].length
|
|
14483
|
+
});
|
|
14484
|
+
}
|
|
14485
|
+
}
|
|
14486
|
+
return tags;
|
|
14487
|
+
}
|
|
14488
|
+
function locAt(source, offset, length) {
|
|
14489
|
+
if (source === void 0) return void 0;
|
|
14490
|
+
const start = positionOf(source, offset);
|
|
14491
|
+
const end = positionOf(source, offset + length);
|
|
14492
|
+
return {
|
|
14493
|
+
line: start.line,
|
|
14494
|
+
column: start.column,
|
|
14495
|
+
endLine: end.line,
|
|
14496
|
+
endColumn: end.column,
|
|
14497
|
+
offset,
|
|
14498
|
+
length
|
|
14499
|
+
};
|
|
14500
|
+
}
|
|
14501
|
+
function attr(attrs2, name) {
|
|
14502
|
+
var _a;
|
|
14503
|
+
const m = attrs2.match(
|
|
14504
|
+
new RegExp(`(?:^|\\s)${name}\\s*=\\s*"([^"]*)"|(?:^|\\s)${name}\\s*=\\s*'([^']*)'`, "i")
|
|
14505
|
+
);
|
|
14506
|
+
return m ? (_a = m[1]) != null ? _a : m[2] : void 0;
|
|
14507
|
+
}
|
|
14508
|
+
function add(issues, seen, key, issue) {
|
|
14509
|
+
const existing = seen.get(key);
|
|
14510
|
+
if (existing) {
|
|
14511
|
+
if (!issue.loc || !existing.locs) return;
|
|
14512
|
+
if (existing.locs.some((l) => l.offset === issue.loc.offset)) return;
|
|
14513
|
+
if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
|
|
14514
|
+
existing.locsTruncated = true;
|
|
14515
|
+
return;
|
|
14516
|
+
}
|
|
14517
|
+
existing.locs.push(issue.loc);
|
|
14518
|
+
return;
|
|
14519
|
+
}
|
|
14520
|
+
seen.set(key, issue);
|
|
14521
|
+
issues.push(issue);
|
|
14522
|
+
}
|
|
14523
|
+
function withLoc(issue, loc) {
|
|
14524
|
+
return loc ? __spreadProps(__spreadValues({}, issue), { loc, locs: [loc] }) : issue;
|
|
14525
|
+
}
|
|
14526
|
+
var TEXT_HOSTS = /<\s*(center|v:textbox|div|p|table|h[1-6]|span|font|a)\b/i;
|
|
14527
|
+
function checkLooseText(issues, seen, open, inner, source) {
|
|
14528
|
+
if (TEXT_HOSTS.test(inner)) return;
|
|
14529
|
+
const text = inner.replace(/<!--[\s\S]*?-->/g, "").replace(/<[^>]*>/g, "").replace(/ |\s/g, "");
|
|
14530
|
+
if (text === "") return;
|
|
14531
|
+
add(issues, seen, `loosetext:${open.name}`, withLoc({
|
|
14532
|
+
rule: "vml-unrendered-text",
|
|
14533
|
+
severity: "error",
|
|
14534
|
+
message: `<v:${open.name}> has label text with no element around it, which Outlook does not draw.`,
|
|
14535
|
+
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>.`
|
|
14536
|
+
}, locAt(source, open.offset, open.length)));
|
|
14537
|
+
}
|
|
14538
|
+
function checkVml(html, options) {
|
|
14539
|
+
if (!html || !html.trim()) return EMPTY_VML;
|
|
14540
|
+
const source = (options == null ? void 0 : options.positions) ? html : void 0;
|
|
14541
|
+
const tags = vmlTags(msoBlocks(html));
|
|
14542
|
+
if (tags.length === 0) return EMPTY_VML;
|
|
14543
|
+
const issues = [];
|
|
14544
|
+
const seen = /* @__PURE__ */ new Map();
|
|
14545
|
+
const openShapes = [];
|
|
14546
|
+
let groupDepth = 0;
|
|
14547
|
+
for (const tag of tags) {
|
|
14548
|
+
const { name } = tag;
|
|
14549
|
+
const isShape = SHAPE_TAGS.has(name);
|
|
14550
|
+
const isGroup = GROUP_TAGS.has(name);
|
|
14551
|
+
const loc = locAt(source, tag.offset, tag.length);
|
|
14552
|
+
if (tag.closing) {
|
|
14553
|
+
if (isGroup) groupDepth = Math.max(0, groupDepth - 1);
|
|
14554
|
+
if (isShape) {
|
|
14555
|
+
const idx = openShapes.map((t) => t.name).lastIndexOf(name);
|
|
14556
|
+
if (idx !== -1) {
|
|
14557
|
+
const open = openShapes[idx];
|
|
14558
|
+
const inner = html.slice(open.offset + open.length, tag.offset);
|
|
14559
|
+
checkLooseText(issues, seen, open, inner, source);
|
|
14560
|
+
}
|
|
14561
|
+
if (idx === -1) {
|
|
14562
|
+
add(issues, seen, `stray:${name}`, withLoc({
|
|
14563
|
+
rule: "vml-unbalanced-tag",
|
|
14564
|
+
severity: "error",
|
|
14565
|
+
message: `Closing </v:${name}> has no matching opening tag.`,
|
|
14566
|
+
detail: `Outlook stops rendering the shape when VML tags do not balance. Check that every conditional-comment block opens and closes the tags it is responsible for.`
|
|
14567
|
+
}, loc));
|
|
14568
|
+
} else {
|
|
14569
|
+
openShapes.splice(idx, 1);
|
|
14570
|
+
}
|
|
14571
|
+
}
|
|
14572
|
+
continue;
|
|
14573
|
+
}
|
|
14574
|
+
if (isGroup && !tag.selfClosing) groupDepth++;
|
|
14575
|
+
if (isShape) {
|
|
14576
|
+
const parent = openShapes[openShapes.length - 1];
|
|
14577
|
+
if (parent && groupDepth === 0) {
|
|
14578
|
+
add(issues, seen, `nested:${parent.name}>${name}`, withLoc({
|
|
14579
|
+
rule: "vml-nested-shape",
|
|
14580
|
+
severity: "error",
|
|
14581
|
+
message: `<v:${name}> is nested inside <v:${parent.name}>. Outlook does not support nesting one VML shape inside another.`,
|
|
14582
|
+
detail: `Verified in Outlook Classic, and the damage is not local. Three things happen: the containing <v:${parent.name}> does not render at all (its fill and everything inside it disappear, leaving the inner shape stranded); the table structure around it terminates early, so content *after* the shape falls out of the layout and renders outside the email frame; and every VML shape further down the email stops drawing its text, so buttons and headings after this point ship as blank coloured blocks. Confirmed with byte-identical probes either side of one nested shape: the one before it renders its labels, the one after it does not. Every other client renders the HTML fallback correctly, which is why none of this is visible outside the Word engine. Fix: lift the inner shape out of <v:${parent.name}>, or drop the container shape and keep the inner one (a framed background can degrade to a solid fill colour on the <td> instead).`
|
|
14583
|
+
}, loc));
|
|
14584
|
+
}
|
|
14585
|
+
if (!tag.selfClosing) openShapes.push(tag);
|
|
14586
|
+
}
|
|
14587
|
+
const style = attr(tag.attrs, "style");
|
|
14588
|
+
if (style && (isShape || isGroup)) {
|
|
14589
|
+
for (const prop of ["width", "height"]) {
|
|
14590
|
+
const m = style.match(new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]*)`, "i"));
|
|
14591
|
+
if (!m) continue;
|
|
14592
|
+
const value = m[1].trim();
|
|
14593
|
+
if (value === "" || /^[a-z%]+$/i.test(value)) {
|
|
14594
|
+
add(issues, seen, `dim:${name}:${prop}:${value}`, withLoc({
|
|
14595
|
+
rule: "vml-invalid-dimension",
|
|
14596
|
+
severity: "error",
|
|
14597
|
+
message: `<v:${name}> has an invalid ${prop}: "${prop}:${value}" is missing its number.`,
|
|
14598
|
+
detail: `Verified in Outlook Classic: the shape still draws, but at a size Outlook picks rather than the one you meant, and the content inside is clipped to it \u2014 roughly half the intended height in the case measured, with the text inside cut off. It fails quietly: there is no gap or broken image to notice, just content that is silently missing. Usually a template variable that resolved to an empty string. Set an explicit value in pixels, e.g. ${prop}:400px.`
|
|
14599
|
+
}, loc));
|
|
14600
|
+
}
|
|
14601
|
+
}
|
|
14602
|
+
}
|
|
14603
|
+
if (name === "roundrect") {
|
|
14604
|
+
const arcsize = attr(tag.attrs, "arcsize");
|
|
14605
|
+
if (arcsize !== void 0) {
|
|
14606
|
+
const raw = arcsize.trim();
|
|
14607
|
+
const pct = raw.endsWith("%") ? parseFloat(raw) : parseFloat(raw) * 100;
|
|
14608
|
+
if (Number.isFinite(pct) && (pct < 0 || pct > 100)) {
|
|
14609
|
+
add(issues, seen, `arcsize:${raw}`, withLoc({
|
|
14610
|
+
rule: "vml-arcsize-range",
|
|
14611
|
+
severity: "warning",
|
|
14612
|
+
message: `<v:roundrect> has arcsize="${raw}", outside the valid 0%\u2013100% range.`,
|
|
14613
|
+
detail: `arcsize is a fraction of half the shape's smaller side: 0% is square, 100% is fully circular. Verified in Outlook Classic: out-of-range values are clamped, so 120% draws exactly the same corner as 100%. Nothing visibly breaks today, which is why this is a warning and not an error \u2014 but the radius you get is the renderer's clamp rather than a value you chose, and it is not guaranteed across clients. For a fully rounded button, say arcsize="100%".`
|
|
14614
|
+
}, loc));
|
|
14615
|
+
}
|
|
14616
|
+
}
|
|
14617
|
+
}
|
|
14618
|
+
}
|
|
14619
|
+
for (const tag of openShapes) {
|
|
14620
|
+
add(issues, seen, `unclosed:${tag.name}`, withLoc({
|
|
14621
|
+
rule: "vml-unbalanced-tag",
|
|
14622
|
+
severity: "error",
|
|
14623
|
+
message: `<v:${tag.name}> is never closed.`,
|
|
14624
|
+
detail: `Outlook needs the matching </v:${tag.name}>, usually in a later conditional-comment block. Without it the shape swallows the rest of the email.`
|
|
14625
|
+
}, locAt(source, tag.offset, tag.length)));
|
|
14626
|
+
}
|
|
14627
|
+
return { hasVml: true, issues };
|
|
14628
|
+
}
|
|
14629
|
+
|
|
14333
14630
|
// src/visual-checker.ts
|
|
14334
14631
|
import * as csstree8 from "css-tree";
|
|
14335
14632
|
var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
|
|
@@ -14511,8 +14808,8 @@ function radiusLengths(value) {
|
|
|
14511
14808
|
return lengths.filter((v) => {
|
|
14512
14809
|
if (/^0(?:px|rem|em|%)?$/.test(v)) return false;
|
|
14513
14810
|
if (v === "50%") return false;
|
|
14514
|
-
const
|
|
14515
|
-
return !(v.endsWith("px") && Number.isFinite(
|
|
14811
|
+
const px2 = parseFloat(v);
|
|
14812
|
+
return !(v.endsWith("px") && Number.isFinite(px2) && px2 >= 500);
|
|
14516
14813
|
});
|
|
14517
14814
|
}
|
|
14518
14815
|
function collect($) {
|
|
@@ -14660,6 +14957,7 @@ var EMPTY_AUDIT = {
|
|
|
14660
14957
|
templateVariables: EMPTY_TEMPLATE,
|
|
14661
14958
|
overflow: EMPTY_OVERFLOW,
|
|
14662
14959
|
visual: EMPTY_VISUAL,
|
|
14960
|
+
vml: EMPTY_VML,
|
|
14663
14961
|
darkContrast: [],
|
|
14664
14962
|
mobileContrast: [],
|
|
14665
14963
|
design: EMPTY_DESIGN
|
|
@@ -14687,13 +14985,14 @@ function runAudit($, html, framework, options) {
|
|
|
14687
14985
|
const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
|
|
14688
14986
|
const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
|
|
14689
14987
|
const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
|
|
14988
|
+
const vml = skip.has("vml") ? EMPTY_VML : checkVml(html, { positions: !!source });
|
|
14690
14989
|
const darkContrast = skip.has("darkContrast") || skip.has("accessibility") ? [] : dedupeByElement([
|
|
14691
14990
|
...checkDarkModeContrast(html, accessibility.issues),
|
|
14692
14991
|
...checkDarkStylesContrastFromDom($, accessibility.issues)
|
|
14693
14992
|
]);
|
|
14694
14993
|
const design = skip.has("design") ? EMPTY_DESIGN : checkDesignConsistencyFromDom($);
|
|
14695
14994
|
const mobileContrast = skip.has("mobileContrast") || skip.has("accessibility") ? [] : checkMobileContrastFromDom($, accessibility.issues);
|
|
14696
|
-
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual, darkContrast, mobileContrast, design };
|
|
14995
|
+
return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual, vml, darkContrast, mobileContrast, design };
|
|
14697
14996
|
}
|
|
14698
14997
|
function auditEmail(html, options) {
|
|
14699
14998
|
return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
|
|
@@ -14905,6 +15204,7 @@ export {
|
|
|
14905
15204
|
analyzeEmail,
|
|
14906
15205
|
analyzeImages,
|
|
14907
15206
|
analyzeSpam,
|
|
15207
|
+
arcsizeToRadius,
|
|
14908
15208
|
auditEmail,
|
|
14909
15209
|
caveatApplies,
|
|
14910
15210
|
checkAccessibility,
|
|
@@ -14916,6 +15216,7 @@ export {
|
|
|
14916
15216
|
checkSize,
|
|
14917
15217
|
checkTemplateVariables,
|
|
14918
15218
|
checkVisual,
|
|
15219
|
+
checkVml,
|
|
14919
15220
|
colorDistance,
|
|
14920
15221
|
contrastRatio,
|
|
14921
15222
|
createSession,
|
|
@@ -14935,6 +15236,8 @@ export {
|
|
|
14935
15236
|
heuristicTokenCount,
|
|
14936
15237
|
parseColor,
|
|
14937
15238
|
relativeLuminance,
|
|
15239
|
+
renderOutlookBranch,
|
|
15240
|
+
resolveMsoBranch,
|
|
14938
15241
|
rgbToOklab,
|
|
14939
15242
|
simulateDarkMode,
|
|
14940
15243
|
structuralWarnings,
|
|
@@ -14942,6 +15245,7 @@ export {
|
|
|
14942
15245
|
transformForAllClients,
|
|
14943
15246
|
transformForClient,
|
|
14944
15247
|
validateLinks,
|
|
15248
|
+
vmlToCss,
|
|
14945
15249
|
warningsForClient,
|
|
14946
15250
|
wcagGrade
|
|
14947
15251
|
};
|