email-builder-utils 1.1.49 → 1.1.52

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.
@@ -33,23 +33,23 @@ function buildVMLShape({ shape, widthPx, heightPx, imageUrl, backgroundColor, bo
33
33
  const hAlign = hAlignMap[textAlign] || "center";
34
34
  const safeFontSize = Math.max(Math.round(textSize), 10);
35
35
  const textboxMarkup = text && !msoHasBakedText
36
- ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
37
- <div style="display:table;width:100%;height:100%;">
38
- <div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
39
- <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
40
- ${text}
41
- </div>
42
- </div>
43
- </div>
36
+ ? `<v:textbox inset="6pt,6pt,6pt,6pt" style="mso-fit-shape-to-text:false;">
37
+ <div style="display:table;width:100%;height:100%;">
38
+ <div style="display:table-cell;vertical-align:${vAlign};text-align:${hAlign};padding:0 6px;">
39
+ <div style="color:${textColor};font-family:Arial, sans-serif;font-size:${safeFontSize}px;line-height:1.3;word-wrap:break-word;">
40
+ ${text}
41
+ </div>
42
+ </div>
43
+ </div>
44
44
  </v:textbox>`
45
45
  : `<v:textbox inset="0,0,0,0"><div style="display:none;">.</div></v:textbox>`;
46
- return `
47
- <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
48
- style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
49
- ${borderAttrs}
50
- fill="true" fillcolor="${fillColor}"${extraAttr}>
51
- ${fillMarkup}
52
- ${textboxMarkup}
46
+ return `
47
+ <v:${tag} xmlns:v="urn:schemas-microsoft-com:vml"
48
+ style="width:${widthPx}px;height:${heightPx}px;display:inline-block;"
49
+ ${borderAttrs}
50
+ fill="true" fillcolor="${fillColor}"${extraAttr}>
51
+ ${fillMarkup}
52
+ ${textboxMarkup}
53
53
  </v:${tag}>`;
54
54
  }
55
55
  function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth, opts, visibilityClass) {
@@ -70,28 +70,28 @@ function appendOutlookForShape(content, outerContainerWidth, innerContainerWidth
70
70
  const valign = opts.verticalAlign || "middle";
71
71
  const shouldHideInOutlook = visibilityClass.includes("hide-desktop");
72
72
  if (shouldHideInOutlook) {
73
- return `<!--[if !mso]><!-->
74
- <table align="${align}" border="0" cellpadding="0" cellspacing="0"
75
- style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
76
- <tr>
77
- <td valign="${valign}"
78
- style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
79
- ${vml}
80
- </td>
81
- </tr>
82
- </table>
73
+ return `<!--[if !mso]><!-->
74
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
75
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
76
+ <tr>
77
+ <td valign="${valign}"
78
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
79
+ ${vml}
80
+ </td>
81
+ </tr>
82
+ </table>
83
83
  <!--<![endif]-->`;
84
84
  }
85
- return `<!--[if mso]>
86
- <table align="${align}" border="0" cellpadding="0" cellspacing="0"
87
- style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
88
- <tr>
89
- <td valign="${valign}"
90
- style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
91
- ${vml}
92
- </td>
93
- </tr>
94
- </table>
85
+ return `<!--[if mso]>
86
+ <table align="${align}" border="0" cellpadding="0" cellspacing="0"
87
+ style="width:${widthPx}px;height:${heightPx}px;border-collapse:collapse;" class="${visibilityClass}">
88
+ <tr>
89
+ <td valign="${valign}"
90
+ style="padding:${pad.top || 0}px ${pad.right || 0}px ${pad.bottom || 0}px ${pad.left || 0}px;">
91
+ ${vml}
92
+ </td>
93
+ </tr>
94
+ </table>
95
95
  <![endif]-->`;
96
96
  }
97
97
  async function convertShapeBlock(blockData) {
@@ -146,58 +146,58 @@ async function convertShapeBlock(blockData) {
146
146
  let nonMsoContent = "";
147
147
  // --- Case 1: Image + Text ---
148
148
  if (imageUrl && text) {
149
- nonMsoContent = `
150
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
151
- border:${borderWidth}px ${borderStyle} ${borderColor};
152
- border-radius:${resolvedBorderRadius};
153
- background-color:${finalBackgroundColor};
154
- background-image:url('${imageUrl}');
155
- background-position:center center;
156
- background-size:cover;
157
- background-repeat:no-repeat;
158
- overflow:hidden;${alignmentStyle}${customCss || ""}">
159
- <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
160
- style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
161
- <tr>
162
- <td align="${textAlignStyle}" valign="${verticalAlign}"
163
- width="${resolvedWidthPx}" height="${resolvedHeightPx}"
164
- style="padding:6px;vertical-align:${verticalAlign};text-align:${textAlignStyle};overflow:hidden;box-sizing:border-box;">
165
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text}</div>
166
- </td>
167
- </tr>
168
- </table>
149
+ nonMsoContent = `
150
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;max-width:100%;box-sizing:border-box;
151
+ border:${borderWidth}px ${borderStyle} ${borderColor};
152
+ border-radius:${resolvedBorderRadius};
153
+ background-color:${finalBackgroundColor};
154
+ background-image:url('${imageUrl}');
155
+ background-position:center center;
156
+ background-size:cover;
157
+ background-repeat:no-repeat;
158
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
159
+ <table border="0" cellpadding="0" cellspacing="0" width="100%"
160
+ style="width:100%;height:${resolvedHeightPx}px;border-collapse:collapse;">
161
+ <tr>
162
+ <td align="${textAlignStyle}" valign="${verticalAlign}"
163
+ height="${resolvedHeightPx}"
164
+ style="padding:6px;vertical-align:${verticalAlign};text-align:${textAlignStyle};overflow:hidden;box-sizing:border-box;">
165
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text}</div>
166
+ </td>
167
+ </tr>
168
+ </table>
169
169
  </div>`;
170
170
  }
171
171
  // --- Case 2: Image only ---
172
172
  else if (imageUrl) {
173
- nonMsoContent = `
174
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
175
- border:${borderWidth}px ${borderStyle} ${borderColor};
176
- border-radius:${resolvedBorderRadius};
177
- overflow:hidden;${alignmentStyle}${customCss || ""}">
178
- <img src="${imageUrl}" alt="${text || "shape image"}"
179
- width="${resolvedWidthPx}" height="${resolvedHeightPx}"
180
- style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
173
+ nonMsoContent = `
174
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;max-width:100%;box-sizing:border-box;
175
+ border:${borderWidth}px ${borderStyle} ${borderColor};
176
+ border-radius:${resolvedBorderRadius};
177
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
178
+ <img src="${imageUrl}" alt="${text || "shape image"}"
179
+ width="${resolvedWidthPx}" height="${resolvedHeightPx}"
180
+ style="width:100%;height:100%;object-fit:cover;border-radius:${resolvedBorderRadius};display:block;" />
181
181
  </div>`;
182
182
  }
183
183
  // --- Case 3: Text only ---
184
184
  else {
185
- nonMsoContent = `
186
- <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;
187
- background-color:${finalBackgroundColor};
188
- border:${borderWidth}px ${borderStyle} ${borderColor};
189
- border-radius:${resolvedBorderRadius};
190
- overflow:hidden;${alignmentStyle}${customCss || ""}">
191
- <table border="0" cellpadding="0" cellspacing="0" width="${resolvedWidthPx}"
192
- style="width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;border-collapse:collapse;">
193
- <tr>
194
- <td align="${textAlignStyle}" valign="${verticalAlign}"
195
- width="${resolvedWidthPx}" height="${resolvedHeightPx}"
196
- style="padding:8px;vertical-align:${verticalAlign};text-align:${textAlignStyle};box-sizing:border-box;">
197
- <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text || ""}</div>
198
- </td>
199
- </tr>
200
- </table>
185
+ nonMsoContent = `
186
+ <div style="display:inline-block;width:${resolvedWidthPx}px;height:${resolvedHeightPx}px;max-width:100%;box-sizing:border-box;
187
+ background-color:${finalBackgroundColor};
188
+ border:${borderWidth}px ${borderStyle} ${borderColor};
189
+ border-radius:${resolvedBorderRadius};
190
+ overflow:hidden;${alignmentStyle}${customCss || ""}">
191
+ <table border="0" cellpadding="0" cellspacing="0" width="100%"
192
+ style="width:100%;height:${resolvedHeightPx}px;border-collapse:collapse;">
193
+ <tr>
194
+ <td align="${textAlignStyle}" valign="${verticalAlign}"
195
+ height="${resolvedHeightPx}"
196
+ style="padding:8px;vertical-align:${verticalAlign};text-align:${textAlignStyle};box-sizing:border-box;">
197
+ <div style="${textSizeStyle}text-align:${textAlignStyle};max-width:90%;overflow:hidden;">${text || ""}</div>
198
+ </td>
199
+ </tr>
200
+ </table>
201
201
  </div>`;
202
202
  }
203
203
  // Outlook (VML) fallback
@@ -242,15 +242,15 @@ async function convertShapeBlock(blockData) {
242
242
  hideOnMobile: Boolean(props.hideOnMobile),
243
243
  });
244
244
  // Combine into table wrapper
245
- return `
246
- <table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}" data-block-type="shape" data-block-props="${shapeBlockProps}">
247
- <tr>
248
- <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
249
- ${outlookContent}
250
- <!--[if !mso]><!-->
251
- ${nonMsoContent}
252
- <!--<![endif]-->
253
- </td>
254
- </tr>
245
+ return `
246
+ <table width="100%" style="border-collapse:collapse;table-layout:fixed;max-width:600px;" class="${visibilityClass}" data-block-type="shape" data-block-props="${shapeBlockProps}">
247
+ <tr>
248
+ <td style="padding:${padding.top || 0}px ${padding.right || 0}px ${padding.bottom || 0}px ${padding.left || 0}px;text-align:${alignment};">
249
+ ${outlookContent}
250
+ <!--[if !mso]><!-->
251
+ ${nonMsoContent}
252
+ <!--<![endif]-->
253
+ </td>
254
+ </tr>
255
255
  </table>`;
256
256
  }
@@ -1 +1 @@
1
- {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/text.ts"],"names":[],"mappings":"AAKA,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,MAAM,UA4ItE"}
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../../src/utils/blocks/text.ts"],"names":[],"mappings":"AASA,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,MAAM,UAkLtE"}
@@ -8,42 +8,69 @@ const common_1 = require("../common");
8
8
  function convertTextBlock(blockData, cellWidthInPx) {
9
9
  const { style, props } = blockData.data;
10
10
  const visibilityClass = (0, common_1.getVisibilityClass)(props);
11
- const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, backgroundImage, backgroundPosition, backgroundRepeat, backgroundSize, whiteSpace: _whiteSpace, ...rest } = style;
12
- const bgImageStr = typeof backgroundImage === 'string' ? backgroundImage : '';
13
- const customCssStr = rest.customCss || '';
14
- const gradientInCustomCss = !bgImageStr.includes('gradient(') && customCssStr.includes('gradient(')
15
- ? (customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || '')
16
- : '';
17
- const effectiveGradient = bgImageStr.includes('gradient(') ? bgImageStr : gradientInCustomCss;
11
+ const { width, backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth, textContainerBackgroundColor, textContainerPadding, fontSize, backgroundImage, whiteSpace: _whiteSpace, // strip from outer td — pre-wrap on a td preserves editor whitespace
12
+ ...rest } = style;
13
+ // Detect background image or gradient (may live in backgroundImage or customCss).
14
+ // Same multi-client approach as Grid block: outer wrapper <td> carries the background
15
+ // via CSS (Gmail/New Outlook), background attribute (Yahoo), and VML (Old Outlook).
16
+ const bgImageStr = typeof backgroundImage === "string" ? backgroundImage : "";
17
+ const customCssStr = rest.customCss || "";
18
+ const gradientInCustomCss = !bgImageStr.includes("gradient(") && customCssStr.includes("gradient(")
19
+ ? customCssStr.match(/(?:linear|radial|conic)-gradient\([^)]+(?:\([^)]*\)[^)]*)*\)/)?.[0] || ""
20
+ : "";
21
+ const effectiveGradient = bgImageStr.includes("gradient(")
22
+ ? bgImageStr
23
+ : gradientInCustomCss;
18
24
  const isGradient = Boolean(effectiveGradient);
19
25
  const parsedGradient = isGradient ? (0, gradientUtils_1.parseGradient)(effectiveGradient) : null;
20
26
  const rawBgImageUrl = !isGradient && bgImageStr
21
- ? bgImageStr.replace(/^url\(['"]?/, '').replace(/['"]?\)$/, '')
27
+ ? bgImageStr.replace(/^url\(['"]?/, "").replace(/['"]?\)$/, "")
22
28
  : null;
23
29
  const hasBgImage = Boolean(rawBgImageUrl || isGradient);
24
30
  const fallbackBgColor = textContainerBackgroundColor ||
25
31
  parsedGradient?.fallback ||
26
32
  (0, gradientUtils_1.extractCssFallbackColor)(customCssStr) ||
27
- '#ffffff';
28
- const textBoxStyle = { backgroundColor, padding, borderRadius, borderStyle, borderColor, borderWidth };
29
- const convertedTextStyle = (0, buildStyles_1.buildStyles)(textBoxStyle, { perChanges: [], pxChanges: buildStyles_1.allPxAttributes });
33
+ "#ffffff";
34
+ // Text box decoration styles (border, background, padding) no width
35
+ const textBoxStyle = {
36
+ backgroundColor,
37
+ padding,
38
+ borderRadius,
39
+ borderStyle,
40
+ borderColor,
41
+ borderWidth,
42
+ };
43
+ const convertedTextStyle = (0, buildStyles_1.buildStyles)(textBoxStyle, {
44
+ perChanges: [],
45
+ pxChanges: buildStyles_1.allPxAttributes,
46
+ });
47
+ // Strip gradient from customCss when it is hoisted to the outer bg wrapper.
30
48
  const innerCustomCss = gradientInCustomCss
31
- ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, '').trim()
49
+ ? customCssStr.replace(/background-image\s*:[^;]+;?/gi, "").trim()
32
50
  : customCssStr;
33
- const restForStyles = gradientInCustomCss ? { ...rest, customCss: innerCustomCss } : rest;
51
+ const restForStyles = gradientInCustomCss
52
+ ? { ...rest, customCss: innerCustomCss }
53
+ : rest;
54
+ // Outer td styles: strip container background when a bg-image wrapper is present
55
+ // so the outer wrapper's background is not double-applied.
34
56
  const styles = (0, buildStyles_1.buildStyles)({
35
57
  padding: textContainerPadding,
36
58
  backgroundColor: hasBgImage ? undefined : textContainerBackgroundColor,
37
59
  ...restForStyles,
38
- }, { perChanges: [], pxChanges: buildStyles_1.allPxAttributes });
60
+ }, {
61
+ perChanges: [],
62
+ pxChanges: buildStyles_1.allPxAttributes,
63
+ });
39
64
  const sanitizedText = (props.text ?? "")
40
65
  .replace(/<p(\s[^>]*)?>/gi, (_, attrs) => `<div${attrs || ""}>`)
41
66
  .replace(/<\/p>/gi, "</div>");
42
67
  const navigateToUrl = props.navigateToUrl || "";
43
68
  const fontSizeStyle = fontSize != null ? `font-size:${fontSize}px;` : "";
69
+ // Email clients apply `a { color: blue }` which overrides inherited color.
70
+ // Inject the block color directly onto <a> tags that don't already have one.
44
71
  const blockTextColor = rest.color;
45
72
  const processedText = blockTextColor
46
- ? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = '') => {
73
+ ? sanitizedText.replace(/<a(\s[^>]*)?>/gi, (match, attrs = "") => {
47
74
  if (/style\s*=\s*["'][^"']*\bcolor\s*:/i.test(attrs))
48
75
  return match;
49
76
  if (/\bstyle\s*=/i.test(attrs)) {
@@ -52,52 +79,56 @@ function convertTextBlock(blockData, cellWidthInPx) {
52
79
  return `<a${attrs} style="color:${blockTextColor};">`;
53
80
  })
54
81
  : sanitizedText;
55
- const colorStyle = blockTextColor ? `color:${blockTextColor};` : '';
82
+ const colorStyle = blockTextColor ? `color:${blockTextColor};` : "";
83
+ // Use display:block + width:100% so text fills the column naturally.
84
+ // display:inline-block with a pixel width (e.g. 400px) breaks narrow grid cells.
56
85
  const convertedTextBox = `<div style="display:block; width:100%; box-sizing:border-box; ${colorStyle}${fontSizeStyle}${convertedTextStyle}">${processedText.replaceAll(/\n/g, "<br>")}</div>`;
57
- const safeCellWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : undefined;
58
- const textContent = (0, outlookSupport_1.appendOutlookSupport)(convertedTextBox, styles, hasBgImage ? '' : visibilityClass, safeCellWidth);
59
- const linkColorStyle = blockTextColor ? `color:${blockTextColor};` : 'color:inherit;';
86
+ const safeCellWidth = cellWidthInPx
87
+ ? Math.min(cellWidthInPx, 600)
88
+ : undefined;
89
+ // When a bg-image wrapper is present, visibilityClass moves to the outer table.
90
+ const textContent = (0, outlookSupport_1.appendOutlookSupport)(convertedTextBox, styles, hasBgImage ? "" : visibilityClass, safeCellWidth);
91
+ const linkColorStyle = blockTextColor
92
+ ? `color:${blockTextColor};`
93
+ : "color:inherit;";
60
94
  if (hasBgImage) {
61
95
  const msoWidth = cellWidthInPx ? Math.min(cellWidthInPx, 600) : 600;
62
96
  const vmlFill = isGradient
63
97
  ? (() => {
64
98
  const vmlAngle = (0, gradientUtils_1.cssAngleToVml)(parsedGradient?.angle || 180);
65
- const c1 = parsedGradient?.fallback || '#ffffff';
99
+ const c1 = parsedGradient?.fallback || "#ffffff";
66
100
  const c2 = parsedGradient?.colors[parsedGradient.colors.length - 1] || c1;
67
101
  return `<v:fill type="gradient" color="${c1}" color2="${c2}" angle="${vmlAngle}" />`;
68
102
  })()
69
103
  : `<v:fill type="frame" src="${rawBgImageUrl}" color="${fallbackBgColor}" />`;
70
- const bgPosition = backgroundPosition ?? 'center center';
71
- const bgSize = backgroundSize ?? 'cover';
72
- const bgRepeat = backgroundRepeat ?? 'no-repeat';
73
104
  const bgCss = isGradient
74
105
  ? `background:${effectiveGradient};`
75
- : `background-image:url('${rawBgImageUrl}'); background-position:${bgPosition}; background-size:${bgSize}; background-repeat:${bgRepeat};`;
76
- const wrappedContent = `
77
- <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
78
- style="border-collapse:collapse;width:100%;max-width:${msoWidth}px;" class="${visibilityClass}">
79
- <tr>
80
- <td width="100%" bgcolor="${fallbackBgColor}" valign="top"
81
- ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ''}
82
- style="width:100%;max-width:${msoWidth}px;background-color:${fallbackBgColor};${bgCss}">
83
-
84
- <!--[if gte mso 9]>
85
- <v:rect xmlns:v="urn:schemas-microsoft-com:vml"
86
- fill="true" stroke="false"
87
- style="width:${msoWidth}px;">
88
- ${vmlFill}
89
- <v:textbox inset="0,0,0,0">
90
- <![endif]-->
91
-
92
- ${textContent}
93
-
94
- <!--[if gte mso 9]>
95
- </v:textbox>
96
- </v:rect>
97
- <![endif]-->
98
-
99
- </td>
100
- </tr>
106
+ : `background-image:url('${rawBgImageUrl}'); background-position:center center; background-size:cover; background-repeat:no-repeat;`;
107
+ const wrappedContent = `
108
+ <table border="0" cellpadding="0" cellspacing="0" width="100%" role="presentation"
109
+ style="border-collapse:collapse;width:100%;max-width:${msoWidth}px;" class="${visibilityClass}">
110
+ <tr>
111
+ <td width="100%"${(0, common_1.buildOutlookBgAttr)(fallbackBgColor)} valign="top"
112
+ ${!isGradient && rawBgImageUrl ? `background="${rawBgImageUrl}"` : ""}
113
+ style="width:100%;max-width:${msoWidth}px;background-color:${fallbackBgColor};${bgCss}">
114
+
115
+ <!--[if gte mso 9]>
116
+ <v:rect xmlns:v="urn:schemas-microsoft-com:vml"
117
+ fill="true" stroke="false"
118
+ style="width:${msoWidth}px;">
119
+ ${vmlFill}
120
+ <v:textbox inset="0,0,0,0">
121
+ <![endif]-->
122
+
123
+ ${textContent}
124
+
125
+ <!--[if gte mso 9]>
126
+ </v:textbox>
127
+ </v:rect>
128
+ <![endif]-->
129
+
130
+ </td>
131
+ </tr>
101
132
  </table>`;
102
133
  return navigateToUrl
103
134
  ? `<a href="${navigateToUrl}" rel="noreferrer noopener" style="${linkColorStyle}text-decoration:none;cursor:pointer;">${wrappedContent}</a>`
@@ -88,64 +88,64 @@ async function convertVideoBlock(blockData, cellWidthInPx) {
88
88
  // min-height fallback for clients that ignore the spacer GIF (e.g. some webmail).
89
89
  const minHeight = Math.round(thumbnailW * 0.3);
90
90
  // VML block — shown only in Outlook (Word engine supports VML, not standard HTML).
91
- const outlookContent = hideOnDesktop ? '' : `<!--[if vml]>
92
- <v:group xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
93
- coordsize="${thumbnailW},${thumbnailH}" coordorigin="0,0"
94
- href="${videoLink}"
95
- style="width:${thumbnailW}px;height:${thumbnailH}px;">
96
- <v:rect fill="t" stroked="f" style="position:absolute;width:${thumbnailW};height:${thumbnailH};">
97
- <v:fill src="${resolvedThumbnail}" type="frame"/>
98
- </v:rect>
99
- <v:oval fill="t" strokecolor="#ffffff" strokeweight="3px"
100
- style="position:absolute;left:${ovalLeft};top:${ovalTop};width:${ovalSize};height:${ovalSize}">
101
- <v:fill color="#ffffff" opacity="100%" />
102
- </v:oval>
103
- <v:shape coordsize="24,32" path="m,l,32,24,16,xe" fillcolor="#000000" stroked="f"
104
- style="position:absolute;left:${triLeft};top:${triTop};width:${triW};height:${triH};" />
105
- </v:group>
91
+ const outlookContent = hideOnDesktop ? '' : `<!--[if vml]>
92
+ <v:group xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
93
+ coordsize="${thumbnailW},${thumbnailH}" coordorigin="0,0"
94
+ href="${videoLink}"
95
+ style="width:${thumbnailW}px;height:${thumbnailH}px;">
96
+ <v:rect fill="t" stroked="f" style="position:absolute;width:${thumbnailW};height:${thumbnailH};">
97
+ <v:fill src="${resolvedThumbnail}" type="frame"/>
98
+ </v:rect>
99
+ <v:oval fill="t" strokecolor="#ffffff" strokeweight="3px"
100
+ style="position:absolute;left:${ovalLeft};top:${ovalTop};width:${ovalSize};height:${ovalSize}">
101
+ <v:fill color="#ffffff" opacity="100%" />
102
+ </v:oval>
103
+ <v:shape coordsize="24,32" path="m,l,32,24,16,xe" fillcolor="#000000" stroked="f"
104
+ style="position:absolute;left:${triLeft};top:${triTop};width:${triW};height:${triH};" />
105
+ </v:group>
106
106
  <![endif]-->`;
107
107
  // Non-VML block — shown in all clients except Outlook.
108
- const nonMsoContent = `<!--[if !vml]><!-->
109
- <a href="${videoLink}" target="_blank"
110
- style="display:block; text-decoration:none; background-image:url('${resolvedThumbnail}'); background-size:cover; background-position:center; ${borderCss}${radiusCss}">
111
- <table cellpadding="0" cellspacing="0" border="0" width="100%"
112
- background="${resolvedThumbnail}"
113
- role="presentation"
114
- style="background-image:url('${resolvedThumbnail}'); background-size:cover; background-position:center center; background-repeat:no-repeat; min-height:${minHeight}px;">
115
- <tr>
116
- <td width="25%" style="line-height:0; font-size:0; padding:0;">
117
- <img src="${SPACER_GIF_URL}" width="100%" border="0" alt=""
118
- style="display:block; height:auto; opacity:0; visibility:hidden;" />
119
- </td>
120
- <td width="50%" align="center" valign="middle"
121
- style="text-align:center; vertical-align:middle; padding:0;">
122
- <img src="${PLAY_ICON_URL}" width="${playIconW}" height="${playIconH}" alt="Play"
123
- style="display:block; width:${playIconW}px; height:${playIconH}px; border:0; margin:0 auto;" />
124
- </td>
125
- <td width="25%" style="padding:0;">&nbsp;</td>
126
- </tr>
127
- </table>
128
- </a>
108
+ const nonMsoContent = `<!--[if !vml]><!-->
109
+ <a href="${videoLink}" target="_blank"
110
+ style="display:block; text-decoration:none; background-image:url('${resolvedThumbnail}'); background-size:cover; background-position:center; ${borderCss}${radiusCss}">
111
+ <table cellpadding="0" cellspacing="0" border="0" width="100%"
112
+ background="${resolvedThumbnail}"
113
+ role="presentation"
114
+ style="background-image:url('${resolvedThumbnail}'); background-size:cover; background-position:center center; background-repeat:no-repeat; min-height:${minHeight}px;">
115
+ <tr>
116
+ <td width="25%" style="line-height:0; font-size:0; padding:0;">
117
+ <img src="${SPACER_GIF_URL}" width="100%" border="0" alt=""
118
+ style="display:block; height:auto; opacity:0; visibility:hidden;" />
119
+ </td>
120
+ <td width="50%" align="center" valign="middle"
121
+ style="text-align:center; vertical-align:middle; padding:0;">
122
+ <img src="${PLAY_ICON_URL}" width="${playIconW}" height="${playIconH}" alt="Play"
123
+ style="display:block; width:${playIconW}px; height:${playIconH}px; border:0; margin:0 auto;" />
124
+ </td>
125
+ <td width="25%" style="padding:0;">&nbsp;</td>
126
+ </tr>
127
+ </table>
128
+ </a>
129
129
  <!--<![endif]-->`;
130
- return `
131
- <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
132
- style="border-collapse:collapse; max-width:600px; margin:0; padding:0;"
133
- class="${visibilityClass}" data-block-type="video"
134
- data-video-url="${videoUrl || ''}" data-youtube-url="${youtubeVideoUrl || ''}"
135
- data-thumbnail-url="${thumbnailUrl || ''}" data-alt-text="${altText || ''}"
136
- data-width="${typeof style?.width === 'number' ? style.width : 100}">
137
- <tr>
138
- <td align="${align}" style="${outerContainerStyles}">
139
- <table border="0" cellpadding="0" cellspacing="0" role="presentation"
140
- align="${align}"
141
- style="border-collapse:collapse; max-width:${thumbnailW}px; width:${percentWidth};">
142
- <tr>
143
- <td align="${align}" style="padding:0;">
144
- ${outlookContent}${nonMsoContent}
145
- </td>
146
- </tr>
147
- </table>
148
- </td>
149
- </tr>
130
+ return `
131
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
132
+ style="border-collapse:collapse; max-width:600px; margin:0; padding:0;"
133
+ class="${visibilityClass}" data-block-type="video"
134
+ data-video-url="${videoUrl || ''}" data-youtube-url="${youtubeVideoUrl || ''}"
135
+ data-thumbnail-url="${thumbnailUrl || ''}" data-alt-text="${altText || ''}"
136
+ data-width="${typeof style?.width === 'number' ? style.width : 100}">
137
+ <tr>
138
+ <td align="${align}" style="${outerContainerStyles}">
139
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation"
140
+ align="${align}"
141
+ style="border-collapse:collapse; max-width:${thumbnailW}px; width:${percentWidth};">
142
+ <tr>
143
+ <td align="${align}" style="padding:0;">
144
+ ${outlookContent}${nonMsoContent}
145
+ </td>
146
+ </tr>
147
+ </table>
148
+ </td>
149
+ </tr>
150
150
  </table>`;
151
151
  }
@@ -1 +1 @@
1
- {"version":3,"file":"buildStyles.d.ts","sourceRoot":"","sources":["../../src/utils/buildStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,UAA8C,CAAC;AAC7E,eAAO,MAAM,sBAAsB,UAAsB,CAAC;AAC1D,eAAO,MAAM,eAAe,UAAoD,CAAC;AAEjF,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAQ/E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAe7D;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,GAAG,EACV,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,UAmEzE"}
1
+ {"version":3,"file":"buildStyles.d.ts","sourceRoot":"","sources":["../../src/utils/buildStyles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,UAA8C,CAAC;AAC7E,eAAO,MAAM,sBAAsB,UAAsB,CAAC;AAC1D,eAAO,MAAM,eAAe,UAAoD,CAAC;AAEjF,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAQ/E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAkB7D;AACD,wBAAgB,WAAW,CACzB,KAAK,EAAE,GAAG,EACV,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,UAqFzE"}
@@ -20,12 +20,15 @@ function sanitizeFontFamily(fontFamily) {
20
20
  .split(',')
21
21
  .map(font => {
22
22
  const trimmed = font.trim();
23
+ // Strip any surrounding quotes (single or double) from either end
23
24
  const unquoted = trimmed.replace(/^["']|["']$/g, '').trim();
24
25
  if (!unquoted)
25
26
  return '';
27
+ // Generic families and single-token names need no quotes
26
28
  if (GENERIC_FONT_FAMILIES.has(unquoted.toLowerCase()) || !/\s/.test(unquoted)) {
27
29
  return unquoted;
28
30
  }
31
+ // Multi-word font name: wrap in single quotes, escaping any embedded single quotes
29
32
  return `'${unquoted.replace(/'/g, "\\'")}'`;
30
33
  })
31
34
  .filter(Boolean)
@@ -35,18 +38,28 @@ function buildStyles(style, { pxChanges, perChanges }) {
35
38
  if (!style)
36
39
  style = {};
37
40
  const stylesObj = {};
38
- const INVALID_KEYS = [
39
- "columns", "cellWidths", "cellWidth", "childWidth",
40
- "visibility", "hideOnMobile", "hideOnDesktop", "label", "alignment",
41
- ];
42
41
  Object.entries(style).forEach(([key, value]) => {
43
42
  if (key === "customCss")
44
43
  return;
44
+ const INVALID_KEYS = [
45
+ "columns",
46
+ "cellWidths",
47
+ "cellWidth",
48
+ "childWidth",
49
+ "visibility",
50
+ "hideOnMobile",
51
+ "hideOnDesktop",
52
+ "label",
53
+ "alignment",
54
+ ];
45
55
  if (INVALID_KEYS.includes(key))
46
56
  return;
57
+ // Prevent null/undefined/"" from leaking into CSS
47
58
  if (value === undefined || value === null || value === "")
48
59
  return;
49
- if ((key === "padding" || key === "buttonPadding") && typeof value === "object") {
60
+ // FIX 1 SANITIZE padding objects
61
+ if ((key === "padding" || key === "buttonPadding") &&
62
+ typeof value === "object") {
50
63
  const pad = value;
51
64
  const safePad = {
52
65
  top: Number.isFinite(pad.top) ? pad.top : 0,
@@ -59,21 +72,26 @@ function buildStyles(style, { pxChanges, perChanges }) {
59
72
  if (key === "fontFamily" && typeof value === "string") {
60
73
  value = sanitizeFontFamily((0, fontFallback_1.withFontFallback)(value));
61
74
  }
75
+ // Wrap backgroundImage values in url() if not already wrapped — skip gradients
62
76
  if (key === "backgroundImage" && typeof value === "string"
63
77
  && !String(value).startsWith("url(")
64
78
  && !String(value).toLowerCase().includes("gradient(")) {
65
79
  value = `url('${value}')`;
66
80
  }
81
+ // lineHeight: values >= 4 are pixel values; smaller values are unitless multipliers (e.g. 1.5)
67
82
  if (key === "lineHeight" && typeof value === "number") {
68
83
  stylesObj["line-height"] = value >= 4 ? `${value}px` : String(value);
69
84
  return;
70
85
  }
71
86
  const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
87
+ // FIX 2 — Sanitize invalid px/per values
72
88
  if (pxChanges.includes(key)) {
73
89
  if (typeof value === "number") {
74
- stylesObj[cssKey] = `${Math.round(value * 100) / 100}px`;
90
+ const rounded = Math.round(value * 100) / 100;
91
+ stylesObj[cssKey] = `${rounded}px`;
75
92
  }
76
93
  else if (typeof value === "string" && value.includes("null")) {
94
+ // Skip invalid styles
77
95
  return;
78
96
  }
79
97
  else {
@@ -5,4 +5,5 @@ export declare function getVisibilityClass(props?: {
5
5
  hideOnMobile?: boolean;
6
6
  }): string;
7
7
  export declare function encodeBlockPropsAttr(props: Record<string, any>): string;
8
+ export declare function buildOutlookBgAttr(color: string): string;
8
9
  //# sourceMappingURL=common.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAqBvD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAIrD,CAAC;AAGF,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,UASA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAQvE"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/utils/common.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAqBvD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAIrD,CAAC;AAGF,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,UASA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAQvE;AAYD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGxD"}