@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/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 downleveled = downlevelCSS(html);
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, attr) {
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[attr]) != null ? _d : (_c = (_b = raw.startTag) == null ? void 0 : _b.attrs) == null ? void 0 : _c[attr];
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 px = mediaPx(m[2]);
13204
- if (px === null) continue;
13205
- if (m[1] === "max" ? ctx.width > px : ctx.width < px) return false;
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 px = match[2].toLowerCase() === "pt" ? parseFloat(match[1]) * 1.333 : parseFloat(match[1]);
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 px >= 18 || px >= 14 && bold;
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 attrs = ["href", "src", "alt"];
14151
- for (const attr of attrs) {
14152
- const value = $(el).attr(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:${attr}:${variable}`;
14269
+ const key = `attr:${attr2}:${variable}`;
14160
14270
  if (seen.has(key)) continue;
14161
14271
  seen.add(key);
14162
- const loc = locOfAttr(el, attr);
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 ${attr} attribute.`,
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 attr = $el.attr("width");
14193
- if (attr && /^\d+$/.test(attr.trim())) return parseInt(attr.trim(), 10);
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(/&nbsp;|\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 px = parseFloat(v);
14515
- return !(v.endsWith("px") && Number.isFinite(px) && px >= 500);
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
  };