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,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,13 +67,11 @@ class Premailer
|
|
|
66
67
|
style = el.attributes['style'].to_s
|
|
67
68
|
|
|
68
69
|
declarations = []
|
|
69
|
-
style.scan(/\[SPEC
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
raise e if @options[:rule_set_exceptions]
|
|
75
|
-
end
|
|
70
|
+
style.scan(/\[SPEC=([\d]+)\[(.[^\]]*)\]\]/m).each do |declaration|
|
|
71
|
+
rs = CssParser::RuleSet.new(block: declaration[1].to_s, specificity: declaration[0].to_i)
|
|
72
|
+
declarations << rs
|
|
73
|
+
rescue ArgumentError => e
|
|
74
|
+
raise e if @options[:rule_set_exceptions]
|
|
76
75
|
end
|
|
77
76
|
|
|
78
77
|
# Perform style folding
|
|
@@ -84,9 +83,9 @@ class Premailer
|
|
|
84
83
|
end
|
|
85
84
|
|
|
86
85
|
# Duplicate CSS attributes as HTML attributes
|
|
87
|
-
if Premailer::RELATED_ATTRIBUTES.
|
|
86
|
+
if Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
88
87
|
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_attr, html_attr|
|
|
89
|
-
if el[html_attr].nil?
|
|
88
|
+
if el[html_attr].nil? && !merged[css_attr].empty?
|
|
90
89
|
new_val = merged[css_attr].dup
|
|
91
90
|
|
|
92
91
|
# Remove url() function wrapper
|
|
@@ -96,7 +95,7 @@ class Premailer
|
|
|
96
95
|
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
97
96
|
|
|
98
97
|
# For width and height tags, remove px units
|
|
99
|
-
new_val.gsub!(/(\d+)px/, '\1') if
|
|
98
|
+
new_val.gsub!(/(\d+)px/, '\1') if WIDTH_AND_HEIGHT.include?(html_attr)
|
|
100
99
|
|
|
101
100
|
# For color-related tags, convert RGB to hex if specified by options
|
|
102
101
|
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
@@ -105,7 +104,7 @@ class Premailer
|
|
|
105
104
|
end
|
|
106
105
|
|
|
107
106
|
unless @options[:preserve_style_attribute]
|
|
108
|
-
merged.instance_variable_get(
|
|
107
|
+
merged.instance_variable_get(:@declarations).tap do |declarations|
|
|
109
108
|
declarations.delete(css_attr)
|
|
110
109
|
end
|
|
111
110
|
end
|
|
@@ -121,9 +120,9 @@ class Premailer
|
|
|
121
120
|
|
|
122
121
|
doc = write_unmergable_css_rules(doc, @unmergable_rules) unless @options[:drop_unmergeable_css_rules]
|
|
123
122
|
|
|
124
|
-
if @options[:remove_classes]
|
|
123
|
+
if @options[:remove_classes] || @options[:remove_comments]
|
|
125
124
|
doc.traverse do |el|
|
|
126
|
-
if el.comment?
|
|
125
|
+
if el.comment? && @options[:remove_comments]
|
|
127
126
|
el.remove
|
|
128
127
|
elsif el.element?
|
|
129
128
|
el.remove_attribute('class') if @options[:remove_classes]
|
|
@@ -135,7 +134,7 @@ class Premailer
|
|
|
135
134
|
# find all anchor's targets and hash them
|
|
136
135
|
targets = []
|
|
137
136
|
doc.search("a[@href^='#']").each do |el|
|
|
138
|
-
target = el.get_attribute('href')[1
|
|
137
|
+
target = el.get_attribute('href')[1..]
|
|
139
138
|
targets << target
|
|
140
139
|
el.set_attribute('href', "#" + Digest::SHA256.hexdigest(target))
|
|
141
140
|
end
|
|
@@ -157,7 +156,7 @@ class Premailer
|
|
|
157
156
|
end
|
|
158
157
|
|
|
159
158
|
@processed_doc = doc
|
|
160
|
-
if
|
|
159
|
+
if xhtml?
|
|
161
160
|
# we don't want to encode carriage returns
|
|
162
161
|
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
|
|
163
162
|
else
|
|
@@ -181,15 +180,14 @@ class Premailer
|
|
|
181
180
|
else
|
|
182
181
|
style_tag = doc.create_element "style", styles
|
|
183
182
|
head = doc.at_css('head')
|
|
184
|
-
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element
|
|
185
|
-
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"))
|
|
186
185
|
head << style_tag
|
|
187
186
|
end
|
|
188
187
|
end
|
|
189
188
|
doc
|
|
190
189
|
end
|
|
191
190
|
|
|
192
|
-
|
|
193
191
|
# Converts the HTML document to a format suitable for plain-text e-mail.
|
|
194
192
|
#
|
|
195
193
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
|
@@ -199,17 +197,17 @@ class Premailer
|
|
|
199
197
|
html_src = ''
|
|
200
198
|
begin
|
|
201
199
|
html_src = @doc.at("body").inner_html
|
|
202
|
-
rescue
|
|
200
|
+
rescue StandardError
|
|
203
201
|
end
|
|
204
202
|
|
|
205
|
-
html_src = @doc.to_html unless html_src
|
|
203
|
+
html_src = @doc.to_html unless html_src && !html_src.empty?
|
|
206
204
|
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
|
207
205
|
end
|
|
208
206
|
|
|
209
207
|
# Gets the original HTML as a string.
|
|
210
208
|
# @return [String] HTML.
|
|
211
209
|
def to_s
|
|
212
|
-
if
|
|
210
|
+
if xhtml?
|
|
213
211
|
@doc.to_xhtml(:encoding => nil)
|
|
214
212
|
else
|
|
215
213
|
@doc.to_html(:encoding => nil)
|
|
@@ -223,13 +221,13 @@ class Premailer
|
|
|
223
221
|
thing = nil
|
|
224
222
|
|
|
225
223
|
# TODO: duplicate options
|
|
226
|
-
if @options[:with_html_string]
|
|
224
|
+
if @options[:with_html_string] || @options[:inline] || input.respond_to?(:read)
|
|
227
225
|
thing = input
|
|
228
226
|
elsif @is_local_file
|
|
229
227
|
@base_dir = File.dirname(input)
|
|
230
228
|
thing = File.open(input, 'r')
|
|
231
229
|
else
|
|
232
|
-
thing = URI.
|
|
230
|
+
thing = URI.parse(input).open
|
|
233
231
|
end
|
|
234
232
|
|
|
235
233
|
if thing.respond_to?(:read)
|
|
@@ -240,7 +238,7 @@ class Premailer
|
|
|
240
238
|
doc = nil
|
|
241
239
|
|
|
242
240
|
# Handle HTML entities
|
|
243
|
-
if @options[:replace_html_entities] == true
|
|
241
|
+
if (@options[:replace_html_entities] == true) && thing.is_a?(String)
|
|
244
242
|
HTML_ENTITIES.map do |entity, replacement|
|
|
245
243
|
thing.gsub! entity, replacement
|
|
246
244
|
end
|
|
@@ -249,15 +247,15 @@ class Premailer
|
|
|
249
247
|
doc = if @options[:html_fragment]
|
|
250
248
|
::Nokogiri::HTML.fragment(thing, encoding)
|
|
251
249
|
else
|
|
252
|
-
::Nokogiri::HTML(thing, nil, encoding
|
|
250
|
+
::Nokogiri::HTML(thing, nil, encoding, &:recover)
|
|
253
251
|
end
|
|
254
252
|
|
|
255
253
|
# Fix for removing any CDATA tags from both style and script tags inserted per
|
|
256
254
|
# https://github.com/sparklemotion/nokogiri/issues/311 and
|
|
257
255
|
# https://github.com/premailer/premailer/issues/199
|
|
258
|
-
|
|
256
|
+
['style', 'script'].each do |tag|
|
|
259
257
|
doc.search(tag).children.each do |child|
|
|
260
|
-
child.swap(child.text
|
|
258
|
+
child.swap(child.text) if child.cdata?
|
|
261
259
|
end
|
|
262
260
|
end
|
|
263
261
|
|
|
@@ -281,7 +279,7 @@ class Premailer
|
|
|
281
279
|
page.traverse do |node|
|
|
282
280
|
all_nodes.push(node)
|
|
283
281
|
|
|
284
|
-
if node != page
|
|
282
|
+
if node != page
|
|
285
283
|
index_ancestry(page, node, node.parent, descendants)
|
|
286
284
|
end
|
|
287
285
|
|
|
@@ -292,7 +290,7 @@ class Premailer
|
|
|
292
290
|
# Index the node by all class attributes it possesses.
|
|
293
291
|
# Classes are modestly selective. Usually more than tag names
|
|
294
292
|
# but less selective than ids.
|
|
295
|
-
if node.has_attribute?("class")
|
|
293
|
+
if node.has_attribute?("class")
|
|
296
294
|
node.get_attribute("class").split(/\s+/).each do |c|
|
|
297
295
|
c = '.' + c
|
|
298
296
|
index[c] = (index[c] || Set.new).add(node)
|
|
@@ -301,7 +299,7 @@ class Premailer
|
|
|
301
299
|
|
|
302
300
|
# Index the node by its "id" attribute if it has one.
|
|
303
301
|
# This is usually the most selective of the three.
|
|
304
|
-
if node.has_attribute?("id")
|
|
302
|
+
if node.has_attribute?("id")
|
|
305
303
|
id = '#' + node.get_attribute("id")
|
|
306
304
|
index[id] = (index[id] || Set.new).add(node)
|
|
307
305
|
end
|
|
@@ -314,7 +312,7 @@ class Premailer
|
|
|
314
312
|
index.default = Set.new
|
|
315
313
|
descendants.default = Set.new
|
|
316
314
|
|
|
317
|
-
|
|
315
|
+
[index, Set.new(all_nodes), descendants]
|
|
318
316
|
end
|
|
319
317
|
|
|
320
318
|
# @param doc The top level document
|
|
@@ -323,9 +321,9 @@ class Premailer
|
|
|
323
321
|
# @param descendants The running hash map of node -> set of nodes that maps descendants of a node.
|
|
324
322
|
# @return The descendants argument after updating it.
|
|
325
323
|
def index_ancestry(doc, elem, parent, descendants)
|
|
326
|
-
if parent
|
|
324
|
+
if parent
|
|
327
325
|
descendants[parent] = (descendants[parent] || Set.new).add(elem)
|
|
328
|
-
if doc != parent
|
|
326
|
+
if doc != parent
|
|
329
327
|
index_ancestry(doc, elem, parent.parent, descendants)
|
|
330
328
|
end
|
|
331
329
|
end
|
|
@@ -354,14 +352,14 @@ class Premailer
|
|
|
354
352
|
# It will return nil when such a selector is passed, so you can take
|
|
355
353
|
# action on the falsity of the return value.
|
|
356
354
|
def match_selector(index, all_nodes, descendants, selector)
|
|
357
|
-
if /[^-a-zA-Z0-9_\s.#]/.match(selector)
|
|
355
|
+
if /[^-a-zA-Z0-9_\s.#]/.match?(selector)
|
|
358
356
|
return nil
|
|
359
357
|
end
|
|
360
358
|
|
|
361
359
|
take_children = false
|
|
362
360
|
selector.split(/\s+/).reduce(all_nodes) do |base, spec|
|
|
363
361
|
desc = base
|
|
364
|
-
if take_children
|
|
362
|
+
if take_children
|
|
365
363
|
desc = Set.new
|
|
366
364
|
base.each do |n|
|
|
367
365
|
desc.merge(descendants[n])
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
class Premailer
|
|
2
3
|
module Adapter
|
|
3
4
|
# Nokogiri adapter
|
|
4
5
|
module Nokogumbo
|
|
6
|
+
WIDTH_AND_HEIGHT = ['width', 'height'].freeze
|
|
5
7
|
|
|
6
8
|
include AdapterHelper::RgbToHex
|
|
7
9
|
# Merge CSS into the HTML document.
|
|
@@ -19,19 +21,19 @@ class Premailer
|
|
|
19
21
|
# Iterate through the rules and merge them into the HTML
|
|
20
22
|
@css_parser.each_selector(:all) do |selector, declaration, specificity, media_types|
|
|
21
23
|
# Save un-mergable rules separately
|
|
22
|
-
selector.gsub!(/:link([\s]*)+/i) { |
|
|
24
|
+
selector.gsub!(/:link([\s]*)+/i) { |_m| $1 }
|
|
23
25
|
|
|
24
26
|
# Convert element names to lower case
|
|
25
|
-
selector.gsub!(/([\s]|^)([\w]+)/) { |
|
|
27
|
+
selector.gsub!(/([\s]|^)([\w]+)/) { |_m| $1.to_s + $2.to_s.downcase }
|
|
26
28
|
|
|
27
|
-
if Premailer.
|
|
28
|
-
@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]
|
|
29
31
|
else
|
|
30
32
|
begin
|
|
31
|
-
if selector
|
|
33
|
+
if Premailer::RE_RESET_SELECTORS.match?(selector) && !!@options[:preserve_reset]
|
|
32
34
|
# this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
|
|
33
35
|
# however, this doesn't mean for testing pur
|
|
34
|
-
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration))
|
|
36
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selectors: selector, block: declaration))
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
# Change single ID CSS selectors into xpath so that we can match more
|
|
@@ -39,14 +41,14 @@ class Premailer
|
|
|
39
41
|
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
|
40
42
|
|
|
41
43
|
doc.search(selector).each do |el|
|
|
42
|
-
if el.elem?
|
|
44
|
+
if el.elem? && ((el.name != 'head') && (el.parent.name != 'head'))
|
|
43
45
|
# Add a style attribute or append to the existing one
|
|
44
46
|
block = "[SPEC=#{specificity}[#{declaration}]]"
|
|
45
47
|
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
|
46
48
|
end
|
|
47
49
|
end
|
|
48
50
|
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
|
|
49
|
-
|
|
51
|
+
warn "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
|
50
52
|
next
|
|
51
53
|
end
|
|
52
54
|
end
|
|
@@ -60,13 +62,11 @@ class Premailer
|
|
|
60
62
|
style = el.attributes['style'].to_s
|
|
61
63
|
|
|
62
64
|
declarations = []
|
|
63
|
-
style.scan(/\[SPEC
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
raise e if @options[:rule_set_exceptions]
|
|
69
|
-
end
|
|
65
|
+
style.scan(/\[SPEC=([\d]+)\[(.[^\]]*)\]\]/m).each do |declaration|
|
|
66
|
+
rs = CssParser::RuleSet.new(block: declaration[1].to_s, specificity: declaration[0].to_i)
|
|
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
|
|
@@ -78,9 +78,9 @@ class Premailer
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
# Duplicate CSS attributes as HTML attributes
|
|
81
|
-
if Premailer::RELATED_ATTRIBUTES.
|
|
81
|
+
if Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
82
82
|
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_attr, html_attr|
|
|
83
|
-
if el[html_attr].nil?
|
|
83
|
+
if el[html_attr].nil? && !merged[css_attr].empty?
|
|
84
84
|
new_val = merged[css_attr].dup
|
|
85
85
|
|
|
86
86
|
# Remove url() function wrapper
|
|
@@ -90,7 +90,7 @@ class Premailer
|
|
|
90
90
|
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
91
91
|
|
|
92
92
|
# For width and height tags, remove px units
|
|
93
|
-
new_val.gsub!(/(\d+)px/, '\1') if
|
|
93
|
+
new_val.gsub!(/(\d+)px/, '\1') if WIDTH_AND_HEIGHT.include?(html_attr)
|
|
94
94
|
|
|
95
95
|
# For color-related tags, convert RGB to hex if specified by options
|
|
96
96
|
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
@@ -99,7 +99,7 @@ class Premailer
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
unless @options[:preserve_style_attribute]
|
|
102
|
-
merged.instance_variable_get(
|
|
102
|
+
merged.instance_variable_get(:@declarations).tap do |declarations|
|
|
103
103
|
declarations.delete(css_attr)
|
|
104
104
|
end
|
|
105
105
|
end
|
|
@@ -115,9 +115,9 @@ class Premailer
|
|
|
115
115
|
|
|
116
116
|
doc = write_unmergable_css_rules(doc, @unmergable_rules) unless @options[:drop_unmergeable_css_rules]
|
|
117
117
|
|
|
118
|
-
if @options[:remove_classes]
|
|
118
|
+
if @options[:remove_classes] || @options[:remove_comments]
|
|
119
119
|
doc.traverse do |el|
|
|
120
|
-
if el.comment?
|
|
120
|
+
if el.comment? && @options[:remove_comments]
|
|
121
121
|
el.remove
|
|
122
122
|
elsif el.element?
|
|
123
123
|
el.remove_attribute('class') if @options[:remove_classes]
|
|
@@ -129,7 +129,7 @@ class Premailer
|
|
|
129
129
|
# find all anchor's targets and hash them
|
|
130
130
|
targets = []
|
|
131
131
|
doc.search("a[@href^='#']").each do |el|
|
|
132
|
-
target = el.get_attribute('href')[1
|
|
132
|
+
target = el.get_attribute('href')[1..]
|
|
133
133
|
targets << target
|
|
134
134
|
el.set_attribute('href', "#" + Digest::SHA256.hexdigest(target))
|
|
135
135
|
end
|
|
@@ -151,7 +151,7 @@ class Premailer
|
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
@processed_doc = doc
|
|
154
|
-
if
|
|
154
|
+
if xhtml?
|
|
155
155
|
# we don't want to encode carriage returns
|
|
156
156
|
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
|
|
157
157
|
else
|
|
@@ -175,15 +175,14 @@ class Premailer
|
|
|
175
175
|
else
|
|
176
176
|
style_tag = doc.create_element "style", styles
|
|
177
177
|
head = doc.at_css('head')
|
|
178
|
-
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element
|
|
179
|
-
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"))
|
|
180
180
|
head << style_tag
|
|
181
181
|
end
|
|
182
182
|
end
|
|
183
183
|
doc
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
|
|
187
186
|
# Converts the HTML document to a format suitable for plain-text e-mail.
|
|
188
187
|
#
|
|
189
188
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
|
@@ -193,17 +192,17 @@ class Premailer
|
|
|
193
192
|
html_src = ''
|
|
194
193
|
begin
|
|
195
194
|
html_src = @doc.at("body").inner_html
|
|
196
|
-
rescue
|
|
195
|
+
rescue StandardError
|
|
197
196
|
end
|
|
198
197
|
|
|
199
|
-
html_src = @doc.to_html unless html_src
|
|
198
|
+
html_src = @doc.to_html unless html_src && !html_src.empty?
|
|
200
199
|
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
|
201
200
|
end
|
|
202
201
|
|
|
203
202
|
# Gets the original HTML as a string.
|
|
204
203
|
# @return [String] HTML.
|
|
205
204
|
def to_s
|
|
206
|
-
if
|
|
205
|
+
if xhtml?
|
|
207
206
|
@doc.to_xhtml(:encoding => nil)
|
|
208
207
|
else
|
|
209
208
|
@doc.to_html(:encoding => nil)
|
|
@@ -217,13 +216,13 @@ class Premailer
|
|
|
217
216
|
thing = nil
|
|
218
217
|
|
|
219
218
|
# TODO: duplicate options
|
|
220
|
-
if @options[:with_html_string]
|
|
219
|
+
if @options[:with_html_string] || @options[:inline] || input.respond_to?(:read)
|
|
221
220
|
thing = input
|
|
222
221
|
elsif @is_local_file
|
|
223
222
|
@base_dir = File.dirname(input)
|
|
224
223
|
thing = File.open(input, 'r')
|
|
225
224
|
else
|
|
226
|
-
thing = URI.
|
|
225
|
+
thing = URI.parse(input).open
|
|
227
226
|
end
|
|
228
227
|
|
|
229
228
|
if thing.respond_to?(:read)
|
|
@@ -234,7 +233,7 @@ class Premailer
|
|
|
234
233
|
doc = nil
|
|
235
234
|
|
|
236
235
|
# Handle HTML entities
|
|
237
|
-
if @options[:replace_html_entities] == true
|
|
236
|
+
if (@options[:replace_html_entities] == true) && thing.is_a?(String)
|
|
238
237
|
HTML_ENTITIES.map do |entity, replacement|
|
|
239
238
|
thing.gsub! entity, replacement
|
|
240
239
|
end
|
|
@@ -248,15 +247,14 @@ class Premailer
|
|
|
248
247
|
# Fix for removing any CDATA tags from both style and script tags inserted per
|
|
249
248
|
# https://github.com/sparklemotion/nokogiri/issues/311 and
|
|
250
249
|
# https://github.com/premailer/premailer/issues/199
|
|
251
|
-
|
|
250
|
+
['style', 'script'].each do |tag|
|
|
252
251
|
doc.search(tag).children.each do |child|
|
|
253
|
-
child.swap(child.text
|
|
252
|
+
child.swap(child.text) if child.cdata?
|
|
254
253
|
end
|
|
255
254
|
end
|
|
256
255
|
|
|
257
256
|
doc
|
|
258
257
|
end
|
|
259
|
-
|
|
260
258
|
end
|
|
261
259
|
end
|
|
262
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,7 +7,7 @@ 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
13
|
\(\s* # literal open, with optional whitespace
|
|
@@ -15,7 +16,7 @@ module AdapterHelper
|
|
|
15
16
|
(\d{1,3}) # capture 1-3 digits
|
|
16
17
|
(?:\s*,\s*|\s+) # comma or whitespacee
|
|
17
18
|
(\d{1,3}) # capture 1-3 digits
|
|
18
|
-
\s*(
|
|
19
|
+
\s*(?:/\s*\d*\.?\d*%?)? # optional alpha modifier
|
|
19
20
|
\s*\) # literal close, with optional whitespace
|
|
20
21
|
}x
|
|
21
22
|
|
|
@@ -23,7 +24,7 @@ module AdapterHelper
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def ensure_hex(color)
|
|
26
|
-
match_data =
|
|
27
|
+
match_data = rgb?(color)
|
|
27
28
|
if match_data
|
|
28
29
|
"#{to_hex(match_data[1])}#{to_hex(match_data[2])}#{to_hex(match_data[3])}"
|
|
29
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
|
data/lib/premailer/executor.rb
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'optparse'
|
|
2
3
|
require 'premailer'
|
|
3
4
|
|
|
4
5
|
# defaults
|
|
5
6
|
options = {
|
|
6
|
-
:base_url
|
|
7
|
+
:base_url => nil,
|
|
7
8
|
:link_query_string => nil,
|
|
8
|
-
:remove_classes
|
|
9
|
-
:verbose
|
|
10
|
-
:line_length
|
|
11
|
-
:adapter
|
|
9
|
+
:remove_classes => false,
|
|
10
|
+
:verbose => false,
|
|
11
|
+
:line_length => 65,
|
|
12
|
+
:adapter => :nokogiri
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
mode = :html
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
parser = OptionParser.new do |opts|
|
|
17
18
|
opts.banner = "Improve the rendering of HTML emails by making CSS inline among other things. Takes a path to a local file, a URL or a pipe as input.\n\n"
|
|
18
19
|
opts.define_head "Usage: premailer <optional uri|optional path> [options]"
|
|
19
20
|
opts.separator ""
|
|
@@ -53,11 +54,11 @@ opts = OptionParser.new do |opts|
|
|
|
53
54
|
options[:remove_scripts] = true
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
opts.on("-l", "--line-length N", Integer, "Line length for plaintext (default: #{options[:line_length]
|
|
57
|
+
opts.on("-l", "--line-length N", Integer, "Line length for plaintext (default: #{options[:line_length]})") do |v|
|
|
57
58
|
options[:line_length] = v
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
opts.on("-e", "--entities", "Output HTML entities instead of UTF-8 when using Nokogiri") do |
|
|
61
|
+
opts.on("-e", "--entities", "Output HTML entities instead of UTF-8 when using Nokogiri") do |_v|
|
|
61
62
|
options[:output_encoding] = "US-ASCII"
|
|
62
63
|
end
|
|
63
64
|
|
|
@@ -79,9 +80,9 @@ opts = OptionParser.new do |opts|
|
|
|
79
80
|
exit
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
|
-
|
|
83
|
+
parser.parse!
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
warn "Processing in #{mode} mode with options #{options.inspect}" if options[:verbose]
|
|
85
86
|
|
|
86
87
|
premailer = nil
|
|
87
88
|
input = nil
|
|
@@ -98,7 +99,7 @@ end
|
|
|
98
99
|
if input
|
|
99
100
|
premailer = Premailer.new(input, options)
|
|
100
101
|
else
|
|
101
|
-
puts
|
|
102
|
+
puts parser
|
|
102
103
|
exit 1
|
|
103
104
|
end
|
|
104
105
|
|