premailer 1.19.0 → 1.30.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 +143 -81
- data/lib/premailer/adapter/nokogiri_fast.rb +46 -40
- data/lib/premailer/adapter/nokogumbo.rb +37 -31
- 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 +20 -17
- data/lib/premailer/premailer.rb +162 -125
- data/lib/premailer/version.rb +2 -1
- data/lib/premailer.rb +2 -0
- metadata +14 -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,19 +67,25 @@ 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.
|
|
86
|
+
if Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
80
87
|
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_attr, html_attr|
|
|
81
|
-
if el[html_attr].nil?
|
|
88
|
+
if el[html_attr].nil? && !merged[css_attr].empty?
|
|
82
89
|
new_val = merged[css_attr].dup
|
|
83
90
|
|
|
84
91
|
# Remove url() function wrapper
|
|
@@ -88,7 +95,7 @@ class Premailer
|
|
|
88
95
|
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
89
96
|
|
|
90
97
|
# For width and height tags, remove px units
|
|
91
|
-
new_val.gsub!(/(\d+)px/, '\1') if
|
|
98
|
+
new_val.gsub!(/(\d+)px/, '\1') if WIDTH_AND_HEIGHT.include?(html_attr)
|
|
92
99
|
|
|
93
100
|
# For color-related tags, convert RGB to hex if specified by options
|
|
94
101
|
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
@@ -97,7 +104,7 @@ class Premailer
|
|
|
97
104
|
end
|
|
98
105
|
|
|
99
106
|
unless @options[:preserve_style_attribute]
|
|
100
|
-
merged.instance_variable_get(
|
|
107
|
+
merged.instance_variable_get(:@declarations).tap do |declarations|
|
|
101
108
|
declarations.delete(css_attr)
|
|
102
109
|
end
|
|
103
110
|
end
|
|
@@ -113,9 +120,9 @@ class Premailer
|
|
|
113
120
|
|
|
114
121
|
doc = write_unmergable_css_rules(doc, @unmergable_rules) unless @options[:drop_unmergeable_css_rules]
|
|
115
122
|
|
|
116
|
-
if @options[:remove_classes]
|
|
123
|
+
if @options[:remove_classes] || @options[:remove_comments]
|
|
117
124
|
doc.traverse do |el|
|
|
118
|
-
if el.comment?
|
|
125
|
+
if el.comment? && @options[:remove_comments]
|
|
119
126
|
el.remove
|
|
120
127
|
elsif el.element?
|
|
121
128
|
el.remove_attribute('class') if @options[:remove_classes]
|
|
@@ -127,7 +134,7 @@ class Premailer
|
|
|
127
134
|
# find all anchor's targets and hash them
|
|
128
135
|
targets = []
|
|
129
136
|
doc.search("a[@href^='#']").each do |el|
|
|
130
|
-
target = el.get_attribute('href')[1
|
|
137
|
+
target = el.get_attribute('href')[1..]
|
|
131
138
|
targets << target
|
|
132
139
|
el.set_attribute('href', "#" + Digest::SHA256.hexdigest(target))
|
|
133
140
|
end
|
|
@@ -149,7 +156,7 @@ class Premailer
|
|
|
149
156
|
end
|
|
150
157
|
|
|
151
158
|
@processed_doc = doc
|
|
152
|
-
if
|
|
159
|
+
if xhtml?
|
|
153
160
|
# we don't want to encode carriage returns
|
|
154
161
|
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
|
|
155
162
|
else
|
|
@@ -173,15 +180,14 @@ class Premailer
|
|
|
173
180
|
else
|
|
174
181
|
style_tag = doc.create_element "style", styles
|
|
175
182
|
head = doc.at_css('head')
|
|
176
|
-
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element
|
|
177
|
-
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"))
|
|
178
185
|
head << style_tag
|
|
179
186
|
end
|
|
180
187
|
end
|
|
181
188
|
doc
|
|
182
189
|
end
|
|
183
190
|
|
|
184
|
-
|
|
185
191
|
# Converts the HTML document to a format suitable for plain-text e-mail.
|
|
186
192
|
#
|
|
187
193
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
|
@@ -191,17 +197,17 @@ class Premailer
|
|
|
191
197
|
html_src = ''
|
|
192
198
|
begin
|
|
193
199
|
html_src = @doc.at("body").inner_html
|
|
194
|
-
rescue
|
|
200
|
+
rescue StandardError
|
|
195
201
|
end
|
|
196
202
|
|
|
197
|
-
html_src = @doc.to_html unless html_src
|
|
203
|
+
html_src = @doc.to_html unless html_src && !html_src.empty?
|
|
198
204
|
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
|
199
205
|
end
|
|
200
206
|
|
|
201
207
|
# Gets the original HTML as a string.
|
|
202
208
|
# @return [String] HTML.
|
|
203
209
|
def to_s
|
|
204
|
-
if
|
|
210
|
+
if xhtml?
|
|
205
211
|
@doc.to_xhtml(:encoding => nil)
|
|
206
212
|
else
|
|
207
213
|
@doc.to_html(:encoding => nil)
|
|
@@ -215,13 +221,13 @@ class Premailer
|
|
|
215
221
|
thing = nil
|
|
216
222
|
|
|
217
223
|
# TODO: duplicate options
|
|
218
|
-
if @options[:with_html_string]
|
|
224
|
+
if @options[:with_html_string] || @options[:inline] || input.respond_to?(:read)
|
|
219
225
|
thing = input
|
|
220
226
|
elsif @is_local_file
|
|
221
227
|
@base_dir = File.dirname(input)
|
|
222
228
|
thing = File.open(input, 'r')
|
|
223
229
|
else
|
|
224
|
-
thing = URI.
|
|
230
|
+
thing = URI.parse(input).open
|
|
225
231
|
end
|
|
226
232
|
|
|
227
233
|
if thing.respond_to?(:read)
|
|
@@ -232,7 +238,7 @@ class Premailer
|
|
|
232
238
|
doc = nil
|
|
233
239
|
|
|
234
240
|
# Handle HTML entities
|
|
235
|
-
if @options[:replace_html_entities] == true
|
|
241
|
+
if (@options[:replace_html_entities] == true) && thing.is_a?(String)
|
|
236
242
|
HTML_ENTITIES.map do |entity, replacement|
|
|
237
243
|
thing.gsub! entity, replacement
|
|
238
244
|
end
|
|
@@ -241,15 +247,15 @@ class Premailer
|
|
|
241
247
|
doc = if @options[:html_fragment]
|
|
242
248
|
::Nokogiri::HTML.fragment(thing, encoding)
|
|
243
249
|
else
|
|
244
|
-
::Nokogiri::HTML(thing, nil, encoding
|
|
250
|
+
::Nokogiri::HTML(thing, nil, encoding, &:recover)
|
|
245
251
|
end
|
|
246
252
|
|
|
247
253
|
# Fix for removing any CDATA tags from both style and script tags inserted per
|
|
248
254
|
# https://github.com/sparklemotion/nokogiri/issues/311 and
|
|
249
255
|
# https://github.com/premailer/premailer/issues/199
|
|
250
|
-
|
|
256
|
+
['style', 'script'].each do |tag|
|
|
251
257
|
doc.search(tag).children.each do |child|
|
|
252
|
-
child.swap(child.text
|
|
258
|
+
child.swap(child.text) if child.cdata?
|
|
253
259
|
end
|
|
254
260
|
end
|
|
255
261
|
|
|
@@ -273,7 +279,7 @@ class Premailer
|
|
|
273
279
|
page.traverse do |node|
|
|
274
280
|
all_nodes.push(node)
|
|
275
281
|
|
|
276
|
-
if node != page
|
|
282
|
+
if node != page
|
|
277
283
|
index_ancestry(page, node, node.parent, descendants)
|
|
278
284
|
end
|
|
279
285
|
|
|
@@ -284,7 +290,7 @@ class Premailer
|
|
|
284
290
|
# Index the node by all class attributes it possesses.
|
|
285
291
|
# Classes are modestly selective. Usually more than tag names
|
|
286
292
|
# but less selective than ids.
|
|
287
|
-
if node.has_attribute?("class")
|
|
293
|
+
if node.has_attribute?("class")
|
|
288
294
|
node.get_attribute("class").split(/\s+/).each do |c|
|
|
289
295
|
c = '.' + c
|
|
290
296
|
index[c] = (index[c] || Set.new).add(node)
|
|
@@ -293,7 +299,7 @@ class Premailer
|
|
|
293
299
|
|
|
294
300
|
# Index the node by its "id" attribute if it has one.
|
|
295
301
|
# This is usually the most selective of the three.
|
|
296
|
-
if node.has_attribute?("id")
|
|
302
|
+
if node.has_attribute?("id")
|
|
297
303
|
id = '#' + node.get_attribute("id")
|
|
298
304
|
index[id] = (index[id] || Set.new).add(node)
|
|
299
305
|
end
|
|
@@ -306,7 +312,7 @@ class Premailer
|
|
|
306
312
|
index.default = Set.new
|
|
307
313
|
descendants.default = Set.new
|
|
308
314
|
|
|
309
|
-
|
|
315
|
+
[index, Set.new(all_nodes), descendants]
|
|
310
316
|
end
|
|
311
317
|
|
|
312
318
|
# @param doc The top level document
|
|
@@ -315,9 +321,9 @@ class Premailer
|
|
|
315
321
|
# @param descendants The running hash map of node -> set of nodes that maps descendants of a node.
|
|
316
322
|
# @return The descendants argument after updating it.
|
|
317
323
|
def index_ancestry(doc, elem, parent, descendants)
|
|
318
|
-
if parent
|
|
324
|
+
if parent
|
|
319
325
|
descendants[parent] = (descendants[parent] || Set.new).add(elem)
|
|
320
|
-
if doc != parent
|
|
326
|
+
if doc != parent
|
|
321
327
|
index_ancestry(doc, elem, parent.parent, descendants)
|
|
322
328
|
end
|
|
323
329
|
end
|
|
@@ -346,14 +352,14 @@ class Premailer
|
|
|
346
352
|
# It will return nil when such a selector is passed, so you can take
|
|
347
353
|
# action on the falsity of the return value.
|
|
348
354
|
def match_selector(index, all_nodes, descendants, selector)
|
|
349
|
-
if /[^-a-zA-Z0-9_\s.#]/.match(selector)
|
|
355
|
+
if /[^-a-zA-Z0-9_\s.#]/.match?(selector)
|
|
350
356
|
return nil
|
|
351
357
|
end
|
|
352
358
|
|
|
353
359
|
take_children = false
|
|
354
360
|
selector.split(/\s+/).reduce(all_nodes) do |base, spec|
|
|
355
361
|
desc = base
|
|
356
|
-
if take_children
|
|
362
|
+
if take_children
|
|
357
363
|
desc = Set.new
|
|
358
364
|
base.each do |n|
|
|
359
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,19 +62,25 @@ class Premailer
|
|
|
60
62
|
style = el.attributes['style'].to_s
|
|
61
63
|
|
|
62
64
|
declarations = []
|
|
63
|
-
style.scan(/\[SPEC
|
|
64
|
-
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)
|
|
65
67
|
declarations << rs
|
|
68
|
+
rescue ArgumentError => e
|
|
69
|
+
raise e if @options[:rule_set_exceptions]
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
# Perform style folding
|
|
69
73
|
merged = CssParser.merge(declarations)
|
|
70
|
-
|
|
74
|
+
begin
|
|
75
|
+
merged.expand_shorthand!
|
|
76
|
+
rescue ArgumentError => e
|
|
77
|
+
raise e if @options[:rule_set_exceptions]
|
|
78
|
+
end
|
|
71
79
|
|
|
72
80
|
# Duplicate CSS attributes as HTML attributes
|
|
73
|
-
if Premailer::RELATED_ATTRIBUTES.
|
|
81
|
+
if Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
74
82
|
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_attr, html_attr|
|
|
75
|
-
if el[html_attr].nil?
|
|
83
|
+
if el[html_attr].nil? && !merged[css_attr].empty?
|
|
76
84
|
new_val = merged[css_attr].dup
|
|
77
85
|
|
|
78
86
|
# Remove url() function wrapper
|
|
@@ -82,7 +90,7 @@ class Premailer
|
|
|
82
90
|
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
83
91
|
|
|
84
92
|
# For width and height tags, remove px units
|
|
85
|
-
new_val.gsub!(/(\d+)px/, '\1') if
|
|
93
|
+
new_val.gsub!(/(\d+)px/, '\1') if WIDTH_AND_HEIGHT.include?(html_attr)
|
|
86
94
|
|
|
87
95
|
# For color-related tags, convert RGB to hex if specified by options
|
|
88
96
|
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
@@ -91,7 +99,7 @@ class Premailer
|
|
|
91
99
|
end
|
|
92
100
|
|
|
93
101
|
unless @options[:preserve_style_attribute]
|
|
94
|
-
merged.instance_variable_get(
|
|
102
|
+
merged.instance_variable_get(:@declarations).tap do |declarations|
|
|
95
103
|
declarations.delete(css_attr)
|
|
96
104
|
end
|
|
97
105
|
end
|
|
@@ -107,9 +115,9 @@ class Premailer
|
|
|
107
115
|
|
|
108
116
|
doc = write_unmergable_css_rules(doc, @unmergable_rules) unless @options[:drop_unmergeable_css_rules]
|
|
109
117
|
|
|
110
|
-
if @options[:remove_classes]
|
|
118
|
+
if @options[:remove_classes] || @options[:remove_comments]
|
|
111
119
|
doc.traverse do |el|
|
|
112
|
-
if el.comment?
|
|
120
|
+
if el.comment? && @options[:remove_comments]
|
|
113
121
|
el.remove
|
|
114
122
|
elsif el.element?
|
|
115
123
|
el.remove_attribute('class') if @options[:remove_classes]
|
|
@@ -121,7 +129,7 @@ class Premailer
|
|
|
121
129
|
# find all anchor's targets and hash them
|
|
122
130
|
targets = []
|
|
123
131
|
doc.search("a[@href^='#']").each do |el|
|
|
124
|
-
target = el.get_attribute('href')[1
|
|
132
|
+
target = el.get_attribute('href')[1..]
|
|
125
133
|
targets << target
|
|
126
134
|
el.set_attribute('href', "#" + Digest::SHA256.hexdigest(target))
|
|
127
135
|
end
|
|
@@ -143,7 +151,7 @@ class Premailer
|
|
|
143
151
|
end
|
|
144
152
|
|
|
145
153
|
@processed_doc = doc
|
|
146
|
-
if
|
|
154
|
+
if xhtml?
|
|
147
155
|
# we don't want to encode carriage returns
|
|
148
156
|
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
|
|
149
157
|
else
|
|
@@ -167,15 +175,14 @@ class Premailer
|
|
|
167
175
|
else
|
|
168
176
|
style_tag = doc.create_element "style", styles
|
|
169
177
|
head = doc.at_css('head')
|
|
170
|
-
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element
|
|
171
|
-
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"))
|
|
172
180
|
head << style_tag
|
|
173
181
|
end
|
|
174
182
|
end
|
|
175
183
|
doc
|
|
176
184
|
end
|
|
177
185
|
|
|
178
|
-
|
|
179
186
|
# Converts the HTML document to a format suitable for plain-text e-mail.
|
|
180
187
|
#
|
|
181
188
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
|
@@ -185,17 +192,17 @@ class Premailer
|
|
|
185
192
|
html_src = ''
|
|
186
193
|
begin
|
|
187
194
|
html_src = @doc.at("body").inner_html
|
|
188
|
-
rescue
|
|
195
|
+
rescue StandardError
|
|
189
196
|
end
|
|
190
197
|
|
|
191
|
-
html_src = @doc.to_html unless html_src
|
|
198
|
+
html_src = @doc.to_html unless html_src && !html_src.empty?
|
|
192
199
|
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
|
193
200
|
end
|
|
194
201
|
|
|
195
202
|
# Gets the original HTML as a string.
|
|
196
203
|
# @return [String] HTML.
|
|
197
204
|
def to_s
|
|
198
|
-
if
|
|
205
|
+
if xhtml?
|
|
199
206
|
@doc.to_xhtml(:encoding => nil)
|
|
200
207
|
else
|
|
201
208
|
@doc.to_html(:encoding => nil)
|
|
@@ -209,13 +216,13 @@ class Premailer
|
|
|
209
216
|
thing = nil
|
|
210
217
|
|
|
211
218
|
# TODO: duplicate options
|
|
212
|
-
if @options[:with_html_string]
|
|
219
|
+
if @options[:with_html_string] || @options[:inline] || input.respond_to?(:read)
|
|
213
220
|
thing = input
|
|
214
221
|
elsif @is_local_file
|
|
215
222
|
@base_dir = File.dirname(input)
|
|
216
223
|
thing = File.open(input, 'r')
|
|
217
224
|
else
|
|
218
|
-
thing = URI.
|
|
225
|
+
thing = URI.parse(input).open
|
|
219
226
|
end
|
|
220
227
|
|
|
221
228
|
if thing.respond_to?(:read)
|
|
@@ -226,7 +233,7 @@ class Premailer
|
|
|
226
233
|
doc = nil
|
|
227
234
|
|
|
228
235
|
# Handle HTML entities
|
|
229
|
-
if @options[:replace_html_entities] == true
|
|
236
|
+
if (@options[:replace_html_entities] == true) && thing.is_a?(String)
|
|
230
237
|
HTML_ENTITIES.map do |entity, replacement|
|
|
231
238
|
thing.gsub! entity, replacement
|
|
232
239
|
end
|
|
@@ -240,15 +247,14 @@ class Premailer
|
|
|
240
247
|
# Fix for removing any CDATA tags from both style and script tags inserted per
|
|
241
248
|
# https://github.com/sparklemotion/nokogiri/issues/311 and
|
|
242
249
|
# https://github.com/premailer/premailer/issues/199
|
|
243
|
-
|
|
250
|
+
['style', 'script'].each do |tag|
|
|
244
251
|
doc.search(tag).children.each do |child|
|
|
245
|
-
child.swap(child.text
|
|
252
|
+
child.swap(child.text) if child.cdata?
|
|
246
253
|
end
|
|
247
254
|
end
|
|
248
255
|
|
|
249
256
|
doc
|
|
250
257
|
end
|
|
251
|
-
|
|
252
258
|
end
|
|
253
259
|
end
|
|
254
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
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
class Premailer
|
|
3
|
+
class CachedRuleSet < CssParser::RuleSet
|
|
4
|
+
# we call this early to find errors but css-parser calls it in .merge again
|
|
5
|
+
# so to prevent slowdown and bugs we refuse to run it twice on the same ruleset
|
|
6
|
+
# ideally should be upstreamed into css-parser
|
|
7
|
+
def expand_shorthand!
|
|
8
|
+
super unless @expand_shorthand
|
|
9
|
+
ensure
|
|
10
|
+
@expand_shorthand = true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
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
|
|