premailer 1.26.0 → 1.28.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 +69 -0
- data/lib/premailer/adapter/nokogiri.rb +25 -27
- data/lib/premailer/adapter/nokogiri_fast.rb +35 -39
- data/lib/premailer/adapter/nokogumbo.rb +27 -31
- data/lib/premailer/adapter/rgb_to_hex.rb +3 -3
- data/lib/premailer/adapter.rb +7 -11
- data/lib/premailer/executor.rb +8 -8
- data/lib/premailer/html_to_plain_text.rb +16 -14
- data/lib/premailer/premailer.rb +142 -110
- data/lib/premailer/version.rb +1 -1
- metadata +12 -147
|
@@ -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.
|
|
29
|
+
if Premailer.media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
|
|
30
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 Premailer::RE_RESET_SELECTORS.match?(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(selectors: selector, block: 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,14 +41,14 @@ 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
|
|
@@ -62,13 +62,11 @@ class Premailer
|
|
|
62
62
|
style = el.attributes['style'].to_s
|
|
63
63
|
|
|
64
64
|
declarations = []
|
|
65
|
-
style.scan(/\[SPEC
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
raise e if @options[:rule_set_exceptions]
|
|
71
|
-
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]
|
|
72
70
|
end
|
|
73
71
|
|
|
74
72
|
# Perform style folding
|
|
@@ -80,9 +78,9 @@ class Premailer
|
|
|
80
78
|
end
|
|
81
79
|
|
|
82
80
|
# Duplicate CSS attributes as HTML attributes
|
|
83
|
-
if Premailer::RELATED_ATTRIBUTES.
|
|
81
|
+
if Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
84
82
|
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_attr, html_attr|
|
|
85
|
-
if el[html_attr].nil?
|
|
83
|
+
if el[html_attr].nil? && !merged[css_attr].empty?
|
|
86
84
|
new_val = merged[css_attr].dup
|
|
87
85
|
|
|
88
86
|
# Remove url() function wrapper
|
|
@@ -117,9 +115,9 @@ class Premailer
|
|
|
117
115
|
|
|
118
116
|
doc = write_unmergable_css_rules(doc, @unmergable_rules) unless @options[:drop_unmergeable_css_rules]
|
|
119
117
|
|
|
120
|
-
if @options[:remove_classes]
|
|
118
|
+
if @options[:remove_classes] || @options[:remove_comments]
|
|
121
119
|
doc.traverse do |el|
|
|
122
|
-
if el.comment?
|
|
120
|
+
if el.comment? && @options[:remove_comments]
|
|
123
121
|
el.remove
|
|
124
122
|
elsif el.element?
|
|
125
123
|
el.remove_attribute('class') if @options[:remove_classes]
|
|
@@ -131,7 +129,7 @@ class Premailer
|
|
|
131
129
|
# find all anchor's targets and hash them
|
|
132
130
|
targets = []
|
|
133
131
|
doc.search("a[@href^='#']").each do |el|
|
|
134
|
-
target = el.get_attribute('href')[1
|
|
132
|
+
target = el.get_attribute('href')[1..]
|
|
135
133
|
targets << target
|
|
136
134
|
el.set_attribute('href', "#" + Digest::SHA256.hexdigest(target))
|
|
137
135
|
end
|
|
@@ -153,7 +151,7 @@ class Premailer
|
|
|
153
151
|
end
|
|
154
152
|
|
|
155
153
|
@processed_doc = doc
|
|
156
|
-
if
|
|
154
|
+
if xhtml?
|
|
157
155
|
# we don't want to encode carriage returns
|
|
158
156
|
@processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
|
|
159
157
|
else
|
|
@@ -177,15 +175,14 @@ class Premailer
|
|
|
177
175
|
else
|
|
178
176
|
style_tag = doc.create_element "style", styles
|
|
179
177
|
head = doc.at_css('head')
|
|
180
|
-
head ||= doc.root.first_element_child.add_previous_sibling(doc.create_element
|
|
181
|
-
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"))
|
|
182
180
|
head << style_tag
|
|
183
181
|
end
|
|
184
182
|
end
|
|
185
183
|
doc
|
|
186
184
|
end
|
|
187
185
|
|
|
188
|
-
|
|
189
186
|
# Converts the HTML document to a format suitable for plain-text e-mail.
|
|
190
187
|
#
|
|
191
188
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
|
@@ -195,17 +192,17 @@ class Premailer
|
|
|
195
192
|
html_src = ''
|
|
196
193
|
begin
|
|
197
194
|
html_src = @doc.at("body").inner_html
|
|
198
|
-
rescue
|
|
195
|
+
rescue StandardError
|
|
199
196
|
end
|
|
200
197
|
|
|
201
|
-
html_src = @doc.to_html unless html_src
|
|
198
|
+
html_src = @doc.to_html unless html_src && !html_src.empty?
|
|
202
199
|
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
|
203
200
|
end
|
|
204
201
|
|
|
205
202
|
# Gets the original HTML as a string.
|
|
206
203
|
# @return [String] HTML.
|
|
207
204
|
def to_s
|
|
208
|
-
if
|
|
205
|
+
if xhtml?
|
|
209
206
|
@doc.to_xhtml(:encoding => nil)
|
|
210
207
|
else
|
|
211
208
|
@doc.to_html(:encoding => nil)
|
|
@@ -219,13 +216,13 @@ class Premailer
|
|
|
219
216
|
thing = nil
|
|
220
217
|
|
|
221
218
|
# TODO: duplicate options
|
|
222
|
-
if @options[:with_html_string]
|
|
219
|
+
if @options[:with_html_string] || @options[:inline] || input.respond_to?(:read)
|
|
223
220
|
thing = input
|
|
224
221
|
elsif @is_local_file
|
|
225
222
|
@base_dir = File.dirname(input)
|
|
226
223
|
thing = File.open(input, 'r')
|
|
227
224
|
else
|
|
228
|
-
thing = URI.
|
|
225
|
+
thing = URI.parse(input).open
|
|
229
226
|
end
|
|
230
227
|
|
|
231
228
|
if thing.respond_to?(:read)
|
|
@@ -236,7 +233,7 @@ class Premailer
|
|
|
236
233
|
doc = nil
|
|
237
234
|
|
|
238
235
|
# Handle HTML entities
|
|
239
|
-
if @options[:replace_html_entities] == true
|
|
236
|
+
if (@options[:replace_html_entities] == true) && thing.is_a?(String)
|
|
240
237
|
HTML_ENTITIES.map do |entity, replacement|
|
|
241
238
|
thing.gsub! entity, replacement
|
|
242
239
|
end
|
|
@@ -252,13 +249,12 @@ class Premailer
|
|
|
252
249
|
# https://github.com/premailer/premailer/issues/199
|
|
253
250
|
['style', 'script'].each do |tag|
|
|
254
251
|
doc.search(tag).children.each do |child|
|
|
255
|
-
child.swap(child.text
|
|
252
|
+
child.swap(child.text) if child.cdata?
|
|
256
253
|
end
|
|
257
254
|
end
|
|
258
255
|
|
|
259
256
|
doc
|
|
260
257
|
end
|
|
261
|
-
|
|
262
258
|
end
|
|
263
259
|
end
|
|
264
260
|
end
|
|
@@ -7,7 +7,7 @@ module AdapterHelper
|
|
|
7
7
|
str.to_i.to_s(16).rjust(2, '0').upcase
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def rgb?(color)
|
|
11
11
|
pattern = %r{
|
|
12
12
|
rgb
|
|
13
13
|
\(\s* # literal open, with optional whitespace
|
|
@@ -16,7 +16,7 @@ module AdapterHelper
|
|
|
16
16
|
(\d{1,3}) # capture 1-3 digits
|
|
17
17
|
(?:\s*,\s*|\s+) # comma or whitespacee
|
|
18
18
|
(\d{1,3}) # capture 1-3 digits
|
|
19
|
-
\s*(
|
|
19
|
+
\s*(?:/\s*\d*\.?\d*%?)? # optional alpha modifier
|
|
20
20
|
\s*\) # literal close, with optional whitespace
|
|
21
21
|
}x
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@ module AdapterHelper
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def ensure_hex(color)
|
|
27
|
-
match_data =
|
|
27
|
+
match_data = rgb?(color)
|
|
28
28
|
if match_data
|
|
29
29
|
"#{to_hex(match_data[1])}#{to_hex(match_data[2])}#{to_hex(match_data[3])}"
|
|
30
30
|
else
|
data/lib/premailer/adapter.rb
CHANGED
|
@@ -6,7 +6,6 @@ class Premailer
|
|
|
6
6
|
# * nokogiri_fast
|
|
7
7
|
# * nokogumbo
|
|
8
8
|
module Adapter
|
|
9
|
-
|
|
10
9
|
autoload :Nokogiri, 'premailer/adapter/nokogiri'
|
|
11
10
|
autoload :NokogiriFast, 'premailer/adapter/nokogiri_fast'
|
|
12
11
|
autoload :Nokogumbo, 'premailer/adapter/nokogumbo'
|
|
@@ -16,12 +15,12 @@ class Premailer
|
|
|
16
15
|
["nokogiri", :nokogiri],
|
|
17
16
|
["nokogiri", :nokogiri_fast],
|
|
18
17
|
["nokogumbo", :nokogumbo]
|
|
19
|
-
]
|
|
18
|
+
].freeze
|
|
20
19
|
|
|
21
20
|
# Returns the adapter to use.
|
|
22
21
|
def self.use
|
|
23
22
|
return @use if @use
|
|
24
|
-
self.use =
|
|
23
|
+
self.use = default
|
|
25
24
|
@use
|
|
26
25
|
end
|
|
27
26
|
|
|
@@ -35,12 +34,10 @@ class Premailer
|
|
|
35
34
|
return :nokogumbo if defined?(::Nokogumbo)
|
|
36
35
|
|
|
37
36
|
REQUIREMENT_MAP.each do |(library, adapter)|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
next
|
|
43
|
-
end
|
|
37
|
+
require library
|
|
38
|
+
return adapter
|
|
39
|
+
rescue LoadError
|
|
40
|
+
next
|
|
44
41
|
end
|
|
45
42
|
|
|
46
43
|
raise "No suitable adapter for Premailer was found, please install nokogiri or nokogumbo"
|
|
@@ -57,10 +54,9 @@ class Premailer
|
|
|
57
54
|
def self.find(adapter)
|
|
58
55
|
return adapter if adapter.is_a?(Module)
|
|
59
56
|
|
|
60
|
-
Premailer::Adapter.const_get(
|
|
57
|
+
Premailer::Adapter.const_get(adapter.to_s.split('_').map(&:capitalize).join.to_s)
|
|
61
58
|
rescue NameError
|
|
62
59
|
raise ArgumentError, "Invalid adapter: #{adapter}"
|
|
63
60
|
end
|
|
64
|
-
|
|
65
61
|
end
|
|
66
62
|
end
|
data/lib/premailer/executor.rb
CHANGED
|
@@ -4,12 +4,12 @@ require 'premailer'
|
|
|
4
4
|
|
|
5
5
|
# defaults
|
|
6
6
|
options = {
|
|
7
|
-
:base_url
|
|
7
|
+
:base_url => nil,
|
|
8
8
|
:link_query_string => nil,
|
|
9
|
-
:remove_classes
|
|
10
|
-
:verbose
|
|
11
|
-
:line_length
|
|
12
|
-
:adapter
|
|
9
|
+
:remove_classes => false,
|
|
10
|
+
:verbose => false,
|
|
11
|
+
:line_length => 65,
|
|
12
|
+
:adapter => :nokogiri
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
mode = :html
|
|
@@ -54,11 +54,11 @@ parser = OptionParser.new do |opts|
|
|
|
54
54
|
options[:remove_scripts] = true
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
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|
|
|
58
58
|
options[:line_length] = v
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
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|
|
|
62
62
|
options[:output_encoding] = "US-ASCII"
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -82,7 +82,7 @@ parser = OptionParser.new do |opts|
|
|
|
82
82
|
end
|
|
83
83
|
parser.parse!
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
warn "Processing in #{mode} mode with options #{options.inspect}" if options[:verbose]
|
|
86
86
|
|
|
87
87
|
premailer = nil
|
|
88
88
|
input = nil
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
require 'htmlentities'
|
|
4
3
|
|
|
5
4
|
# Support functions for Premailer
|
|
6
5
|
module HtmlToPlainText
|
|
7
|
-
|
|
8
6
|
# Returns the text in UTF-8 format with all HTML tags removed
|
|
9
7
|
#
|
|
10
8
|
# HTML content can be omitted from the output by surrounding it in the following comments:
|
|
@@ -60,7 +58,7 @@ module HtmlToPlainText
|
|
|
60
58
|
|
|
61
59
|
# handle headings (H1-H6)
|
|
62
60
|
txt.gsub!(/(<\/h[1-6]>)/i, "\n\\1") # move closing tags to new lines
|
|
63
|
-
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|
|
|
64
62
|
hlevel = $1.to_i
|
|
65
63
|
|
|
66
64
|
htext = $2
|
|
@@ -69,17 +67,21 @@ module HtmlToPlainText
|
|
|
69
67
|
|
|
70
68
|
# determine maximum line length
|
|
71
69
|
hlength = 0
|
|
72
|
-
htext.each_line
|
|
70
|
+
htext.each_line do |l|
|
|
71
|
+
llength = l.strip.length
|
|
72
|
+
hlength = llength if llength > hlength
|
|
73
|
+
end
|
|
73
74
|
hlength = line_length if hlength > line_length
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
83
85
|
|
|
84
86
|
"\n\n" + htext + "\n\n"
|
|
85
87
|
end
|
|
@@ -118,8 +120,8 @@ module HtmlToPlainText
|
|
|
118
120
|
txt.gsub!(/[\n]{3,}/, "\n\n")
|
|
119
121
|
|
|
120
122
|
# the word messes up the parens
|
|
121
|
-
txt.gsub!(/\(([ \n])(http[^)]+)([\n ])\)/) do |
|
|
122
|
-
($1 == "\n" ? $1 : ''
|
|
123
|
+
txt.gsub!(/\(([ \n])(http[^)]+)([\n ])\)/) do |_s|
|
|
124
|
+
($1 == "\n" ? $1 : '') + '( ' + $2 + ' )' + ($3 == "\n" ? $1 : '')
|
|
123
125
|
end
|
|
124
126
|
|
|
125
127
|
txt.strip
|