premailer 1.5.4 → 1.5.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,7 +20,6 @@ script is my solution.
20
20
 
21
21
  Download the Premailer gem from GemCutter.
22
22
 
23
- gem sources -a http://gemcutter.org
24
23
  sudo gem install premailer
25
24
 
26
25
  === Example
@@ -48,19 +47,20 @@ Contributions are most welcome. Premailer was rotting away in a private SVN rep
48
47
  A few areas that are particularly in need of love:
49
48
  * Testing suite
50
49
  There were unit tests but they were so funky that it was better to just strip them out.
51
- * Test running Premailer on local files
52
50
  * Create a binary file for easing command line use, allowing the output to be piped in *nix systems
53
- * Ruby 1.9 testing
54
51
  * Test with Rails
55
52
  * Move un-repeated background images defined in CSS to <tt><td background=""></tt> for Outlook
56
53
  * Correctly parse http://www.webstandards.org/files/acid2/test.html
57
54
 
58
55
  === Credits and code
59
56
 
60
- Premailer is written in Ruby.
57
+ Thanks to {all the wonderful contributors}[http://github.com/alexdunae/premailer/contributors] for their updates.
58
+
59
+ Thanks to {Greenhood + Company}[http://www.greenhood.com/] for sponsoring some of the 1.5.6 updates,
60
+ and to {Campaign Monitor}[http://www.campaignmonitor.com] for supporting the web interface.
61
61
 
62
62
  The web interface can be found at http://premailer.dialect.ca .
63
63
 
64
64
  The source code can be found at http://github.com/alexdunae/premailer .
65
65
 
66
- Written by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-2009.
66
+ Written by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-2010.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  #
3
3
  # = Premailer
4
- require 'trollop'
4
+ require 'rubygems'
5
+ require File.join(File.dirname(__FILE__), 'trollop')
5
6
  require File.join(File.dirname(__FILE__), '../lib/premailer')
6
7
 
7
8
  opts = Trollop::options do
@@ -23,7 +24,9 @@ EOS
23
24
  opt :query_string, "Query string to append to links", :type => String, :short => 'q'
24
25
  opt :line_length, "Length of lines when creating plaintext version", :type => :int, :default => 65, :short => 'l'
25
26
  opt :remove_classes, "Remove classes from the HTML document?", :default => false
27
+ opt :css, "Manually specify css stylesheets", :type => String, :multi => true
26
28
  opt :verbose, '', :default => false, :short => 'v'
29
+ opt :io_exceptions, "Abort on I/O errors loading style resources", :default => false, :short => 'e'
27
30
  end
28
31
 
29
32
  inputfile = ARGV.shift
@@ -38,7 +41,10 @@ premailer_opts = {
38
41
  :query_string => opts[:query_string],
39
42
  :show_warnings => opts[:show_warnings] ? Premailer::Warnings::SAFE : Premailer::Warnings::NONE,
40
43
  :line_length => opts[:line_length],
41
- :remove_classes => opts[:remove_classes]
44
+ :remove_classes => opts[:remove_classes],
45
+ :css => opts[:css],
46
+ :verbose => opts[:verbose],
47
+ :io_exceptions => opts[:io_exceptions],
42
48
  }
43
49
 
44
50
  premailer = Premailer.new(inputfile, premailer_opts)
@@ -1,9 +1,7 @@
1
- # Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-09
2
-
3
1
  require 'yaml'
4
2
  require 'open-uri'
5
- require 'hpricot'
3
+ require 'cgi'
4
+ require 'nokogiri'
6
5
  require 'css_parser'
7
-
8
- require File.dirname(__FILE__) + "/premailer/html_to_plain_text"
9
- require File.dirname(__FILE__) + "/premailer/premailer"
6
+ require 'premailer/html_to_plain_text'
7
+ require 'premailer/premailer'
@@ -1,4 +1,4 @@
1
- require 'text/reform'
1
+ # coding: utf-8
2
2
  require 'htmlentities'
3
3
 
4
4
  # Support functions for Premailer
@@ -9,9 +9,9 @@ module HtmlToPlainText
9
9
  # TODO:
10
10
  # - add support for DL, OL
11
11
  def convert_to_text(html, line_length = 65, from_charset = 'UTF-8')
12
- r = Text::Reform.new(:trim => true,
13
- :squeeze => false,
14
- :break => Text::Reform.break_wrap)
12
+ #r = Text::Reform.new(:trim => true,
13
+ # :squeeze => false,
14
+ # :break => Text::Reform.break_wrap)
15
15
 
16
16
  txt = html
17
17
 
@@ -20,22 +20,29 @@ module HtmlToPlainText
20
20
  txt = he.decode(txt)
21
21
 
22
22
  # handle headings (H1-H6)
23
- txt.gsub!(/[ \t]*<h([0-9]+)[^>]*>(.*)<\/h[0-9]+>/i) do |s|
23
+ txt.gsub!(/(<\/h[1-6]>)/i, "\n\\1") # move closing tags to new lines
24
+ txt.gsub!(/[\s]*<h([1-6]+)[^>]*>[\s]*(.*)[\s]*<\/h[1-6]+>/i) do |s|
24
25
  hlevel = $1.to_i
25
- # cleanup text inside of headings
26
- htext = $2.gsub(/<\/?[^>]*>/i, '').strip
27
- hlength = (htext.length > line_length ?
28
- line_length :
29
- htext.length)
26
+
27
+ htext = $2
28
+ htext.gsub!(/<br[\s]*\/?>/i, "\n") # handle <br>s
29
+ htext.gsub!(/<\/?[^>]*>/i, '') # strip tags
30
+
31
+ # determine maximum line length
32
+ hlength = 0
33
+ htext.each_line { |l| llength = l.strip.length; hlength = llength if llength > hlength }
34
+ hlength = line_length if hlength > line_length
30
35
 
31
36
  case hlevel
32
37
  when 1 # H1, asterisks above and below
33
- ('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength) + "\n"
38
+ htext = ('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength)
34
39
  when 2 # H1, dashes above and below
35
- ('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength) + "\n"
40
+ htext = ('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength)
36
41
  else # H3-H6, dashes below
37
- htext + "\n" + ('-' * htext.length) + "\n"
42
+ htext = htext + "\n" + ('-' * hlength)
38
43
  end
44
+
45
+ "\n\n" + htext + "\n\n"
39
46
  end
40
47
 
41
48
  # links
@@ -56,7 +63,7 @@ module HtmlToPlainText
56
63
  txt.gsub!(/<\/?[^>]*>/, '')
57
64
 
58
65
  # wrap text
59
- txt = r.format(('[' * line_length), txt)
66
+ #txt = r.format(('[' * line_length), txt)
60
67
 
61
68
  # remove linefeeds (\r\n and \r -> \n)
62
69
  txt.gsub!(/\r\n?/, "\n")
@@ -33,21 +33,48 @@ class Premailer
33
33
  include HtmlToPlainText
34
34
  include CssParser
35
35
 
36
- VERSION = '1.5.4'
36
+ VERSION = '1.5.6'
37
37
 
38
38
  CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
39
39
 
40
40
  RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
41
41
 
42
- # should also exclude :first-letter, etc...
42
+ # list of CSS attributes that can be rendered as HTML attributes
43
+ #
44
+ # TODO: too much repetition
45
+ # TODO: background=""
46
+ RELATED_ATTRIBUTES = {
47
+ 'h1' => {'text-align' => 'align'},
48
+ 'h2' => {'text-align' => 'align'},
49
+ 'h3' => {'text-align' => 'align'},
50
+ 'h4' => {'text-align' => 'align'},
51
+ 'h5' => {'text-align' => 'align'},
52
+ 'h6' => {'text-align' => 'align'},
53
+ 'p' => {'text-align' => 'align'},
54
+ 'div' => {'text-align' => 'align'},
55
+ 'blockquote' => {'text-align' => 'align'},
56
+ 'body' => {'background-color' => 'bgcolor'},
57
+ 'table' => {'background-color' => 'bgcolor'},
58
+ 'tr' => {'text-align' => 'align', 'background-color' => 'bgcolor'},
59
+ 'th' => {'text-align' => 'align', 'background-color' => 'bgcolor', 'vertical-align' => 'valign'},
60
+ 'td' => {'text-align' => 'align', 'background-color' => 'bgcolor', 'vertical-align' => 'valign'},
61
+ 'img' => {'float' => 'align'}
62
+ }
43
63
 
44
64
  # URI of the HTML file used
45
65
  attr_reader :html_file
46
66
 
47
- # processed HTML document (Hpricot)
67
+ # base URL used to resolve links
68
+ attr_reader :base_url
69
+
70
+ # base directory used to resolve links for local files
71
+ attr_reader :base_dir
72
+
73
+
74
+ # processed HTML document (Nokogiri)
48
75
  attr_reader :processed_doc
49
76
 
50
- # source HTML document (Hpricot)
77
+ # source HTML document (Nokogiri)
51
78
  attr_reader :doc
52
79
 
53
80
  module Warnings
@@ -62,42 +89,61 @@ class Premailer
62
89
 
63
90
  # Create a new Premailer object.
64
91
  #
65
- # +path+ is the path to the HTML file to process. Can be either the URL of a
66
- # remote file or a local path.
92
+ # +html+ is the HTML data to process. It can be either an IO object, the URL of a
93
+ # remote file, a local path or a raw HTML string. If passing an HTML string you
94
+ # must set the +:with_html_string+ option to +true+.
67
95
  #
68
96
  # ==== Options
69
97
  # [+line_length+] Line length used by to_plain_text. Boolean, default is 65.
70
98
  # [+warn_level+] What level of CSS compatibility warnings to show (see Warnings).
71
99
  # [+link_query_string+] A string to append to every <a href=""> link. Do not include the initial +?+.
72
100
  # [+base_url+] Used to calculate absolute URLs for local files.
73
- def initialize(path, options = {})
101
+ # [+css+] Manually specify a CSS stylesheet.
102
+ # [+css_to_attributes+] Copy related CSS attributes into HTML attributes (e.g. +background-color+ to +bgcolor+)
103
+ # [+with_html_string+] Whether the +html+ param should be treated as a raw string.
104
+ def initialize(html, options = {})
74
105
  @options = {:warn_level => Warnings::SAFE,
75
106
  :line_length => 65,
76
107
  :link_query_string => nil,
77
108
  :base_url => nil,
78
- :remove_classes => false}.merge(options)
79
- @html_file = path
80
-
81
- @is_local_file = true
82
- if path =~ /^(http|https|ftp)\:\/\//i
83
- @is_local_file = false
84
- end
109
+ :remove_classes => false,
110
+ :css => [],
111
+ :css_to_attributes => true,
112
+ :with_html_string => false,
113
+ :verbose => false,
114
+ :io_exceptions => false}.merge(options)
115
+
116
+ @html_file = html
117
+ @is_local_file = @options[:with_html_string] || Premailer.local_data?(html)
118
+
119
+ @css_files = @options[:css]
85
120
 
86
121
  @css_warnings = []
87
122
 
88
- @css_parser = CssParser::Parser.new({:absolute_paths => true,
89
- :import => true,
90
- :io_exceptions => false
91
- })
123
+ @base_url = nil
124
+ @base_dir = nil
125
+
126
+ if @options[:base_url]
127
+ @base_url = URI.parse(@options.delete[:base_url])
128
+ elsif not @is_local_file
129
+ @base_url = URI.parse(@html_file)
130
+ end
131
+
132
+ @css_parser = CssParser::Parser.new({
133
+ :absolute_paths => true,
134
+ :import => true,
135
+ :io_exceptions => @options[:io_exceptions]
136
+ })
92
137
 
93
- @doc, @html_charset = load_html(@html_file)
138
+ @doc = load_html(@html_file)
139
+
140
+ @html_charset = @doc.encoding
94
141
  @processed_doc = @doc
95
-
96
- if @is_local_file and @options[:base_url]
97
- @processed_doc = convert_inline_links(@processed_doc, @options[:base_url])
98
- elsif not @is_local_file
99
- @processed_doc = convert_inline_links(@processed_doc, @html_file)
142
+ @processed_doc = convert_inline_links(@processed_doc, @base_url) if @base_url
143
+ if options[:link_query_string]
144
+ @processed_doc = append_query_string(@processed_doc, options[:link_query_string])
100
145
  end
146
+ load_css_from_options!
101
147
  load_css_from_html!
102
148
  end
103
149
 
@@ -119,7 +165,7 @@ class Premailer
119
165
  def to_plain_text
120
166
  html_src = ''
121
167
  begin
122
- html_src = @doc.search("body").innerHTML
168
+ html_src = @doc.search("body").inner_html
123
169
  rescue
124
170
  html_src = @doc.to_html
125
171
  end
@@ -151,17 +197,17 @@ class Premailer
151
197
  unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
152
198
  else
153
199
 
154
- doc.search(selector) do |el|
200
+ doc.css(selector).each do |el|
155
201
  if el.elem?
156
202
  # Add a style attribute or append to the existing one
157
203
  block = "[SPEC=#{specificity}[#{declaration}]]"
158
- el['style'] = (el.attributes['style'] ||= '') + ' ' + block
204
+ el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
159
205
  end
160
206
  end
161
207
  end
162
208
  end
163
209
 
164
- # Read <style> attributes and perform folding
210
+ # Read STYLE attributes and perform folding
165
211
  doc.search("*[@style]").each do |el|
166
212
  style = el.attributes['style'].to_s
167
213
 
@@ -172,9 +218,30 @@ class Premailer
172
218
  declarations << rs
173
219
  end
174
220
 
175
- # Perform style folding and save
221
+ # Perform style folding
176
222
  merged = CssParser.merge(declarations)
223
+ merged.expand_shorthand!
224
+
225
+ #if @options[:prefer_cellpadding] and (el.name == 'td' or el.name == 'th') and el['cellpadding'].nil?
226
+ # if cellpadding = equivalent_cellpadding(merged)
227
+ # el['cellpadding'] = cellpadding
228
+ # merged['padding-left'] = nil
229
+ # merged['padding-right'] = nil
230
+ # merged['padding-top'] = nil
231
+ # merged['padding-bottom'] = nil
232
+ # end
233
+ #end
234
+
235
+ # Duplicate CSS attributes as HTML attributes
236
+ if RELATED_ATTRIBUTES.has_key?(el.name)
237
+ RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
238
+ el[html_att] = merged[css_att].gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
239
+ end
240
+ end
241
+
242
+ merged.create_dimensions_shorthand!
177
243
 
244
+ # write the inline STYLE attribute
178
245
  el['style'] = Premailer.escape_string(merged.declarations_to_s)
179
246
  end
180
247
 
@@ -189,41 +256,67 @@ class Premailer
189
256
 
190
257
 
191
258
  protected
192
- # Load the HTML file and convert it into an Hpricot document.
259
+ # Load the HTML file and convert it into an Nokogiri document.
193
260
  #
194
- # Returns an Hpricot document and a string with the HTML file's character set.
261
+ # Returns an Nokogiri document.
195
262
  def load_html(path) # :nodoc:
196
- if @is_local_file
197
- Hpricot(File.open(path, "r") {|f| f.read })
263
+ if @options[:with_html_string]
264
+ Nokogiri::HTML.parse(path)
265
+ elsif @options[:inline]
266
+ Nokogiri::HTML(path)
198
267
  else
199
- Hpricot(open(path))
268
+ if @is_local_file
269
+ if path.is_a?(IO) || path.is_a?(StringIO)
270
+ Nokogiri::HTML(path.read)
271
+ else
272
+ @base_dir = File.dirname(path)
273
+ Nokogiri::HTML(File.open(path, "r") {|f| f.read })
274
+ end
275
+ else
276
+ Nokogiri::HTML(open(path))
277
+ end
200
278
  end
201
279
  end
202
-
280
+
281
+ def load_css_from_local_file!(path)
282
+ css_block = ''
283
+ begin
284
+ File.open(path, "r") do |file|
285
+ while line = file.gets
286
+ css_block << line
287
+ end
288
+ end
289
+ @css_parser.add_block!(css_block, {:base_uri => @base_url, :base_dir => @base_dir})
290
+ rescue; end
291
+ end
292
+
293
+ def load_css_from_options! # :nodoc:
294
+ @css_files.each do |css_file|
295
+ if Premailer.local_data?(css_file)
296
+ load_css_from_local_file!(css_file)
297
+ else
298
+ @css_parser.load_uri!(css_file)
299
+ end
300
+ end
301
+ end
302
+
203
303
  # Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
204
304
  def load_css_from_html! # :nodoc:
205
305
  if tags = @doc.search("link[@rel='stylesheet'], style")
206
306
  tags.each do |tag|
207
-
208
307
  if tag.to_s.strip =~ /^\<link/i and tag.attributes['href'] and media_type_ok?(tag.attributes['media'])
209
308
 
210
309
  link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
211
- if @is_local_file
212
- css_block = ''
213
- begin
214
- File.open(link_uri, "r") do |file|
215
- while line = file.gets
216
- css_block << line
217
- end
218
- end
219
- @css_parser.add_block!(css_block, {:base_uri => @html_file})
220
- rescue; end
310
+ if Premailer.local_data?(link_uri)
311
+ puts "Loading css from local file: " + link_uri if @options[:verbose]
312
+ load_css_from_local_file!(link_uri)
221
313
  else
314
+ puts "Loading css from uri: " + link_uri if @options[:verbose]
222
315
  @css_parser.load_uri!(link_uri)
223
316
  end
224
317
 
225
- elsif tag.to_s.strip =~ /^\<style/i
226
- @css_parser.add_block!(tag.innerHTML, :base_uri => URI.parse(@html_file))
318
+ elsif tag.to_s.strip =~ /^\<style/i
319
+ @css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
227
320
  end
228
321
  end
229
322
  tags.remove
@@ -240,9 +333,9 @@ protected
240
333
  # Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
241
334
  # and write it into the <tt>body</tt>.
242
335
  #
243
- # <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
336
+ # <tt>doc</tt> is an Nokogiri document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
244
337
  #
245
- # Returns an Hpricot document.
338
+ # Returns an Nokogiri document.
246
339
  def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
247
340
  styles = ''
248
341
  unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
@@ -251,7 +344,26 @@ protected
251
344
 
252
345
  unless styles.empty?
253
346
  style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
254
- doc.search("head").append(style_tag)
347
+ doc.css("head").children.last.after(style_tag)
348
+ end
349
+ doc
350
+ end
351
+
352
+ def append_query_string(doc, qs)
353
+ doc.search('a').each do|el|
354
+ href = el.attributes['href'].to_s
355
+ next if href.nil? or href.empty?
356
+
357
+ href = URI.parse(href)
358
+
359
+ if href.query
360
+ href.query = href.query + '&amp' + qs
361
+ else
362
+ href.query = qs
363
+ end
364
+
365
+ el['href'] = href.to_s
366
+
255
367
  end
256
368
  doc
257
369
  end
@@ -261,13 +373,13 @@ protected
261
373
  # Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
262
374
  # as well as CSS <tt>url()</tt> declarations found in inline <tt>style</tt> attributes.
263
375
  #
264
- # <tt>doc</tt> is an Hpricot document and <tt>base_uri</tt> is either a string or a URI.
376
+ # <tt>doc</tt> is an Nokogiri document and <tt>base_uri</tt> is either a string or a URI.
265
377
  #
266
- # Returns an Hpricot document.
378
+ # Returns an Nokogiri document.
267
379
  def convert_inline_links(doc, base_uri) # :nodoc:
268
380
  base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
269
381
 
270
- append_qs = @options[:link_query_string] ||= ''
382
+ append_qs = @options[:link_query_string] || ''
271
383
 
272
384
  ['href', 'src', 'background'].each do |attribute|
273
385
  tags = doc.search("*[@#{attribute}]")
@@ -297,17 +409,7 @@ protected
297
409
 
298
410
  # make sure 'merged' is a URI
299
411
  merged = URI.parse(merged.to_s) unless merged.kind_of?(URI)
300
-
301
- # only append a querystring to <a> tags
302
- if tag.name =~ /^a$/i and not append_qs.empty?
303
- if merged.query
304
- merged.query = merged.query + '&' + append_qs
305
- else
306
- merged.query = append_qs
307
- end
308
- end
309
412
  tag[attribute] = merged.to_s
310
-
311
413
  end # end of each tag
312
414
  end # end of each attrs
313
415
 
@@ -317,13 +419,27 @@ protected
317
419
  doc
318
420
  end
319
421
 
422
+ # here be deprecated methods
423
+ public
424
+
425
+ def local_uri?(uri) # :nodoc:
426
+ warn "[DEPRECATION] `local_uri?` is deprecated. Please use `Premailer.local_data?` instead."
427
+ Premailer.local_data?(uri)
428
+ end
429
+
430
+ # here be instance methods
431
+
320
432
  def self.escape_string(str) # :nodoc:
321
433
  str.gsub(/"/, "'")
322
434
  end
323
435
 
324
436
  def self.resolve_link(path, base_path) # :nodoc:
437
+ path.strip!
325
438
  resolved = nil
326
- if base_path.kind_of?(URI)
439
+ if path =~ /(http[s]?|ftp):\/\//i
440
+ resolved = path
441
+ return Premailer.canonicalize(resolved)
442
+ elsif base_path.kind_of?(URI)
327
443
  resolved = base_path.merge(path)
328
444
  return Premailer.canonicalize(resolved)
329
445
  elsif base_path.kind_of?(String) and base_path =~ /^(http[s]?|ftp):\/\//i
@@ -336,67 +452,80 @@ protected
336
452
  end
337
453
  end
338
454
 
455
+ # Test the passed variable to see if we are in local or remote mode.
456
+ #
457
+ # IO objects return true, as do strings that look like URLs.
458
+ def self.local_data?(data)
459
+ if data.is_a?(IO) || data.is_a?(StringIO)
460
+ return true
461
+ elsif data =~ /^(http|https|ftp)\:\/\//i
462
+ return false
463
+ else
464
+ return true
465
+ end
466
+ end
467
+
339
468
  # from http://www.ruby-forum.com/topic/140101
340
469
  def self.canonicalize(uri) # :nodoc:
341
470
  u = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
342
471
  u.normalize!
343
472
  newpath = u.path
344
473
  while newpath.gsub!(%r{([^/]+)/\.\./?}) { |match|
345
- $1 == '..' ? match : ''
346
- } do end
347
- newpath = newpath.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/')
348
- u.path = newpath
349
- u.to_s
350
- end
474
+ $1 == '..' ? match : ''
475
+ } do end
476
+ newpath = newpath.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/')
477
+ u.path = newpath
478
+ u.to_s
479
+ end
351
480
 
352
- # Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
353
- def check_client_support # :nodoc:
354
- @client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
481
+ # Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
482
+ def check_client_support # :nodoc:
483
+ @client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
355
484
 
356
- warnings = []
357
- properties = []
485
+ warnings = []
486
+ properties = []
358
487
 
359
- # Get a list off CSS properties
360
- @processed_doc.search("*[@style]").each do |el|
361
- style_url = el.attributes['style'].gsub(/([\w\-]+)[\s]*\:/i) do |s|
362
- properties.push($1)
488
+ # Get a list off CSS properties
489
+ @processed_doc.search("*[@style]").each do |el|
490
+ style_url = el.attributes['style'].to_s.gsub(/([\w\-]+)[\s]*\:/i) do |s|
491
+ properties.push($1)
492
+ end
363
493
  end
364
- end
365
494
 
366
- properties.uniq!
495
+ properties.uniq!
367
496
 
368
- property_support = @client_support['css_properties']
369
- properties.each do |prop|
370
- if property_support.include?(prop) and
371
- property_support[prop].include?('support') and
372
- property_support[prop]['support'] >= @options[:warn_level]
373
- warnings.push({:message => "#{prop} CSS property",
374
- :level => WARN_LABEL[property_support[prop]['support']],
375
- :clients => property_support[prop]['unsupported_in'].join(', ')})
497
+ property_support = @client_support['css_properties']
498
+ properties.each do |prop|
499
+ if property_support.include?(prop) and
500
+ property_support[prop].include?('support') and
501
+ property_support[prop]['support'] >= @options[:warn_level]
502
+ warnings.push({:message => "#{prop} CSS property",
503
+ :level => WARN_LABEL[property_support[prop]['support']],
504
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
505
+ end
376
506
  end
377
- end
378
507
 
379
- @client_support['attributes'].each do |attribute, data|
380
- next unless data['support'] >= @options[:warn_level]
381
- if @doc.search("*[@#{attribute}]").length > 0
382
- warnings.push({:message => "#{attribute} HTML attribute",
383
- :level => WARN_LABEL[property_support[prop]['support']],
384
- :clients => property_support[prop]['unsupported_in'].join(', ')})
508
+ @client_support['attributes'].each do |attribute, data|
509
+ next unless data['support'] >= @options[:warn_level]
510
+ if @doc.search("*[@#{attribute}]").length > 0
511
+ warnings.push({:message => "#{attribute} HTML attribute",
512
+ :level => WARN_LABEL[property_support[prop]['support']],
513
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
514
+ end
385
515
  end
386
- end
387
516
 
388
- @client_support['elements'].each do |element, data|
389
- next unless data['support'] >= @options[:warn_level]
390
- if @doc.search("element").length > 0
391
- warnings.push({:message => "#{element} HTML element",
392
- :level => WARN_LABEL[property_support[prop]['support']],
393
- :clients => property_support[prop]['unsupported_in'].join(', ')})
517
+ @client_support['elements'].each do |element, data|
518
+ next unless data['support'] >= @options[:warn_level]
519
+ if @doc.search("element").length > 0
520
+ warnings.push({:message => "#{element} HTML element",
521
+ :level => WARN_LABEL[property_support[prop]['support']],
522
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
523
+ end
394
524
  end
395
- end
396
525
 
397
- return warnings
526
+ return warnings
527
+ end
398
528
  end
399
- end
400
529
 
401
530
 
402
531
 
metadata CHANGED
@@ -1,7 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: premailer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.4
4
+ hash: 15
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 5
9
+ - 6
10
+ version: 1.5.6
5
11
  platform: ruby
6
12
  authors:
7
13
  - Alex Dunae
@@ -9,69 +15,74 @@ autorequire:
9
15
  bindir: bin
10
16
  cert_chain: []
11
17
 
12
- date: 2009-12-03 00:00:00 -08:00
18
+ date: 2010-11-03 00:00:00 -07:00
13
19
  default_executable:
14
20
  dependencies:
15
21
  - !ruby/object:Gem::Dependency
16
- name: hpricot
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
22
+ name: nokogiri
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
20
26
  requirements:
21
27
  - - ">="
22
28
  - !ruby/object:Gem::Version
23
- version: "0.6"
24
- version:
29
+ hash: 7
30
+ segments:
31
+ - 1
32
+ - 4
33
+ - 0
34
+ version: 1.4.0
35
+ type: :runtime
36
+ version_requirements: *id001
25
37
  - !ruby/object:Gem::Dependency
26
38
  name: css_parser
27
- type: :runtime
28
- version_requirement:
29
- version_requirements: !ruby/object:Gem::Requirement
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
30
42
  requirements:
31
43
  - - ">="
32
44
  - !ruby/object:Gem::Version
33
- version: 0.9.0
34
- version:
35
- - !ruby/object:Gem::Dependency
36
- name: text-reform
45
+ hash: 21
46
+ segments:
47
+ - 1
48
+ - 1
49
+ - 3
50
+ version: 1.1.3
37
51
  type: :runtime
38
- version_requirement:
39
- version_requirements: !ruby/object:Gem::Requirement
40
- requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- version: 0.2.0
44
- version:
52
+ version_requirements: *id002
45
53
  - !ruby/object:Gem::Dependency
46
54
  name: htmlentities
47
- type: :runtime
48
- version_requirement:
49
- version_requirements: !ruby/object:Gem::Requirement
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
50
58
  requirements:
51
59
  - - ">="
52
60
  - !ruby/object:Gem::Version
61
+ hash: 63
62
+ segments:
63
+ - 4
64
+ - 0
65
+ - 0
53
66
  version: 4.0.0
54
- version:
67
+ type: :runtime
68
+ version_requirements: *id003
55
69
  description: Improve the rendering of HTML emails by making CSS inline, converting links and warning about unsupported code.
56
70
  email: code@dunae.ca
57
71
  executables:
58
72
  - premailer
59
73
  extensions: []
60
74
 
61
- extra_rdoc_files: []
62
-
75
+ extra_rdoc_files:
76
+ - README.rdoc
63
77
  files:
64
78
  - init.rb
65
- - rakefile.rb
79
+ - bin/premailer
80
+ - bin/trollop.rb
66
81
  - lib/premailer.rb
67
82
  - lib/premailer/html_to_plain_text.rb
68
83
  - lib/premailer/premailer.rb
69
- - CHANGELOG.rdoc
70
- - LICENSE.rdoc
71
- - README.rdoc
72
84
  - misc/client_support.yaml
73
- - bin/premailer
74
- - bin/trollop.rb
85
+ - README.rdoc
75
86
  has_rdoc: true
76
87
  homepage: http://premailer.dialect.ca/
77
88
  licenses: []
@@ -86,21 +97,27 @@ rdoc_options:
86
97
  require_paths:
87
98
  - lib
88
99
  required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
89
101
  requirements:
90
102
  - - ">="
91
103
  - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
92
107
  version: "0"
93
- version:
94
108
  required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
95
110
  requirements:
96
111
  - - ">="
97
112
  - !ruby/object:Gem::Version
113
+ hash: 3
114
+ segments:
115
+ - 0
98
116
  version: "0"
99
- version:
100
117
  requirements: []
101
118
 
102
119
  rubyforge_project:
103
- rubygems_version: 1.3.5
120
+ rubygems_version: 1.3.7
104
121
  signing_key:
105
122
  specification_version: 3
106
123
  summary: Preflight for HTML e-mail.
@@ -1,57 +0,0 @@
1
- = Premailer CHANGELOG
2
-
3
- == Version 1.5.4
4
- * new bin/premailer script
5
- * added missing htmlentities depenency to gemspec (thanks to http://github.com/usefulthink )
6
- * fixed handling of unspecified <link> media types
7
-
8
- == Version 1.5.3
9
- * improved plaintext conversion
10
-
11
- == Version 1.5.2
12
- * released to GitHub
13
- * fixed handling of mailto links
14
- * various minor updates
15
-
16
- == Version 1.5.1
17
- * bugfix (http://code.google.com/p/premailer/issues/detail?id=1 and http://code.google.com/p/premailer/issues/detail?id=2) thanks to Russell Norris
18
- * bugfix (http://code.google.com/p/premailer/issues/detail?id=4) thanks to Dave Holmes
19
-
20
- == Version 1.5.0
21
- * preview release of Ruby gem
22
-
23
- == Version 1.4
24
- * incremental parsing improvements
25
- * respect <tt>@media</tt> rule (http://www.w3.org/TR/CSS21/media.html#at-media-rule)
26
- * better quote escaping
27
-
28
- == Version 1.3
29
- * separate CSS parser into its own library
30
- * handle <tt>background: red url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4%2F58BAAT%2FAf9jgNErAAAAAElFTkSuQmCC);</tt>
31
- * preserve <tt>:hover</tt> etc... in head styles
32
-
33
- == Version 1.2
34
- * respect <tt>LINK</tt> media types
35
- * better style folding
36
- * incremental parsing improvements
37
-
38
- == Version 1.1
39
- * proper calculation of selector specificity per CSS 2.1 spec
40
- * support for <tt>@import</tt>
41
- * preliminary support for shorthand CSS properties (<tt>margin</tt>, <tt>padding</tt>)
42
- * preliminary separation of CSS parser
43
-
44
- == Version 1.0
45
- * ported web interface to eRuby
46
- * incremental parsing improvements
47
-
48
- == Version 0.9
49
- * initial proof-of-concept
50
- * PHP web version
51
-
52
- == TODO: Future
53
- * complete shorthand properties support (<tt>border-width</tt>, <tt>font</tt>, <tt>background</tt>)
54
- * UTF-8 and other charsets (test page: http://kianga.kcore.de/2004/09/21/utf8_test)
55
- * make warnings for <tt>border</tt> match <tt>border-left</tt>, etc...
56
- * Integrate CSS validator
57
- * Remove unused classes and IDs
@@ -1,42 +0,0 @@
1
- = Premailer License
2
-
3
- Copyright (c) 2007-09 Alex Dunae
4
-
5
- Premailer is copyrighted free software by Alex Dunae (http://dunae.ca/).
6
- You can redistribute it and/or modify it under the conditions below:
7
-
8
- 1. You may make and give away verbatim copies of the source form of the
9
- software without restriction, provided that you duplicate all of the
10
- original copyright notices and associated disclaimers.
11
-
12
- 2. You may modify your copy of the software in any way, provided that
13
- you do at least ONE of the following:
14
-
15
- a) place your modifications in the Public Domain or otherwise
16
- make them Freely Available, such as by posting said
17
- modifications to the internet or an equivalent medium, or by
18
- allowing the author to include your modifications in the software.
19
-
20
- b) use the modified software only within your corporation or
21
- organization.
22
-
23
- c) rename any non-standard executables so the names do not conflict
24
- with standard executables, which must also be provided.
25
-
26
- d) make other distribution arrangements with the author.
27
-
28
- 3. You may modify and include the part of the software into any other
29
- software (possibly commercial) as long as clear acknowledgement and
30
- a link back to the original software (http://code.dunae.ca/premailer.web/)
31
- is provided.
32
-
33
- 5. The scripts and library files supplied as input to or produced as
34
- output from the software do not automatically fall under the
35
- copyright of the software, but belong to whomever generated them,
36
- and may be sold commercially, and may be aggregated with this
37
- software.
38
-
39
- 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
40
- IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
41
- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
42
- PURPOSE.
@@ -1,42 +0,0 @@
1
- require 'rake'
2
- require 'fileutils'
3
- require 'lib/premailer'
4
-
5
- desc 'Default: parse a URL.'
6
- task :default => [:inline]
7
-
8
- desc 'Parse a URL and write out the output.'
9
- task :inline do
10
- url = ENV['url']
11
- output = ENV['output']
12
-
13
- if !url or url.empty? or !output or output.empty?
14
- puts 'Usage: rake inline url=http://example.com/ output=output.html'
15
- exit
16
- end
17
-
18
- premailer = Premailer.new(url, :warn_level => Premailer::Warnings::SAFE)
19
- fout = File.open(output, "w")
20
- fout.puts premailer.to_inline_css
21
- fout.close
22
-
23
- puts "Succesfully parsed '#{url}' into '#{output}'"
24
- puts premailer.warnings.length.to_s + ' CSS warnings were found'
25
- end
26
-
27
- task :text do
28
- url = ENV['url']
29
- output = ENV['output']
30
-
31
- if !url or url.empty? or !output or output.empty?
32
- puts 'Usage: rake text url=http://example.com/ output=output.txt'
33
- exit
34
- end
35
-
36
- premailer = Premailer.new(url, :warn_level => Premailer::Warnings::SAFE)
37
- fout = File.open(output, "w")
38
- fout.puts premailer.to_plain_text
39
- fout.close
40
-
41
- puts "Succesfully parsed '#{url}' into '#{output}'"
42
- end