premailer 1.29.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa4f38aaadca592e45c38537b54ad6e88bd15c0c43c07efed0beb25af8194766
4
- data.tar.gz: cde54fad4ac9397bd09b88509aa2b5a22daaa1721172a7c98e0e78c94e8707ad
3
+ metadata.gz: 9d0463a94da370db30e1e80fa7851c90f9b24743518137a2d04b2c21ed7246cd
4
+ data.tar.gz: 73bf8f6e3f7cade9389ff65a6fef58112e8cf2a756f0271adc8b3278bd998a5b
5
5
  SHA512:
6
- metadata.gz: bc0358601f237a6ec2cf74764dfb28e5d800cf1ca486d88ca44a7e4d3913ade8ff00b76b98017fa6f565f5812bd6f9c3e66be819708a2764a70fca051458f09f
7
- data.tar.gz: 9ea81979f54bd5fd9d8edb2e390e5900d53b7462064bc1465999dba639218db5a479c74e93e6e179d6d6a51abbf066685a21b033d5003899ab037a021db385f7
6
+ metadata.gz: 3d98924c3ff4088566f7952d8b56c2ae8747fc99e6ba3f286dcce5e9dd788b3b5c80d1faf60c1a8ff3bb77dc4be136477b1f862cd00b4f1f55b100c9d6ae1289
7
+ data.tar.gz: aff5c151b261c9f35eb58a9a87407cb5c6ee67804ae2ec9585ad4b59043eb6132e443223ad650565dfda7a9fdfe8c528315b5946c5b2d4ea1281ecb8e65bb373
@@ -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
- el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
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
- # Add a style attribute or append to the existing one
48
- block = "[SPEC=#{specificity}[#{declaration}]]"
49
- el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
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
- # Read STYLE attributes and perform folding
63
- doc.search("*[@style]").each do |el|
64
- style = el.attributes['style'].to_s
65
-
66
- declarations = style.scan(/\[SPEC=([\d]+)\[(.[^\]]*)\]\]/m).filter_map do |declaration|
67
- rs = Premailer::CachedRuleSet.new(block: declaration[1].to_s, specificity: declaration[0].to_i)
68
- rs.expand_shorthand!
69
- rs
70
- rescue ArgumentError => e
71
- raise e if @options[:rule_set_exceptions]
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
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  class Premailer
3
3
  # Premailer version.
4
- VERSION = '1.29.0'
4
+ VERSION = '1.30.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: premailer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.29.0
4
+ version: 1.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Dunae