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.
@@ -39,9 +39,9 @@ class Premailer
39
39
  CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
40
40
 
41
41
  # Unmergable selectors regexp.
42
- RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
42
+ RE_UNMERGABLE_SELECTORS = /(:(visited|active|hover|focus|after|before|selection|target|first-(line|letter))|^@)/i
43
43
  # Reset selectors regexp.
44
- RE_RESET_SELECTORS = /^(\:\#outlook|body.*|\.ReadMsgBody|\.ExternalClass|img|\#backgroundTable)$/
44
+ RE_RESET_SELECTORS = /^(:\#outlook|body.*|\.ReadMsgBody|\.ExternalClass|img|\#backgroundTable)$/
45
45
 
46
46
  # list of HTMLEntities to fix
47
47
  # source: http://stackoverflow.com/questions/2812781/how-to-convert-webpage-apostrophe-8217-to-ascii-39-in-ruby-1-
@@ -57,23 +57,23 @@ class Premailer
57
57
  "–" => '-',
58
58
  "—" => '--',
59
59
  "―" => '--'
60
- }
60
+ }.freeze
61
61
 
62
62
  # list of CSS attributes that can be rendered as HTML attributes
63
63
  #
64
64
  # @todo too much repetition
65
65
  # @todo background=""
66
66
  RELATED_ATTRIBUTES = {
67
- 'h1' => {'text-align' => 'align'},
68
- 'h2' => {'text-align' => 'align'},
69
- 'h3' => {'text-align' => 'align'},
70
- 'h4' => {'text-align' => 'align'},
71
- 'h5' => {'text-align' => 'align'},
72
- 'h6' => {'text-align' => 'align'},
73
- 'p' => {'text-align' => 'align'},
74
- 'div' => {'text-align' => 'align'},
75
- 'blockquote' => {'text-align' => 'align'},
76
- 'body' => {'background-color' => 'bgcolor'},
67
+ 'h1' => { 'text-align' => 'align' },
68
+ 'h2' => { 'text-align' => 'align' },
69
+ 'h3' => { 'text-align' => 'align' },
70
+ 'h4' => { 'text-align' => 'align' },
71
+ 'h5' => { 'text-align' => 'align' },
72
+ 'h6' => { 'text-align' => 'align' },
73
+ 'p' => { 'text-align' => 'align' },
74
+ 'div' => { 'text-align' => 'align' },
75
+ 'blockquote' => { 'text-align' => 'align' },
76
+ 'body' => { 'background-color' => 'bgcolor' },
77
77
  'table' => {
78
78
  '-premailer-align' => 'align',
79
79
  'background-color' => 'bgcolor',
@@ -107,7 +107,7 @@ class Premailer
107
107
  '-premailer-width' => 'width',
108
108
  '-premailer-height' => 'height'
109
109
  }
110
- }
110
+ }.freeze
111
111
 
112
112
  # URI of the HTML file used
113
113
  attr_reader :html_file
@@ -142,7 +142,7 @@ class Premailer
142
142
  include Warnings
143
143
 
144
144
  # Waning level names
145
- WARN_LABEL = ['NONE', 'SAFE', 'POOR', 'RISKY']
145
+ WARN_LABEL = ['NONE', 'SAFE', 'POOR', 'RISKY'].freeze
146
146
 
147
147
  # Create a new Premailer object.
148
148
  #
@@ -182,38 +182,40 @@ class Premailer
182
182
  # @option options [Boolean] :html_fragment Handle HTML fragment without any HTML content wrappers. Default is false.
183
183
  # @option options [Boolean] :drop_unmergeable_css_rules Do not include unmergeable css rules in a <tt><style><tt> tag. Default is false.
184
184
  def initialize(html, options = {})
185
- @options = {:warn_level => Warnings::SAFE,
186
- :line_length => 65,
187
- :link_query_string => nil,
188
- :base_url => nil,
189
- :rgb_to_hex_attributes => true,
190
- :remove_classes => false,
191
- :remove_ids => false,
192
- :remove_comments => false,
193
- :remove_scripts => true,
194
- :reset_contenteditable => true,
195
- :css => [],
196
- :css_to_attributes => true,
197
- :preserve_style_attribute => false,
198
- :with_html_string => false,
199
- :css_string => nil,
200
- :preserve_styles => false,
201
- :preserve_reset => true,
202
- :verbose => false,
203
- :debug => false,
204
- :io_exceptions => false,
205
- :rule_set_exceptions => true,
206
- :include_link_tags => true,
207
- :include_style_tags => true,
208
- :input_encoding => 'ASCII-8BIT',
209
- :output_encoding => nil,
210
- :replace_html_entities => false,
211
- :escape_url_attributes => true,
212
- :unescaped_ampersand => false,
213
- :create_shorthands => true,
214
- :html_fragment => false,
215
- :adapter => Adapter.use,
216
- :drop_unmergeable_css_rules => false}.merge(options)
185
+ @options = {
186
+ :warn_level => Warnings::SAFE,
187
+ :line_length => 65,
188
+ :link_query_string => nil,
189
+ :base_url => nil,
190
+ :rgb_to_hex_attributes => true,
191
+ :remove_classes => false,
192
+ :remove_ids => false,
193
+ :remove_comments => false,
194
+ :remove_scripts => true,
195
+ :reset_contenteditable => true,
196
+ :css => [],
197
+ :css_to_attributes => true,
198
+ :preserve_style_attribute => false,
199
+ :with_html_string => false,
200
+ :css_string => nil,
201
+ :preserve_styles => false,
202
+ :preserve_reset => true,
203
+ :verbose => false,
204
+ :debug => false,
205
+ :io_exceptions => false,
206
+ :rule_set_exceptions => true,
207
+ :include_link_tags => true,
208
+ :include_style_tags => true,
209
+ :input_encoding => 'ASCII-8BIT',
210
+ :output_encoding => nil,
211
+ :replace_html_entities => false,
212
+ :escape_url_attributes => true,
213
+ :unescaped_ampersand => false,
214
+ :create_shorthands => true,
215
+ :html_fragment => false,
216
+ :adapter => Adapter.use,
217
+ :drop_unmergeable_css_rules => false
218
+ }.merge(options)
217
219
 
218
220
  @html_file = html
219
221
  @is_local_file = @options[:with_html_string] || Premailer.local_data?(html)
@@ -228,20 +230,22 @@ class Premailer
228
230
 
229
231
  if @options[:base_url]
230
232
  @base_url = Addressable::URI.parse(@options.delete(:base_url))
231
- elsif not @is_local_file
233
+ elsif !@is_local_file
232
234
  @base_url = Addressable::URI.parse(@html_file)
233
235
  end
234
236
 
235
- @css_parser = CssParser::Parser.new({
236
- :absolute_paths => true,
237
- :import => true,
238
- :io_exceptions => @options[:io_exceptions],
239
- :rule_set_exceptions => @options[:rule_set_exceptions]
240
- })
237
+ @css_parser = CssParser::Parser.new(
238
+ {
239
+ :absolute_paths => true,
240
+ :import => true,
241
+ :io_exceptions => @options[:io_exceptions],
242
+ :rule_set_exceptions => @options[:rule_set_exceptions]
243
+ }
244
+ )
241
245
 
242
246
  @adapter_class = Adapter.find @options[:adapter]
243
247
 
244
- self.extend(@adapter_class)
248
+ extend(@adapter_class)
245
249
 
246
250
  @doc = load_html(@html_file)
247
251
 
@@ -262,7 +266,8 @@ class Premailer
262
266
  @css_warnings
263
267
  end
264
268
 
265
- protected
269
+ protected
270
+
266
271
  def load_css_from_local_file!(path)
267
272
  css_block = +''
268
273
  path.delete_prefix!('file:')
@@ -274,13 +279,13 @@ protected
274
279
  end
275
280
 
276
281
  load_css_from_string(css_block)
277
- rescue => e
282
+ rescue StandardError => e
278
283
  raise e if @options[:io_exceptions]
279
284
  end
280
285
  end
281
286
 
282
287
  def load_css_from_string(css_string)
283
- @css_parser.add_block!(css_string, {:base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld]})
288
+ @css_parser.add_block!(css_string, { :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld] })
284
289
  end
285
290
 
286
291
  # @private
@@ -296,20 +301,20 @@ protected
296
301
  end
297
302
  end
298
303
 
299
- # Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
304
+ # Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
300
305
  def load_css_from_html! # :nodoc:
301
306
  tags = @doc.search("link[@rel='stylesheet']:not([@data-premailer='ignore']), style:not([@data-premailer='ignore'])")
302
307
  if tags
303
308
  tags.each do |tag|
304
- if tag.to_s.strip =~ /^\<link/i && tag.attributes['href'] && media_type_ok?(tag.attributes['media']) && @options[:include_link_tags]
309
+ if tag.to_s.strip =~ /^<link/i && tag.attributes['href'] && media_type_ok?(tag.attributes['media']) && @options[:include_link_tags]
305
310
  # A user might want to <link /> to a local css file that is also mirrored on the site
306
311
  # but the local one is different (e.g. newer) than the live file, premailer will now choose the local file
307
312
 
308
- if tag.attributes['href'].to_s.include? @base_url.to_s and @html_file.kind_of?(String)
309
- if @options[:with_html_string]
310
- link_uri = tag.attributes['href'].to_s.sub(@base_url.to_s, '')
313
+ if tag.attributes['href'].to_s.include?(@base_url.to_s) && @html_file.is_a?(String)
314
+ link_uri = if @options[:with_html_string]
315
+ tag.attributes['href'].to_s.sub(@base_url.to_s, '')
311
316
  else
312
- link_uri = File.join(File.dirname(@html_file), tag.attributes['href'].to_s.sub!(@base_url.to_s, ''))
317
+ File.join(File.dirname(@html_file), tag.attributes['href'].to_s.sub!(@base_url.to_s, ''))
313
318
  end
314
319
  # if the file does not exist locally, try to grab the remote reference
315
320
  unless File.exist?(link_uri)
@@ -320,14 +325,14 @@ protected
320
325
  end
321
326
 
322
327
  if Premailer.local_data?(link_uri)
323
- $stderr.puts "Loading css from local file: " + link_uri if @options[:verbose]
328
+ warn "Loading css from local file: " + link_uri if @options[:verbose]
324
329
  load_css_from_local_file!(link_uri)
325
330
  else
326
- $stderr.puts "Loading css from uri: " + link_uri if @options[:verbose]
327
- @css_parser.load_uri!(link_uri, {:only_media_types => [:screen, :handheld]})
331
+ warn "Loading css from uri: " + link_uri if @options[:verbose]
332
+ @css_parser.load_uri!(link_uri, { :only_media_types => [:screen, :handheld] })
328
333
  end
329
334
 
330
- elsif tag.to_s.strip =~ /^\<style/i && @options[:include_style_tags]
335
+ elsif tag.to_s.strip =~ /^<style/i && @options[:include_style_tags]
331
336
  @css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
332
337
  end
333
338
  end
@@ -335,10 +340,28 @@ protected
335
340
  end
336
341
  end
337
342
 
343
+ # here be deprecated methods
344
+ public
338
345
 
346
+ # Free native Nokogiri/libxml2 memory held by this instance.
347
+ #
348
+ # After calling +to_inline_css+ or +to_plain_text+, the Premailer instance
349
+ # retains live Nokogiri documents that pin native libxml2 memory. Ruby's GC
350
+ # cannot see this native memory pressure, so in high-throughput scenarios
351
+ # (e.g. email rendering in Sidekiq workers) native memory can accumulate
352
+ # unboundedly. Call +cleanup!+ when you are done with the instance to
353
+ # release that memory immediately.
354
+ #
355
+ # After calling this method the instance is no longer usable.
356
+ def cleanup!
357
+ @processed_doc&.unlink
358
+ @doc&.unlink
359
+ @doc = nil
360
+ @processed_doc = nil
361
+ @css_parser = nil
362
+ @unmergable_rules = nil
363
+ end
339
364
 
340
- # here be deprecated methods
341
- public
342
365
  # @private
343
366
  # @deprecated
344
367
  def local_uri?(uri) # :nodoc:
@@ -346,12 +369,12 @@ public
346
369
  Premailer.local_data?(uri)
347
370
  end
348
371
 
349
- # here be instance methods
372
+ # here be instance methods
350
373
 
351
374
  # @private
352
375
  def media_type_ok?(media_types)
353
376
  media_types = media_types.to_s
354
- return true if media_types.nil? or media_types.empty?
377
+ return true if media_types.nil? || media_types.empty?
355
378
  media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
356
379
  end
357
380
 
@@ -364,32 +387,32 @@ public
364
387
 
365
388
  begin
366
389
  current_host = @base_url.host
367
- rescue
390
+ rescue StandardError
368
391
  current_host = nil
369
392
  end
370
393
 
371
- $stderr.puts "Attempting to append_query_string: #{queries}" if @options[:verbose]
394
+ warn "Attempting to append_query_string: #{queries}" if @options[:verbose]
372
395
 
373
396
  doc.search('a').each do |el|
374
397
  href = el.attributes['href'].to_s.strip
375
- next if href.nil? or href.empty?
398
+ next if href.nil? || href.empty?
376
399
 
377
- next if /[\#\{\[\<\%]/.match?(href[0,1]) # don't bother with anchors or special-looking links
400
+ next if /[\#\{\[\<\%]/.match?(href[0, 1]) # don't bother with anchors or special-looking links
378
401
 
379
402
  begin
380
403
  href = Addressable::URI.parse(href)
381
404
 
382
- if current_host and !href.host.nil? and href.host != current_host
383
- $stderr.puts "Skipping append_query_string for: #{href.to_s} because host is no good" if @options[:verbose]
405
+ if current_host && !href.host.nil? && (href.host != current_host)
406
+ warn "Skipping append_query_string for: #{href} because host is no good" if @options[:verbose]
384
407
  next
385
408
  end
386
409
 
387
- if href.scheme and href.scheme != 'http' and href.scheme != 'https'
388
- puts "Skipping append_query_string for: #{href.to_s} because scheme is no good" if @options[:verbose]
410
+ if href.scheme && (href.scheme != 'http') && (href.scheme != 'https')
411
+ puts "Skipping append_query_string for: #{href} because scheme is no good" if @options[:verbose]
389
412
  next
390
413
  end
391
414
 
392
- if href.query and not href.query.empty?
415
+ if href.query && !href.query.empty?
393
416
  amp = @options[:unescaped_ampersand] ? '&' : '&amp;'
394
417
  href.query = href.query + amp + queries
395
418
  else
@@ -398,19 +421,18 @@ public
398
421
 
399
422
  el['href'] = href.to_s
400
423
  rescue Addressable::URI::InvalidURIError => e
401
- $stderr.puts "Skipping append_query_string for: #{href.to_s} (#{e.message})" if @options[:verbose]
424
+ warn "Skipping append_query_string for: #{href} (#{e.message})" if @options[:verbose]
402
425
  next
403
426
  end
404
-
405
427
  end
406
428
  doc
407
429
  end
408
430
 
409
431
  # Check for an XHTML doctype
410
- def is_xhtml?
432
+ def xhtml?
411
433
  intro = @doc.to_xhtml.strip.split("\n")[0..2].join(' ')
412
434
  is_xhtml = !!(intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i)
413
- $stderr.puts "Is XHTML? #{is_xhtml.inspect}\nChecked:\n#{intro}" if @options[:debug]
435
+ warn "Is XHTML? #{is_xhtml.inspect}\nChecked:\n#{intro}" if @options[:debug]
414
436
  is_xhtml
415
437
  end
416
438
 
@@ -423,9 +445,8 @@ public
423
445
  #
424
446
  # Returns a document.
425
447
  def convert_inline_links(doc, base_uri) # :nodoc:
426
- base_uri = Addressable::URI.parse(base_uri) unless base_uri.kind_of?(Addressable::URI)
448
+ base_uri = Addressable::URI.parse(base_uri) unless base_uri.is_a?(Addressable::URI)
427
449
 
428
- append_qs = @options[:link_query_string] || ''
429
450
  escape_attrs = @options[:escape_url_attributes]
430
451
 
431
452
  ['href', 'src', 'background'].each do |attribute|
@@ -443,23 +464,23 @@ public
443
464
  if /^http/i.match?(tag.attributes[attribute].to_s)
444
465
  begin
445
466
  merged = Addressable::URI.parse(tag.attributes[attribute])
446
- rescue; next; end
467
+ rescue StandardError; next; end
447
468
  else
448
469
  begin
449
470
  merged = Premailer.resolve_link(tag.attributes[attribute].to_s, base_uri)
450
- rescue
471
+ rescue StandardError
451
472
  begin
452
473
  next unless escape_attrs
453
474
  merged = Premailer.resolve_link(Addressable::URI.escape(tag.attributes[attribute].to_s), base_uri)
454
- rescue; end
475
+ rescue StandardError; end
455
476
  end
456
477
  end
457
478
 
458
479
  # make sure 'merged' is a URI
459
- merged = Addressable::URI.parse(merged.to_s) unless merged.kind_of?(Addressable::URI)
480
+ merged = Addressable::URI.parse(merged.to_s) unless merged.is_a?(Addressable::URI)
460
481
  tag[attribute] = merged.to_s
461
- end # end of each tag
462
- end # end of each attrs
482
+ end
483
+ end
463
484
 
464
485
  doc.search("*[@style]").each do |el|
465
486
  el['style'] = CssParser.convert_uris(el.attributes['style'].to_s, base_uri)
@@ -468,22 +489,21 @@ public
468
489
  end
469
490
 
470
491
  # @private
471
- def self.is_media_query?(media_types)
472
- media_types && media_types.any?{|mt| mt.to_s.count('()') >= 2 }
492
+ def self.media_query?(media_types)
493
+ media_types&.any? { |mt| mt.to_s.count('()') >= 2 }
473
494
  end
474
495
 
475
496
  # @private
476
497
  def self.resolve_link(path, base_path) # :nodoc:
477
498
  path = +path
478
499
  path.strip!
479
- resolved = nil
480
500
  if /\A(?:(https?|ftp|file):)\/\//i.match?(path)
481
501
  resolved = path
482
502
  Premailer.canonicalize(resolved)
483
- elsif base_path.kind_of?(Addressable::URI)
503
+ elsif base_path.is_a?(Addressable::URI)
484
504
  resolved = base_path.join(path)
485
505
  Premailer.canonicalize(resolved)
486
- elsif base_path.kind_of?(String) and base_path =~ /\A(?:(?:https?|ftp|file):)\/\//i
506
+ elsif base_path.is_a?(String) && base_path =~ (/\A(?:(?:https?|ftp|file):)\/\//i)
487
507
  resolved = Addressable::URI.parse(base_path)
488
508
  resolved = resolved.join(path)
489
509
  Premailer.canonicalize(resolved)
@@ -496,13 +516,13 @@ public
496
516
  #
497
517
  # IO objects return true, as do strings that look like URLs.
498
518
  def self.local_data?(data)
499
- return false if data.kind_of?(String) && data =~ /\A(?:(https?|ftp):)\/\//i
519
+ return false if data.is_a?(String) && data =~ /\A(?:(https?|ftp):)\/\//i
500
520
  true
501
521
  end
502
522
 
503
523
  # from http://www.ruby-forum.com/topic/140101
504
524
  def self.canonicalize(uri) # :nodoc:
505
- u = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI.parse(uri.to_s)
525
+ u = uri.is_a?(Addressable::URI) ? uri : Addressable::URI.parse(uri.to_s)
506
526
  u.normalize!
507
527
  u.to_s
508
528
  end
@@ -517,7 +537,7 @@ public
517
537
 
518
538
  # Get a list off CSS properties
519
539
  @processed_doc.search("*[@style]").each do |el|
520
- style_url = el.attributes['style'].to_s.gsub(/([\w\-]+)[\s]*\:/i) do |s|
540
+ el.attributes['style'].to_s.gsub(/([\w\-]+)[\s]*:/i) do |_s|
521
541
  properties.push($1)
522
542
  end
523
543
  end
@@ -526,30 +546,42 @@ public
526
546
 
527
547
  property_support = @client_support['css_properties']
528
548
  properties.each do |prop|
529
- if property_support.include?(prop) and
530
- property_support[prop].include?('support') and
531
- property_support[prop]['support'] >= @options[:warn_level]
532
- warnings.push({:message => "#{prop} CSS property",
549
+ if property_support.include?(prop) &&
550
+ property_support[prop].include?('support') &&
551
+ (property_support[prop]['support'] >= @options[:warn_level])
552
+ warnings.push(
553
+ {
554
+ :message => "#{prop} CSS property",
533
555
  :level => WARN_LABEL[property_support[prop]['support']],
534
- :clients => property_support[prop]['unsupported_in'].join(', ')})
556
+ :clients => property_support[prop]['unsupported_in'].join(', ')
557
+ }
558
+ )
535
559
  end
536
560
  end
537
561
 
538
562
  @client_support['attributes'].each do |attribute, data|
539
563
  next unless data['support'] >= @options[:warn_level]
540
564
  if @doc.search("*[@#{attribute}]").length > 0
541
- warnings.push({:message => "#{attribute} HTML attribute",
565
+ warnings.push(
566
+ {
567
+ :message => "#{attribute} HTML attribute",
542
568
  :level => WARN_LABEL[data['support']],
543
- :clients => data['unsupported_in'].join(', ')})
569
+ :clients => data['unsupported_in'].join(', ')
570
+ }
571
+ )
544
572
  end
545
573
  end
546
574
 
547
575
  @client_support['elements'].each do |element, data|
548
576
  next unless data['support'] >= @options[:warn_level]
549
577
  if @doc.search(element).length > 0
550
- warnings.push({:message => "#{element} HTML element",
578
+ warnings.push(
579
+ {
580
+ :message => "#{element} HTML element",
551
581
  :level => WARN_LABEL[data['support']],
552
- :clients => data['unsupported_in'].join(', ')})
582
+ :clients => data['unsupported_in'].join(', ')
583
+ }
584
+ )
553
585
  end
554
586
  end
555
587
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  class Premailer
3
3
  # Premailer version.
4
- VERSION = '1.26.0'
4
+ VERSION = '1.28.0'
5
5
  end