@emailens/engine 0.11.7 → 0.11.9

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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { E as EmailClient, F as Framework, T as TransformResult, C as CSSWarning, a as CodeFix, D as DiffResult, b as ExportPromptOptions, A as AiProvider, c as AiFixResult, S as SupportLevel, d as SpamAnalysisOptions, e as SpamReport, L as LinkReport, f as AccessibilityReport, g as AccessibilityIssue, I as ImageReport, h as InboxPreview, i as SizeReport, j as TemplateReport, O as OverflowReport, V as VmlReport, k as VisualReport, l as DesignReport, m as DeliverabilityReport } from './types-bvGDId6f.cjs';
2
- export { B as BaseIssue, n as ClientTruncation, o as DeliverabilityCheck, p as DeliverabilityIssue, q as DesignIssue, r as EstimateOptions, s as ExportScope, t as FixType, u as ImageInfo, v as ImageIssue, w as InboxPreviewIssue, x as InputFormat, y as LinkIssue, z as OverflowIssue, P as PreviewResult, G as Severity, H as SizeIssue, J as SourceLocation, K as SpamIssue, M as TemplateIssue, N as TokenEstimate, Q as TokenEstimateWithWarnings, R as VisualIssue, U as VmlIssue, W as estimateAiFixTokens, X as generateFixPrompt, Y as heuristicTokenCount } from './types-bvGDId6f.cjs';
1
+ import { E as EmailClient, F as Framework, T as TransformResult, C as CSSWarning, a as CodeFix, D as DiffResult, b as ExportPromptOptions, A as AiProvider, c as AiFixResult, S as SupportLevel, d as SpamAnalysisOptions, e as SpamReport, L as LinkReport, f as AccessibilityReport, g as AccessibilityIssue, I as ImageReport, h as InboxPreview, i as SizeReport, j as TemplateReport, O as OverflowReport, V as VmlReport, k as VisualReport, l as DesignReport, m as DeliverabilityReport } from './types-DiseE-UH.cjs';
2
+ export { B as BaseIssue, n as ClientTruncation, o as DeliverabilityCheck, p as DeliverabilityIssue, q as DesignIssue, r as EstimateOptions, s as ExportScope, t as FixType, u as ImageInfo, v as ImageIssue, w as InboxPreviewIssue, x as InputFormat, y as LinkInfo, z as LinkIssue, G as OverflowIssue, P as PreviewResult, H as Severity, J as SizeIssue, K as SourceLocation, M as SpamIssue, N as TemplateIssue, Q as TokenEstimate, R as TokenEstimateWithWarnings, U as VisualIssue, W as VmlIssue, X as estimateAiFixTokens, Y as generateFixPrompt, Z as heuristicTokenCount } from './types-DiseE-UH.cjs';
3
3
  import * as cheerio from 'cheerio';
4
- export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy } from './react-email-DuZAgJtw.cjs';
4
+ export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy } from './react-email-DRkmpgX-.cjs';
5
5
 
6
6
  declare const EMAIL_CLIENTS: EmailClient[];
7
7
  declare function getClient(id: string): EmailClient | undefined;
@@ -410,12 +410,32 @@ declare function checkVml(html: string, options?: {
410
410
  * `roundrect` and `rect` are the whole surface. Anything else is left alone
411
411
  * rather than approximated.
412
412
  *
413
- * Deliberately NOT emulated: how Word *mis*-renders a nested shape. That is a
414
- * real and confirmed failure (the container disappears and text stops drawing
415
- * further down the document), but reproducing it means perturbing Chromium's
416
- * layout into a picture that is wrong in a third way, different from both the
417
- * correct render and Outlook's. `checkVml` reports it with a line number
418
- * instead, which is more useful and cannot mislead.
413
+ * How Word *mis*-renders a nested shape is emulated, but only the parts with
414
+ * evidence behind them. This note used to say the failure was not emulated at
415
+ * all, on the grounds that reproducing it meant perturbing Chromium into a
416
+ * picture wrong in a third way. That argument does not survive contact with
417
+ * the specific effects:
418
+ *
419
+ * - the container's fill and geometry disappear -> drop the box. Exact.
420
+ * - VML shapes further down stop drawing text -> blank their labels.
421
+ * Exact, and scoped to
422
+ * shapes we created.
423
+ * - the table structure terminates early, so later
424
+ * content escapes the frame -> WITHDRAWN. It had no
425
+ * methodology, and the
426
+ * fixture built to
427
+ * measure it contradicts
428
+ * it.
429
+ *
430
+ * Two of the three needed no distortion at all; the third turned out not to be
431
+ * a rendering problem but an evidence problem. Refusing to draw any of it
432
+ * meant the preview showed a tidy box with a button in it, a picture no Word
433
+ * engine produces, sitting beside a finding that said the region was
434
+ * destroyed. Between a warning and a screenshot, people believe the
435
+ * screenshot.
436
+ *
437
+ * Affected regions carry `data-vml-outlook` so a surface can label them, since
438
+ * a blank button should read as "Outlook does this" and not as our bug.
419
439
  */
420
440
  /**
421
441
  * `arcsize` is a VgFraction: a proportion of *half the shape's shorter side*,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { E as EmailClient, F as Framework, T as TransformResult, C as CSSWarning, a as CodeFix, D as DiffResult, b as ExportPromptOptions, A as AiProvider, c as AiFixResult, S as SupportLevel, d as SpamAnalysisOptions, e as SpamReport, L as LinkReport, f as AccessibilityReport, g as AccessibilityIssue, I as ImageReport, h as InboxPreview, i as SizeReport, j as TemplateReport, O as OverflowReport, V as VmlReport, k as VisualReport, l as DesignReport, m as DeliverabilityReport } from './types-bvGDId6f.js';
2
- export { B as BaseIssue, n as ClientTruncation, o as DeliverabilityCheck, p as DeliverabilityIssue, q as DesignIssue, r as EstimateOptions, s as ExportScope, t as FixType, u as ImageInfo, v as ImageIssue, w as InboxPreviewIssue, x as InputFormat, y as LinkIssue, z as OverflowIssue, P as PreviewResult, G as Severity, H as SizeIssue, J as SourceLocation, K as SpamIssue, M as TemplateIssue, N as TokenEstimate, Q as TokenEstimateWithWarnings, R as VisualIssue, U as VmlIssue, W as estimateAiFixTokens, X as generateFixPrompt, Y as heuristicTokenCount } from './types-bvGDId6f.js';
1
+ import { E as EmailClient, F as Framework, T as TransformResult, C as CSSWarning, a as CodeFix, D as DiffResult, b as ExportPromptOptions, A as AiProvider, c as AiFixResult, S as SupportLevel, d as SpamAnalysisOptions, e as SpamReport, L as LinkReport, f as AccessibilityReport, g as AccessibilityIssue, I as ImageReport, h as InboxPreview, i as SizeReport, j as TemplateReport, O as OverflowReport, V as VmlReport, k as VisualReport, l as DesignReport, m as DeliverabilityReport } from './types-DiseE-UH.js';
2
+ export { B as BaseIssue, n as ClientTruncation, o as DeliverabilityCheck, p as DeliverabilityIssue, q as DesignIssue, r as EstimateOptions, s as ExportScope, t as FixType, u as ImageInfo, v as ImageIssue, w as InboxPreviewIssue, x as InputFormat, y as LinkInfo, z as LinkIssue, G as OverflowIssue, P as PreviewResult, H as Severity, J as SizeIssue, K as SourceLocation, M as SpamIssue, N as TemplateIssue, Q as TokenEstimate, R as TokenEstimateWithWarnings, U as VisualIssue, W as VmlIssue, X as estimateAiFixTokens, Y as generateFixPrompt, Z as heuristicTokenCount } from './types-DiseE-UH.js';
3
3
  import * as cheerio from 'cheerio';
4
- export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy } from './react-email-BROBcC8u.js';
4
+ export { C as CompileError, a as CompileReactEmailOptions, S as SandboxStrategy } from './react-email-B82A1R_K.js';
5
5
 
6
6
  declare const EMAIL_CLIENTS: EmailClient[];
7
7
  declare function getClient(id: string): EmailClient | undefined;
@@ -410,12 +410,32 @@ declare function checkVml(html: string, options?: {
410
410
  * `roundrect` and `rect` are the whole surface. Anything else is left alone
411
411
  * rather than approximated.
412
412
  *
413
- * Deliberately NOT emulated: how Word *mis*-renders a nested shape. That is a
414
- * real and confirmed failure (the container disappears and text stops drawing
415
- * further down the document), but reproducing it means perturbing Chromium's
416
- * layout into a picture that is wrong in a third way, different from both the
417
- * correct render and Outlook's. `checkVml` reports it with a line number
418
- * instead, which is more useful and cannot mislead.
413
+ * How Word *mis*-renders a nested shape is emulated, but only the parts with
414
+ * evidence behind them. This note used to say the failure was not emulated at
415
+ * all, on the grounds that reproducing it meant perturbing Chromium into a
416
+ * picture wrong in a third way. That argument does not survive contact with
417
+ * the specific effects:
418
+ *
419
+ * - the container's fill and geometry disappear -> drop the box. Exact.
420
+ * - VML shapes further down stop drawing text -> blank their labels.
421
+ * Exact, and scoped to
422
+ * shapes we created.
423
+ * - the table structure terminates early, so later
424
+ * content escapes the frame -> WITHDRAWN. It had no
425
+ * methodology, and the
426
+ * fixture built to
427
+ * measure it contradicts
428
+ * it.
429
+ *
430
+ * Two of the three needed no distortion at all; the third turned out not to be
431
+ * a rendering problem but an evidence problem. Refusing to draw any of it
432
+ * meant the preview showed a tidy box with a button in it, a picture no Word
433
+ * engine produces, sitting beside a finding that said the region was
434
+ * destroyed. Between a warning and a screenshot, people believe the
435
+ * screenshot.
436
+ *
437
+ * Affected regions carry `data-vml-outlook` so a surface can label them, since
438
+ * a blank button should read as "Outlook does this" and not as our bug.
419
439
  */
420
440
  /**
421
441
  * `arcsize` is a VgFraction: a proportion of *half the shape's shorter side*,
package/dist/index.js CHANGED
@@ -8949,10 +8949,6 @@ function splitTopLevel(value) {
8949
8949
  return parts;
8950
8950
  }
8951
8951
 
8952
- // src/downlevel.ts
8953
- import * as cheerio from "cheerio";
8954
- import * as csstree from "css-tree";
8955
-
8956
8952
  // src/color-utils.ts
8957
8953
  var NAMED_COLORS = {
8958
8954
  aliceblue: [240, 248, 255],
@@ -9390,6 +9386,8 @@ function colorDistance(a, b) {
9390
9386
  }
9391
9387
 
9392
9388
  // src/downlevel.ts
9389
+ import * as cheerio from "cheerio";
9390
+ import * as csstree from "css-tree";
9393
9391
  var LOGICAL_PROPERTY_MAP = {
9394
9392
  "padding-inline": ["padding-left", "padding-right"],
9395
9393
  "padding-block": ["padding-top", "padding-bottom"],
@@ -9840,10 +9838,23 @@ function vmlToCss(html) {
9840
9838
  solid ? `background-color:${solid}` : "",
9841
9839
  `padding:${padding}`
9842
9840
  ].filter(Boolean).join(";");
9841
+ if (/data-vml="/.test(content)) {
9842
+ return `<div data-vml="rect" data-vml-outlook="container-dropped">${content}</div>`;
9843
+ }
9843
9844
  return `<div data-vml="rect" style="${background}${box}">${content}</div>`;
9844
9845
  }
9845
9846
  );
9846
- return out.replace(/<\/?v:(?:fill|textbox|stroke|shadow|imagedata|path|formulas|handles)[^>]*?\/?>/gi, "");
9847
+ out = out.replace(/<\/?v:(?:fill|textbox|stroke|shadow|imagedata|path|formulas|handles)[^>]*?\/?>/gi, "");
9848
+ return blankLabelsAfterNestedShape(out);
9849
+ }
9850
+ function blankLabelsAfterNestedShape(html) {
9851
+ const at = html.indexOf('data-vml-outlook="container-dropped"');
9852
+ if (at === -1) return html;
9853
+ const tail = html.slice(at).replace(
9854
+ /(<div data-vml="roundrect")([^>]*>)([\s\S]*?)(<\/div>)/gi,
9855
+ (whole, open, rest, label, close) => label.trim() ? `${open}${rest.slice(0, -1)} data-vml-outlook="label-dropped">${close}` : whole
9856
+ );
9857
+ return html.slice(0, at) + tail;
9847
9858
  }
9848
9859
  function renderOutlookBranch(html) {
9849
9860
  if (!/<!--\[if/i.test(html)) return html;
@@ -9895,6 +9906,7 @@ var EMPTY_SPAM = { score: 100, level: "low", issues: [] };
9895
9906
  var EMPTY_LINKS = {
9896
9907
  totalLinks: 0,
9897
9908
  issues: [],
9909
+ links: [],
9898
9910
  breakdown: { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, javascript: 0, protocolRelative: 0, other: 0 }
9899
9911
  };
9900
9912
  var EMPTY_ACCESSIBILITY = { score: 100, issues: [] };
@@ -10274,13 +10286,17 @@ var CLIENT_CONFIGS = {
10274
10286
  strippedProperties: OUTLOOK_WORD_UNSUPPORTED,
10275
10287
  stripMode: "strip",
10276
10288
  valueStrips: [
10277
- { prop: "background", pattern: /linear-gradient|radial-gradient/ },
10278
- { prop: "background-image", pattern: /linear-gradient|radial-gradient/ }
10289
+ // Gradients and image URLs alike: the Word engine paints neither. Only
10290
+ // VML does, and vmlToCss runs after this pass, so a hero built the
10291
+ // bulletproof way keeps the background this strip would otherwise take.
10292
+ { prop: "background", pattern: /linear-gradient|radial-gradient|url\(/ },
10293
+ { prop: "background-image", pattern: /linear-gradient|radial-gradient|url\(/ }
10279
10294
  ],
10280
10295
  inlineAndStripStyles: false,
10281
10296
  stripExternalStylesheets: false,
10282
10297
  stripForms: false,
10283
10298
  stripSvg: false,
10299
+ stripBackgroundAttribute: true,
10284
10300
  additionalChecks: outlookWindowsAdditionalChecks
10285
10301
  },
10286
10302
  "outlook-web": {
@@ -10501,6 +10517,11 @@ function applyTransform(html, config, framework) {
10501
10517
  $(el).replaceWith('<img alt="[SVG not supported]" />');
10502
10518
  });
10503
10519
  }
10520
+ if (config.stripBackgroundAttribute) {
10521
+ $("[background]").each((_, el) => {
10522
+ $(el).removeAttr("background");
10523
+ });
10524
+ }
10504
10525
  if (config.strippedProperties.size > 0 || config.valueStrips && config.valueStrips.length > 0) {
10505
10526
  $("[style]").each((_, el) => {
10506
10527
  const style = $(el).attr("style") || "";
@@ -10525,7 +10546,9 @@ function applyTransform(html, config, framework) {
10525
10546
  for (const vs of (_a = config.valueStrips) != null ? _a : []) {
10526
10547
  if (prop === vs.prop && vs.pattern.test(value)) {
10527
10548
  removed.push(prop);
10528
- props.delete(prop);
10549
+ const fallback = prop === "background" ? backgroundShorthandColor(value) : null;
10550
+ if (fallback) props.set(prop, fallback);
10551
+ else props.delete(prop);
10529
10552
  return;
10530
10553
  }
10531
10554
  }
@@ -12995,6 +13018,7 @@ function isPlaceholderHref(href) {
12995
13018
  }
12996
13019
  function validateLinksFromDom($) {
12997
13020
  const issues = [];
13021
+ const inventory = [];
12998
13022
  const breakdown = { https: 0, http: 0, mailto: 0, tel: 0, anchor: 0, javascript: 0, protocolRelative: 0, other: 0 };
12999
13023
  const links = $("a");
13000
13024
  const totalLinks = links.length;
@@ -13004,13 +13028,22 @@ function validateLinksFromDom($) {
13004
13028
  rule: "no-links",
13005
13029
  message: "Email contains no links"
13006
13030
  });
13007
- return { totalLinks: 0, issues, breakdown };
13031
+ return { totalLinks: 0, issues, breakdown, links: [] };
13008
13032
  }
13009
13033
  const hrefCounts = /* @__PURE__ */ new Map();
13010
13034
  links.each((_, el) => {
13011
13035
  const href = $(el).attr("href") || "";
13012
13036
  const text = $(el).text().trim();
13013
13037
  const category = classifyHref(href);
13038
+ const entry = {
13039
+ href,
13040
+ text: text.slice(0, 80),
13041
+ scheme: category,
13042
+ isPlaceholder: isPlaceholderHref(href),
13043
+ issues: []
13044
+ };
13045
+ inventory.push(entry);
13046
+ const flag = (rule) => entry.issues.push(rule);
13014
13047
  const elLoc = locOfElement(el);
13015
13048
  const hrefLoc = href ? locOfAttr(el, "href") : elLoc;
13016
13049
  switch (category) {
@@ -13043,6 +13076,7 @@ function validateLinksFromDom($) {
13043
13076
  hrefCounts.set(href, (hrefCounts.get(href) || 0) + 1);
13044
13077
  }
13045
13078
  if (!href || !href.trim()) {
13079
+ flag("empty-href");
13046
13080
  issues.push(__spreadValues({
13047
13081
  severity: "error",
13048
13082
  rule: "empty-href",
@@ -13052,6 +13086,7 @@ function validateLinksFromDom($) {
13052
13086
  return;
13053
13087
  }
13054
13088
  if (category === "javascript" && !isPlaceholderHref(href)) {
13089
+ flag("javascript-href");
13055
13090
  issues.push(__spreadValues({
13056
13091
  severity: "error",
13057
13092
  rule: "javascript-href",
@@ -13062,6 +13097,7 @@ function validateLinksFromDom($) {
13062
13097
  return;
13063
13098
  }
13064
13099
  if (isPlaceholderHref(href)) {
13100
+ flag("placeholder-href");
13065
13101
  issues.push(__spreadValues({
13066
13102
  severity: "warning",
13067
13103
  rule: "placeholder-href",
@@ -13072,6 +13108,7 @@ function validateLinksFromDom($) {
13072
13108
  return;
13073
13109
  }
13074
13110
  if (category === "http") {
13111
+ flag("insecure-link");
13075
13112
  issues.push(__spreadValues({
13076
13113
  severity: "warning",
13077
13114
  rule: "insecure-link",
@@ -13081,6 +13118,7 @@ function validateLinksFromDom($) {
13081
13118
  }, hrefLoc ? { loc: hrefLoc } : {}));
13082
13119
  }
13083
13120
  if (category === "protocol-relative") {
13121
+ flag("protocol-relative");
13084
13122
  issues.push(__spreadValues({
13085
13123
  severity: "warning",
13086
13124
  rule: "protocol-relative",
@@ -13090,6 +13128,7 @@ function validateLinksFromDom($) {
13090
13128
  }, hrefLoc ? { loc: hrefLoc } : {}));
13091
13129
  }
13092
13130
  if (text && GENERIC_LINK_TEXT.has(text.toLowerCase())) {
13131
+ flag("generic-link-text");
13093
13132
  issues.push(__spreadValues({
13094
13133
  severity: "warning",
13095
13134
  rule: "generic-link-text",
@@ -13099,6 +13138,7 @@ function validateLinksFromDom($) {
13099
13138
  }, elLoc ? { loc: elLoc } : {}));
13100
13139
  }
13101
13140
  if (!text && !$(el).attr("aria-label") && !$(el).find("img[alt]").length) {
13141
+ flag("empty-link-text");
13102
13142
  issues.push(__spreadValues({
13103
13143
  severity: "error",
13104
13144
  rule: "empty-link-text",
@@ -13107,6 +13147,7 @@ function validateLinksFromDom($) {
13107
13147
  }, elLoc ? { loc: elLoc } : {}));
13108
13148
  }
13109
13149
  if (category === "mailto" && href.trim().toLowerCase() === "mailto:") {
13150
+ flag("empty-mailto");
13110
13151
  issues.push(__spreadValues({
13111
13152
  severity: "error",
13112
13153
  rule: "empty-mailto",
@@ -13116,6 +13157,7 @@ function validateLinksFromDom($) {
13116
13157
  }, hrefLoc ? { loc: hrefLoc } : {}));
13117
13158
  }
13118
13159
  if (category === "tel" && href.trim().toLowerCase() === "tel:") {
13160
+ flag("empty-tel");
13119
13161
  issues.push(__spreadValues({
13120
13162
  severity: "error",
13121
13163
  rule: "empty-tel",
@@ -13125,6 +13167,7 @@ function validateLinksFromDom($) {
13125
13167
  }, hrefLoc ? { loc: hrefLoc } : {}));
13126
13168
  }
13127
13169
  if (href.length > 2e3) {
13170
+ flag("long-url");
13128
13171
  issues.push(__spreadValues({
13129
13172
  severity: "info",
13130
13173
  rule: "long-url",
@@ -13134,7 +13177,8 @@ function validateLinksFromDom($) {
13134
13177
  }, hrefLoc ? { loc: hrefLoc } : {}));
13135
13178
  }
13136
13179
  });
13137
- links.each((_, el) => {
13180
+ links.each((i, el) => {
13181
+ var _a;
13138
13182
  const href = $(el).attr("href") || "";
13139
13183
  const trimmed = href.trim();
13140
13184
  if (trimmed.startsWith("#") && trimmed.length > 1) {
@@ -13142,6 +13186,7 @@ function validateLinksFromDom($) {
13142
13186
  const target = $(`[id="${targetId}"]`);
13143
13187
  const anchorLoc = locOfAttr(el, "href");
13144
13188
  if (target.length === 0) {
13189
+ (_a = inventory[i]) == null ? void 0 : _a.issues.push("broken-anchor");
13145
13190
  issues.push(__spreadValues({
13146
13191
  severity: "error",
13147
13192
  rule: "broken-anchor",
@@ -13162,7 +13207,7 @@ function validateLinksFromDom($) {
13162
13207
  });
13163
13208
  }
13164
13209
  }
13165
- return { totalLinks, issues, breakdown };
13210
+ return { totalLinks, issues, breakdown, links: inventory };
13166
13211
  }
13167
13212
  function validateLinks(html, options) {
13168
13213
  return fromHtml(html, EMPTY_LINKS, validateLinksFromDom, options);
@@ -14701,7 +14746,20 @@ function checkVml(html, options) {
14701
14746
  rule: "vml-nested-shape",
14702
14747
  severity: "error",
14703
14748
  message: `<v:${name}> is nested inside <v:${parent.name}>. Outlook does not support nesting one VML shape inside another.`,
14704
- 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).`
14749
+ // Two effects, not the three this used to claim. The withdrawn one
14750
+ // said the table structure terminates early so content after the
14751
+ // shape falls outside the email frame. It had no methodology behind
14752
+ // it, and `T1c-blast-radius.html`, the fixture built to measure
14753
+ // exactly this, contradicts it: the block after the nested shape
14754
+ // rendered in place inside the same table with its background and
14755
+ // borders intact, and only the VML labels were missing. The likely
14756
+ // origin is over-reading "layout breaks after it" from a run where a
14757
+ // 600px hero band vanished and everything below shifted up.
14758
+ //
14759
+ // Kept as a warning to whoever edits this next: the probe sentence
14760
+ // below covers the downstream text suppression only. Do not let it
14761
+ // lend its authority to a neighbouring clause again.
14762
+ detail: `Verified in Outlook Classic, and the damage is not local. Two things happen: the containing <v:${parent.name}> does not render at all (its fill and everything inside it disappear, leaving the inner shape stranded); and every VML shape further down the email stops drawing its text, so buttons and headings after this point ship as blank coloured blocks. That second effect was 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).`
14705
14763
  }, loc));
14706
14764
  }
14707
14765
  if (!tag.selfClosing) openShapes.push(tag);