premailer 1.8.7 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ceccb9e67d075de22a76d50df38d99481a6811bf
4
- data.tar.gz: 475cc5974d20a0b8b69bef467d10de3edff7d821
3
+ metadata.gz: 2e8e7d38035296170890e416aa7b33bb1aef052b
4
+ data.tar.gz: 4ee232c94b2585448385d2e8e8b8884380ada987
5
5
  SHA512:
6
- metadata.gz: 2591ab12195ae73b9909b945131cdf06382917afde4e4270dc496b937eac87c98cea081c825209a0e4cfd40f8b2202f1953f980d8203cf90afcadec80b139c17
7
- data.tar.gz: e34eda9604977da572ef0f712c710547e9e7d965370404a2b1a233a2dd04760a7461c70acf4b40008b72824fe2a38f4cb18bd36d49da794bd61bd41a09096721
6
+ metadata.gz: a6a5daedd7752a35cf677cbf62d58697809cc496f44372247f7a080395a13756e9a9a1e633c44ba44faeff55f83c118454cb146c7860108cd5d792dda0d82ab2
7
+ data.tar.gz: d4d53382db2412c1c8e4e022adc114828fd46e79c8fd6f8903089c34a2ffed0b8799252792f0fdf1eca3e0d7844bd6bffa8feccc8bf5b6e6294aec9b9ccda0c1
data/README.md CHANGED
@@ -36,16 +36,17 @@ require 'premailer'
36
36
 
37
37
  premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)
38
38
 
39
- # Write the HTML output
40
- File.open("output.html", "w") do |fout|
41
- fout.puts premailer.to_inline_css
42
- end
43
-
44
39
  # Write the plain-text output
40
+ # This must come before to_inline_css (https://github.com/premailer/premailer/issues/201)
45
41
  File.open("output.txt", "w") do |fout|
46
42
  fout.puts premailer.to_plain_text
47
43
  end
48
44
 
45
+ # Write the HTML output
46
+ File.open("output.html", "w") do |fout|
47
+ fout.puts premailer.to_inline_css
48
+ end
49
+
49
50
  # Output any CSS warnings
50
51
  premailer.warnings.each do |w|
51
52
  puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
@@ -4,17 +4,20 @@ class Premailer
4
4
  # Manages the adapter classes. Currently supports:
5
5
  #
6
6
  # * nokogiri
7
+ # * nokogiri_fast
7
8
  # * nokogumbo
8
9
  # * hpricot
9
10
  module Adapter
10
11
 
11
12
  autoload :Hpricot, 'premailer/adapter/hpricot'
12
13
  autoload :Nokogiri, 'premailer/adapter/nokogiri'
14
+ autoload :NokogiriFast, 'premailer/adapter/nokogiri_fast'
13
15
  autoload :Nokogumbo, 'premailer/adapter/nokogumbo'
14
16
 
15
17
  # adapter to required file mapping.
16
18
  REQUIREMENT_MAP = [
17
19
  ["nokogiri", :nokogiri],
20
+ ["nokogiri", :nokogiri_fast],
18
21
  ["nokogumbo", :nokogumbo],
19
22
  ["hpricot", :hpricot],
20
23
  ]
@@ -32,6 +35,7 @@ class Premailer
32
35
  # @raise [RuntimeError] unless suitable adapter found.
33
36
  def self.default
34
37
  return :nokogiri if defined?(::Nokogiri)
38
+ return :nokogiri_fast if defined?(::NokogiriFast)
35
39
  return :nokogumbo if defined?(::Nokogumbo)
36
40
  return :hpricot if defined?(::Hpricot)
37
41
 
@@ -0,0 +1,354 @@
1
+ require 'nokogiri'
2
+
3
+ class Premailer
4
+ module Adapter
5
+ # NokogiriFast adapter
6
+ module NokogiriFast
7
+
8
+ # Merge CSS into the HTML document.
9
+ #
10
+ # @return [String] an HTML.
11
+ def to_inline_css
12
+ doc = @processed_doc
13
+ @unmergable_rules = CssParser::Parser.new
14
+
15
+ # Give all styles already in style attributes a specificity of 1000
16
+ # per http://www.w3.org/TR/CSS21/cascade.html#specificity
17
+ doc.search("*[@style]").each do |el|
18
+ el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
19
+ end
20
+
21
+ # Create an index for nodes by tag name/id/class
22
+ # Also precompute the map of nodes to descendants
23
+ index, all_nodes, descendants = make_index(doc)
24
+
25
+ # Iterate through the rules and merge them into the HTML
26
+ @css_parser.each_selector(:all) do |selector, declaration, specificity, media_types|
27
+
28
+ # Save un-mergable rules separately
29
+ selector.gsub!(/:link([\s]*)+/i) { |m| $1 }
30
+
31
+ # Convert element names to lower case
32
+ selector.gsub!(/([\s]|^)([\w]+)/) { |m| $1.to_s + $2.to_s.downcase }
33
+
34
+ if Premailer.is_media_query?(media_types) || selector =~ Premailer::RE_UNMERGABLE_SELECTORS
35
+ @unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration), media_types) unless @options[:preserve_styles]
36
+ else
37
+ begin
38
+ if selector =~ Premailer::RE_RESET_SELECTORS
39
+ # this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
40
+ # however, this doesn't mean for testing pur
41
+ @unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
42
+ end
43
+
44
+ # Try the new index based technique. If not supported, fall back to the old brute force one.
45
+ nodes = match_selector(index, all_nodes, descendants, selector) || doc.search(selector)
46
+ nodes.each do |el|
47
+ if el.elem? and (el.name != 'head' and el.parent.name != 'head')
48
+ # Add a style attribute or append to the existing one
49
+ block = "[SPEC=#{specificity}[#{declaration}]]"
50
+ el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
51
+ end
52
+ end
53
+ rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
54
+ $stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
55
+ next
56
+ end
57
+ end
58
+ end
59
+
60
+ # Remove script tags
61
+ doc.search("script").remove if @options[:remove_scripts]
62
+
63
+ # Read STYLE attributes and perform folding
64
+ doc.search("*[@style]").each do |el|
65
+ style = el.attributes['style'].to_s
66
+
67
+ declarations = []
68
+ style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
69
+ rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
70
+ declarations << rs
71
+ end
72
+
73
+ # Perform style folding
74
+ merged = CssParser.merge(declarations)
75
+ merged.expand_shorthand!
76
+
77
+ # Duplicate CSS attributes as HTML attributes
78
+ if Premailer::RELATED_ATTRIBUTES.has_key?(el.name) && @options[:css_to_attributes]
79
+ Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
80
+ el[html_att] = merged[css_att].gsub(/url\(['|"](.*)['|"]\)/, '\1').gsub(/;$|\s*!important/, '').strip if el[html_att].nil? and not merged[css_att].empty?
81
+ merged.instance_variable_get("@declarations").tap do |declarations|
82
+ declarations.delete(css_att)
83
+ end
84
+ end
85
+ end
86
+ # Collapse multiple rules into one as much as possible.
87
+ merged.create_shorthand! if @options[:create_shorthands]
88
+
89
+ # write the inline STYLE attribute
90
+ # split by ';' but ignore those in brackets
91
+ attributes = Premailer.escape_string(merged.declarations_to_s).split(/;(?![^(]*\))/).map(&:strip)
92
+ attributes = attributes.map { |attr| [attr.split(':').first, attr] }.sort_by { |pair| pair.first }.map { |pair| pair[1] }
93
+ el['style'] = attributes.join('; ') + ";"
94
+ end
95
+
96
+ doc = write_unmergable_css_rules(doc, @unmergable_rules)
97
+
98
+ if @options[:remove_classes] or @options[:remove_comments]
99
+ doc.traverse do |el|
100
+ if el.comment? and @options[:remove_comments]
101
+ el.remove
102
+ elsif el.element?
103
+ el.remove_attribute('class') if @options[:remove_classes]
104
+ end
105
+ end
106
+ end
107
+
108
+ if @options[:remove_ids]
109
+ # find all anchor's targets and hash them
110
+ targets = []
111
+ doc.search("a[@href^='#']").each do |el|
112
+ target = el.get_attribute('href')[1..-1]
113
+ targets << target
114
+ el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
115
+ end
116
+ # hash ids that are links target, delete others
117
+ doc.search("*[@id]").each do |el|
118
+ id = el.get_attribute('id')
119
+ if targets.include?(id)
120
+ el.set_attribute('id', Digest::MD5.hexdigest(id))
121
+ else
122
+ el.remove_attribute('id')
123
+ end
124
+ end
125
+ end
126
+
127
+ if @options[:reset_contenteditable]
128
+ doc.search('*[@contenteditable]').each do |el|
129
+ el.remove_attribute('contenteditable')
130
+ end
131
+ end
132
+
133
+ @processed_doc = doc
134
+ if is_xhtml?
135
+ # we don't want to encode carriage returns
136
+ @processed_doc.to_xhtml(:encoding => @options[:output_encoding]).gsub(/&\#(xD|13);/i, "\r")
137
+ else
138
+ @processed_doc.to_html(:encoding => @options[:output_encoding])
139
+ end
140
+ end
141
+
142
+ # Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
143
+ # and write it into the <tt>body</tt>.
144
+ #
145
+ # <tt>doc</tt> is an Nokogiri document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
146
+ #
147
+ # @return [::Nokogiri::XML] a document.
148
+ def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
149
+ styles = unmergable_rules.to_s
150
+
151
+ unless styles.empty?
152
+ style_tag = "<style type=\"text/css\">\n#{styles}</style>"
153
+ unless (body = doc.search('body')).empty?
154
+ if doc.at_css('body').children && !doc.at_css('body').children.empty?
155
+ doc.at_css('body').children.before(::Nokogiri::XML.fragment(style_tag))
156
+ else
157
+ doc.at_css('body').add_child(::Nokogiri::XML.fragment(style_tag))
158
+ end
159
+ else
160
+ doc.inner_html = style_tag += doc.inner_html
161
+ end
162
+ end
163
+ doc
164
+ end
165
+
166
+
167
+ # Converts the HTML document to a format suitable for plain-text e-mail.
168
+ #
169
+ # If present, uses the <body> element as its base; otherwise uses the whole document.
170
+ #
171
+ # @return [String] a plain text.
172
+ def to_plain_text
173
+ html_src = ''
174
+ begin
175
+ html_src = @doc.at("body").inner_html
176
+ rescue;
177
+ end
178
+
179
+ html_src = @doc.to_html unless html_src and not html_src.empty?
180
+ convert_to_text(html_src, @options[:line_length], @html_encoding)
181
+ end
182
+
183
+ # Gets the original HTML as a string.
184
+ # @return [String] HTML.
185
+ def to_s
186
+ if is_xhtml?
187
+ @doc.to_xhtml(:encoding => nil)
188
+ else
189
+ @doc.to_html(:encoding => nil)
190
+ end
191
+ end
192
+
193
+ # Load the HTML file and convert it into an Nokogiri document.
194
+ #
195
+ # @return [::Nokogiri::XML] a document.
196
+ def load_html(input) # :nodoc:
197
+ thing = nil
198
+
199
+ # TODO: duplicate options
200
+ if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
201
+ thing = input
202
+ elsif @is_local_file
203
+ @base_dir = File.dirname(input)
204
+ thing = File.open(input, 'r')
205
+ else
206
+ thing = open(input)
207
+ end
208
+
209
+ if thing.respond_to?(:read)
210
+ thing = thing.read
211
+ end
212
+
213
+ return nil unless thing
214
+ doc = nil
215
+
216
+ # Handle HTML entities
217
+ if @options[:replace_html_entities] == true and thing.is_a?(String)
218
+ HTML_ENTITIES.map do |entity, replacement|
219
+ thing.gsub! entity, replacement
220
+ end
221
+ end
222
+ # Default encoding is ASCII-8BIT (binary) per http://groups.google.com/group/nokogiri-talk/msg/0b81ef0dc180dc74
223
+ # However, we really don't want to hardcode this. ASCII-8BIT should be the default, but not the only option.
224
+ if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
225
+ thing = thing.force_encoding(@options[:input_encoding]).encode!
226
+ doc = ::Nokogiri::HTML5(thing)
227
+ else
228
+ default_encoding = RUBY_PLATFORM == 'java' ? nil : 'BINARY'
229
+ doc = ::Nokogiri::HTML5(thing)
230
+ end
231
+
232
+ # Fix for removing any CDATA tags from both style and script tags inserted per
233
+ # https://github.com/sparklemotion/nokogiri/issues/311 and
234
+ # https://github.com/premailer/premailer/issues/199
235
+ %w(style script).each do |tag|
236
+ doc.search(tag).children.each do |child|
237
+ child.swap(child.text()) if child.cdata?
238
+ end
239
+ end
240
+
241
+ doc
242
+ end
243
+
244
+ private
245
+
246
+ # For very large documents, it is useful to trade off some memory for performance.
247
+ # We can build an index of the nodes so we can quickly select by id/class/tagname
248
+ # instead of search the tree again and again.
249
+ #
250
+ # @param page The Nokogiri HTML document to index.
251
+ # @return [index, set_of_all_nodes, descendants] The index is a hash from key to set of nodes.
252
+ # The "descendants" is a hash mapping a node to the set of its descendant nodes.
253
+ def make_index(page)
254
+ index = {} # Contains a map of tag/class/id names to set of nodes.
255
+ all_nodes = [] # A plain array of all nodes in the doc. The superset.
256
+ descendants = {} # Maps node -> set of descendants
257
+
258
+ page.traverse do |node|
259
+ all_nodes.push(node)
260
+
261
+ if node != page then
262
+ index_ancestry(page, node, node.parent, descendants)
263
+ end
264
+
265
+ # Index the node by tag name. This is the least selective
266
+ # of the three index types empirically.
267
+ index[node.name] = (index[node.name] || Set.new).add(node)
268
+
269
+ # Index the node by all class attributes it possesses.
270
+ # Classes are modestly selective. Usually more than tag names
271
+ # but less selective than ids.
272
+ if node.has_attribute?("class") then
273
+ node.get_attribute("class").split(/\s+/).each do |c|
274
+ c = '.' + c
275
+ index[c] = (index[c] || Set.new).add(node)
276
+ end
277
+ end
278
+
279
+ # Index the node by its "id" attribute if it has one.
280
+ # This is usually the most selective of the three.
281
+ if node.has_attribute?("id") then
282
+ id = '#' + node.get_attribute("id")
283
+ index[id] = (index[id] || Set.new).add(node)
284
+ end
285
+ end
286
+
287
+ # If an index key isn't there, then we should treat it as an empty set.
288
+ # This makes the index total and we don't need to special case presence.
289
+ # Note that the default value will never be modified. So we don't need
290
+ # default_proc.
291
+ index.default = Set.new
292
+ descendants.default = Set.new
293
+
294
+ return index, Set.new(all_nodes), descendants
295
+ end
296
+
297
+ # @param doc The top level document
298
+ # @param elem The element whose ancestry is to be captured
299
+ # @param parent the current parent in the process of capturing. Should be set to elem.parent for starters.
300
+ # @param descendants The running hash map of node -> set of nodes that maps descendants of a node.
301
+ # @return The descendants argument after updating it.
302
+ def index_ancestry(doc, elem, parent, descendants)
303
+ if parent then
304
+ descendants[parent] = (descendants[parent] || Set.new).add(elem)
305
+ if doc != parent then
306
+ index_ancestry(doc, elem, parent.parent, descendants)
307
+ end
308
+ end
309
+ descendants
310
+ end
311
+
312
+ # @param index An index hash returned by make_index
313
+ # @param base The base set of nodes within which the given spec is to be matched.
314
+ # @param intersection_selector A CSS intersection selector string of the form
315
+ # "hello.world" or "#blue.diamond". This should not contain spaces.
316
+ # @return Set of nodes matching the given spec that are present in the base set.
317
+ def narrow_down_nodes(index, base, intersection_selector)
318
+ intersection_selector.split(/(?=[.#])/).reduce(base) do |acc, sel|
319
+ acc = index[sel].intersection(acc)
320
+ acc
321
+ end
322
+ end
323
+
324
+ # @param index An index returned by make_index
325
+ # @param allNodes The set of all nodes in the DOM to search
326
+ # @param selector A simple CSS tree matching selector of the form "div.container p.item span"
327
+ # @return Set of matching nodes
328
+ #
329
+ # Note that fancy CSS selector syntax is not supported. Anything
330
+ # not matching the regex /^[-a-zA-Z0-9\s_.#]*$/ should not be passed.
331
+ # It will return nil when such a selector is passed, so you can take
332
+ # action on the falsity of the return value.
333
+ def match_selector(index, all_nodes, descendants, selector)
334
+ if /[^-a-zA-Z0-9_\s.#]/.match(selector) then
335
+ return nil
336
+ end
337
+
338
+ take_children = false
339
+ selector.split(/\s+/).reduce(all_nodes) do |base, spec|
340
+ desc = base
341
+ if take_children then
342
+ desc = Set.new
343
+ base.each do |n|
344
+ desc.merge(descendants[n])
345
+ end
346
+ else
347
+ take_children = true
348
+ end
349
+ narrow_down_nodes(index, desc, spec)
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
@@ -45,7 +45,7 @@ opts = OptionParser.new do |opts|
45
45
  end
46
46
 
47
47
  opts.on("-j", "--remove-scripts", "Remove <script> elements") do |v|
48
- options[:remove_classes] = v
48
+ options[:remove_scripts] = v
49
49
  end
50
50
 
51
51
  opts.on("-l", "--line-length N", Integer, "Line length for plaintext (default: #{options[:line_length].to_s})") do |v|
@@ -1,4 +1,4 @@
1
1
  class Premailer
2
2
  # Premailer version.
3
- VERSION = '1.8.7'.freeze
3
+ VERSION = '1.9.0'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: premailer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.7
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Dunae
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-28 00:00:00.000000000 Z
11
+ date: 2017-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: css_parser
@@ -56,7 +56,7 @@ dependencies:
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">"
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0.8'
62
62
  - - "!="
@@ -66,7 +66,7 @@ dependencies:
66
66
  prerelease: false
67
67
  version_requirements: !ruby/object:Gem::Requirement
68
68
  requirements:
69
- - - "~>"
69
+ - - ">"
70
70
  - !ruby/object:Gem::Version
71
71
  version: '0.8'
72
72
  - - "!="
@@ -93,6 +93,9 @@ dependencies:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
95
  version: 1.4.4
96
+ - - "<="
97
+ - !ruby/object:Gem::Version
98
+ version: 1.6.8
96
99
  type: :development
97
100
  prerelease: false
98
101
  version_requirements: !ruby/object:Gem::Requirement
@@ -100,20 +103,23 @@ dependencies:
100
103
  - - ">="
101
104
  - !ruby/object:Gem::Version
102
105
  version: 1.4.4
106
+ - - "<="
107
+ - !ruby/object:Gem::Version
108
+ version: 1.6.8
103
109
  - !ruby/object:Gem::Dependency
104
110
  name: yard
105
111
  requirement: !ruby/object:Gem::Requirement
106
112
  requirements:
107
- - - "~>"
113
+ - - ">="
108
114
  - !ruby/object:Gem::Version
109
- version: 0.8.7.6
115
+ version: '0'
110
116
  type: :development
111
117
  prerelease: false
112
118
  version_requirements: !ruby/object:Gem::Requirement
113
119
  requirements:
114
- - - "~>"
120
+ - - ">="
115
121
  - !ruby/object:Gem::Version
116
- version: 0.8.7.6
122
+ version: '0'
117
123
  - !ruby/object:Gem::Dependency
118
124
  name: redcarpet
119
125
  requirement: !ruby/object:Gem::Requirement
@@ -199,6 +205,7 @@ files:
199
205
  - lib/premailer/adapter.rb
200
206
  - lib/premailer/adapter/hpricot.rb
201
207
  - lib/premailer/adapter/nokogiri.rb
208
+ - lib/premailer/adapter/nokogiri_fast.rb
202
209
  - lib/premailer/adapter/nokogumbo.rb
203
210
  - lib/premailer/executor.rb
204
211
  - lib/premailer/html_to_plain_text.rb
@@ -207,7 +214,8 @@ files:
207
214
  - misc/client_support.yaml
208
215
  homepage: http://premailer.dialect.ca/
209
216
  licenses: []
210
- metadata: {}
217
+ metadata:
218
+ yard.run: yri
211
219
  post_install_message:
212
220
  rdoc_options: []
213
221
  require_paths:
@@ -229,4 +237,3 @@ signing_key:
229
237
  specification_version: 4
230
238
  summary: Preflight for HTML e-mail.
231
239
  test_files: []
232
- has_rdoc: true