@emailens/engine 0.11.0 → 0.11.1

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.
@@ -1,5 +1,5 @@
1
- import { w as InputFormat } from '../types-BFhPXh3c.cjs';
2
- export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy, c as compileReactEmail } from '../react-email-BWIfwG7U.cjs';
1
+ import { x as InputFormat } from '../types-bvGDId6f.cjs';
2
+ export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy, c as compileReactEmail } from '../react-email-DuZAgJtw.cjs';
3
3
 
4
4
  /**
5
5
  * Compile an MJML source string into an HTML email string.
@@ -1,5 +1,5 @@
1
- import { w as InputFormat } from '../types-BFhPXh3c.js';
2
- export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy, c as compileReactEmail } from '../react-email-D24HkW2Z.js';
1
+ import { x as InputFormat } from '../types-bvGDId6f.js';
2
+ export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy, c as compileReactEmail } from '../react-email-BROBcC8u.js';
3
3
 
4
4
  /**
5
5
  * Compile an MJML source string into an HTML email string.
package/dist/index.cjs CHANGED
@@ -90,6 +90,7 @@ __export(index_exports, {
90
90
  checkSize: () => checkSize,
91
91
  checkTemplateVariables: () => checkTemplateVariables,
92
92
  checkVisual: () => checkVisual,
93
+ checkVml: () => checkVml,
93
94
  colorDistance: () => colorDistance,
94
95
  contrastRatio: () => contrastRatio,
95
96
  createSession: () => createSession,
@@ -9907,6 +9908,7 @@ var EMPTY_INBOX_PREVIEW = { subject: null, preheader: null, subjectLength: 0, pr
9907
9908
  var EMPTY_SIZE = { htmlBytes: 0, humanSize: "0 B", clipped: false, issues: [] };
9908
9909
  var EMPTY_TEMPLATE = { unresolvedCount: 0, issues: [] };
9909
9910
  var EMPTY_OVERFLOW = { hasOverflow: false, issues: [] };
9911
+ var EMPTY_VML = { hasVml: false, issues: [] };
9910
9912
  var EMPTY_VISUAL = { issues: [] };
9911
9913
  var EMAIL_MAX_WIDTH = 600;
9912
9914
  var UNBREAKABLE_STRING_LENGTH = 30;
@@ -11115,11 +11117,11 @@ function locOfElement(el) {
11115
11117
  if (!raw) return void 0;
11116
11118
  return toLoc((_a = raw.startTag) != null ? _a : raw);
11117
11119
  }
11118
- function locOfAttr(el, attr) {
11120
+ function locOfAttr(el, attr2) {
11119
11121
  var _a, _b, _c, _d;
11120
11122
  const raw = el == null ? void 0 : el.sourceCodeLocation;
11121
11123
  if (!raw) return void 0;
11122
- 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];
11124
+ 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];
11123
11125
  return attrLoc ? toLoc(attrLoc) : locOfElement(el);
11124
11126
  }
11125
11127
  function locInAttr(attrLoc, source, property, occurrence = 0) {
@@ -14262,22 +14264,22 @@ function checkTemplateVariablesFromDom($, source) {
14262
14264
  for (const sel of attrSelectors) {
14263
14265
  $(sel).each((_, el) => {
14264
14266
  const attrs = ["href", "src", "alt"];
14265
- for (const attr of attrs) {
14266
- const value = $(el).attr(attr);
14267
+ for (const attr2 of attrs) {
14268
+ const value = $(el).attr(attr2);
14267
14269
  if (!value) continue;
14268
14270
  for (const [pattern, label] of TEMPLATE_VARIABLE_PATTERNS) {
14269
14271
  pattern.lastIndex = 0;
14270
14272
  let match;
14271
14273
  while ((match = pattern.exec(value)) !== null) {
14272
14274
  const variable = match[0];
14273
- const key = `attr:${attr}:${variable}`;
14275
+ const key = `attr:${attr2}:${variable}`;
14274
14276
  if (seen.has(key)) continue;
14275
14277
  seen.add(key);
14276
- const loc = locOfAttr(el, attr);
14278
+ const loc = locOfAttr(el, attr2);
14277
14279
  issues.push(__spreadValues({
14278
14280
  rule: "unresolved-variable",
14279
14281
  severity: "error",
14280
- message: `Unresolved ${label} variable "${variable}" found in ${attr} attribute.`,
14282
+ message: `Unresolved ${label} variable "${variable}" found in ${attr2} attribute.`,
14281
14283
  variable,
14282
14284
  location: "attribute"
14283
14285
  }, loc ? { loc } : {}));
@@ -14303,8 +14305,8 @@ function fixedPxWidth($el) {
14303
14305
  const style = $el.attr("style") || "";
14304
14306
  const styleMatch = style.match(/(?:^|[;\s])width\s*:\s*(\d+)px/i);
14305
14307
  if (styleMatch) return parseInt(styleMatch[1], 10);
14306
- const attr = $el.attr("width");
14307
- if (attr && /^\d+$/.test(attr.trim())) return parseInt(attr.trim(), 10);
14308
+ const attr2 = $el.attr("width");
14309
+ if (attr2 && /^\d+$/.test(attr2.trim())) return parseInt(attr2.trim(), 10);
14308
14310
  return null;
14309
14311
  }
14310
14312
  function isFluid(style) {
@@ -14444,6 +14446,193 @@ function checkOverflow(html, options) {
14444
14446
  );
14445
14447
  }
14446
14448
 
14449
+ // src/vml-checker.ts
14450
+ var SHAPE_TAGS = /* @__PURE__ */ new Set([
14451
+ "rect",
14452
+ "roundrect",
14453
+ "oval",
14454
+ "line",
14455
+ "polyline",
14456
+ "curve",
14457
+ "arc",
14458
+ "shape",
14459
+ "image",
14460
+ "background"
14461
+ ]);
14462
+ var GROUP_TAGS = /* @__PURE__ */ new Set(["group"]);
14463
+ function msoBlocks(html) {
14464
+ const blocks = [];
14465
+ const re = /<!--\[if([^\]]*)\]>([\s\S]*?)<!\[endif\]-->/g;
14466
+ let m;
14467
+ while ((m = re.exec(html)) !== null) {
14468
+ const condition = m[1];
14469
+ if (/!\s*(mso|vml)/i.test(condition)) continue;
14470
+ if (!/mso|vml/i.test(condition)) continue;
14471
+ blocks.push({ inner: m[2], offset: m.index + m[0].length - m[2].length - "<![endif]-->".length });
14472
+ }
14473
+ return blocks;
14474
+ }
14475
+ function vmlTags(blocks) {
14476
+ const tags = [];
14477
+ const re = /<(\/?)v:([a-z]+)((?:[^>"']|"[^"]*"|'[^']*')*?)(\/?)>/gi;
14478
+ for (const block of blocks) {
14479
+ let m;
14480
+ re.lastIndex = 0;
14481
+ while ((m = re.exec(block.inner)) !== null) {
14482
+ tags.push({
14483
+ name: m[2].toLowerCase(),
14484
+ closing: m[1] === "/",
14485
+ selfClosing: m[4] === "/",
14486
+ attrs: m[3],
14487
+ offset: block.offset + m.index,
14488
+ length: m[0].length
14489
+ });
14490
+ }
14491
+ }
14492
+ return tags;
14493
+ }
14494
+ function locAt(source, offset, length) {
14495
+ if (source === void 0) return void 0;
14496
+ const start = positionOf(source, offset);
14497
+ const end = positionOf(source, offset + length);
14498
+ return {
14499
+ line: start.line,
14500
+ column: start.column,
14501
+ endLine: end.line,
14502
+ endColumn: end.column,
14503
+ offset,
14504
+ length
14505
+ };
14506
+ }
14507
+ function attr(attrs, name) {
14508
+ var _a;
14509
+ const m = attrs.match(
14510
+ new RegExp(`(?:^|\\s)${name}\\s*=\\s*"([^"]*)"|(?:^|\\s)${name}\\s*=\\s*'([^']*)'`, "i")
14511
+ );
14512
+ return m ? (_a = m[1]) != null ? _a : m[2] : void 0;
14513
+ }
14514
+ function add(issues, seen, key, issue) {
14515
+ const existing = seen.get(key);
14516
+ if (existing) {
14517
+ if (!issue.loc || !existing.locs) return;
14518
+ if (existing.locs.some((l) => l.offset === issue.loc.offset)) return;
14519
+ if (existing.locs.length >= MAX_WARNING_LOCATIONS) {
14520
+ existing.locsTruncated = true;
14521
+ return;
14522
+ }
14523
+ existing.locs.push(issue.loc);
14524
+ return;
14525
+ }
14526
+ seen.set(key, issue);
14527
+ issues.push(issue);
14528
+ }
14529
+ function withLoc(issue, loc) {
14530
+ return loc ? __spreadProps(__spreadValues({}, issue), { loc, locs: [loc] }) : issue;
14531
+ }
14532
+ var TEXT_HOSTS = /<\s*(center|v:textbox|div|p|table|h[1-6]|span|font|a)\b/i;
14533
+ function checkLooseText(issues, seen, open, inner, source) {
14534
+ if (TEXT_HOSTS.test(inner)) return;
14535
+ const text = inner.replace(/<!--[\s\S]*?-->/g, "").replace(/<[^>]*>/g, "").replace(/&nbsp;|\s/g, "");
14536
+ if (text === "") return;
14537
+ add(issues, seen, `loosetext:${open.name}`, withLoc({
14538
+ rule: "vml-unrendered-text",
14539
+ severity: "error",
14540
+ message: `<v:${open.name}> has label text with no element around it, which Outlook does not draw.`,
14541
+ 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>.`
14542
+ }, locAt(source, open.offset, open.length)));
14543
+ }
14544
+ function checkVml(html, options) {
14545
+ if (!html || !html.trim()) return EMPTY_VML;
14546
+ const source = (options == null ? void 0 : options.positions) ? html : void 0;
14547
+ const tags = vmlTags(msoBlocks(html));
14548
+ if (tags.length === 0) return EMPTY_VML;
14549
+ const issues = [];
14550
+ const seen = /* @__PURE__ */ new Map();
14551
+ const openShapes = [];
14552
+ let groupDepth = 0;
14553
+ for (const tag of tags) {
14554
+ const { name } = tag;
14555
+ const isShape = SHAPE_TAGS.has(name);
14556
+ const isGroup = GROUP_TAGS.has(name);
14557
+ const loc = locAt(source, tag.offset, tag.length);
14558
+ if (tag.closing) {
14559
+ if (isGroup) groupDepth = Math.max(0, groupDepth - 1);
14560
+ if (isShape) {
14561
+ const idx = openShapes.map((t) => t.name).lastIndexOf(name);
14562
+ if (idx !== -1) {
14563
+ const open = openShapes[idx];
14564
+ const inner = html.slice(open.offset + open.length, tag.offset);
14565
+ checkLooseText(issues, seen, open, inner, source);
14566
+ }
14567
+ if (idx === -1) {
14568
+ add(issues, seen, `stray:${name}`, withLoc({
14569
+ rule: "vml-unbalanced-tag",
14570
+ severity: "error",
14571
+ message: `Closing </v:${name}> has no matching opening tag.`,
14572
+ 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.`
14573
+ }, loc));
14574
+ } else {
14575
+ openShapes.splice(idx, 1);
14576
+ }
14577
+ }
14578
+ continue;
14579
+ }
14580
+ if (isGroup && !tag.selfClosing) groupDepth++;
14581
+ if (isShape) {
14582
+ const parent = openShapes[openShapes.length - 1];
14583
+ if (parent && groupDepth === 0) {
14584
+ add(issues, seen, `nested:${parent.name}>${name}`, withLoc({
14585
+ rule: "vml-nested-shape",
14586
+ severity: "error",
14587
+ message: `<v:${name}> is nested inside <v:${parent.name}>. Outlook does not support nesting one VML shape inside another.`,
14588
+ 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).`
14589
+ }, loc));
14590
+ }
14591
+ if (!tag.selfClosing) openShapes.push(tag);
14592
+ }
14593
+ const style = attr(tag.attrs, "style");
14594
+ if (style && (isShape || isGroup)) {
14595
+ for (const prop of ["width", "height"]) {
14596
+ const m = style.match(new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]*)`, "i"));
14597
+ if (!m) continue;
14598
+ const value = m[1].trim();
14599
+ if (value === "" || /^[a-z%]+$/i.test(value)) {
14600
+ add(issues, seen, `dim:${name}:${prop}:${value}`, withLoc({
14601
+ rule: "vml-invalid-dimension",
14602
+ severity: "error",
14603
+ message: `<v:${name}> has an invalid ${prop}: "${prop}:${value}" is missing its number.`,
14604
+ 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.`
14605
+ }, loc));
14606
+ }
14607
+ }
14608
+ }
14609
+ if (name === "roundrect") {
14610
+ const arcsize = attr(tag.attrs, "arcsize");
14611
+ if (arcsize !== void 0) {
14612
+ const raw = arcsize.trim();
14613
+ const pct = raw.endsWith("%") ? parseFloat(raw) : parseFloat(raw) * 100;
14614
+ if (Number.isFinite(pct) && (pct < 0 || pct > 100)) {
14615
+ add(issues, seen, `arcsize:${raw}`, withLoc({
14616
+ rule: "vml-arcsize-range",
14617
+ severity: "warning",
14618
+ message: `<v:roundrect> has arcsize="${raw}", outside the valid 0%\u2013100% range.`,
14619
+ 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%".`
14620
+ }, loc));
14621
+ }
14622
+ }
14623
+ }
14624
+ }
14625
+ for (const tag of openShapes) {
14626
+ add(issues, seen, `unclosed:${tag.name}`, withLoc({
14627
+ rule: "vml-unbalanced-tag",
14628
+ severity: "error",
14629
+ message: `<v:${tag.name}> is never closed.`,
14630
+ 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.`
14631
+ }, locAt(source, tag.offset, tag.length)));
14632
+ }
14633
+ return { hasVml: true, issues };
14634
+ }
14635
+
14447
14636
  // src/visual-checker.ts
14448
14637
  var csstree8 = __toESM(require("css-tree"), 1);
14449
14638
  var CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
@@ -14774,6 +14963,7 @@ var EMPTY_AUDIT = {
14774
14963
  templateVariables: EMPTY_TEMPLATE,
14775
14964
  overflow: EMPTY_OVERFLOW,
14776
14965
  visual: EMPTY_VISUAL,
14966
+ vml: EMPTY_VML,
14777
14967
  darkContrast: [],
14778
14968
  mobileContrast: [],
14779
14969
  design: EMPTY_DESIGN
@@ -14801,13 +14991,14 @@ function runAudit($, html, framework, options) {
14801
14991
  const templateVariables = skip.has("templateVariables") ? EMPTY_TEMPLATE : checkTemplateVariablesFromDom($, source);
14802
14992
  const overflow = skip.has("overflow") ? EMPTY_OVERFLOW : checkOverflowFromDom($, source);
14803
14993
  const visual = skip.has("visual") ? EMPTY_VISUAL : checkVisualFromDom($, source);
14994
+ const vml = skip.has("vml") ? EMPTY_VML : checkVml(html, { positions: !!source });
14804
14995
  const darkContrast = skip.has("darkContrast") || skip.has("accessibility") ? [] : dedupeByElement([
14805
14996
  ...checkDarkModeContrast(html, accessibility.issues),
14806
14997
  ...checkDarkStylesContrastFromDom($, accessibility.issues)
14807
14998
  ]);
14808
14999
  const design = skip.has("design") ? EMPTY_DESIGN : checkDesignConsistencyFromDom($);
14809
15000
  const mobileContrast = skip.has("mobileContrast") || skip.has("accessibility") ? [] : checkMobileContrastFromDom($, accessibility.issues);
14810
- return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual, darkContrast, mobileContrast, design };
15001
+ return { compatibility: { warnings, scores }, spam, links, accessibility, images, inboxPreview, size, templateVariables, overflow, visual, vml, darkContrast, mobileContrast, design };
14811
15002
  }
14812
15003
  function auditEmail(html, options) {
14813
15004
  return fromHtml(html, EMPTY_AUDIT, ($, h) => runAudit($, h, options == null ? void 0 : options.framework, options), options);
@@ -15041,6 +15232,7 @@ var CompileError = class extends Error {
15041
15232
  checkSize,
15042
15233
  checkTemplateVariables,
15043
15234
  checkVisual,
15235
+ checkVml,
15044
15236
  colorDistance,
15045
15237
  contrastRatio,
15046
15238
  createSession,