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
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'nokogiri'
|
|
2
3
|
|
|
3
4
|
class Premailer
|
|
4
5
|
module Adapter
|
|
5
6
|
# NokogiriFast adapter
|
|
6
7
|
module NokogiriFast
|
|
8
|
+
WIDTH_AND_HEIGHT = ['width', 'height'].freeze
|
|
7
9
|
|
|
8
10
|
include AdapterHelper::RgbToHex
|
|
9
11
|
# Merge CSS into the HTML document.
|
|
@@ -25,34 +27,33 @@ class Premailer
|
|
|
25
27
|
|
|
26
28
|
# Iterate through the rules and merge them into the HTML
|
|
27
29
|
@css_parser.each_selector(:all) do |selector, declaration, specificity, media_types|
|
|
28
|
-
|
|
29
30
|
# Save un-mergable rules separately
|
|
30
|
-
selector.gsub!(/:link([\s]*)+/i) { |
|
|
31
|
+
selector.gsub!(/:link([\s]*)+/i) { |_m| $1 }
|
|
31
32
|
|
|
32
33
|
# Convert element names to lower case
|
|
33
|
-
selector.gsub!(/([\s]|^)([\w]+)/) { |
|
|
34
|
+
selector.gsub!(/([\s]|^)([\w]+)/) { |_m| $1.to_s + $2.to_s.downcase }
|
|
34
35
|
|
|
35
|
-
if Premailer.
|
|
36
|
-
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration), media_types) unless @options[:preserve_styles]
|
|
36
|
+
if Premailer.media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
|
|
37
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selectors: selector, block: declaration), media_types) unless @options[:preserve_styles]
|
|
37
38
|
else
|
|
38
39
|
begin
|
|
39
|
-
if selector
|
|
40
|
+
if Premailer::RE_RESET_SELECTORS.match?(selector) && !!@options[:preserve_reset]
|
|
40
41
|
# this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
|
|
41
42
|
# however, this doesn't mean for testing pur
|
|
42
|
-
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration))
|
|
43
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selectors: selector, block: declaration))
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
# Try the new index based technique. If not supported, fall back to the old brute force one.
|
|
46
47
|
nodes = match_selector(index, all_nodes, descendants, selector) || doc.search(selector)
|
|
47
48
|
nodes.each do |el|
|
|
48
|
-
if el.elem?
|
|
49
|
+
if el.elem? && ((el.name != 'head') && (el.parent.name != 'head'))
|
|
49
50
|
# Add a style attribute or append to the existing one
|
|
50
51
|
block = "[SPEC=#{specificity}[#{declaration}]]"
|
|
51
52
|
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
|
|
55
|
-
|
|
56
|
+
warn "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
|
56
57
|
next
|
|
57
58
|
end
|
|
58
59
|
end
|
|
@@ -66,29 +67,50 @@ class Premailer
|
|
|
66
67
|
style = el.attributes['style'].to_s
|
|
67
68
|
|
|
68
69
|
declarations = []
|
|
69
|
-
style.scan(/\[SPEC
|
|
70
|
-
rs = CssParser::RuleSet.new(
|
|
70
|
+
style.scan(/\[SPEC=([\d]+)\[(.[^\]]*)\]\]/m).each do |declaration|
|
|
71
|
+
rs = CssParser::RuleSet.new(block: declaration[1].to_s, specificity: declaration[0].to_i)
|
|
71
72
|
declarations << rs
|
|
73
|
+
rescue ArgumentError => e
|
|
74
|
+
raise e if @options[:rule_set_exceptions]
|
|
72
75
|
end
|
|
73
76
|
|
|
74
77
|
# Perform style folding
|
|
75
78
|
merged = CssParser.merge(declarations)
|
|
76
|
-
|
|
79
|
+
begin
|
|
80
|
+
merged.expand_shorthand!
|
|
81
|
+
rescue ArgumentError => e
|
|
82
|
+
raise e if @options[:rule_set_exceptions]
|
|
83
|
+
end
|
|
77
84
|
|
|
78
85
|
# Duplicate CSS attributes as HTML attributes
|
|
79
|
-
if Premailer::RELATED_ATTRIBUTES.
|
|
80
|
-
Premailer::RELATED_ATTRIBUTES[el.name].each do |
|
|
81
|
-
if el[
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
if Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
87
|
+
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_attr, html_attr|
|
|
88
|
+
if el[html_attr].nil? && !merged[css_attr].empty?
|
|
89
|
+
new_val = merged[css_attr].dup
|
|
90
|
+
|
|
91
|
+
# Remove url() function wrapper
|
|
92
|
+
new_val.gsub!(/url\((['"])(.*?)\1\)/, '\2')
|
|
93
|
+
|
|
94
|
+
# Remove !important, trailing semi-colon, and leading/trailing whitespace
|
|
95
|
+
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
96
|
+
|
|
97
|
+
# For width and height tags, remove px units
|
|
98
|
+
new_val.gsub!(/(\d+)px/, '\1') if WIDTH_AND_HEIGHT.include?(html_attr)
|
|
99
|
+
|
|
100
|
+
# For color-related tags, convert RGB to hex if specified by options
|
|
101
|
+
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
102
|
+
|
|
103
|
+
el[html_attr] = new_val
|
|
84
104
|
end
|
|
105
|
+
|
|
85
106
|
unless @options[:preserve_style_attribute]
|
|
86
|
-
merged.instance_variable_get(
|
|
87
|
-
|
|
107
|
+
merged.instance_variable_get(:@declarations).tap do |declarations|
|
|
108
|
+
declarations.delete(css_attr)
|
|
88
109
|
end
|
|
89
110
|
end
|
|
90
111
|
end
|
|
91
112
|
end
|
|
113
|
+
|
|
92
114
|
# Collapse multiple rules into one as much as possible.
|
|
93
115
|
merged.create_shorthand! if @options[:create_shorthands]
|
|
94
116
|
|
|
@@ -98,9 +120,9 @@ class Premailer
|
|
|
98
120
|
|
|
99
121
|
doc = write_unmergable_css_rules(doc, @unmergable_rules) unless @options[:drop_unmergeable_css_rules]
|
|
100
122
|
|
|
101
|
-
if @options[:remove_classes]
|
|
123
|
+
if @options[:remove_classes] || @options[:remove_comments]
|
|
102
124
|
doc.traverse do |el|
|
|
103
|
-
if el.comment?
|
|
125
|
+
if el.comment? && @options[:remove_comments]
|
|
104
126
|
el.remove
|
|
105
127
|
elsif el.element?
|
|
106
128
|
el.remove_attribute('class') if @options[:remove_classes]
|
|
@@ -112,15 +134,15 @@ class Premailer
|
|
|
112
134
|
# find all anchor's targets and hash them
|
|
113
135
|
targets = []
|
|
114
136
|
doc.search("a[@href^='#']").each do |el|
|
|
115
|
-
target = el.get_attribute('href')[1
|
|
137
|
+
target = el.get_attribute('href')[1..]
|
|
116
138
|
targets << target
|
|
117
|
-
el.set_attribute('href', "#" + Digest::
|
|
139
|
+
el.set_attribute('href', "#" + Digest::SHA256.hexdigest(target))
|
|
118
140
|
end
|
|
119
141
|
# hash ids that are links target, delete others
|
|
120
142
|
doc.search("*[@id]").each do |el|
|
|
121
143
|
id = el.get_attribute('id')
|
|
122
144
|
if targets.include?(id)
|
|
123
|
-
el.set_attribute('id', Digest::
|
|
145
|
+
el.set_attribute('id', Digest::SHA256.hexdigest(id))
|
|
124
146
|
else
|
|
125
147
|
el.remove_attribute('id')
|
|
126
148
|
end
|
|
@@ -134,7 +156,7 @@ class Premailer
|
|
|
134
156
|
end
|
|
135
157
|
|
|
136
158
|
@processed_doc = doc
|
|
137
|
-
if
|
|
159
|
+
if xhtml?
|
|
138
160
|
# we don't want to encode carriage returns
|
|
139
161
|
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
|
|
140
162
|
else
|
|
@@ -158,15 +180,14 @@ class Premailer
|
|
|
158
180
|
else
|
|
159
181
|
style_tag = doc.create_element "style", styles
|
|
160
182
|
head = doc.at_css('head')
|
|
161
|
-
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element
|
|
162
|
-
head ||= doc.add_child(doc.create_element
|
|
183
|
+
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element("head")) if doc.root&.first_element_child
|
|
184
|
+
head ||= doc.add_child(doc.create_element("head"))
|
|
163
185
|
head << style_tag
|
|
164
186
|
end
|
|
165
187
|
end
|
|
166
188
|
doc
|
|
167
189
|
end
|
|
168
190
|
|
|
169
|
-
|
|
170
191
|
# Converts the HTML document to a format suitable for plain-text e-mail.
|
|
171
192
|
#
|
|
172
193
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
|
@@ -176,17 +197,17 @@ class Premailer
|
|
|
176
197
|
html_src = ''
|
|
177
198
|
begin
|
|
178
199
|
html_src = @doc.at("body").inner_html
|
|
179
|
-
rescue
|
|
200
|
+
rescue StandardError
|
|
180
201
|
end
|
|
181
202
|
|
|
182
|
-
html_src = @doc.to_html unless html_src
|
|
203
|
+
html_src = @doc.to_html unless html_src && !html_src.empty?
|
|
183
204
|
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
|
184
205
|
end
|
|
185
206
|
|
|
186
207
|
# Gets the original HTML as a string.
|
|
187
208
|
# @return [String] HTML.
|
|
188
209
|
def to_s
|
|
189
|
-
if
|
|
210
|
+
if xhtml?
|
|
190
211
|
@doc.to_xhtml(:encoding => nil)
|
|
191
212
|
else
|
|
192
213
|
@doc.to_html(:encoding => nil)
|
|
@@ -200,13 +221,13 @@ class Premailer
|
|
|
200
221
|
thing = nil
|
|
201
222
|
|
|
202
223
|
# TODO: duplicate options
|
|
203
|
-
if @options[:with_html_string]
|
|
224
|
+
if @options[:with_html_string] || @options[:inline] || input.respond_to?(:read)
|
|
204
225
|
thing = input
|
|
205
226
|
elsif @is_local_file
|
|
206
227
|
@base_dir = File.dirname(input)
|
|
207
228
|
thing = File.open(input, 'r')
|
|
208
229
|
else
|
|
209
|
-
thing =
|
|
230
|
+
thing = URI.parse(input).open
|
|
210
231
|
end
|
|
211
232
|
|
|
212
233
|
if thing.respond_to?(:read)
|
|
@@ -217,31 +238,24 @@ class Premailer
|
|
|
217
238
|
doc = nil
|
|
218
239
|
|
|
219
240
|
# Handle HTML entities
|
|
220
|
-
if @options[:replace_html_entities] == true
|
|
241
|
+
if (@options[:replace_html_entities] == true) && thing.is_a?(String)
|
|
221
242
|
HTML_ENTITIES.map do |entity, replacement|
|
|
222
243
|
thing.gsub! entity, replacement
|
|
223
244
|
end
|
|
224
245
|
end
|
|
225
|
-
|
|
226
|
-
# However, we really don't want to hardcode this. ASCII-8BIT should be the default, but not the only option.
|
|
227
|
-
encoding = if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
|
|
228
|
-
thing = thing.force_encoding(@options[:input_encoding]).encode!
|
|
229
|
-
@options[:input_encoding]
|
|
230
|
-
else
|
|
231
|
-
@options[:input_encoding] || (RUBY_PLATFORM == 'java' ? nil : 'BINARY')
|
|
232
|
-
end
|
|
246
|
+
encoding = @options[:input_encoding] || (RUBY_PLATFORM == 'java' ? nil : 'BINARY')
|
|
233
247
|
doc = if @options[:html_fragment]
|
|
234
248
|
::Nokogiri::HTML.fragment(thing, encoding)
|
|
235
249
|
else
|
|
236
|
-
::Nokogiri::HTML(thing, nil, encoding
|
|
250
|
+
::Nokogiri::HTML(thing, nil, encoding, &:recover)
|
|
237
251
|
end
|
|
238
252
|
|
|
239
253
|
# Fix for removing any CDATA tags from both style and script tags inserted per
|
|
240
254
|
# https://github.com/sparklemotion/nokogiri/issues/311 and
|
|
241
255
|
# https://github.com/premailer/premailer/issues/199
|
|
242
|
-
|
|
256
|
+
['style', 'script'].each do |tag|
|
|
243
257
|
doc.search(tag).children.each do |child|
|
|
244
|
-
child.swap(child.text
|
|
258
|
+
child.swap(child.text) if child.cdata?
|
|
245
259
|
end
|
|
246
260
|
end
|
|
247
261
|
|
|
@@ -265,7 +279,7 @@ class Premailer
|
|
|
265
279
|
page.traverse do |node|
|
|
266
280
|
all_nodes.push(node)
|
|
267
281
|
|
|
268
|
-
if node != page
|
|
282
|
+
if node != page
|
|
269
283
|
index_ancestry(page, node, node.parent, descendants)
|
|
270
284
|
end
|
|
271
285
|
|
|
@@ -276,7 +290,7 @@ class Premailer
|
|
|
276
290
|
# Index the node by all class attributes it possesses.
|
|
277
291
|
# Classes are modestly selective. Usually more than tag names
|
|
278
292
|
# but less selective than ids.
|
|
279
|
-
if node.has_attribute?("class")
|
|
293
|
+
if node.has_attribute?("class")
|
|
280
294
|
node.get_attribute("class").split(/\s+/).each do |c|
|
|
281
295
|
c = '.' + c
|
|
282
296
|
index[c] = (index[c] || Set.new).add(node)
|
|
@@ -285,7 +299,7 @@ class Premailer
|
|
|
285
299
|
|
|
286
300
|
# Index the node by its "id" attribute if it has one.
|
|
287
301
|
# This is usually the most selective of the three.
|
|
288
|
-
if node.has_attribute?("id")
|
|
302
|
+
if node.has_attribute?("id")
|
|
289
303
|
id = '#' + node.get_attribute("id")
|
|
290
304
|
index[id] = (index[id] || Set.new).add(node)
|
|
291
305
|
end
|
|
@@ -298,7 +312,7 @@ class Premailer
|
|
|
298
312
|
index.default = Set.new
|
|
299
313
|
descendants.default = Set.new
|
|
300
314
|
|
|
301
|
-
|
|
315
|
+
[index, Set.new(all_nodes), descendants]
|
|
302
316
|
end
|
|
303
317
|
|
|
304
318
|
# @param doc The top level document
|
|
@@ -307,9 +321,9 @@ class Premailer
|
|
|
307
321
|
# @param descendants The running hash map of node -> set of nodes that maps descendants of a node.
|
|
308
322
|
# @return The descendants argument after updating it.
|
|
309
323
|
def index_ancestry(doc, elem, parent, descendants)
|
|
310
|
-
if parent
|
|
324
|
+
if parent
|
|
311
325
|
descendants[parent] = (descendants[parent] || Set.new).add(elem)
|
|
312
|
-
if doc != parent
|
|
326
|
+
if doc != parent
|
|
313
327
|
index_ancestry(doc, elem, parent.parent, descendants)
|
|
314
328
|
end
|
|
315
329
|
end
|
|
@@ -338,14 +352,14 @@ class Premailer
|
|
|
338
352
|
# It will return nil when such a selector is passed, so you can take
|
|
339
353
|
# action on the falsity of the return value.
|
|
340
354
|
def match_selector(index, all_nodes, descendants, selector)
|
|
341
|
-
if /[^-a-zA-Z0-9_\s.#]/.match(selector)
|
|
355
|
+
if /[^-a-zA-Z0-9_\s.#]/.match?(selector)
|
|
342
356
|
return nil
|
|
343
357
|
end
|
|
344
358
|
|
|
345
359
|
take_children = false
|
|
346
360
|
selector.split(/\s+/).reduce(all_nodes) do |base, spec|
|
|
347
361
|
desc = base
|
|
348
|
-
if take_children
|
|
362
|
+
if take_children
|
|
349
363
|
desc = Set.new
|
|
350
364
|
base.each do |n|
|
|
351
365
|
desc.merge(descendants[n])
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
class Premailer
|
|
4
3
|
module Adapter
|
|
5
4
|
# Nokogiri adapter
|
|
6
5
|
module Nokogumbo
|
|
6
|
+
WIDTH_AND_HEIGHT = ['width', 'height'].freeze
|
|
7
7
|
|
|
8
8
|
include AdapterHelper::RgbToHex
|
|
9
9
|
# Merge CSS into the HTML document.
|
|
@@ -21,19 +21,19 @@ class Premailer
|
|
|
21
21
|
# Iterate through the rules and merge them into the HTML
|
|
22
22
|
@css_parser.each_selector(:all) do |selector, declaration, specificity, media_types|
|
|
23
23
|
# Save un-mergable rules separately
|
|
24
|
-
selector.gsub!(/:link([\s]*)+/i) { |
|
|
24
|
+
selector.gsub!(/:link([\s]*)+/i) { |_m| $1 }
|
|
25
25
|
|
|
26
26
|
# Convert element names to lower case
|
|
27
|
-
selector.gsub!(/([\s]|^)([\w]+)/) { |
|
|
27
|
+
selector.gsub!(/([\s]|^)([\w]+)/) { |_m| $1.to_s + $2.to_s.downcase }
|
|
28
28
|
|
|
29
|
-
if Premailer.
|
|
30
|
-
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration), media_types) unless @options[:preserve_styles]
|
|
29
|
+
if Premailer.media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
|
|
30
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selectors: selector, block: declaration), media_types) unless @options[:preserve_styles]
|
|
31
31
|
else
|
|
32
32
|
begin
|
|
33
|
-
if selector
|
|
33
|
+
if Premailer::RE_RESET_SELECTORS.match?(selector) && !!@options[:preserve_reset]
|
|
34
34
|
# this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
|
|
35
35
|
# however, this doesn't mean for testing pur
|
|
36
|
-
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration))
|
|
36
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selectors: selector, block: declaration))
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Change single ID CSS selectors into xpath so that we can match more
|
|
@@ -41,52 +41,71 @@ class Premailer
|
|
|
41
41
|
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
|
42
42
|
|
|
43
43
|
doc.search(selector).each do |el|
|
|
44
|
-
if el.elem?
|
|
44
|
+
if el.elem? && ((el.name != 'head') && (el.parent.name != 'head'))
|
|
45
45
|
# Add a style attribute or append to the existing one
|
|
46
46
|
block = "[SPEC=#{specificity}[#{declaration}]]"
|
|
47
47
|
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
|
|
51
|
-
|
|
51
|
+
warn "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
|
52
52
|
next
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Remove script tags
|
|
58
|
-
if @options[:remove_scripts]
|
|
59
|
-
doc.search("script").remove
|
|
60
|
-
end
|
|
58
|
+
doc.search("script").remove if @options[:remove_scripts]
|
|
61
59
|
|
|
62
60
|
# Read STYLE attributes and perform folding
|
|
63
61
|
doc.search("*[@style]").each do |el|
|
|
64
62
|
style = el.attributes['style'].to_s
|
|
65
63
|
|
|
66
64
|
declarations = []
|
|
67
|
-
style.scan(/\[SPEC
|
|
68
|
-
rs = CssParser::RuleSet.new(
|
|
65
|
+
style.scan(/\[SPEC=([\d]+)\[(.[^\]]*)\]\]/m).each do |declaration|
|
|
66
|
+
rs = CssParser::RuleSet.new(block: declaration[1].to_s, specificity: declaration[0].to_i)
|
|
69
67
|
declarations << rs
|
|
68
|
+
rescue ArgumentError => e
|
|
69
|
+
raise e if @options[:rule_set_exceptions]
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Perform style folding
|
|
73
73
|
merged = CssParser.merge(declarations)
|
|
74
|
-
|
|
74
|
+
begin
|
|
75
|
+
merged.expand_shorthand!
|
|
76
|
+
rescue ArgumentError => e
|
|
77
|
+
raise e if @options[:rule_set_exceptions]
|
|
78
|
+
end
|
|
75
79
|
|
|
76
80
|
# Duplicate CSS attributes as HTML attributes
|
|
77
|
-
if Premailer::RELATED_ATTRIBUTES.
|
|
78
|
-
Premailer::RELATED_ATTRIBUTES[el.name].each do |
|
|
79
|
-
if el[
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
if Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
82
|
+
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_attr, html_attr|
|
|
83
|
+
if el[html_attr].nil? && !merged[css_attr].empty?
|
|
84
|
+
new_val = merged[css_attr].dup
|
|
85
|
+
|
|
86
|
+
# Remove url() function wrapper
|
|
87
|
+
new_val.gsub!(/url\((['"])(.*?)\1\)/, '\2')
|
|
88
|
+
|
|
89
|
+
# Remove !important, trailing semi-colon, and leading/trailing whitespace
|
|
90
|
+
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
91
|
+
|
|
92
|
+
# For width and height tags, remove px units
|
|
93
|
+
new_val.gsub!(/(\d+)px/, '\1') if WIDTH_AND_HEIGHT.include?(html_attr)
|
|
94
|
+
|
|
95
|
+
# For color-related tags, convert RGB to hex if specified by options
|
|
96
|
+
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
97
|
+
|
|
98
|
+
el[html_attr] = new_val
|
|
82
99
|
end
|
|
100
|
+
|
|
83
101
|
unless @options[:preserve_style_attribute]
|
|
84
|
-
merged.instance_variable_get(
|
|
85
|
-
|
|
102
|
+
merged.instance_variable_get(:@declarations).tap do |declarations|
|
|
103
|
+
declarations.delete(css_attr)
|
|
86
104
|
end
|
|
87
105
|
end
|
|
88
106
|
end
|
|
89
107
|
end
|
|
108
|
+
|
|
90
109
|
# Collapse multiple rules into one as much as possible.
|
|
91
110
|
merged.create_shorthand! if @options[:create_shorthands]
|
|
92
111
|
|
|
@@ -96,9 +115,9 @@ class Premailer
|
|
|
96
115
|
|
|
97
116
|
doc = write_unmergable_css_rules(doc, @unmergable_rules) unless @options[:drop_unmergeable_css_rules]
|
|
98
117
|
|
|
99
|
-
if @options[:remove_classes]
|
|
118
|
+
if @options[:remove_classes] || @options[:remove_comments]
|
|
100
119
|
doc.traverse do |el|
|
|
101
|
-
if el.comment?
|
|
120
|
+
if el.comment? && @options[:remove_comments]
|
|
102
121
|
el.remove
|
|
103
122
|
elsif el.element?
|
|
104
123
|
el.remove_attribute('class') if @options[:remove_classes]
|
|
@@ -110,15 +129,15 @@ class Premailer
|
|
|
110
129
|
# find all anchor's targets and hash them
|
|
111
130
|
targets = []
|
|
112
131
|
doc.search("a[@href^='#']").each do |el|
|
|
113
|
-
target = el.get_attribute('href')[1
|
|
132
|
+
target = el.get_attribute('href')[1..]
|
|
114
133
|
targets << target
|
|
115
|
-
el.set_attribute('href', "#" + Digest::
|
|
134
|
+
el.set_attribute('href', "#" + Digest::SHA256.hexdigest(target))
|
|
116
135
|
end
|
|
117
136
|
# hash ids that are links target, delete others
|
|
118
137
|
doc.search("*[@id]").each do |el|
|
|
119
138
|
id = el.get_attribute('id')
|
|
120
139
|
if targets.include?(id)
|
|
121
|
-
el.set_attribute('id', Digest::
|
|
140
|
+
el.set_attribute('id', Digest::SHA256.hexdigest(id))
|
|
122
141
|
else
|
|
123
142
|
el.remove_attribute('id')
|
|
124
143
|
end
|
|
@@ -132,7 +151,7 @@ class Premailer
|
|
|
132
151
|
end
|
|
133
152
|
|
|
134
153
|
@processed_doc = doc
|
|
135
|
-
if
|
|
154
|
+
if xhtml?
|
|
136
155
|
# we don't want to encode carriage returns
|
|
137
156
|
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
|
|
138
157
|
else
|
|
@@ -156,15 +175,14 @@ class Premailer
|
|
|
156
175
|
else
|
|
157
176
|
style_tag = doc.create_element "style", styles
|
|
158
177
|
head = doc.at_css('head')
|
|
159
|
-
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element
|
|
160
|
-
head ||= doc.add_child(doc.create_element
|
|
178
|
+
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element("head")) if doc.root&.first_element_child
|
|
179
|
+
head ||= doc.add_child(doc.create_element("head"))
|
|
161
180
|
head << style_tag
|
|
162
181
|
end
|
|
163
182
|
end
|
|
164
183
|
doc
|
|
165
184
|
end
|
|
166
185
|
|
|
167
|
-
|
|
168
186
|
# Converts the HTML document to a format suitable for plain-text e-mail.
|
|
169
187
|
#
|
|
170
188
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
|
@@ -174,17 +192,17 @@ class Premailer
|
|
|
174
192
|
html_src = ''
|
|
175
193
|
begin
|
|
176
194
|
html_src = @doc.at("body").inner_html
|
|
177
|
-
rescue
|
|
195
|
+
rescue StandardError
|
|
178
196
|
end
|
|
179
197
|
|
|
180
|
-
html_src = @doc.to_html unless html_src
|
|
198
|
+
html_src = @doc.to_html unless html_src && !html_src.empty?
|
|
181
199
|
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
|
182
200
|
end
|
|
183
201
|
|
|
184
202
|
# Gets the original HTML as a string.
|
|
185
203
|
# @return [String] HTML.
|
|
186
204
|
def to_s
|
|
187
|
-
if
|
|
205
|
+
if xhtml?
|
|
188
206
|
@doc.to_xhtml(:encoding => nil)
|
|
189
207
|
else
|
|
190
208
|
@doc.to_html(:encoding => nil)
|
|
@@ -198,13 +216,13 @@ class Premailer
|
|
|
198
216
|
thing = nil
|
|
199
217
|
|
|
200
218
|
# TODO: duplicate options
|
|
201
|
-
if @options[:with_html_string]
|
|
219
|
+
if @options[:with_html_string] || @options[:inline] || input.respond_to?(:read)
|
|
202
220
|
thing = input
|
|
203
221
|
elsif @is_local_file
|
|
204
222
|
@base_dir = File.dirname(input)
|
|
205
223
|
thing = File.open(input, 'r')
|
|
206
224
|
else
|
|
207
|
-
thing =
|
|
225
|
+
thing = URI.parse(input).open
|
|
208
226
|
end
|
|
209
227
|
|
|
210
228
|
if thing.respond_to?(:read)
|
|
@@ -215,16 +233,11 @@ class Premailer
|
|
|
215
233
|
doc = nil
|
|
216
234
|
|
|
217
235
|
# Handle HTML entities
|
|
218
|
-
if @options[:replace_html_entities] == true
|
|
236
|
+
if (@options[:replace_html_entities] == true) && thing.is_a?(String)
|
|
219
237
|
HTML_ENTITIES.map do |entity, replacement|
|
|
220
238
|
thing.gsub! entity, replacement
|
|
221
239
|
end
|
|
222
240
|
end
|
|
223
|
-
# Default encoding is ASCII-8BIT (binary) per http://groups.google.com/group/nokogiri-talk/msg/0b81ef0dc180dc74
|
|
224
|
-
# However, we really don't want to hardcode this. ASCII-8BIT should be the default, but not the only option.
|
|
225
|
-
if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
|
|
226
|
-
thing = thing.force_encoding(@options[:input_encoding]).encode!
|
|
227
|
-
end
|
|
228
241
|
doc = if @options[:html_fragment]
|
|
229
242
|
::Nokogiri::HTML5.fragment(thing)
|
|
230
243
|
else
|
|
@@ -234,15 +247,14 @@ class Premailer
|
|
|
234
247
|
# Fix for removing any CDATA tags from both style and script tags inserted per
|
|
235
248
|
# https://github.com/sparklemotion/nokogiri/issues/311 and
|
|
236
249
|
# https://github.com/premailer/premailer/issues/199
|
|
237
|
-
|
|
250
|
+
['style', 'script'].each do |tag|
|
|
238
251
|
doc.search(tag).children.each do |child|
|
|
239
|
-
child.swap(child.text
|
|
252
|
+
child.swap(child.text) if child.cdata?
|
|
240
253
|
end
|
|
241
254
|
end
|
|
242
255
|
|
|
243
256
|
doc
|
|
244
257
|
end
|
|
245
|
-
|
|
246
258
|
end
|
|
247
259
|
end
|
|
248
260
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
# RGB helper for adapters, currently only nokogiri supported
|
|
2
3
|
|
|
3
4
|
module AdapterHelper
|
|
@@ -6,23 +7,24 @@ module AdapterHelper
|
|
|
6
7
|
str.to_i.to_s(16).rjust(2, '0').upcase
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
def
|
|
10
|
+
def rgb?(color)
|
|
10
11
|
pattern = %r{
|
|
11
12
|
rgb
|
|
12
|
-
\(\s*
|
|
13
|
-
(\d{1,3})
|
|
14
|
-
|
|
15
|
-
(\d{1,3})
|
|
16
|
-
|
|
17
|
-
(\d{1,3})
|
|
18
|
-
\s*\)
|
|
13
|
+
\(\s* # literal open, with optional whitespace
|
|
14
|
+
(\d{1,3}) # capture 1-3 digits
|
|
15
|
+
(?:\s*,\s*|\s+) # comma or whitespace
|
|
16
|
+
(\d{1,3}) # capture 1-3 digits
|
|
17
|
+
(?:\s*,\s*|\s+) # comma or whitespacee
|
|
18
|
+
(\d{1,3}) # capture 1-3 digits
|
|
19
|
+
\s*(?:/\s*\d*\.?\d*%?)? # optional alpha modifier
|
|
20
|
+
\s*\) # literal close, with optional whitespace
|
|
19
21
|
}x
|
|
20
22
|
|
|
21
23
|
pattern.match(color)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def ensure_hex(color)
|
|
25
|
-
match_data =
|
|
27
|
+
match_data = rgb?(color)
|
|
26
28
|
if match_data
|
|
27
29
|
"#{to_hex(match_data[1])}#{to_hex(match_data[2])}#{to_hex(match_data[3])}"
|
|
28
30
|
else
|
data/lib/premailer/adapter.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
class Premailer
|
|
2
3
|
# Manages the adapter classes. Currently supports:
|
|
3
4
|
#
|
|
@@ -5,7 +6,6 @@ class Premailer
|
|
|
5
6
|
# * nokogiri_fast
|
|
6
7
|
# * nokogumbo
|
|
7
8
|
module Adapter
|
|
8
|
-
|
|
9
9
|
autoload :Nokogiri, 'premailer/adapter/nokogiri'
|
|
10
10
|
autoload :NokogiriFast, 'premailer/adapter/nokogiri_fast'
|
|
11
11
|
autoload :Nokogumbo, 'premailer/adapter/nokogumbo'
|
|
@@ -14,13 +14,13 @@ class Premailer
|
|
|
14
14
|
REQUIREMENT_MAP = [
|
|
15
15
|
["nokogiri", :nokogiri],
|
|
16
16
|
["nokogiri", :nokogiri_fast],
|
|
17
|
-
["nokogumbo", :nokogumbo]
|
|
18
|
-
]
|
|
17
|
+
["nokogumbo", :nokogumbo]
|
|
18
|
+
].freeze
|
|
19
19
|
|
|
20
20
|
# Returns the adapter to use.
|
|
21
21
|
def self.use
|
|
22
22
|
return @use if @use
|
|
23
|
-
self.use =
|
|
23
|
+
self.use = default
|
|
24
24
|
@use
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -34,15 +34,13 @@ class Premailer
|
|
|
34
34
|
return :nokogumbo if defined?(::Nokogumbo)
|
|
35
35
|
|
|
36
36
|
REQUIREMENT_MAP.each do |(library, adapter)|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
next
|
|
42
|
-
end
|
|
37
|
+
require library
|
|
38
|
+
return adapter
|
|
39
|
+
rescue LoadError
|
|
40
|
+
next
|
|
43
41
|
end
|
|
44
42
|
|
|
45
|
-
raise
|
|
43
|
+
raise "No suitable adapter for Premailer was found, please install nokogiri or nokogumbo"
|
|
46
44
|
end
|
|
47
45
|
|
|
48
46
|
# Sets the adapter to use.
|
|
@@ -56,10 +54,9 @@ class Premailer
|
|
|
56
54
|
def self.find(adapter)
|
|
57
55
|
return adapter if adapter.is_a?(Module)
|
|
58
56
|
|
|
59
|
-
Premailer::Adapter.const_get(
|
|
57
|
+
Premailer::Adapter.const_get(adapter.to_s.split('_').map(&:capitalize).join.to_s)
|
|
60
58
|
rescue NameError
|
|
61
59
|
raise ArgumentError, "Invalid adapter: #{adapter}"
|
|
62
60
|
end
|
|
63
|
-
|
|
64
61
|
end
|
|
65
62
|
end
|