Class: Email::Styles
- Inherits:
-
Object
- Object
- Email::Styles
- Defined in:
- lib/email/styles.rb
Constant Summary collapse
- MAX_IMAGE_DIMENSION =
400
- ONEBOX_IMAGE_BASE_STYLE =
"max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;"
- ONEBOX_IMAGE_THUMBNAIL_STYLE =
"width: 60px;"
- ONEBOX_INLINE_AVATAR_STYLE =
"width: 20px; height: 20px; float: none; vertical-align: middle;"
- @@plugin_callbacks =
[]
Instance Attribute Summary collapse
-
#fragment ⇒ Object
Returns the value of attribute fragment.
Class Method Summary collapse
Instance Method Summary collapse
- #add_styles(node, new_styles) ⇒ Object
- #custom_styles ⇒ Object
- #deduplicate_style(style) ⇒ Object
- #deduplicate_styles ⇒ Object
- #format_basic ⇒ Object
- #format_custom ⇒ Object
- #format_html ⇒ Object
-
#initialize(html, opts = nil) ⇒ Styles
constructor
A new instance of Styles.
- #inline_secure_images(attachments, attachments_index) ⇒ Object
- #make_all_links_absolute ⇒ Object
- #onebox_styles ⇒ Object
-
#plugin_styles ⇒ Object
this method is reserved for styles specific to plugin.
- #strip_avatars_and_emojis ⇒ Object
- #strip_hashtag_link_icons ⇒ Object
- #stripped_media ⇒ Object
- #stripped_secure_image_uploads ⇒ Object
- #stripped_upload_sha_map ⇒ Object
- #to_html ⇒ Object
- #to_s ⇒ Object
Constructor Details
#initialize(html, opts = nil) ⇒ Styles
Returns a new instance of Styles.
21 22 23 24 25 26 |
# File 'lib/email/styles.rb', line 21 def initialize(html, opts = nil) @html = html @opts = opts || {} @fragment = Nokogiri::HTML5.parse(@html) @custom_styles = nil end |
Instance Attribute Details
#fragment ⇒ Object
Returns the value of attribute fragment.
17 18 19 |
# File 'lib/email/styles.rb', line 17 def fragment @fragment end |
Class Method Details
.register_plugin_style(&block) ⇒ Object
28 29 30 |
# File 'lib/email/styles.rb', line 28 def self.register_plugin_style(&block) @@plugin_callbacks.push(block) end |
Instance Method Details
#add_styles(node, new_styles) ⇒ Object
32 33 34 35 36 37 38 39 40 |
# File 'lib/email/styles.rb', line 32 def add_styles(node, new_styles) existing = node["style"] if existing.present? # merge styles node["style"] = "#{new_styles}; #{existing}" else node["style"] = new_styles end end |
#custom_styles ⇒ Object
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/email/styles.rb', line 42 def custom_styles return @custom_styles unless @custom_styles.nil? css = EmailStyle.new.compiled_css @custom_styles = {} if !css.blank? # there is a minor race condition here, CssParser could be # loaded by ::CssParser::Parser not loaded require "css_parser" unless defined?(::CssParser::Parser) parser = ::CssParser::Parser.new(import: false) parser.load_string!(css) parser.each_selector do |selector, value| @custom_styles[selector] ||= +"" @custom_styles[selector] << value end end @custom_styles end |
#deduplicate_style(style) ⇒ Object
377 378 379 380 381 382 383 384 385 386 387 |
# File 'lib/email/styles.rb', line 377 def deduplicate_style(style) styles = {} style .split(";") .select(&:present?) .map { _1.split(":", 2).map(&:strip) } .each { |k, v| styles[k] = v if k.present? && v.present? } styles.map { |k, v| "#{k}:#{v}" }.join(";") end |
#deduplicate_styles ⇒ Object
389 390 391 392 393 |
# File 'lib/email/styles.rb', line 389 def deduplicate_styles @fragment .css("[style]") .each { |element| element["style"] = deduplicate_style element["style"] } end |
#format_basic ⇒ Object
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/email/styles.rb', line 64 def format_basic uri = URI(Discourse.base_url) # Remove SVGs @fragment.css('svg, img[src$=".svg"]').remove # images @fragment .css("img") .each do |img| next if img["class"] == "site-logo" if (img["class"] && img["class"]["emoji"]) || (img["src"] && img["src"][%r{/_?emoji/}]) img["width"] = img["height"] = 20 else # use dimensions of original iPhone screen for 'too big, let device rescale' if img["width"].to_i > (320) || img["height"].to_i > (480) img["width"] = img["height"] = "auto" end end if img["src"] # ensure all urls are absolute img["src"] = "#{Discourse.base_url}#{img["src"]}" if img["src"][%r{\A/[^/]}] # ensure no schemaless urls img["src"] = "#{uri.scheme}:#{img["src"]}" if img["src"][%r{\A//}] end end # add max-width to big images big_images = @fragment.css('img[width="auto"][height="auto"]') - @fragment.css("aside.onebox img") - @fragment.css("img.site-logo, img.emoji") big_images.each { |img| add_styles(img, "max-width: 100%;") if img["style"] !~ /max-width/ } # topic featured link @fragment .css("a.topic-featured-link") .each do |e| e[ "style" ] = "color:#858585;padding:2px 8px;border:1px solid #e6e6e6;border-radius:2px;box-shadow:0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);" end # attachments @fragment .css("a.attachment") .each do |a| # ensure all urls are absolute a["href"] = "#{Discourse.base_url}#{a["href"]}" if a["href"] =~ %r{\A/[^/]} # ensure no schemaless urls a["href"] = "#{uri.scheme}:#{a["href"]}" if a["href"] && a["href"].starts_with?("//") end end |
#format_custom ⇒ Object
308 309 310 |
# File 'lib/email/styles.rb', line 308 def format_custom custom_styles.each { |selector, value| style(selector, value) } end |
#format_html ⇒ Object
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/email/styles.rb', line 215 def format_html correct_first_body_margin strip_hashtag_link_icons reset_tables html_lang = SiteSetting.default_locale.sub("_", "-") style("html", nil, :lang => html_lang, "xml:lang" => html_lang) style("body", "line-height: 1.4; text-align:#{Rtl.new(nil).enabled? ? "right" : "left"};") style("body", nil, dir: Rtl.new(nil).enabled? ? "rtl" : "ltr") style( ".with-dir", "text-align:#{Rtl.new(nil).enabled? ? "right" : "left"};", dir: Rtl.new(nil).enabled? ? "rtl" : "ltr", ) style("blockquote > :first-child", "margin-top: 0;") style("blockquote > :last-child", "margin-bottom: 0;") style("blockquote > p", "padding: 0;") style( ".with-accent-colors", "background-color: #{SiteSetting.email_accent_bg_color}; color: #{SiteSetting.email_accent_fg_color};", ) style("h4", "color: #222;") style("h3", "margin: 30px 0 10px;") style("hr", "background-color: #ddd; height: 1px; border: 1px;") style( "a", "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color};", ) style("ul", "margin: 0 0 0 10px; padding: 0 0 0 20px;") style("li", "padding-bottom: 10px") style("div.summary-footer", "color:#666; font-size:95%; text-align:center; padding-top:15px;") style("span.post-count", "margin: 0 5px; color: #777;") style("pre", "word-wrap: break-word; max-width: 694px;") style("code", "background-color: #f9f9f9; padding: 2px 5px;") style("pre code", "display: block; background-color: #f9f9f9; overflow: auto; padding: 5px;") style("pre.onebox code", "white-space: normal;") style("pre code li", "white-space: pre;") style( ".featured-topic a", "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;", ) style( ".summary-email", "-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;width:100%", ) style(".previous-discussion", "font-size: 17px; color: #444; margin-bottom:10px;") style( ".notification-date", "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px", ) style( ".username", "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;font-weight:bold", ) style(".username-link", "color:#{SiteSetting.email_link_color};") style(".username-title", "color:#777;margin-left:5px;") style( ".user-title", "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:5px;color: #999;", ) style(".post-wrapper", "margin-bottom:25px;") 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;") style(".rtl", "direction: rtl;") style("div.body", "padding-top:5px;") style(".whisper div.body", "font-style: italic; color: #9c9c9c;") style(".lightbox-wrapper .meta", "display: none") style("div.undecorated-link-footer a", "font-weight: normal;") style( ".mso-accent-link", "mso-border-alt: 6px solid #{SiteSetting.email_accent_bg_color}; background-color: #{SiteSetting.email_accent_bg_color};", ) style( ".reply-above-line", "font-size: 10px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color: #b5b5b5;padding: 5px 0px 20px;border-top: 1px dotted #ddd;", ) onebox_styles plugin_styles dark_mode_styles style(".post-excerpt img", "max-width: 50%; max-height: #{MAX_IMAGE_DIMENSION}px;") format_custom end |
#inline_secure_images(attachments, attachments_index) ⇒ Object
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/email/styles.rb', line 339 def inline_secure_images(, ) uploads = stripped_secure_image_uploads upload_shas = stripped_upload_sha_map stripped_media.each do |div| upload = uploads.find do |upl| upl.sha1 == upload_shas[div["data-stripped-secure-media"] || div["data-stripped-secure-upload"]] end next if !upload if [[upload.sha1]] url = [[upload.sha1]].url onebox_type = div["data-onebox-type"] style = if onebox_type onebox_style = ( if onebox_type == "avatar-inline" ONEBOX_INLINE_AVATAR_STYLE else ONEBOX_IMAGE_THUMBNAIL_STYLE end ) "#{onebox_style} #{ONEBOX_IMAGE_BASE_STYLE}" else calculate_width_and_height_style(div) end div.add_next_sibling(<<~HTML) <img src="#{url}" data-embedded-secure-image="true" style="#{style}" /> HTML div.remove end end end |
#make_all_links_absolute ⇒ Object
439 440 441 442 443 444 445 446 447 448 449 450 |
# File 'lib/email/styles.rb', line 439 def make_all_links_absolute site_uri = URI(Discourse.base_url) @fragment .css("a") .each do |link| begin link["href"] = "#{site_uri}#{link["href"]}" if URI(link["href"].to_s).host.blank? rescue URI::Error # leave it end end end |
#onebox_styles ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/email/styles.rb', line 120 def onebox_styles # Links to other topics style("aside.quote", "padding: 12px 25px 2px 12px; margin-bottom: 10px;") style("aside.quote div.info-line", "color: #666; margin: 10px 0") style( "aside.quote .avatar", "margin-right: 5px; width:20px; height:20px; vertical-align:middle;", ) style("aside.quote", "border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin: 0;") style( "blockquote", "border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin-left: 0; padding: 12px;", ) # Oneboxes style( "aside.onebox", "border: 5px solid #e9e9e9; padding: 12px 25px 12px 12px; margin-bottom: 10px;", ) style("aside.onebox header img.site-icon", "width: 16px; height: 16px; margin-right: 3px;") style("aside.onebox header a[href]", "color: #222222; text-decoration: none;") style("aside.onebox .onebox-body", "clear: both") style("aside.onebox .onebox-body img:not(.onebox-avatar-inline)", ONEBOX_IMAGE_BASE_STYLE) style("aside.onebox .onebox-body img.thumbnail", ONEBOX_IMAGE_THUMBNAIL_STYLE) style( "aside.onebox .onebox-body h3, aside.onebox .onebox-body h4", "font-size: 1.17em; margin: 10px 0;", ) style(".onebox-metadata", "color: #919191") style(".github-info", "margin-top: 10px;") style(".github-info .added", "color: #090;") style(".github-info .removed", "color: #e45735;") style(".github-info div", "display: inline; margin-right: 10px;") style(".github-icon-container", "float: left;") style(".github-icon-container *", "fill: #646464; width: 40px; height: 40px;") style( ".github-body-container", 'font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; margin-top: 1em !important;', ) style(".onebox-avatar-inline", ONEBOX_INLINE_AVATAR_STYLE) @fragment.css(".github-body-container .excerpt").remove @fragment.css("aside.quote blockquote > p").each { |p| p["style"] = "padding: 0;" } # Convert all `aside.quote` tags to `blockquote`s @fragment .css("aside.quote") .each do |n| original_node = n.dup original_node.search("div.quote-controls").remove blockquote = ( if original_node.css("blockquote").inner_html.strip.start_with?("<p") original_node.css("blockquote").inner_html else "<p style='padding: 0;'>#{original_node.css("blockquote").inner_html}</p>" end ) n.inner_html = original_node.css("div.title").inner_html + blockquote n.name = "blockquote" end # Finally, convert all `aside` tags to `div`s @fragment.css("aside, article, header").each { |n| n.name = "div" } # iframes can't go in emails, so replace them with clickable links @fragment .css("iframe") .each do |i| begin # sometimes, iframes are blocklisted... if i["src"].blank? i.remove next end src_uri = i["data-original-href"].present? ? URI(i["data-original-href"]) : URI(i["src"]) # If an iframe is protocol relative, use SSL when displaying it 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( Nokogiri::HTML5.fragment( "<p><a href='#{src_uri}'>#{CGI.escapeHTML(display_src)}</a><p>", ), ) rescue URI::Error # If the URL is weird, remove the iframe i.remove end end end |
#plugin_styles ⇒ Object
this method is reserved for styles specific to plugin
313 314 315 |
# File 'lib/email/styles.rb', line 313 def plugin_styles @@plugin_callbacks.each { |block| block.call(@fragment, @opts) } end |
#strip_avatars_and_emojis ⇒ Object
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 |
# File 'lib/email/styles.rb', line 410 def strip_avatars_and_emojis @fragment .search("img") .each do |img| next unless img["src"] if img["src"][/_avatar/] img.parent["style"] = "vertical-align: top;" if img.parent&.name == "td" img.remove end if img["title"] && img["src"][%r{/_?emoji/}] img.add_previous_sibling(img["title"] || "emoji") img.remove end end end |
#strip_hashtag_link_icons ⇒ Object
428 429 430 431 432 433 434 435 436 437 |
# File 'lib/email/styles.rb', line 428 def strip_hashtag_link_icons @fragment .search(".hashtag-cooked") .each do |hashtag| hashtag.children.each(&:remove) hashtag.add_child(<<~HTML) <span>##{hashtag["data-slug"]}</span> HTML end end |
#stripped_media ⇒ Object
317 318 319 320 |
# File 'lib/email/styles.rb', line 317 def stripped_media @stripped_media ||= @fragment.css("[data-stripped-secure-media], [data-stripped-secure-upload]") end |
#stripped_secure_image_uploads ⇒ Object
334 335 336 337 |
# File 'lib/email/styles.rb', line 334 def stripped_secure_image_uploads upload_shas = stripped_upload_sha_map Upload.select(:original_filename, :sha1).where(sha1: upload_shas.values) end |
#stripped_upload_sha_map ⇒ Object
322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/email/styles.rb', line 322 def stripped_upload_sha_map @stripped_upload_sha_map ||= begin upload_shas = {} stripped_media.each do |div| url = div["data-stripped-secure-media"] || div["data-stripped-secure-upload"] upload_shas[url] = Upload.sha1_from_long_url(url) end upload_shas end end |
#to_html ⇒ Object
395 396 397 398 399 400 401 402 403 404 |
# File 'lib/email/styles.rb', line 395 def to_html # needs to be before class + id strip because we need to style redacted # media and also not double-redact already redacted from lower levels replace_secure_uploads_urls if SiteSetting.secure_uploads? strip_classes_and_ids replace_relative_urls deduplicate_styles @fragment.to_html end |
#to_s ⇒ Object
406 407 408 |
# File 'lib/email/styles.rb', line 406 def to_s @fragment.to_s end |