premailer 1.12.0 → 1.29.0
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.
- checksums.yaml +4 -4
- data/README.md +128 -26
- data/bin/premailer +1 -1
- data/lib/premailer/adapter/nokogiri.rb +64 -51
- data/lib/premailer/adapter/nokogiri_fast.rb +67 -53
- data/lib/premailer/adapter/nokogumbo.rb +58 -46
- data/lib/premailer/adapter/rgb_to_hex.rb +11 -9
- data/lib/premailer/adapter.rb +10 -13
- data/lib/premailer/cached_rule_set.rb +13 -0
- data/lib/premailer/executor.rb +12 -11
- data/lib/premailer/html_to_plain_text.rb +34 -34
- data/lib/premailer/premailer.rb +170 -135
- data/lib/premailer/version.rb +2 -1
- data/lib/premailer.rb +3 -1
- metadata +16 -148
data/lib/premailer/premailer.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
# Premailer processes HTML and CSS to improve e-mail deliverability.
|
|
2
3
|
#
|
|
3
4
|
# Premailer's main function is to render all CSS as inline <tt>style</tt>
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
# fout.close
|
|
16
17
|
#
|
|
17
18
|
# # Write the plain-text output
|
|
18
|
-
# fout = File.open("
|
|
19
|
+
# fout = File.open("output.txt", "w")
|
|
19
20
|
# fout.puts premailer.to_plain_text
|
|
20
21
|
# fout.close
|
|
21
22
|
#
|
|
@@ -38,9 +39,9 @@ class Premailer
|
|
|
38
39
|
CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
|
|
39
40
|
|
|
40
41
|
# Unmergable selectors regexp.
|
|
41
|
-
RE_UNMERGABLE_SELECTORS = /(
|
|
42
|
+
RE_UNMERGABLE_SELECTORS = /(:(visited|active|hover|focus|after|before|selection|target|first-(line|letter))|^@)/i
|
|
42
43
|
# Reset selectors regexp.
|
|
43
|
-
RE_RESET_SELECTORS = /^(
|
|
44
|
+
RE_RESET_SELECTORS = /^(:\#outlook|body.*|\.ReadMsgBody|\.ExternalClass|img|\#backgroundTable)$/
|
|
44
45
|
|
|
45
46
|
# list of HTMLEntities to fix
|
|
46
47
|
# source: http://stackoverflow.com/questions/2812781/how-to-convert-webpage-apostrophe-8217-to-ascii-39-in-ruby-1-
|
|
@@ -56,23 +57,23 @@ class Premailer
|
|
|
56
57
|
"–" => '-',
|
|
57
58
|
"—" => '--',
|
|
58
59
|
"―" => '--'
|
|
59
|
-
}
|
|
60
|
+
}.freeze
|
|
60
61
|
|
|
61
62
|
# list of CSS attributes that can be rendered as HTML attributes
|
|
62
63
|
#
|
|
63
64
|
# @todo too much repetition
|
|
64
65
|
# @todo background=""
|
|
65
66
|
RELATED_ATTRIBUTES = {
|
|
66
|
-
'h1' => {'text-align' => 'align'},
|
|
67
|
-
'h2' => {'text-align' => 'align'},
|
|
68
|
-
'h3' => {'text-align' => 'align'},
|
|
69
|
-
'h4' => {'text-align' => 'align'},
|
|
70
|
-
'h5' => {'text-align' => 'align'},
|
|
71
|
-
'h6' => {'text-align' => 'align'},
|
|
72
|
-
'p' => {'text-align' => 'align'},
|
|
73
|
-
'div' => {'text-align' => 'align'},
|
|
74
|
-
'blockquote' => {'text-align' => 'align'},
|
|
75
|
-
'body' => {'background-color' => 'bgcolor'},
|
|
67
|
+
'h1' => { 'text-align' => 'align' },
|
|
68
|
+
'h2' => { 'text-align' => 'align' },
|
|
69
|
+
'h3' => { 'text-align' => 'align' },
|
|
70
|
+
'h4' => { 'text-align' => 'align' },
|
|
71
|
+
'h5' => { 'text-align' => 'align' },
|
|
72
|
+
'h6' => { 'text-align' => 'align' },
|
|
73
|
+
'p' => { 'text-align' => 'align' },
|
|
74
|
+
'div' => { 'text-align' => 'align' },
|
|
75
|
+
'blockquote' => { 'text-align' => 'align' },
|
|
76
|
+
'body' => { 'background-color' => 'bgcolor' },
|
|
76
77
|
'table' => {
|
|
77
78
|
'-premailer-align' => 'align',
|
|
78
79
|
'background-color' => 'bgcolor',
|
|
@@ -106,7 +107,7 @@ class Premailer
|
|
|
106
107
|
'-premailer-width' => 'width',
|
|
107
108
|
'-premailer-height' => 'height'
|
|
108
109
|
}
|
|
109
|
-
}
|
|
110
|
+
}.freeze
|
|
110
111
|
|
|
111
112
|
# URI of the HTML file used
|
|
112
113
|
attr_reader :html_file
|
|
@@ -141,7 +142,7 @@ class Premailer
|
|
|
141
142
|
include Warnings
|
|
142
143
|
|
|
143
144
|
# Waning level names
|
|
144
|
-
WARN_LABEL =
|
|
145
|
+
WARN_LABEL = ['NONE', 'SAFE', 'POOR', 'RISKY'].freeze
|
|
145
146
|
|
|
146
147
|
# Create a new Premailer object.
|
|
147
148
|
#
|
|
@@ -168,7 +169,8 @@ class Premailer
|
|
|
168
169
|
# @option options [Boolean] :preserve_reset Whether to preserve styles associated with the MailChimp reset code. Default is true.
|
|
169
170
|
# @option options [Boolean] :with_html_string Whether the html param should be treated as a raw string. Default is false.
|
|
170
171
|
# @option options [Boolean] :verbose Whether to print errors and warnings to <tt>$stderr</tt>. Default is false.
|
|
171
|
-
# @option options [Boolean] :io_exceptions Throws exceptions on I/O errors.
|
|
172
|
+
# @option options [Boolean] :io_exceptions Throws exceptions on I/O errors. Default is false.
|
|
173
|
+
# @option options [Boolean] :rule_set_exceptions Throws exceptions on invalid values in CSS Parser rule sets. Default is true.
|
|
172
174
|
# @option options [Boolean] :include_link_tags Whether to include css from <tt>link rel=stylesheet</tt> tags. Default is true.
|
|
173
175
|
# @option options [Boolean] :include_style_tags Whether to include css from <tt>style</tt> tags. Default is true.
|
|
174
176
|
# @option options [String] :input_encoding Manually specify the source documents encoding. This is a good idea. Default is ASCII-8BIT.
|
|
@@ -178,40 +180,42 @@ class Premailer
|
|
|
178
180
|
# @option options [String] :output_encoding Output encoding option for Nokogiri adapter. Should be set to "US-ASCII" to output HTML entities instead of Unicode characters.
|
|
179
181
|
# @option options [Boolean] :create_shorthands Combine several properties into a shorthand one, e.g. font: style weight size. Default is true.
|
|
180
182
|
# @option options [Boolean] :html_fragment Handle HTML fragment without any HTML content wrappers. Default is false.
|
|
181
|
-
# @option options [Boolean] :drop_unmergeable_css_rules Do not include unmergeable css rules in a <tt><style><tt> tag. Default is false.
|
|
183
|
+
# @option options [Boolean] :drop_unmergeable_css_rules Do not include unmergeable css rules in a <tt><style><tt> tag. Default is false.
|
|
182
184
|
def initialize(html, options = {})
|
|
183
|
-
@options = {
|
|
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
|
-
|
|
214
|
-
|
|
185
|
+
@options = {
|
|
186
|
+
:warn_level => Warnings::SAFE,
|
|
187
|
+
:line_length => 65,
|
|
188
|
+
:link_query_string => nil,
|
|
189
|
+
:base_url => nil,
|
|
190
|
+
:rgb_to_hex_attributes => true,
|
|
191
|
+
:remove_classes => false,
|
|
192
|
+
:remove_ids => false,
|
|
193
|
+
:remove_comments => false,
|
|
194
|
+
:remove_scripts => true,
|
|
195
|
+
:reset_contenteditable => true,
|
|
196
|
+
:css => [],
|
|
197
|
+
:css_to_attributes => true,
|
|
198
|
+
:preserve_style_attribute => false,
|
|
199
|
+
:with_html_string => false,
|
|
200
|
+
:css_string => nil,
|
|
201
|
+
:preserve_styles => false,
|
|
202
|
+
:preserve_reset => true,
|
|
203
|
+
:verbose => false,
|
|
204
|
+
:debug => false,
|
|
205
|
+
:io_exceptions => false,
|
|
206
|
+
:rule_set_exceptions => true,
|
|
207
|
+
:include_link_tags => true,
|
|
208
|
+
:include_style_tags => true,
|
|
209
|
+
:input_encoding => 'ASCII-8BIT',
|
|
210
|
+
:output_encoding => nil,
|
|
211
|
+
:replace_html_entities => false,
|
|
212
|
+
:escape_url_attributes => true,
|
|
213
|
+
:unescaped_ampersand => false,
|
|
214
|
+
:create_shorthands => true,
|
|
215
|
+
:html_fragment => false,
|
|
216
|
+
:adapter => Adapter.use,
|
|
217
|
+
:drop_unmergeable_css_rules => false
|
|
218
|
+
}.merge(options)
|
|
215
219
|
|
|
216
220
|
@html_file = html
|
|
217
221
|
@is_local_file = @options[:with_html_string] || Premailer.local_data?(html)
|
|
@@ -226,19 +230,22 @@ class Premailer
|
|
|
226
230
|
|
|
227
231
|
if @options[:base_url]
|
|
228
232
|
@base_url = Addressable::URI.parse(@options.delete(:base_url))
|
|
229
|
-
elsif
|
|
233
|
+
elsif !@is_local_file
|
|
230
234
|
@base_url = Addressable::URI.parse(@html_file)
|
|
231
235
|
end
|
|
232
236
|
|
|
233
|
-
@css_parser = CssParser::Parser.new(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
@css_parser = CssParser::Parser.new(
|
|
238
|
+
{
|
|
239
|
+
:absolute_paths => true,
|
|
240
|
+
:import => true,
|
|
241
|
+
:io_exceptions => @options[:io_exceptions],
|
|
242
|
+
:rule_set_exceptions => @options[:rule_set_exceptions]
|
|
243
|
+
}
|
|
244
|
+
)
|
|
238
245
|
|
|
239
246
|
@adapter_class = Adapter.find @options[:adapter]
|
|
240
247
|
|
|
241
|
-
|
|
248
|
+
extend(@adapter_class)
|
|
242
249
|
|
|
243
250
|
@doc = load_html(@html_file)
|
|
244
251
|
|
|
@@ -259,25 +266,26 @@ class Premailer
|
|
|
259
266
|
@css_warnings
|
|
260
267
|
end
|
|
261
268
|
|
|
262
|
-
protected
|
|
269
|
+
protected
|
|
270
|
+
|
|
263
271
|
def load_css_from_local_file!(path)
|
|
264
|
-
css_block = ''
|
|
265
|
-
path.
|
|
272
|
+
css_block = +''
|
|
273
|
+
path.delete_prefix!('file:')
|
|
266
274
|
begin
|
|
267
275
|
File.open(path, "r") do |file|
|
|
268
|
-
while line = file.gets
|
|
276
|
+
while (line = file.gets)
|
|
269
277
|
css_block << line
|
|
270
278
|
end
|
|
271
279
|
end
|
|
272
280
|
|
|
273
281
|
load_css_from_string(css_block)
|
|
274
|
-
rescue => e
|
|
282
|
+
rescue StandardError => e
|
|
275
283
|
raise e if @options[:io_exceptions]
|
|
276
284
|
end
|
|
277
285
|
end
|
|
278
286
|
|
|
279
287
|
def load_css_from_string(css_string)
|
|
280
|
-
@css_parser.add_block!(css_string, {:base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld]})
|
|
288
|
+
@css_parser.add_block!(css_string, { :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld] })
|
|
281
289
|
end
|
|
282
290
|
|
|
283
291
|
# @private
|
|
@@ -293,23 +301,23 @@ protected
|
|
|
293
301
|
end
|
|
294
302
|
end
|
|
295
303
|
|
|
296
|
-
|
|
304
|
+
# Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
|
|
297
305
|
def load_css_from_html! # :nodoc:
|
|
298
306
|
tags = @doc.search("link[@rel='stylesheet']:not([@data-premailer='ignore']), style:not([@data-premailer='ignore'])")
|
|
299
307
|
if tags
|
|
300
308
|
tags.each do |tag|
|
|
301
|
-
if tag.to_s.strip =~
|
|
309
|
+
if tag.to_s.strip =~ /^<link/i && tag.attributes['href'] && media_type_ok?(tag.attributes['media']) && @options[:include_link_tags]
|
|
302
310
|
# A user might want to <link /> to a local css file that is also mirrored on the site
|
|
303
311
|
# but the local one is different (e.g. newer) than the live file, premailer will now choose the local file
|
|
304
312
|
|
|
305
|
-
if tag.attributes['href'].to_s.include?
|
|
306
|
-
if @options[:with_html_string]
|
|
307
|
-
|
|
313
|
+
if tag.attributes['href'].to_s.include?(@base_url.to_s) && @html_file.is_a?(String)
|
|
314
|
+
link_uri = if @options[:with_html_string]
|
|
315
|
+
tag.attributes['href'].to_s.sub(@base_url.to_s, '')
|
|
308
316
|
else
|
|
309
|
-
|
|
317
|
+
File.join(File.dirname(@html_file), tag.attributes['href'].to_s.sub!(@base_url.to_s, ''))
|
|
310
318
|
end
|
|
311
319
|
# if the file does not exist locally, try to grab the remote reference
|
|
312
|
-
unless File.
|
|
320
|
+
unless File.exist?(link_uri)
|
|
313
321
|
link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
|
|
314
322
|
end
|
|
315
323
|
else
|
|
@@ -317,14 +325,14 @@ protected
|
|
|
317
325
|
end
|
|
318
326
|
|
|
319
327
|
if Premailer.local_data?(link_uri)
|
|
320
|
-
|
|
328
|
+
warn "Loading css from local file: " + link_uri if @options[:verbose]
|
|
321
329
|
load_css_from_local_file!(link_uri)
|
|
322
330
|
else
|
|
323
|
-
|
|
324
|
-
@css_parser.load_uri!(link_uri, {:only_media_types => [:screen, :handheld]})
|
|
331
|
+
warn "Loading css from uri: " + link_uri if @options[:verbose]
|
|
332
|
+
@css_parser.load_uri!(link_uri, { :only_media_types => [:screen, :handheld] })
|
|
325
333
|
end
|
|
326
334
|
|
|
327
|
-
elsif tag.to_s.strip =~
|
|
335
|
+
elsif tag.to_s.strip =~ /^<style/i && @options[:include_style_tags]
|
|
328
336
|
@css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
|
|
329
337
|
end
|
|
330
338
|
end
|
|
@@ -332,10 +340,31 @@ protected
|
|
|
332
340
|
end
|
|
333
341
|
end
|
|
334
342
|
|
|
343
|
+
# here be deprecated methods
|
|
344
|
+
public
|
|
335
345
|
|
|
346
|
+
# Free native Nokogiri/libxml2 memory held by this instance.
|
|
347
|
+
#
|
|
348
|
+
# After calling +to_inline_css+ or +to_plain_text+, the Premailer instance
|
|
349
|
+
# retains live Nokogiri documents that pin native libxml2 memory. Ruby's GC
|
|
350
|
+
# cannot see this native memory pressure, so in high-throughput scenarios
|
|
351
|
+
# (e.g. email rendering in Sidekiq workers) native memory can accumulate
|
|
352
|
+
# unboundedly. Call +cleanup!+ when you are done with the instance to
|
|
353
|
+
# release that memory immediately.
|
|
354
|
+
#
|
|
355
|
+
# After calling this method the instance is no longer usable.
|
|
356
|
+
def cleanup!
|
|
357
|
+
@processed_doc&.unlink
|
|
358
|
+
@doc&.unlink
|
|
359
|
+
@doc = nil
|
|
360
|
+
@processed_doc = nil
|
|
361
|
+
@css_parser = nil
|
|
362
|
+
@unmergable_rules = nil
|
|
363
|
+
@html_file = nil
|
|
364
|
+
@css_warnings = nil
|
|
365
|
+
@css_files = nil
|
|
366
|
+
end
|
|
336
367
|
|
|
337
|
-
# here be deprecated methods
|
|
338
|
-
public
|
|
339
368
|
# @private
|
|
340
369
|
# @deprecated
|
|
341
370
|
def local_uri?(uri) # :nodoc:
|
|
@@ -343,70 +372,70 @@ public
|
|
|
343
372
|
Premailer.local_data?(uri)
|
|
344
373
|
end
|
|
345
374
|
|
|
346
|
-
# here be instance methods
|
|
375
|
+
# here be instance methods
|
|
347
376
|
|
|
348
377
|
# @private
|
|
349
378
|
def media_type_ok?(media_types)
|
|
350
379
|
media_types = media_types.to_s
|
|
351
|
-
return true if media_types.nil?
|
|
380
|
+
return true if media_types.nil? || media_types.empty?
|
|
352
381
|
media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
|
|
353
382
|
end
|
|
354
383
|
|
|
355
|
-
def append_query_string(doc,
|
|
356
|
-
return doc if
|
|
384
|
+
def append_query_string(doc, queries)
|
|
385
|
+
return doc if queries.nil?
|
|
357
386
|
|
|
358
|
-
|
|
359
|
-
|
|
387
|
+
queries = +queries
|
|
388
|
+
queries.to_s.gsub!(/^[\?]*/, '').strip!
|
|
389
|
+
return doc if queries.empty?
|
|
360
390
|
|
|
361
391
|
begin
|
|
362
392
|
current_host = @base_url.host
|
|
363
|
-
rescue
|
|
393
|
+
rescue StandardError
|
|
364
394
|
current_host = nil
|
|
365
395
|
end
|
|
366
396
|
|
|
367
|
-
|
|
397
|
+
warn "Attempting to append_query_string: #{queries}" if @options[:verbose]
|
|
368
398
|
|
|
369
|
-
doc.search('a').each do|el|
|
|
399
|
+
doc.search('a').each do |el|
|
|
370
400
|
href = el.attributes['href'].to_s.strip
|
|
371
|
-
next if href.nil?
|
|
401
|
+
next if href.nil? || href.empty?
|
|
372
402
|
|
|
373
|
-
next if
|
|
403
|
+
next if /[\#\{\[\<\%]/.match?(href[0, 1]) # don't bother with anchors or special-looking links
|
|
374
404
|
|
|
375
405
|
begin
|
|
376
406
|
href = Addressable::URI.parse(href)
|
|
377
407
|
|
|
378
|
-
if current_host
|
|
379
|
-
|
|
408
|
+
if current_host && !href.host.nil? && (href.host != current_host)
|
|
409
|
+
warn "Skipping append_query_string for: #{href} because host is no good" if @options[:verbose]
|
|
380
410
|
next
|
|
381
411
|
end
|
|
382
412
|
|
|
383
|
-
if href.scheme
|
|
384
|
-
puts "Skipping append_query_string for: #{href
|
|
413
|
+
if href.scheme && (href.scheme != 'http') && (href.scheme != 'https')
|
|
414
|
+
puts "Skipping append_query_string for: #{href} because scheme is no good" if @options[:verbose]
|
|
385
415
|
next
|
|
386
416
|
end
|
|
387
417
|
|
|
388
|
-
if href.query
|
|
418
|
+
if href.query && !href.query.empty?
|
|
389
419
|
amp = @options[:unescaped_ampersand] ? '&' : '&'
|
|
390
|
-
href.query = href.query + amp +
|
|
420
|
+
href.query = href.query + amp + queries
|
|
391
421
|
else
|
|
392
|
-
href.query =
|
|
422
|
+
href.query = queries
|
|
393
423
|
end
|
|
394
424
|
|
|
395
425
|
el['href'] = href.to_s
|
|
396
426
|
rescue Addressable::URI::InvalidURIError => e
|
|
397
|
-
|
|
427
|
+
warn "Skipping append_query_string for: #{href} (#{e.message})" if @options[:verbose]
|
|
398
428
|
next
|
|
399
429
|
end
|
|
400
|
-
|
|
401
430
|
end
|
|
402
431
|
doc
|
|
403
432
|
end
|
|
404
433
|
|
|
405
434
|
# Check for an XHTML doctype
|
|
406
|
-
def
|
|
435
|
+
def xhtml?
|
|
407
436
|
intro = @doc.to_xhtml.strip.split("\n")[0..2].join(' ')
|
|
408
437
|
is_xhtml = !!(intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i)
|
|
409
|
-
|
|
438
|
+
warn "Is XHTML? #{is_xhtml.inspect}\nChecked:\n#{intro}" if @options[:debug]
|
|
410
439
|
is_xhtml
|
|
411
440
|
end
|
|
412
441
|
|
|
@@ -419,9 +448,8 @@ public
|
|
|
419
448
|
#
|
|
420
449
|
# Returns a document.
|
|
421
450
|
def convert_inline_links(doc, base_uri) # :nodoc:
|
|
422
|
-
base_uri = Addressable::URI.parse(base_uri) unless base_uri.
|
|
451
|
+
base_uri = Addressable::URI.parse(base_uri) unless base_uri.is_a?(Addressable::URI)
|
|
423
452
|
|
|
424
|
-
append_qs = @options[:link_query_string] || ''
|
|
425
453
|
escape_attrs = @options[:escape_url_attributes]
|
|
426
454
|
|
|
427
455
|
['href', 'src', 'background'].each do |attribute|
|
|
@@ -432,30 +460,30 @@ public
|
|
|
432
460
|
tags.each do |tag|
|
|
433
461
|
# skip links that look like they have merge tags
|
|
434
462
|
# and mailto, ftp, etc...
|
|
435
|
-
if
|
|
463
|
+
if /^([\%\<\{\#\[]|data:|tel:|file:|sms:|callto:|facetime:|mailto:|ftp:|gopher:|cid:)/i.match?(tag.attributes[attribute].to_s)
|
|
436
464
|
next
|
|
437
465
|
end
|
|
438
466
|
|
|
439
|
-
if tag.attributes[attribute].to_s
|
|
467
|
+
if /^http/i.match?(tag.attributes[attribute].to_s)
|
|
440
468
|
begin
|
|
441
469
|
merged = Addressable::URI.parse(tag.attributes[attribute])
|
|
442
|
-
rescue; next; end
|
|
470
|
+
rescue StandardError; next; end
|
|
443
471
|
else
|
|
444
472
|
begin
|
|
445
473
|
merged = Premailer.resolve_link(tag.attributes[attribute].to_s, base_uri)
|
|
446
|
-
rescue
|
|
474
|
+
rescue StandardError
|
|
447
475
|
begin
|
|
448
476
|
next unless escape_attrs
|
|
449
477
|
merged = Premailer.resolve_link(Addressable::URI.escape(tag.attributes[attribute].to_s), base_uri)
|
|
450
|
-
rescue; end
|
|
478
|
+
rescue StandardError; end
|
|
451
479
|
end
|
|
452
480
|
end
|
|
453
481
|
|
|
454
482
|
# make sure 'merged' is a URI
|
|
455
|
-
merged = Addressable::URI.parse(merged.to_s) unless merged.
|
|
483
|
+
merged = Addressable::URI.parse(merged.to_s) unless merged.is_a?(Addressable::URI)
|
|
456
484
|
tag[attribute] = merged.to_s
|
|
457
|
-
end
|
|
458
|
-
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
459
487
|
|
|
460
488
|
doc.search("*[@style]").each do |el|
|
|
461
489
|
el['style'] = CssParser.convert_uris(el.attributes['style'].to_s, base_uri)
|
|
@@ -464,21 +492,21 @@ public
|
|
|
464
492
|
end
|
|
465
493
|
|
|
466
494
|
# @private
|
|
467
|
-
def self.
|
|
468
|
-
media_types
|
|
495
|
+
def self.media_query?(media_types)
|
|
496
|
+
media_types&.any? { |mt| mt.to_s.count('()') >= 2 }
|
|
469
497
|
end
|
|
470
498
|
|
|
471
499
|
# @private
|
|
472
500
|
def self.resolve_link(path, base_path) # :nodoc:
|
|
501
|
+
path = +path
|
|
473
502
|
path.strip!
|
|
474
|
-
|
|
475
|
-
if path =~ /\A(?:(https?|ftp|file):)\/\//i
|
|
503
|
+
if /\A(?:(https?|ftp|file):)\/\//i.match?(path)
|
|
476
504
|
resolved = path
|
|
477
505
|
Premailer.canonicalize(resolved)
|
|
478
|
-
elsif base_path.
|
|
506
|
+
elsif base_path.is_a?(Addressable::URI)
|
|
479
507
|
resolved = base_path.join(path)
|
|
480
508
|
Premailer.canonicalize(resolved)
|
|
481
|
-
elsif base_path.
|
|
509
|
+
elsif base_path.is_a?(String) && base_path =~ (/\A(?:(?:https?|ftp|file):)\/\//i)
|
|
482
510
|
resolved = Addressable::URI.parse(base_path)
|
|
483
511
|
resolved = resolved.join(path)
|
|
484
512
|
Premailer.canonicalize(resolved)
|
|
@@ -491,33 +519,28 @@ public
|
|
|
491
519
|
#
|
|
492
520
|
# IO objects return true, as do strings that look like URLs.
|
|
493
521
|
def self.local_data?(data)
|
|
494
|
-
return false
|
|
522
|
+
return false if data.is_a?(String) && data =~ /\A(?:(https?|ftp):)\/\//i
|
|
495
523
|
true
|
|
496
524
|
end
|
|
497
525
|
|
|
498
526
|
# from http://www.ruby-forum.com/topic/140101
|
|
499
527
|
def self.canonicalize(uri) # :nodoc:
|
|
500
|
-
u = uri.
|
|
528
|
+
u = uri.is_a?(Addressable::URI) ? uri : Addressable::URI.parse(uri.to_s)
|
|
501
529
|
u.normalize!
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
$1 == '..' ? match : ''
|
|
505
|
-
} do end
|
|
506
|
-
newpath = newpath.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/')
|
|
507
|
-
u.path = newpath
|
|
508
|
-
u.to_s
|
|
509
|
-
end
|
|
530
|
+
u.to_s
|
|
531
|
+
end
|
|
510
532
|
|
|
511
533
|
# Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
|
|
512
534
|
def check_client_support # :nodoc:
|
|
513
|
-
|
|
535
|
+
kwargs = Psych::VERSION >= '4' ? { aliases: true } : {}
|
|
536
|
+
@client_support ||= Psych.load(File.open(CLIENT_SUPPORT_FILE), **kwargs)
|
|
514
537
|
|
|
515
538
|
warnings = []
|
|
516
539
|
properties = []
|
|
517
540
|
|
|
518
541
|
# Get a list off CSS properties
|
|
519
542
|
@processed_doc.search("*[@style]").each do |el|
|
|
520
|
-
|
|
543
|
+
el.attributes['style'].to_s.gsub(/([\w\-]+)[\s]*:/i) do |_s|
|
|
521
544
|
properties.push($1)
|
|
522
545
|
end
|
|
523
546
|
end
|
|
@@ -526,30 +549,42 @@ public
|
|
|
526
549
|
|
|
527
550
|
property_support = @client_support['css_properties']
|
|
528
551
|
properties.each do |prop|
|
|
529
|
-
if property_support.include?(prop)
|
|
530
|
-
property_support[prop].include?('support')
|
|
531
|
-
property_support[prop]['support'] >= @options[:warn_level]
|
|
532
|
-
warnings.push(
|
|
552
|
+
if property_support.include?(prop) &&
|
|
553
|
+
property_support[prop].include?('support') &&
|
|
554
|
+
(property_support[prop]['support'] >= @options[:warn_level])
|
|
555
|
+
warnings.push(
|
|
556
|
+
{
|
|
557
|
+
:message => "#{prop} CSS property",
|
|
533
558
|
:level => WARN_LABEL[property_support[prop]['support']],
|
|
534
|
-
:clients => property_support[prop]['unsupported_in'].join(', ')
|
|
559
|
+
:clients => property_support[prop]['unsupported_in'].join(', ')
|
|
560
|
+
}
|
|
561
|
+
)
|
|
535
562
|
end
|
|
536
563
|
end
|
|
537
564
|
|
|
538
565
|
@client_support['attributes'].each do |attribute, data|
|
|
539
566
|
next unless data['support'] >= @options[:warn_level]
|
|
540
567
|
if @doc.search("*[@#{attribute}]").length > 0
|
|
541
|
-
warnings.push(
|
|
568
|
+
warnings.push(
|
|
569
|
+
{
|
|
570
|
+
:message => "#{attribute} HTML attribute",
|
|
542
571
|
:level => WARN_LABEL[data['support']],
|
|
543
|
-
:clients => data['unsupported_in'].join(', ')
|
|
572
|
+
:clients => data['unsupported_in'].join(', ')
|
|
573
|
+
}
|
|
574
|
+
)
|
|
544
575
|
end
|
|
545
576
|
end
|
|
546
577
|
|
|
547
578
|
@client_support['elements'].each do |element, data|
|
|
548
579
|
next unless data['support'] >= @options[:warn_level]
|
|
549
580
|
if @doc.search(element).length > 0
|
|
550
|
-
warnings.push(
|
|
581
|
+
warnings.push(
|
|
582
|
+
{
|
|
583
|
+
:message => "#{element} HTML element",
|
|
551
584
|
:level => WARN_LABEL[data['support']],
|
|
552
|
-
:clients => data['unsupported_in'].join(', ')
|
|
585
|
+
:clients => data['unsupported_in'].join(', ')
|
|
586
|
+
}
|
|
587
|
+
)
|
|
553
588
|
end
|
|
554
589
|
end
|
|
555
590
|
|
data/lib/premailer/version.rb
CHANGED
data/lib/premailer.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'yaml'
|
|
2
3
|
require 'open-uri'
|
|
3
|
-
require 'digest/
|
|
4
|
+
require 'digest/sha2'
|
|
4
5
|
require 'cgi'
|
|
5
6
|
require 'addressable/uri'
|
|
6
7
|
require 'css_parser'
|
|
@@ -9,3 +10,4 @@ require 'premailer/adapter'
|
|
|
9
10
|
require 'premailer/adapter/rgb_to_hex'
|
|
10
11
|
require 'premailer/html_to_plain_text'
|
|
11
12
|
require 'premailer/premailer'
|
|
13
|
+
require 'premailer/cached_rule_set'
|