Skip to content

Commit b3ff72d

Browse files
author
Timo Dahlenburg
committed
Apply hodor suggestion
1 parent bcb7be6 commit b3ff72d

3 files changed

Lines changed: 48 additions & 14 deletions

File tree

frontend/email-builder/src/outlook.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,13 @@ function escapeTemplateString(value: string) {
141141
}
142142

143143
function makeSafeTemplate(raw: string) {
144-
return `{{ Safe "${escapeTemplateString(raw)}" }}`;
144+
// Encode angle brackets so DOMParser does not consume Outlook conditional comments
145+
// before the Go template expression is evaluated.
146+
const escaped = escapeTemplateString(raw)
147+
.replace(/</g, '\\x3c')
148+
.replace(/>/g, '\\x3e');
149+
150+
return `{{ Safe "${escaped}" }}`;
145151
}
146152

147153
function getWrapperOptions(style: string | null) {
@@ -155,7 +161,9 @@ function getWrapperOptions(style: string | null) {
155161
}
156162

157163
function buildPresentationTable(contents: string, width: string = '100%') {
158-
return `<table role="presentation" width="${width}" cellpadding="0" cellspacing="0" border="0" style="${PRESENTATION_TABLE_STYLE}">${contents}</table>`;
164+
const widthAttr = width && width !== 'auto' ? ` width="${escapeAttribute(width)}"` : '';
165+
166+
return `<table role="presentation"${widthAttr} cellpadding="0" cellspacing="0" border="0" style="${PRESENTATION_TABLE_STYLE}">${contents}</table>`;
159167
}
160168

161169
function hasSingleChildMatching(div: HTMLDivElement, predicate: (child: Element) => boolean) {
@@ -326,12 +334,14 @@ function buildBulletproofButton(anchor: HTMLAnchorElement, wrapperStyle: string)
326334
const estimatedHeight = Math.max(lineHeight + paddingValues.top + paddingValues.bottom, 32);
327335
const arcsize = Math.max(0, Math.min(50, Math.round((borderRadius / estimatedHeight) * 100)));
328336
const cleanAnchorStyle = anchor.getAttribute('style') || '';
329-
const vml = makeSafeTemplate(`<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${escapeAttribute(href)}" style="height:${estimatedHeight}px;v-text-anchor:middle;width:${estimatedWidth}px;" arcsize="${arcsize}%" strokecolor="${escapeAttribute(buttonColor)}" fillcolor="${escapeAttribute(buttonColor)}"><w:anchorlock/><center style="color:${escapeAttribute(textColor)};font-family:${escapeAttribute(fontFamily)};font-size:${fontSize}px;font-weight:${escapeAttribute(fontWeight)};">${escapeHtml(text)}</center></v:roundrect><![endif]-->`);
337+
const msoStart = makeSafeTemplate('<!--[if mso]>');
338+
const msoEnd = makeSafeTemplate('<![endif]-->');
339+
const vml = `<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${escapeAttribute(href)}" style="height:${estimatedHeight}px;v-text-anchor:middle;width:${estimatedWidth}px;" arcsize="${arcsize}%" strokecolor="${escapeAttribute(buttonColor)}" fillcolor="${escapeAttribute(buttonColor)}"><w:anchorlock/><center style="color:${escapeAttribute(textColor)};font-family:${escapeAttribute(fontFamily)};font-size:${fontSize}px;font-weight:${escapeAttribute(fontWeight)};">${escapeHtml(text)}</center></v:roundrect>`;
330340
const nonMsoStart = makeSafeTemplate('<!--[if !mso]><!-->');
331341
const nonMsoEnd = makeSafeTemplate('<!--<![endif]-->');
332342

333343
return buildPresentationTable(
334-
`<tbody><tr><td align="${escapeAttribute(align)}" style="${escapeAttribute(wrapperStyle)}">${vml}${nonMsoStart}<a href="${escapeAttribute(href)}"${targetAttr} style="${escapeAttribute(cleanAnchorStyle)}">${escapeHtml(text)}</a>${nonMsoEnd}</td></tr></tbody>`
344+
`<tbody><tr><td align="${escapeAttribute(align)}" style="${escapeAttribute(wrapperStyle)}">${msoStart}${vml}${msoEnd}${nonMsoStart}<a href="${escapeAttribute(href)}"${targetAttr} style="${escapeAttribute(cleanAnchorStyle)}">${escapeHtml(text)}</a>${nonMsoEnd}</td></tr></tbody>`
335345
);
336346
}
337347

frontend/email-builder/src/utils.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@ import { TEditorConfiguration } from './documents/editor/core';
33
import { postProcessForOutlook } from './outlook';
44

55
const VIEWPORT_META = '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
6-
const MSO_DOCUMENT_SETTINGS = '<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->';
6+
const MSO_DOCUMENT_SETTINGS = '<!--[if mso]><noscript><xml xmlns:o="urn:schemas-microsoft-com:office:office"><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->';
7+
8+
function injectHeadContents(html: string, contents: string) {
9+
const headMatch = html.match(/<head\b([^>]*)>/i);
10+
if (headMatch) {
11+
return html.replace(/<head\b([^>]*)>/i, `<head$1>${contents}`);
12+
}
13+
14+
const htmlMatch = html.match(/<html\b([^>]*)>/i);
15+
if (htmlMatch) {
16+
return html.replace(/<html\b([^>]*)>/i, `<html$1><head>${contents}</head>`);
17+
}
18+
19+
return `<head>${contents}</head>${html}`;
20+
}
721

822
export function renderHtmlWithMeta(document: TEditorConfiguration, options: { rootBlockId: string }): string {
923
const html = postProcessForOutlook(renderToStaticMarkup(document, options));
1024

11-
return html.replace(
12-
/<head([^>]*)>/i,
13-
`<head$1>${VIEWPORT_META}${MSO_DOCUMENT_SETTINGS}`
14-
);
25+
return injectHeadContents(html, `${VIEWPORT_META}${MSO_DOCUMENT_SETTINGS}`);
1526
}

frontend/src/components/Editor.vue

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,17 @@ export default {
136136
},
137137
138138
methods: {
139+
syncVisualSnapshot(value) {
140+
if (value.contentType === 'visual') {
141+
this.visualSnapshotBody = value.body || '';
142+
this.visualSnapshotSource = value.bodySource;
143+
return;
144+
}
145+
146+
this.visualSnapshotBody = null;
147+
this.visualSnapshotSource = null;
148+
},
149+
139150
onContentTypeChange(to, from) {
140151
if (!this.self.body.trim()) {
141152
this.convertContentType(to, from);
@@ -351,11 +362,7 @@ export default {
351362
// Set initial content type for the selector.
352363
this.contentTypeSel = this.value.contentType;
353364
this.templateId = this.value.templateId;
354-
355-
if (this.value.contentType === 'visual') {
356-
this.visualSnapshotBody = this.value.body || '';
357-
this.visualSnapshotSource = this.value.bodySource;
358-
}
365+
this.syncVisualSnapshot(this.value);
359366
360367
window.addEventListener('keydown', this.onKeyboardShortcut);
361368
@@ -392,6 +399,12 @@ export default {
392399
},
393400
394401
watch: {
402+
value(to) {
403+
this.contentTypeSel = to.contentType;
404+
this.templateId = to.templateId;
405+
this.syncVisualSnapshot(to);
406+
},
407+
395408
validTemplates() {
396409
// When the filtered list of validTemplates changes (visual vs. regular),
397410
// select the appropriate 'default' in the template select list.

0 commit comments

Comments
 (0)