premailer 1.29.0 → 1.32.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 +10 -0
- data/lib/premailer/adapter/nokogiri.rb +116 -57
- data/lib/premailer/premailer.rb +5 -2
- data/lib/premailer/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2186c26ccc50f22227a6dbbf53b4cd3b5ae5f8bebdbdc190b0e7fd0e5a152565
|
|
4
|
+
data.tar.gz: 84becf9c1329983be14a427eef560806fb23ceb05847b7c5484033fe2fda0248
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c489d4a76330f66eeb60793faebc2839591870ee261179c0d9369ef95e5194d967f2b0f1aec31f16edb9355e363766fcb9245e67f13d2bb39b53cf1598ab8e36
|
|
7
|
+
data.tar.gz: 48a0cf85f5d2c2e3cbc2e7a5b21e70bf0e9e9215b2067703b9f9e5574342be25901aa639b1fcbdc3889e1d883905ad30e1e300d188fee68fd043f0de1588e2f2
|
data/README.md
CHANGED
|
@@ -127,6 +127,16 @@ Premailer.new(
|
|
|
127
127
|
|
|
128
128
|
[available options](https://premailer.github.io/premailer/Premailer.html#initialize-instance_method)
|
|
129
129
|
|
|
130
|
+
### Loading CSS from local/private hosts
|
|
131
|
+
|
|
132
|
+
css_parser 3+ blocks fetching remote CSS (`<link>`, `@import`) from hosts that resolve to loopback/private IPs
|
|
133
|
+
or do not resolve at all, to prevent SSRF.
|
|
134
|
+
Those styles are silently dropped (unless `io_exceptions: true`).
|
|
135
|
+
To allow them, when the html is trusted:
|
|
136
|
+
```ruby
|
|
137
|
+
Premailer.new(html, with_html_string: true, allow_local_network: true)
|
|
138
|
+
```
|
|
139
|
+
|
|
130
140
|
## Support for CSS variables
|
|
131
141
|
|
|
132
142
|
The gem does not automatically replace CSS variables with their static values.
|
|
@@ -15,10 +15,21 @@ class Premailer
|
|
|
15
15
|
doc = @processed_doc
|
|
16
16
|
@unmergable_rules = CssParser::Parser.new
|
|
17
17
|
|
|
18
|
+
# Accumulate matched rule sets per node in memory instead of
|
|
19
|
+
# round-tripping them through style attributes as [SPEC=n[...]]
|
|
20
|
+
# string markers (which costs two libxml2 attribute round-trips per
|
|
21
|
+
# element plus a regex re-scan, and dominates allocation churn).
|
|
22
|
+
rules_by_node = {}
|
|
23
|
+
|
|
18
24
|
# Give all styles already in style attributes a specificity of 1000
|
|
19
25
|
# per http://www.w3.org/TR/CSS21/cascade.html#specificity
|
|
26
|
+
# Identical style strings (repeated components) share one rule set so
|
|
27
|
+
# the fold cache below can hit on them.
|
|
28
|
+
rule_set_cache = {}
|
|
20
29
|
doc.search("*[@style]").each do |el|
|
|
21
|
-
|
|
30
|
+
style = el.attributes['style'].to_s
|
|
31
|
+
rs = rule_set_cache.fetch(style) { rule_set_cache[style] = build_rule_set(style, 1000) }
|
|
32
|
+
rules_by_node[el] = rs ? [rs] : []
|
|
22
33
|
end
|
|
23
34
|
# Iterate through the rules and merge them into the HTML
|
|
24
35
|
@css_parser.each_selector(:all) do |selector, declaration, specificity, media_types|
|
|
@@ -42,11 +53,25 @@ class Premailer
|
|
|
42
53
|
# than one element. Added to work around dodgy generated code.
|
|
43
54
|
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
|
44
55
|
|
|
56
|
+
rule_set = nil
|
|
57
|
+
rule_set_missing = false
|
|
45
58
|
doc.search(selector).each do |el|
|
|
46
59
|
if el.elem? && ((el.name != 'head') && (el.parent.name != 'head'))
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
# Enroll the element even when the rule below fails to build:
|
|
61
|
+
# the marker implementation wrote the style attribute before
|
|
62
|
+
# parsing, so matched elements end up with style="" when
|
|
63
|
+
# their only rule is invalid and exceptions are disabled.
|
|
64
|
+
rules = (rules_by_node[el] ||= [])
|
|
65
|
+
next if rule_set_missing
|
|
66
|
+
|
|
67
|
+
# One shared rule set per CSS rule; merge never mutates inputs
|
|
68
|
+
# (CachedRuleSet#expand_shorthand! is idempotent by design).
|
|
69
|
+
rule_set ||= build_rule_set(declaration, specificity)
|
|
70
|
+
if rule_set.nil?
|
|
71
|
+
rule_set_missing = true
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
rules << rule_set
|
|
50
75
|
end
|
|
51
76
|
end
|
|
52
77
|
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
|
|
@@ -59,60 +84,28 @@ class Premailer
|
|
|
59
84
|
# Remove script tags
|
|
60
85
|
doc.search("script").remove if @options[:remove_scripts]
|
|
61
86
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
# Perform style folding. Elements sharing the same matched rule sets
|
|
88
|
+
# (ubiquitous in table-based email markup) fold to the same result, so
|
|
89
|
+
# the merge/expand/collapse/serialize pipeline runs once per unique
|
|
90
|
+
# (element name, rule list) pair instead of once per element.
|
|
91
|
+
fold_cache = {}
|
|
92
|
+
rules_by_node.each do |el, declarations|
|
|
93
|
+
related = Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
94
|
+
# Rule sets are interned above, so default identity hashing makes
|
|
95
|
+
# them usable directly as cache key elements.
|
|
96
|
+
cache_key = [related ? el.name : nil, *declarations]
|
|
97
|
+
|
|
98
|
+
folded = fold_cache[cache_key] ||= fold_declarations(declarations, related ? el.name : nil)
|
|
99
|
+
style, attributes = folded
|
|
100
|
+
|
|
101
|
+
# Write the inline STYLE attribute first so the attribute order for
|
|
102
|
+
# elements that had no style attribute matches the marker-based
|
|
103
|
+
# implementation (style attr was created during selector matching).
|
|
104
|
+
el['style'] = style
|
|
105
|
+
|
|
106
|
+
attributes&.each do |html_attr, value|
|
|
107
|
+
el[html_attr] = value if el[html_attr].nil?
|
|
72
108
|
end
|
|
73
|
-
|
|
74
|
-
# Perform style folding
|
|
75
|
-
merged = CssParser.merge(declarations)
|
|
76
|
-
begin
|
|
77
|
-
merged.expand_shorthand!
|
|
78
|
-
rescue ArgumentError => e
|
|
79
|
-
raise e if @options[:rule_set_exceptions]
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Duplicate CSS attributes as HTML attributes
|
|
83
|
-
if Premailer::RELATED_ATTRIBUTES.key?(el.name) && @options[:css_to_attributes]
|
|
84
|
-
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_attr, html_attr|
|
|
85
|
-
if el[html_attr].nil? && !merged[css_attr].empty?
|
|
86
|
-
new_val = merged[css_attr].dup
|
|
87
|
-
|
|
88
|
-
# Remove url() function wrapper
|
|
89
|
-
new_val.gsub!(/url\((['"])(.*?)\1\)/, '\2')
|
|
90
|
-
|
|
91
|
-
# Remove !important, trailing semi-colon, and leading/trailing whitespace
|
|
92
|
-
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
93
|
-
|
|
94
|
-
# For width and height tags, remove px units
|
|
95
|
-
new_val.gsub!(/(\d+)px/, '\1') if WIDTH_AND_HIGHT.include?(html_attr)
|
|
96
|
-
|
|
97
|
-
# For color-related tags, convert RGB to hex if specified by options
|
|
98
|
-
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
99
|
-
|
|
100
|
-
el[html_attr] = new_val
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
unless @options[:preserve_style_attribute]
|
|
104
|
-
merged.instance_variable_get(:@declarations).tap do |declarations|
|
|
105
|
-
declarations.delete(css_attr)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Collapse multiple rules into one as much as possible.
|
|
112
|
-
merged.create_shorthand! if @options[:create_shorthands]
|
|
113
|
-
|
|
114
|
-
# write the inline STYLE attribute
|
|
115
|
-
el['style'] = merged.declarations_to_s
|
|
116
109
|
end
|
|
117
110
|
|
|
118
111
|
doc = write_unmergable_css_rules(doc, @unmergable_rules) unless @options[:drop_unmergeable_css_rules]
|
|
@@ -161,6 +154,72 @@ class Premailer
|
|
|
161
154
|
end
|
|
162
155
|
end
|
|
163
156
|
|
|
157
|
+
# Merge a list of rule sets into a final style string plus the HTML
|
|
158
|
+
# attribute duplications (bgcolor/align/...) for RELATED_ATTRIBUTES
|
|
159
|
+
# elements. Element-independent: the per-element "attribute already
|
|
160
|
+
# set" guard stays with the caller.
|
|
161
|
+
def fold_declarations(declarations, related_el_name) # :nodoc:
|
|
162
|
+
merged = CssParser.merge(declarations)
|
|
163
|
+
if merged.equal?(declarations[0])
|
|
164
|
+
# CssParser.merge returns its input untouched when given a single
|
|
165
|
+
# rule set; copy it before the destructive fold pipeline below so
|
|
166
|
+
# rule sets shared across elements are not corrupted.
|
|
167
|
+
merged = CssParser::RuleSet.new(block: merged.declarations_to_s, specificity: merged.specificity)
|
|
168
|
+
end
|
|
169
|
+
begin
|
|
170
|
+
merged.expand_shorthand!
|
|
171
|
+
rescue ArgumentError => e
|
|
172
|
+
raise e if @options[:rule_set_exceptions]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
attributes = nil
|
|
176
|
+
# Duplicate CSS attributes as HTML attributes
|
|
177
|
+
if related_el_name
|
|
178
|
+
Premailer::RELATED_ATTRIBUTES[related_el_name].each do |css_attr, html_attr|
|
|
179
|
+
unless merged[css_attr].empty?
|
|
180
|
+
new_val = merged[css_attr].dup
|
|
181
|
+
|
|
182
|
+
# Remove url() function wrapper
|
|
183
|
+
new_val.gsub!(/url\((['"])(.*?)\1\)/, '\2')
|
|
184
|
+
|
|
185
|
+
# Remove !important, trailing semi-colon, and leading/trailing whitespace
|
|
186
|
+
new_val.gsub!(/;$|\s*!important/, '').strip!
|
|
187
|
+
|
|
188
|
+
# For width and height tags, remove px units
|
|
189
|
+
new_val.gsub!(/(\d+)px/, '\1') if WIDTH_AND_HIGHT.include?(html_attr)
|
|
190
|
+
|
|
191
|
+
# For color-related tags, convert RGB to hex if specified by options
|
|
192
|
+
new_val = ensure_hex(new_val) if css_attr.end_with?('color') && @options[:rgb_to_hex_attributes]
|
|
193
|
+
|
|
194
|
+
(attributes ||= []) << [html_attr, new_val]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
unless @options[:preserve_style_attribute]
|
|
198
|
+
merged.instance_variable_get(:@declarations).tap do |declarations|
|
|
199
|
+
declarations.delete(css_attr)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Collapse multiple rules into one as much as possible.
|
|
206
|
+
merged.create_shorthand! if @options[:create_shorthands]
|
|
207
|
+
|
|
208
|
+
# write the inline STYLE attribute
|
|
209
|
+
[merged.declarations_to_s, attributes]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Build an expanded rule set for folding. Returns nil (and optionally
|
|
213
|
+
# swallows the error, mirroring the old fold-time rescue) on bad CSS.
|
|
214
|
+
def build_rule_set(block, specificity) # :nodoc:
|
|
215
|
+
rs = Premailer::CachedRuleSet.new(block: block, specificity: specificity)
|
|
216
|
+
rs.expand_shorthand!
|
|
217
|
+
rs
|
|
218
|
+
rescue ArgumentError => e
|
|
219
|
+
raise e if @options[:rule_set_exceptions]
|
|
220
|
+
nil
|
|
221
|
+
end
|
|
222
|
+
|
|
164
223
|
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
|
165
224
|
# and write it into the <tt>head</tt>.
|
|
166
225
|
#
|
data/lib/premailer/premailer.rb
CHANGED
|
@@ -171,6 +171,7 @@ class Premailer
|
|
|
171
171
|
# @option options [Boolean] :verbose Whether to print errors and warnings to <tt>$stderr</tt>. Default is false.
|
|
172
172
|
# @option options [Boolean] :io_exceptions Throws exceptions on I/O errors. Default is false.
|
|
173
173
|
# @option options [Boolean] :rule_set_exceptions Throws exceptions on invalid values in CSS Parser rule sets. Default is true.
|
|
174
|
+
# @option options [Boolean] :allow_local_network Allow css_parser to fetch remote CSS from local/private network hosts (disables its SSRF protection). Default is false.
|
|
174
175
|
# @option options [Boolean] :include_link_tags Whether to include css from <tt>link rel=stylesheet</tt> tags. Default is true.
|
|
175
176
|
# @option options [Boolean] :include_style_tags Whether to include css from <tt>style</tt> tags. Default is true.
|
|
176
177
|
# @option options [String] :input_encoding Manually specify the source documents encoding. This is a good idea. Default is ASCII-8BIT.
|
|
@@ -204,6 +205,7 @@ class Premailer
|
|
|
204
205
|
:debug => false,
|
|
205
206
|
:io_exceptions => false,
|
|
206
207
|
:rule_set_exceptions => true,
|
|
208
|
+
:allow_local_network => false,
|
|
207
209
|
:include_link_tags => true,
|
|
208
210
|
:include_style_tags => true,
|
|
209
211
|
:input_encoding => 'ASCII-8BIT',
|
|
@@ -239,7 +241,8 @@ class Premailer
|
|
|
239
241
|
:absolute_paths => true,
|
|
240
242
|
:import => true,
|
|
241
243
|
:io_exceptions => @options[:io_exceptions],
|
|
242
|
-
:rule_set_exceptions => @options[:rule_set_exceptions]
|
|
244
|
+
:rule_set_exceptions => @options[:rule_set_exceptions],
|
|
245
|
+
:allow_local_network => @options[:allow_local_network]
|
|
243
246
|
}
|
|
244
247
|
)
|
|
245
248
|
|
|
@@ -396,7 +399,7 @@ class Premailer
|
|
|
396
399
|
|
|
397
400
|
warn "Attempting to append_query_string: #{queries}" if @options[:verbose]
|
|
398
401
|
|
|
399
|
-
doc.search('a').each do |el|
|
|
402
|
+
doc.search('a, area').each do |el|
|
|
400
403
|
href = el.attributes['href'].to_s.strip
|
|
401
404
|
next if href.nil? || href.empty?
|
|
402
405
|
|
data/lib/premailer/version.rb
CHANGED