premailer 1.23.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 +118 -29
- data/bin/premailer +1 -1
- data/lib/premailer/adapter/nokogiri.rb +33 -32
- data/lib/premailer/adapter/nokogiri_fast.rb +42 -44
- data/lib/premailer/adapter/nokogumbo.rb +33 -35
- data/lib/premailer/adapter/rgb_to_hex.rb +4 -3
- data/lib/premailer/adapter.rb +10 -13
- data/lib/premailer/cached_rule_set.rb +1 -0
- data/lib/premailer/executor.rb +12 -11
- data/lib/premailer/html_to_plain_text.rb +20 -17
- data/lib/premailer/premailer.rb +162 -125
- data/lib/premailer/version.rb +2 -1
- data/lib/premailer.rb +1 -0
- metadata +12 -133
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
require 'htmlentities'
|
|
3
3
|
|
|
4
4
|
# Support functions for Premailer
|
|
5
5
|
module HtmlToPlainText
|
|
6
|
-
|
|
7
6
|
# Returns the text in UTF-8 format with all HTML tags removed
|
|
8
7
|
#
|
|
9
8
|
# HTML content can be omitted from the output by surrounding it in the following comments:
|
|
@@ -13,8 +12,8 @@ module HtmlToPlainText
|
|
|
13
12
|
#
|
|
14
13
|
# TODO: add support for DL, OL
|
|
15
14
|
# TODO: this is not safe and needs a real html parser to work
|
|
16
|
-
def convert_to_text(html, line_length = 65,
|
|
17
|
-
txt = html
|
|
15
|
+
def convert_to_text(html, line_length = 65, _from_charset = 'UTF-8')
|
|
16
|
+
txt = +html
|
|
18
17
|
|
|
19
18
|
# strip text ignored html. Useful for removing
|
|
20
19
|
# headers and footers that aren't needed in the
|
|
@@ -50,7 +49,7 @@ module HtmlToPlainText
|
|
|
50
49
|
|
|
51
50
|
if text.empty?
|
|
52
51
|
''
|
|
53
|
-
elsif href.nil? || text.strip.
|
|
52
|
+
elsif href.nil? || text.strip.casecmp(href.strip) == 0
|
|
54
53
|
text.strip
|
|
55
54
|
else
|
|
56
55
|
text.strip + ' ( ' + href.strip + ' )'
|
|
@@ -59,7 +58,7 @@ module HtmlToPlainText
|
|
|
59
58
|
|
|
60
59
|
# handle headings (H1-H6)
|
|
61
60
|
txt.gsub!(/(<\/h[1-6]>)/i, "\n\\1") # move closing tags to new lines
|
|
62
|
-
txt.gsub!(/[\s]*<h([1-6]+)[^>]*>[\s]*(.*)[\s]*<\/h[1-6]+>/i) do |
|
|
61
|
+
txt.gsub!(/[\s]*<h([1-6]+)[^>]*>[\s]*(.*)[\s]*<\/h[1-6]+>/i) do |_s|
|
|
63
62
|
hlevel = $1.to_i
|
|
64
63
|
|
|
65
64
|
htext = $2
|
|
@@ -68,17 +67,21 @@ module HtmlToPlainText
|
|
|
68
67
|
|
|
69
68
|
# determine maximum line length
|
|
70
69
|
hlength = 0
|
|
71
|
-
htext.each_line
|
|
70
|
+
htext.each_line do |l|
|
|
71
|
+
llength = l.strip.length
|
|
72
|
+
hlength = llength if llength > hlength
|
|
73
|
+
end
|
|
72
74
|
hlength = line_length if hlength > line_length
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
htext =
|
|
77
|
+
case hlevel
|
|
78
|
+
when 1 # H1, asterisks above and below
|
|
79
|
+
('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength)
|
|
80
|
+
when 2 # H1, dashes above and below
|
|
81
|
+
('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength)
|
|
82
|
+
else # H3-H6, dashes below
|
|
83
|
+
htext + "\n" + ('-' * hlength)
|
|
84
|
+
end
|
|
82
85
|
|
|
83
86
|
"\n\n" + htext + "\n\n"
|
|
84
87
|
end
|
|
@@ -117,8 +120,8 @@ module HtmlToPlainText
|
|
|
117
120
|
txt.gsub!(/[\n]{3,}/, "\n\n")
|
|
118
121
|
|
|
119
122
|
# the word messes up the parens
|
|
120
|
-
txt.gsub!(/\(([ \n])(http[^)]+)([\n ])\)/) do |
|
|
121
|
-
($1 == "\n" ? $1 : ''
|
|
123
|
+
txt.gsub!(/\(([ \n])(http[^)]+)([\n ])\)/) do |_s|
|
|
124
|
+
($1 == "\n" ? $1 : '') + '( ' + $2 + ' )' + ($3 == "\n" ? $1 : '')
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
txt.strip
|
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
|
#
|
|
@@ -181,39 +182,40 @@ class Premailer
|
|
|
181
182
|
# @option options [Boolean] :html_fragment Handle HTML fragment without any HTML content wrappers. Default is false.
|
|
182
183
|
# @option options [Boolean] :drop_unmergeable_css_rules Do not include unmergeable css rules in a <tt><style><tt> tag. Default is false.
|
|
183
184
|
def initialize(html, options = {})
|
|
184
|
-
@options = {
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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)
|
|
217
219
|
|
|
218
220
|
@html_file = html
|
|
219
221
|
@is_local_file = @options[:with_html_string] || Premailer.local_data?(html)
|
|
@@ -228,20 +230,22 @@ class Premailer
|
|
|
228
230
|
|
|
229
231
|
if @options[:base_url]
|
|
230
232
|
@base_url = Addressable::URI.parse(@options.delete(:base_url))
|
|
231
|
-
elsif
|
|
233
|
+
elsif !@is_local_file
|
|
232
234
|
@base_url = Addressable::URI.parse(@html_file)
|
|
233
235
|
end
|
|
234
236
|
|
|
235
|
-
@css_parser = CssParser::Parser.new(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
)
|
|
241
245
|
|
|
242
246
|
@adapter_class = Adapter.find @options[:adapter]
|
|
243
247
|
|
|
244
|
-
|
|
248
|
+
extend(@adapter_class)
|
|
245
249
|
|
|
246
250
|
@doc = load_html(@html_file)
|
|
247
251
|
|
|
@@ -262,25 +266,26 @@ class Premailer
|
|
|
262
266
|
@css_warnings
|
|
263
267
|
end
|
|
264
268
|
|
|
265
|
-
protected
|
|
269
|
+
protected
|
|
270
|
+
|
|
266
271
|
def load_css_from_local_file!(path)
|
|
267
|
-
css_block = ''
|
|
268
|
-
path.
|
|
272
|
+
css_block = +''
|
|
273
|
+
path.delete_prefix!('file:')
|
|
269
274
|
begin
|
|
270
275
|
File.open(path, "r") do |file|
|
|
271
|
-
while line = file.gets
|
|
276
|
+
while (line = file.gets)
|
|
272
277
|
css_block << line
|
|
273
278
|
end
|
|
274
279
|
end
|
|
275
280
|
|
|
276
281
|
load_css_from_string(css_block)
|
|
277
|
-
rescue => e
|
|
282
|
+
rescue StandardError => e
|
|
278
283
|
raise e if @options[:io_exceptions]
|
|
279
284
|
end
|
|
280
285
|
end
|
|
281
286
|
|
|
282
287
|
def load_css_from_string(css_string)
|
|
283
|
-
@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] })
|
|
284
289
|
end
|
|
285
290
|
|
|
286
291
|
# @private
|
|
@@ -296,20 +301,20 @@ protected
|
|
|
296
301
|
end
|
|
297
302
|
end
|
|
298
303
|
|
|
299
|
-
|
|
304
|
+
# Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
|
|
300
305
|
def load_css_from_html! # :nodoc:
|
|
301
306
|
tags = @doc.search("link[@rel='stylesheet']:not([@data-premailer='ignore']), style:not([@data-premailer='ignore'])")
|
|
302
307
|
if tags
|
|
303
308
|
tags.each do |tag|
|
|
304
|
-
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]
|
|
305
310
|
# A user might want to <link /> to a local css file that is also mirrored on the site
|
|
306
311
|
# but the local one is different (e.g. newer) than the live file, premailer will now choose the local file
|
|
307
312
|
|
|
308
|
-
if tag.attributes['href'].to_s.include?
|
|
309
|
-
if @options[:with_html_string]
|
|
310
|
-
|
|
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, '')
|
|
311
316
|
else
|
|
312
|
-
|
|
317
|
+
File.join(File.dirname(@html_file), tag.attributes['href'].to_s.sub!(@base_url.to_s, ''))
|
|
313
318
|
end
|
|
314
319
|
# if the file does not exist locally, try to grab the remote reference
|
|
315
320
|
unless File.exist?(link_uri)
|
|
@@ -320,14 +325,14 @@ protected
|
|
|
320
325
|
end
|
|
321
326
|
|
|
322
327
|
if Premailer.local_data?(link_uri)
|
|
323
|
-
|
|
328
|
+
warn "Loading css from local file: " + link_uri if @options[:verbose]
|
|
324
329
|
load_css_from_local_file!(link_uri)
|
|
325
330
|
else
|
|
326
|
-
|
|
327
|
-
@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] })
|
|
328
333
|
end
|
|
329
334
|
|
|
330
|
-
elsif tag.to_s.strip =~
|
|
335
|
+
elsif tag.to_s.strip =~ /^<style/i && @options[:include_style_tags]
|
|
331
336
|
@css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
|
|
332
337
|
end
|
|
333
338
|
end
|
|
@@ -335,10 +340,31 @@ protected
|
|
|
335
340
|
end
|
|
336
341
|
end
|
|
337
342
|
|
|
343
|
+
# here be deprecated methods
|
|
344
|
+
public
|
|
338
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
|
|
339
367
|
|
|
340
|
-
# here be deprecated methods
|
|
341
|
-
public
|
|
342
368
|
# @private
|
|
343
369
|
# @deprecated
|
|
344
370
|
def local_uri?(uri) # :nodoc:
|
|
@@ -346,70 +372,70 @@ public
|
|
|
346
372
|
Premailer.local_data?(uri)
|
|
347
373
|
end
|
|
348
374
|
|
|
349
|
-
# here be instance methods
|
|
375
|
+
# here be instance methods
|
|
350
376
|
|
|
351
377
|
# @private
|
|
352
378
|
def media_type_ok?(media_types)
|
|
353
379
|
media_types = media_types.to_s
|
|
354
|
-
return true if media_types.nil?
|
|
380
|
+
return true if media_types.nil? || media_types.empty?
|
|
355
381
|
media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
|
|
356
382
|
end
|
|
357
383
|
|
|
358
|
-
def append_query_string(doc,
|
|
359
|
-
return doc if
|
|
384
|
+
def append_query_string(doc, queries)
|
|
385
|
+
return doc if queries.nil?
|
|
360
386
|
|
|
361
|
-
|
|
362
|
-
|
|
387
|
+
queries = +queries
|
|
388
|
+
queries.to_s.gsub!(/^[\?]*/, '').strip!
|
|
389
|
+
return doc if queries.empty?
|
|
363
390
|
|
|
364
391
|
begin
|
|
365
392
|
current_host = @base_url.host
|
|
366
|
-
rescue
|
|
393
|
+
rescue StandardError
|
|
367
394
|
current_host = nil
|
|
368
395
|
end
|
|
369
396
|
|
|
370
|
-
|
|
397
|
+
warn "Attempting to append_query_string: #{queries}" if @options[:verbose]
|
|
371
398
|
|
|
372
|
-
doc.search('a').each do|el|
|
|
399
|
+
doc.search('a').each do |el|
|
|
373
400
|
href = el.attributes['href'].to_s.strip
|
|
374
|
-
next if href.nil?
|
|
401
|
+
next if href.nil? || href.empty?
|
|
375
402
|
|
|
376
|
-
next if
|
|
403
|
+
next if /[\#\{\[\<\%]/.match?(href[0, 1]) # don't bother with anchors or special-looking links
|
|
377
404
|
|
|
378
405
|
begin
|
|
379
406
|
href = Addressable::URI.parse(href)
|
|
380
407
|
|
|
381
|
-
if current_host
|
|
382
|
-
|
|
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]
|
|
383
410
|
next
|
|
384
411
|
end
|
|
385
412
|
|
|
386
|
-
if href.scheme
|
|
387
|
-
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]
|
|
388
415
|
next
|
|
389
416
|
end
|
|
390
417
|
|
|
391
|
-
if href.query
|
|
418
|
+
if href.query && !href.query.empty?
|
|
392
419
|
amp = @options[:unescaped_ampersand] ? '&' : '&'
|
|
393
|
-
href.query = href.query + amp +
|
|
420
|
+
href.query = href.query + amp + queries
|
|
394
421
|
else
|
|
395
|
-
href.query =
|
|
422
|
+
href.query = queries
|
|
396
423
|
end
|
|
397
424
|
|
|
398
425
|
el['href'] = href.to_s
|
|
399
426
|
rescue Addressable::URI::InvalidURIError => e
|
|
400
|
-
|
|
427
|
+
warn "Skipping append_query_string for: #{href} (#{e.message})" if @options[:verbose]
|
|
401
428
|
next
|
|
402
429
|
end
|
|
403
|
-
|
|
404
430
|
end
|
|
405
431
|
doc
|
|
406
432
|
end
|
|
407
433
|
|
|
408
434
|
# Check for an XHTML doctype
|
|
409
|
-
def
|
|
435
|
+
def xhtml?
|
|
410
436
|
intro = @doc.to_xhtml.strip.split("\n")[0..2].join(' ')
|
|
411
437
|
is_xhtml = !!(intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i)
|
|
412
|
-
|
|
438
|
+
warn "Is XHTML? #{is_xhtml.inspect}\nChecked:\n#{intro}" if @options[:debug]
|
|
413
439
|
is_xhtml
|
|
414
440
|
end
|
|
415
441
|
|
|
@@ -422,9 +448,8 @@ public
|
|
|
422
448
|
#
|
|
423
449
|
# Returns a document.
|
|
424
450
|
def convert_inline_links(doc, base_uri) # :nodoc:
|
|
425
|
-
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)
|
|
426
452
|
|
|
427
|
-
append_qs = @options[:link_query_string] || ''
|
|
428
453
|
escape_attrs = @options[:escape_url_attributes]
|
|
429
454
|
|
|
430
455
|
['href', 'src', 'background'].each do |attribute|
|
|
@@ -435,30 +460,30 @@ public
|
|
|
435
460
|
tags.each do |tag|
|
|
436
461
|
# skip links that look like they have merge tags
|
|
437
462
|
# and mailto, ftp, etc...
|
|
438
|
-
if
|
|
463
|
+
if /^([\%\<\{\#\[]|data:|tel:|file:|sms:|callto:|facetime:|mailto:|ftp:|gopher:|cid:)/i.match?(tag.attributes[attribute].to_s)
|
|
439
464
|
next
|
|
440
465
|
end
|
|
441
466
|
|
|
442
|
-
if tag.attributes[attribute].to_s
|
|
467
|
+
if /^http/i.match?(tag.attributes[attribute].to_s)
|
|
443
468
|
begin
|
|
444
469
|
merged = Addressable::URI.parse(tag.attributes[attribute])
|
|
445
|
-
rescue; next; end
|
|
470
|
+
rescue StandardError; next; end
|
|
446
471
|
else
|
|
447
472
|
begin
|
|
448
473
|
merged = Premailer.resolve_link(tag.attributes[attribute].to_s, base_uri)
|
|
449
|
-
rescue
|
|
474
|
+
rescue StandardError
|
|
450
475
|
begin
|
|
451
476
|
next unless escape_attrs
|
|
452
477
|
merged = Premailer.resolve_link(Addressable::URI.escape(tag.attributes[attribute].to_s), base_uri)
|
|
453
|
-
rescue; end
|
|
478
|
+
rescue StandardError; end
|
|
454
479
|
end
|
|
455
480
|
end
|
|
456
481
|
|
|
457
482
|
# make sure 'merged' is a URI
|
|
458
|
-
merged = Addressable::URI.parse(merged.to_s) unless merged.
|
|
483
|
+
merged = Addressable::URI.parse(merged.to_s) unless merged.is_a?(Addressable::URI)
|
|
459
484
|
tag[attribute] = merged.to_s
|
|
460
|
-
end
|
|
461
|
-
end
|
|
485
|
+
end
|
|
486
|
+
end
|
|
462
487
|
|
|
463
488
|
doc.search("*[@style]").each do |el|
|
|
464
489
|
el['style'] = CssParser.convert_uris(el.attributes['style'].to_s, base_uri)
|
|
@@ -467,21 +492,21 @@ public
|
|
|
467
492
|
end
|
|
468
493
|
|
|
469
494
|
# @private
|
|
470
|
-
def self.
|
|
471
|
-
media_types
|
|
495
|
+
def self.media_query?(media_types)
|
|
496
|
+
media_types&.any? { |mt| mt.to_s.count('()') >= 2 }
|
|
472
497
|
end
|
|
473
498
|
|
|
474
499
|
# @private
|
|
475
500
|
def self.resolve_link(path, base_path) # :nodoc:
|
|
501
|
+
path = +path
|
|
476
502
|
path.strip!
|
|
477
|
-
|
|
478
|
-
if path =~ /\A(?:(https?|ftp|file):)\/\//i
|
|
503
|
+
if /\A(?:(https?|ftp|file):)\/\//i.match?(path)
|
|
479
504
|
resolved = path
|
|
480
505
|
Premailer.canonicalize(resolved)
|
|
481
|
-
elsif base_path.
|
|
506
|
+
elsif base_path.is_a?(Addressable::URI)
|
|
482
507
|
resolved = base_path.join(path)
|
|
483
508
|
Premailer.canonicalize(resolved)
|
|
484
|
-
elsif base_path.
|
|
509
|
+
elsif base_path.is_a?(String) && base_path =~ (/\A(?:(?:https?|ftp|file):)\/\//i)
|
|
485
510
|
resolved = Addressable::URI.parse(base_path)
|
|
486
511
|
resolved = resolved.join(path)
|
|
487
512
|
Premailer.canonicalize(resolved)
|
|
@@ -494,13 +519,13 @@ public
|
|
|
494
519
|
#
|
|
495
520
|
# IO objects return true, as do strings that look like URLs.
|
|
496
521
|
def self.local_data?(data)
|
|
497
|
-
return false
|
|
522
|
+
return false if data.is_a?(String) && data =~ /\A(?:(https?|ftp):)\/\//i
|
|
498
523
|
true
|
|
499
524
|
end
|
|
500
525
|
|
|
501
526
|
# from http://www.ruby-forum.com/topic/140101
|
|
502
527
|
def self.canonicalize(uri) # :nodoc:
|
|
503
|
-
u = uri.
|
|
528
|
+
u = uri.is_a?(Addressable::URI) ? uri : Addressable::URI.parse(uri.to_s)
|
|
504
529
|
u.normalize!
|
|
505
530
|
u.to_s
|
|
506
531
|
end
|
|
@@ -515,7 +540,7 @@ public
|
|
|
515
540
|
|
|
516
541
|
# Get a list off CSS properties
|
|
517
542
|
@processed_doc.search("*[@style]").each do |el|
|
|
518
|
-
|
|
543
|
+
el.attributes['style'].to_s.gsub(/([\w\-]+)[\s]*:/i) do |_s|
|
|
519
544
|
properties.push($1)
|
|
520
545
|
end
|
|
521
546
|
end
|
|
@@ -524,30 +549,42 @@ public
|
|
|
524
549
|
|
|
525
550
|
property_support = @client_support['css_properties']
|
|
526
551
|
properties.each do |prop|
|
|
527
|
-
if property_support.include?(prop)
|
|
528
|
-
property_support[prop].include?('support')
|
|
529
|
-
property_support[prop]['support'] >= @options[:warn_level]
|
|
530
|
-
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",
|
|
531
558
|
:level => WARN_LABEL[property_support[prop]['support']],
|
|
532
|
-
:clients => property_support[prop]['unsupported_in'].join(', ')
|
|
559
|
+
:clients => property_support[prop]['unsupported_in'].join(', ')
|
|
560
|
+
}
|
|
561
|
+
)
|
|
533
562
|
end
|
|
534
563
|
end
|
|
535
564
|
|
|
536
565
|
@client_support['attributes'].each do |attribute, data|
|
|
537
566
|
next unless data['support'] >= @options[:warn_level]
|
|
538
567
|
if @doc.search("*[@#{attribute}]").length > 0
|
|
539
|
-
warnings.push(
|
|
568
|
+
warnings.push(
|
|
569
|
+
{
|
|
570
|
+
:message => "#{attribute} HTML attribute",
|
|
540
571
|
:level => WARN_LABEL[data['support']],
|
|
541
|
-
:clients => data['unsupported_in'].join(', ')
|
|
572
|
+
:clients => data['unsupported_in'].join(', ')
|
|
573
|
+
}
|
|
574
|
+
)
|
|
542
575
|
end
|
|
543
576
|
end
|
|
544
577
|
|
|
545
578
|
@client_support['elements'].each do |element, data|
|
|
546
579
|
next unless data['support'] >= @options[:warn_level]
|
|
547
580
|
if @doc.search(element).length > 0
|
|
548
|
-
warnings.push(
|
|
581
|
+
warnings.push(
|
|
582
|
+
{
|
|
583
|
+
:message => "#{element} HTML element",
|
|
549
584
|
:level => WARN_LABEL[data['support']],
|
|
550
|
-
:clients => data['unsupported_in'].join(', ')
|
|
585
|
+
:clients => data['unsupported_in'].join(', ')
|
|
586
|
+
}
|
|
587
|
+
)
|
|
551
588
|
end
|
|
552
589
|
end
|
|
553
590
|
|
data/lib/premailer/version.rb
CHANGED
data/lib/premailer.rb
CHANGED