@@ -141,7 +141,13 @@ function escapeTemplateString(value: string) {
141141}
142142
143143function 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
147153function getWrapperOptions ( style : string | null ) {
@@ -155,7 +161,9 @@ function getWrapperOptions(style: string | null) {
155161}
156162
157163function 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
161169function hasSingleChildMatching ( div : HTMLDivElement , predicate : ( child : Element ) => boolean ) {
@@ -164,10 +172,7 @@ function hasSingleChildMatching(div: HTMLDivElement, predicate: (child: Element)
164172}
165173
166174function addTableDefaults ( doc : Document ) {
167- doc . querySelectorAll ( 'table' ) . forEach ( ( table ) => {
168- if ( ! table . getAttribute ( 'role' ) ) {
169- table . setAttribute ( 'role' , 'presentation' ) ;
170- }
175+ doc . querySelectorAll ( 'table[role="presentation"]' ) . forEach ( ( table ) => {
171176 if ( ! table . getAttribute ( 'cellpadding' ) ) {
172177 table . setAttribute ( 'cellpadding' , '0' ) ;
173178 }
@@ -189,6 +194,25 @@ function addTableDefaults(doc: Document) {
189194 } ) ;
190195}
191196
197+ function isStandaloneImage ( img : HTMLImageElement ) {
198+ const parent = img . parentElement ;
199+ if ( ! parent ) {
200+ return false ;
201+ }
202+
203+ if ( parent . tagName === 'DIV' ) {
204+ return hasSingleChildMatching ( parent as HTMLDivElement , ( child ) => child . tagName === 'IMG' ) ;
205+ }
206+
207+ if ( parent . tagName === 'A' && parent . children . length === 1 ) {
208+ const grandparent = parent . parentElement ;
209+ return grandparent ?. tagName === 'DIV'
210+ && hasSingleChildMatching ( grandparent as HTMLDivElement , ( child ) => child . tagName === 'A' ) ;
211+ }
212+
213+ return false ;
214+ }
215+
192216function hardenImages ( doc : Document ) {
193217 doc . querySelectorAll ( 'img' ) . forEach ( ( img ) => {
194218 img . setAttribute ( 'border' , '0' ) ;
@@ -198,18 +222,24 @@ function hardenImages(doc: Document) {
198222 img . setAttribute ( 'width' , width ) ;
199223 }
200224
201- img . setAttribute ( 'style' , setStyleValues ( img . getAttribute ( 'style' ) , [
202- [ 'display' , 'block' ] ,
225+ const standaloneImage = isStandaloneImage ( img ) ;
226+ const declarations : Array < [ string , string | null ] > = [
203227 [ 'border' , '0' ] ,
204228 [ 'outline' , 'none' ] ,
205229 [ 'text-decoration' , 'none' ] ,
206230 [ 'height' , 'auto' ] ,
207231 [ '-ms-interpolation-mode' , 'bicubic' ] ,
208- [ 'vertical-align' , null ] ,
209- ] ) ) ;
232+ ] ;
233+
234+ if ( standaloneImage ) {
235+ declarations . unshift ( [ 'display' , 'block' ] ) ;
236+ declarations . push ( [ 'vertical-align' , null ] ) ;
237+ }
238+
239+ img . setAttribute ( 'style' , setStyleValues ( img . getAttribute ( 'style' ) , declarations ) ) ;
210240
211241 const parent = img . parentElement ;
212- if ( parent ?. tagName === 'A' ) {
242+ if ( standaloneImage && parent ?. tagName === 'A' ) {
213243 parent . setAttribute ( 'style' , setStyleValues ( parent . getAttribute ( 'style' ) , [
214244 [ 'display' , 'inline-block' ] ,
215245 [ 'border' , '0' ] ,
@@ -306,7 +336,7 @@ function buildBulletproofButton(anchor: HTMLAnchorElement, wrapperStyle: string)
306336 const targetAttr = target ? ` target="${ escapeAttribute ( target ) } "` : '' ;
307337
308338 if ( fullWidth ) {
309- const anchorStyle = appendMissingStyles ( anchor . getAttribute ( 'style' ) , [
339+ const anchorStyle = setStyleValues ( anchor . getAttribute ( 'style' ) , [
310340 [ 'display' , 'block' ] ,
311341 [ 'text-align' , 'center' ] ,
312342 [ 'border' , '1px solid ' + buttonColor ] ,
@@ -326,12 +356,14 @@ function buildBulletproofButton(anchor: HTMLAnchorElement, wrapperStyle: string)
326356 const estimatedHeight = Math . max ( lineHeight + paddingValues . top + paddingValues . bottom , 32 ) ;
327357 const arcsize = Math . max ( 0 , Math . min ( 50 , Math . round ( ( borderRadius / estimatedHeight ) * 100 ) ) ) ;
328358 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]-->` ) ;
359+ const msoStart = makeSafeTemplate ( '<!--[if mso]>' ) ;
360+ const msoEnd = makeSafeTemplate ( '<![endif]-->' ) ;
361+ 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>` ;
330362 const nonMsoStart = makeSafeTemplate ( '<!--[if !mso]><!-->' ) ;
331363 const nonMsoEnd = makeSafeTemplate ( '<!--<![endif]-->' ) ;
332364
333365 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>`
366+ `<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>`
335367 ) ;
336368}
337369
0 commit comments