2013-06-10 15:33:37 -04:00
#
# HTML emails don't support CSS, so we can use nokogiri to inline attributes based on
# matchers.
#
module Email
class Styles
2014-08-21 16:24:05 +05:30
@@plugin_callbacks = [ ]
2013-06-10 15:33:37 -04:00
2016-05-21 20:13:00 +02:00
attr_accessor :fragment
delegate :css , to : :fragment
2015-10-22 19:10:07 +02:00
def initialize ( html , opts = nil )
2013-06-10 15:33:37 -04:00
@html = html
2015-10-22 19:10:07 +02:00
@opts = opts || { }
2016-01-29 11:13:59 +01:00
@fragment = Nokogiri :: HTML . fragment ( @html )
2013-06-10 15:33:37 -04:00
end
2014-08-21 16:24:05 +05:30
def self . register_plugin_style ( & block )
@@plugin_callbacks . push ( block )
end
2014-05-09 14:39:09 -04:00
def add_styles ( node , new_styles )
existing = node [ 'style' ]
if existing . present?
2014-11-07 16:42:57 -05:00
# merge styles
node [ 'style' ] = " #{ new_styles } ; #{ existing } "
2014-05-09 14:39:09 -04:00
else
node [ 'style' ] = new_styles
end
end
2013-06-13 12:15:05 -04:00
def format_basic
2014-06-13 17:11:04 -04:00
uri = URI ( Discourse . base_url )
2014-10-27 23:51:55 +05:30
# images
2016-01-29 11:13:59 +01:00
@fragment . css ( 'img' ) . each do | img |
2013-11-28 17:20:56 -05:00
next if img [ 'class' ] == 'site-logo'
2016-05-24 15:00:25 +02:00
if img [ 'class' ] == " emoji " || img [ 'src' ] =~ / (plugins|images) \/ emoji /
2013-07-26 17:27:46 +10:00
img [ 'width' ] = 20
img [ 'height' ] = 20
2013-06-13 12:15:05 -04:00
else
2014-11-14 17:33:42 -08:00
# use dimensions of original iPhone screen for 'too big, let device rescale'
if img [ 'width' ] . to_i > 320 or img [ 'height' ] . to_i > 480
2014-11-14 16:23:52 -08:00
img [ 'width' ] = 'auto'
img [ 'height' ] = 'auto'
end
2013-06-13 12:15:05 -04:00
end
2013-08-27 00:08:38 +02:00
# ensure all urls are absolute
2013-08-14 11:32:17 -04:00
if img [ 'src' ] =~ / ^ \/ [^ \/ ] /
2013-06-13 12:15:05 -04:00
img [ 'src' ] = " #{ Discourse . base_url } #{ img [ 'src' ] } "
end
2013-08-27 00:08:38 +02:00
# ensure no schemaless urls
2013-12-03 10:10:53 -05:00
if img [ 'src' ] && img [ 'src' ] . starts_with? ( " // " )
2014-06-13 17:11:04 -04:00
img [ 'src' ] = " #{ uri . scheme } : #{ img [ 'src' ] } "
2013-08-27 00:08:38 +02:00
end
2013-07-22 15:06:37 -04:00
end
2014-10-27 23:51:55 +05:30
2015-11-04 12:38:39 +01:00
# add max-width to big images
2016-01-29 11:13:59 +01:00
big_images = @fragment . css ( 'img[width="auto"][height="auto"]' ) -
@fragment . css ( 'aside.onebox img' ) -
@fragment . css ( 'img.site-logo, img.emoji' )
2015-11-04 12:38:39 +01:00
big_images . each do | img |
2016-01-29 11:13:59 +01:00
add_styles ( img , 'max-width: 100%;' ) if img [ 'style' ] !~ / max-width /
2015-11-04 12:38:39 +01:00
end
2014-10-27 23:51:55 +05:30
# attachments
2016-01-29 11:13:59 +01:00
@fragment . css ( 'a.attachment' ) . each do | a |
2014-10-27 23:51:55 +05:30
# ensure all urls are absolute
if a [ 'href' ] =~ / ^ \/ [^ \/ ] /
a [ 'href' ] = " #{ Discourse . base_url } #{ a [ 'href' ] } "
end
# ensure no schemaless urls
if a [ 'href' ] && a [ 'href' ] . starts_with? ( " // " )
a [ 'href' ] = " #{ uri . scheme } : #{ a [ 'href' ] } "
end
end
2013-07-26 17:27:46 +10:00
end
2013-07-22 15:06:37 -04:00
2013-07-26 17:27:46 +10:00
def format_notification
style ( '.previous-discussion' , 'font-size: 17px; color: #444;' )
2014-05-14 16:40:54 -04:00
style ( '.notification-date' , " text-align:right;color: # 999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px " )
2013-07-26 17:27:46 +10:00
style ( '.username' , " font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color: # 3b5998;text-decoration:none;font-weight:bold " )
2015-03-24 11:25:47 -04:00
style ( '.user-title' , " font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: # 999; " )
style ( '.user-name' , " font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: # 3b5998;font-weight:normal; " )
2014-10-14 16:56:43 -07:00
style ( '.post-wrapper' , " margin-bottom:25px; " )
2013-07-26 17:27:46 +10:00
style ( '.user-avatar' , 'vertical-align:top;width:55px;' )
style ( '.user-avatar img' , nil , width : '45' , height : '45' )
style ( 'hr' , 'background-color: #ddd; height: 1px; border: 1px;' )
2014-08-27 14:38:03 +03:00
style ( '.rtl' , 'direction: rtl;' )
2013-07-27 08:08:58 +10:00
style ( 'td.body' , 'padding-top:5px;' , colspan : " 2 " )
2016-01-11 17:47:17 +01:00
style ( '.whisper td.body' , 'font-style: italic; color: #9c9c9c;' )
2016-05-21 06:17:54 -07:00
style ( '.lightbox-wrapper .meta' , 'display: none' )
2013-07-27 08:08:58 +10:00
correct_first_body_margin
2013-07-26 17:27:46 +10:00
correct_footer_style
reset_tables
2014-05-09 14:39:09 -04:00
onebox_styles
2014-08-21 16:24:05 +05:30
plugin_styles
2014-05-09 14:39:09 -04:00
end
def onebox_styles
# Links to other topics
2015-10-22 16:08:52 -04:00
style ( 'aside.quote' , 'border-left: 5px solid #e9e9e9; background-color: #f8f8f8; padding: 12px 25px 2px 12px; margin-bottom: 10px;' )
style ( 'aside.quote blockquote' , 'border: 0px; padding: 0; margin: 7px 0; background-color: clear;' )
style ( 'aside.quote blockquote > p' , 'padding: 0;' )
2014-05-09 14:39:09 -04:00
style ( 'aside.quote div.info-line' , 'color: #666; margin: 10px 0' )
2014-10-23 09:00:16 -07:00
style ( 'aside.quote .avatar' , 'margin-right: 5px; width:20px; height:20px' )
2014-05-09 14:39:09 -04:00
2015-10-22 16:08:52 -04:00
style ( 'blockquote' , 'border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin: 0;' )
style ( 'blockquote > p' , 'padding: 1em;' )
2014-05-09 14:39:09 -04:00
# Oneboxes
2014-05-13 14:44:40 -04:00
style ( 'aside.onebox' , " padding: 12px 25px 2px 12px; border-left: 5px solid # bebebe; background: # eee; margin-bottom: 10px; " )
2014-05-14 16:40:54 -04:00
style ( 'aside.onebox img' , " max-height: 80%; max-width: 25%; height: auto; float: left; margin-right: 10px; margin-bottom: 10px " )
2014-05-09 14:39:09 -04:00
style ( 'aside.onebox h3' , " border-bottom: 0 " )
style ( 'aside.onebox .source' , " margin-bottom: 8px " )
style ( 'aside.onebox .source a[href]' , " color: # 333; font-weight: normal " )
style ( 'aside.clearfix' , " clear: both " )
2014-05-13 14:44:40 -04:00
# Finally, convert all `aside` tags to `div`s
2016-01-29 11:13:59 +01:00
@fragment . css ( 'aside, article, header' ) . each do | n |
2014-05-13 14:44:40 -04:00
n . name = " div "
end
2014-07-14 16:41:05 -04:00
# iframes can't go in emails, so replace them with clickable links
2016-01-29 11:13:59 +01:00
@fragment . css ( 'iframe' ) . each do | i |
2014-07-14 16:41:05 -04:00
begin
src_uri = URI ( i [ 'src' ] )
# If an iframe is protocol relative, use SSL when displaying it
2016-07-04 11:29:12 +02:00
display_src = " #{ src_uri . scheme || 'https' } :// #{ src_uri . host } #{ src_uri . path } #{ src_uri . query . nil? ? '' : '?' + src_uri . query } #{ src_uri . fragment . nil? ? '' : '#' + src_uri . fragment } "
i . replace " <p><a href=' #{ src_uri . to_s } '> #{ CGI . escapeHTML ( display_src ) } </a><p> "
2014-07-14 16:41:05 -04:00
rescue URI :: InvalidURIError
# If the URL is weird, remove it
i . remove
end
end
2013-06-13 12:15:05 -04:00
end
def format_html
2016-02-27 19:07:15 +11:00
style ( 'h4' , 'color: #222;' )
2014-05-13 14:44:40 -04:00
style ( 'h3' , 'margin: 15px 0 20px 0;' )
2013-07-26 17:27:46 +10:00
style ( 'hr' , 'background-color: #ddd; height: 1px; border: 1px;' )
2014-04-17 14:40:30 -04:00
style ( 'a' , 'text-decoration: none; font-weight: bold; color: #006699;' )
2013-07-26 17:27:46 +10:00
style ( 'ul' , 'margin: 0 0 0 10px; padding: 0 0 0 20px;' )
style ( 'li' , 'padding-bottom: 10px' )
2015-04-25 00:54:22 -07:00
style ( 'div.digest-post' , 'margin-left: 15px; margin-top: -5px; max-width: 694px;' )
2013-08-09 14:43:02 -04:00
style ( 'div.digest-post h1' , 'font-size: 20px;' )
2014-11-14 00:48:45 -08:00
style ( 'div.footer' , 'color:#666; font-size:95%; text-align:center; padding-top:15px;' )
2013-11-29 13:00:10 -05:00
style ( 'span.post-count' , 'margin: 0 5px; color: #777;' )
2013-12-16 14:41:59 -05:00
style ( 'pre' , 'word-wrap: break-word; max-width: 694px;' )
2013-12-02 10:04:18 -05:00
style ( 'code' , 'background-color: #f1f1ff; padding: 2px 5px;' )
2013-12-16 14:41:59 -05:00
style ( 'pre code' , 'display: block; background-color: #f1f1ff; padding: 5px;' )
2014-11-28 11:44:59 -08:00
style ( '.featured-topic a' , 'text-decoration: none; font-weight: bold; color: #006699; line-height:1.5em;' )
2014-01-22 15:30:30 -05:00
2014-05-09 14:39:09 -04:00
onebox_styles
2014-08-21 16:24:05 +05:30
plugin_styles
end
# this method is reserved for styles specific to plugin
def plugin_styles
2016-01-29 11:13:59 +01:00
@@plugin_callbacks . each { | block | block . call ( @fragment , @opts ) }
2013-07-26 17:27:46 +10:00
end
2013-06-10 15:33:37 -04:00
2013-07-26 17:27:46 +10:00
def to_html
strip_classes_and_ids
2014-06-13 17:11:04 -04:00
replace_relative_urls
2016-01-29 11:13:59 +01:00
@fragment . to_html . tap do | result |
2013-07-26 17:27:46 +10:00
result . gsub! ( / \ [email-indent \ ] / , " <div style='margin-left: 15px'> " )
result . gsub! ( / \ [ \/ email-indent \ ] / , " </div> " )
2013-06-10 15:33:37 -04:00
end
2013-07-26 17:27:46 +10:00
end
2013-06-10 15:33:37 -04:00
2014-09-13 10:56:31 +05:30
def strip_avatars_and_emojis
2016-01-29 11:13:59 +01:00
@fragment . search ( 'img' ) . each do | img |
2014-10-29 02:08:18 +05:30
if img [ 'src' ] =~ / _avatar /
2014-09-25 10:56:23 +05:30
img . parent [ 'style' ] = " vertical-align: top; " if img . parent . name == 'td'
2014-09-13 10:56:31 +05:30
img . remove
end
2015-08-19 09:12:08 +10:00
if img [ 'title' ] && ( img [ 'src' ] =~ / images \/ emoji / || img [ 'src' ] =~ / uploads \/ default \/ _emoji / )
img . add_previous_sibling ( img [ 'title' ] || " emoji " )
img . remove
end
2014-09-13 10:56:31 +05:30
end
2015-02-26 12:50:56 +01:00
2016-01-29 11:13:59 +01:00
@fragment . to_s
2014-09-13 10:56:31 +05:30
end
2016-05-21 06:17:54 -07:00
def make_all_links_absolute
site_uri = URI ( Discourse . base_url )
@fragment . css ( " a " ) . each do | link |
begin
link [ " href " ] = " #{ site_uri } #{ link [ 'href' ] } " unless URI ( link [ " href " ] . to_s ) . host . present?
rescue URI :: InvalidURIError , URI :: InvalidComponentError
# leave it
end
end
end
2013-07-26 17:27:46 +10:00
private
2013-06-10 15:33:37 -04:00
2014-06-13 17:11:04 -04:00
def replace_relative_urls
forum_uri = URI ( Discourse . base_url )
host = forum_uri . host
scheme = forum_uri . scheme
2016-01-29 11:13:59 +01:00
@fragment . css ( '[href]' ) . each do | element |
2014-06-13 17:11:04 -04:00
href = element [ 'href' ]
if href =~ / ^ \/ \/ #{ host } /
element [ 'href' ] = " #{ scheme } : #{ href } "
end
end
end
2013-07-27 08:08:58 +10:00
def correct_first_body_margin
2016-01-29 11:13:59 +01:00
@fragment . css ( '.body p' ) . each do | element |
2014-06-09 15:28:03 -04:00
element [ 'style' ] = " margin-top:0; border: 0; "
2013-07-27 08:08:58 +10:00
end
end
2013-07-26 17:27:46 +10:00
def correct_footer_style
2016-01-08 02:14:58 -08:00
footernum = 0
2016-01-29 11:13:59 +01:00
@fragment . css ( '.footer' ) . each do | element |
2013-07-28 23:00:02 -07:00
element [ 'style' ] = " color: # 666; "
2016-01-08 02:14:58 -08:00
linknum = 0
2013-07-26 17:27:46 +10:00
element . css ( 'a' ) . each do | inner |
2016-01-08 02:14:58 -08:00
# we want the first footer link to be specially highlighted as IMPORTANT
if footernum == 0 and linknum == 0
2016-01-20 11:03:13 -08:00
inner [ 'style' ] = " background-color: # 006699; color: # ffffff; border-top: 4px solid # 006699; border-right: 6px solid # 006699; border-bottom: 4px solid # 006699; border-left: 6px solid # 006699; display: inline-block; "
2016-01-08 02:14:58 -08:00
else
inner [ 'style' ] = " color: # 666; "
end
linknum += 1
2013-07-26 17:27:46 +10:00
end
2016-01-08 02:14:58 -08:00
footernum += 1
2013-06-10 15:33:37 -04:00
end
2013-07-26 17:27:46 +10:00
end
2013-06-10 15:33:37 -04:00
2013-07-26 17:27:46 +10:00
def strip_classes_and_ids
2016-01-29 11:13:59 +01:00
@fragment . css ( '*' ) . each do | element |
2013-07-26 17:27:46 +10:00
element . delete ( 'class' )
element . delete ( 'id' )
2013-06-11 12:27:11 -04:00
end
2013-06-13 12:15:05 -04:00
end
2013-06-11 12:27:11 -04:00
2013-07-26 17:27:46 +10:00
def reset_tables
2014-09-25 10:56:23 +05:30
style ( 'table' , nil , cellspacing : '0' , cellpadding : '0' , border : '0' )
2013-06-10 15:33:37 -04:00
end
2013-07-26 17:27:46 +10:00
def style ( selector , style , attribs = { } )
2016-01-29 11:13:59 +01:00
@fragment . css ( selector ) . each do | element |
2014-05-09 14:39:09 -04:00
add_styles ( element , style ) if style
2016-01-29 11:13:59 +01:00
attribs . each do | k , v |
element [ k ] = v
end
2013-07-26 17:27:46 +10:00
end
end
2013-06-10 15:33:37 -04:00
end
2013-07-26 17:27:46 +10:00
end