premailer 1.7.1 → 1.7.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,3 @@
1
- #!/usr/bin/ruby
2
- #
3
1
  # Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-10
4
2
  #
5
3
  # Premailer processes HTML and CSS to improve e-mail deliverability.
@@ -33,11 +31,12 @@ class Premailer
33
31
  include HtmlToPlainText
34
32
  include CssParser
35
33
 
36
- VERSION = '1.7.1'
34
+ VERSION = '1.7.3'
37
35
 
38
36
  CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
39
37
 
40
38
  RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
39
+ RE_RESET_SELECTORS = /^(\:\#outlook|body.*|\.ReadMsgBody|\.ExternalClass|img|\#backgroundTable)$/
41
40
 
42
41
  # list of CSS attributes that can be rendered as HTML attributes
43
42
  #
@@ -54,10 +53,33 @@ class Premailer
54
53
  'div' => {'text-align' => 'align'},
55
54
  'blockquote' => {'text-align' => 'align'},
56
55
  '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'},
56
+ 'table' => {
57
+ 'background-color' => 'bgcolor',
58
+ '-premailer-width' => 'width',
59
+ '-premailer-height' => 'height',
60
+ '-premailer-cellpadding' => 'cellpadding',
61
+ '-premailer-cellspacing' => 'cellspacing',
62
+ },
63
+ 'tr' => {
64
+ 'text-align' => 'align',
65
+ 'background-color' => 'bgcolor',
66
+ '-premailer-height' => 'height'
67
+ },
68
+ 'th' => {
69
+ 'text-align' => 'align',
70
+ 'background-color' => 'bgcolor',
71
+ 'vertical-align' => 'valign',
72
+ '-premailer-width' => 'width',
73
+ '-premailer-height' => 'height'
74
+ },
75
+ 'td' => {
76
+ 'text-align' => 'align',
77
+ 'background-color' => 'bgcolor',
78
+ 'vertical-align' => 'valign',
79
+ '-premailer-width' => 'width',
80
+ '-premailer-height' => 'height',
81
+ '-premailer-colspan' => 'colspan'
82
+ },
61
83
  'img' => {'float' => 'align'}
62
84
  }
63
85
 
@@ -100,13 +122,14 @@ class Premailer
100
122
  # [+warn_level+] What level of CSS compatibility warnings to show (see Warnings).
101
123
  # [+link_query_string+] A string to append to every <tt>a href=""</tt> link. Do not include the initial <tt>?</tt>.
102
124
  # [+base_url+] Used to calculate absolute URLs for local files.
103
- # [+css+] Manually specify a CSS stylesheet.
125
+ # [+css+] Manually specify CSS stylesheets.
104
126
  # [+css_to_attributes+] Copy related CSS attributes into HTML attributes (e.g. +background-color+ to +bgcolor+)
105
127
  # [+css_string+] Pass CSS as a string
106
128
  # [+remove_ids+] Remove ID attributes whenever possible and convert IDs used as anchors to hashed to avoid collisions in webmail programs. Default is +false+.
107
129
  # [+remove_classes+] Remove class attributes. Default is +false+.
108
130
  # [+remove_comments+] Remove html comments. Default is +false+.
109
131
  # [+preserve_styles+] Whether to preserve any <tt>link rel=stylesheet</tt> and <tt>style</tt> elements. Default is +false+.
132
+ # [+preserve_reset+] Whether to preserve styles associated with the MailChimp reset code
110
133
  # [+with_html_string+] Whether the +html+ param should be treated as a raw string.
111
134
  # [+verbose+] Whether to print errors and warnings to <tt>$stderr</tt>. Default is +false+.
112
135
  # [+adapter+] Which HTML parser to use, either <tt>:nokogiri</tt> or <tt>:hpricot</tt>. Default is <tt>:hpricot</tt>.
@@ -123,6 +146,7 @@ class Premailer
123
146
  :with_html_string => false,
124
147
  :css_string => nil,
125
148
  :preserve_styles => false,
149
+ :preserve_reset => true,
126
150
  :verbose => false,
127
151
  :debug => false,
128
152
  :io_exceptions => false,
@@ -131,7 +155,7 @@ class Premailer
131
155
  @html_file = html
132
156
  @is_local_file = @options[:with_html_string] || Premailer.local_data?(html)
133
157
 
134
- @css_files = @options[:css]
158
+ @css_files = [@options[:css]].flatten
135
159
 
136
160
  @css_warnings = []
137
161
 
@@ -207,9 +231,19 @@ protected
207
231
  def load_css_from_html! # :nodoc:
208
232
  if tags = @doc.search("link[@rel='stylesheet'], style")
209
233
  tags.each do |tag|
210
- if tag.to_s.strip =~ /^\<link/i and tag.attributes['href'] and media_type_ok?(tag.attributes['media'])
211
-
212
- link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
234
+ if tag.to_s.strip =~ /^\<link/i && tag.attributes['href'] && media_type_ok?(tag.attributes['media'])
235
+ # A user might want to <link /> to a local css file that is also mirrored on the site
236
+ # but the local one is different (e.g. newer) than the live file, premailer will now choose the local file
237
+
238
+ if tag.attributes['href'].to_s.include? @base_url.to_s and @html_file.kind_of?(String)
239
+ link_uri = File.join(File.dirname(@html_file), tag.attributes['href'].to_s.sub!(@base_url.to_s, ''))
240
+ end
241
+
242
+ # if the file does not exist locally, try to grab the remote reference
243
+ if link_uri.nil? or not File.exists?(link_uri)
244
+ link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
245
+ end
246
+
213
247
  if Premailer.local_data?(link_uri)
214
248
  $stderr.puts "Loading css from local file: " + link_uri if @options[:verbose]
215
249
  load_css_from_local_file!(link_uri)
@@ -239,9 +273,9 @@ public
239
273
 
240
274
  def media_type_ok?(media_types) # :nodoc:
241
275
  return true if media_types.nil? or media_types.empty?
242
- return media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
276
+ media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
243
277
  rescue
244
- return true
278
+ true
245
279
  end
246
280
 
247
281
  def append_query_string(doc, qs)
@@ -261,7 +295,8 @@ public
261
295
  doc.search('a').each do|el|
262
296
  href = el.attributes['href'].to_s.strip
263
297
  next if href.nil? or href.empty?
264
- next if href[0,1] == '#' # don't bother with anchors
298
+
299
+ next if href[0,1] =~ /[\#\{\[\<\%]/ # don't bother with anchors or special-looking links
265
300
 
266
301
  begin
267
302
  href = URI.parse(href)
@@ -294,9 +329,8 @@ public
294
329
 
295
330
  # Check for an XHTML doctype
296
331
  def is_xhtml?
297
- intro = @doc.to_s.strip.split("\n")[0..2].join(' ')
298
- is_xhtml = (intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i)
299
- is_xhtml = is_xhtml ? true : false
332
+ intro = @doc.to_html.strip.split("\n")[0..2].join(' ')
333
+ is_xhtml = !!(intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i)
300
334
  $stderr.puts "Is XHTML? #{is_xhtml.inspect}\nChecked:\n#{intro}" if @options[:debug]
301
335
  is_xhtml
302
336
  end
@@ -322,7 +356,7 @@ public
322
356
  tags.each do |tag|
323
357
  # skip links that look like they have merge tags
324
358
  # and mailto, ftp, etc...
325
- if tag.attributes[attribute].to_s =~ /^(\{|\[|<|\#|data:|tel:|file:|sms:|callto:|facetime:|mailto:|ftp:|gopher:)/i
359
+ if tag.attributes[attribute].to_s =~ /^([\%\<\{\#\[]|data:|tel:|file:|sms:|callto:|facetime:|mailto:|ftp:|gopher:)/i
326
360
  next
327
361
  end
328
362
 
@@ -362,17 +396,16 @@ public
362
396
  resolved = nil
363
397
  if path =~ /(http[s]?|ftp):\/\//i
364
398
  resolved = path
365
- return Premailer.canonicalize(resolved)
399
+ Premailer.canonicalize(resolved)
366
400
  elsif base_path.kind_of?(URI)
367
401
  resolved = base_path.merge(path)
368
- return Premailer.canonicalize(resolved)
402
+ Premailer.canonicalize(resolved)
369
403
  elsif base_path.kind_of?(String) and base_path =~ /^(http[s]?|ftp):\/\//i
370
404
  resolved = URI.parse(base_path)
371
405
  resolved = resolved.merge(path)
372
- return Premailer.canonicalize(resolved)
406
+ Premailer.canonicalize(resolved)
373
407
  else
374
-
375
- return File.expand_path(path, File.dirname(base_path))
408
+ File.expand_path(path, File.dirname(base_path))
376
409
  end
377
410
  end
378
411
 
@@ -380,13 +413,9 @@ public
380
413
  #
381
414
  # IO objects return true, as do strings that look like URLs.
382
415
  def self.local_data?(data)
383
- if data.is_a?(IO) || data.is_a?(StringIO)
384
- return true
385
- elsif data =~ /^(http|https|ftp)\:\/\//i
386
- return false
387
- else
388
- return true
389
- end
416
+ return true if data.is_a?(IO) || data.is_a?(StringIO)
417
+ return false if data =~ /^(http|https|ftp)\:\/\//i
418
+ true
390
419
  end
391
420
 
392
421
  # from http://www.ruby-forum.com/topic/140101
@@ -404,7 +433,7 @@ public
404
433
 
405
434
  # Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
406
435
  def check_client_support # :nodoc:
407
- @client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
436
+ @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
408
437
 
409
438
  warnings = []
410
439
  properties = []
@@ -450,3 +479,4 @@ public
450
479
  return warnings
451
480
  end
452
481
  end
482
+
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This binary used only in development environment
4
+
5
+ require 'rubygems'
6
+ $LOAD_PATH.unshift(File.expand_path('./lib', File.dirname(__FILE__)))
7
+
8
+ require 'premailer/executor'
9
+
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "premailer"
3
+ s.version = "1.7.3"
4
+ s.date = Time.now.strftime('%Y-%m-%d')
5
+ s.summary = "Preflight for HTML e-mail."
6
+ s.email = "code@dunae.ca"
7
+ s.homepage = "http://premailer.dialect.ca/"
8
+ s.description = "Improve the rendering of HTML emails by making CSS inline, converting links and warning about unsupported code."
9
+ s.has_rdoc = true
10
+ s.author = "Alex Dunae"
11
+ s.rdoc_options << '--all' << '--inline-source' << '--line-numbers' << '--charset' << 'utf-8'
12
+ s.extra_rdoc_files = ["README.rdoc"]
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
16
+ s.add_dependency('css_parser', '>= 1.1.9')
17
+ s.add_dependency('htmlentities', '>= 4.0.0')
18
+ s.add_development_dependency('hpricot', '>= 0.8.3')
19
+ s.add_development_dependency('nokogiri', '>= 1.4.4')
20
+ s.add_development_dependency('rake', ['~> 0.8', '!= 0.9.0'])
21
+ s.add_development_dependency('rdoc', '>= 2.4.2')
22
+ end
@@ -0,0 +1,69 @@
1
+ $:.unshift File.expand_path('../lib', __FILE__)
2
+
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+ require 'rdoc/task'
6
+ require 'rubygems/package_task'
7
+ require 'fileutils'
8
+ require 'premailer'
9
+
10
+ def gemspec
11
+ @gemspec ||= begin
12
+ file = File.expand_path('../premailer.gemspec', __FILE__)
13
+ eval(File.read(file), binding, file)
14
+ end
15
+ end
16
+
17
+ Gem::PackageTask.new(gemspec) do |pkg|
18
+ pkg.need_tar = true
19
+ end
20
+
21
+ desc 'Default: parse a URL.'
22
+ task :default => [:inline]
23
+
24
+ desc 'Parse a URL and write out the output.'
25
+ task :inline do
26
+ url = ENV['url']
27
+ output = ENV['output']
28
+
29
+ if !url or url.empty? or !output or output.empty?
30
+ puts 'Usage: rake inline url=http://example.com/ output=output.html'
31
+ exit
32
+ end
33
+
34
+ premailer = Premailer.new(url, :warn_level => Premailer::Warnings::SAFE, :verbose => true, :adapter => :nokogiri)
35
+ fout = File.open(output, "w")
36
+ fout.puts premailer.to_inline_css
37
+ fout.close
38
+
39
+ puts "Succesfully parsed '#{url}' into '#{output}'"
40
+ puts premailer.warnings.length.to_s + ' CSS warnings were found'
41
+ end
42
+
43
+ task :text do
44
+ url = ENV['url']
45
+ output = ENV['output']
46
+
47
+ if !url or url.empty? or !output or output.empty?
48
+ puts 'Usage: rake text url=http://example.com/ output=output.txt'
49
+ exit
50
+ end
51
+
52
+ premailer = Premailer.new(url, :warn_level => Premailer::Warnings::SAFE)
53
+ fout = File.open(output, "w")
54
+ fout.puts premailer.to_plain_text
55
+ fout.close
56
+
57
+ puts "Succesfully parsed '#{url}' into '#{output}'"
58
+ end
59
+
60
+ Rake::TestTask.new do |t|
61
+ t.test_files = FileList['test/test_*.rb']
62
+ t.verbose = false
63
+ end
64
+
65
+ RDoc::Task.new do |rd|
66
+ rd.main = "README.rdoc"
67
+ rd.rdoc_files.include("README.rdoc", "LICENSE.rdoc", "lib/**/*.rb")
68
+ rd.title = 'Premailer Documentation'
69
+ end
@@ -0,0 +1,142 @@
1
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
2
+ <!--
3
+
4
+ You can read this newsletter online at
5
+ [webversion]
6
+
7
+ -->
8
+ <html>
9
+ <head>
10
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
11
+ <title>Premailer Test</title>
12
+ <link rel="stylesheet" type="text/css" href="styles.css">
13
+ <style type="text/css">
14
+ @import "import.css" screen, handheld;
15
+ </style>
16
+ <style type="text/css">
17
+ @import "noimport.css" print;
18
+ </style>
19
+ <style type="text/css">
20
+ @media only screen and (max-device-width: 480px) {
21
+ #iphone { display: block; }
22
+ }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div id="wrapper">
27
+ <p class="hide" id="hide01">This line should be hidden.</p>
28
+ <p class="hide" id="iphone">This is an iPhone style.</p>
29
+ <table width="646" class="container" cellspacing="0" cellpadding="0">
30
+ <tr><td id="webversion" colspan="6">Having trouble reading this newsletter? <webversion>Click here to see it in your browser</webversion></td></tr>
31
+
32
+ <tr><td height="13" colspan="6" class="frame">&#x00a0;</td></tr>
33
+
34
+
35
+
36
+ <table width="646" class="container" cellspacing="0" cellpadding="0">
37
+
38
+ <tr>
39
+ <td class="frame" width="13">&#x00a0;</td>
40
+ <td class="gutter" width="60">&#x00a0;</td>
41
+
42
+ <td class="content" colspan="2" width="500">
43
+
44
+ <h1><span>Premailer Test</span></h1>
45
+
46
+ <table width="500" cellpadding="0" cellspacing="0">
47
+ <tr>
48
+ <td width="20">&#x00a0;</td>
49
+ <td colspan="2" width="460">
50
+ <h2>Lorem ipsum dolor</h2>
51
+ <h3>Suspendisse id velit vitae ligula volutpat condimentum</h3>
52
+ <p class="dt">Morbi commodo, ipsum sed</p>
53
+
54
+
55
+ <p class="unaligned"><img src="2009-placeholder.png" alt="Image" align="right" class="right">Lorem&nbsp;ipsum&#x00a0;dolor sit amet, consectetuer adipiscing elit. Morbi commodo, ipsum sed pharetra gravida, orci magna rhoncus neque, id pulvinar odio lorem non turpis. Nullam sit amet enim. Suspendisse id velit vitae ligula volutpat condimentum. Aliquam erat volutpat. Sed quis velit. <a href="http://premailer.dialect.ca/">Nulla facilisi</a>. Nulla libero.</p>
56
+
57
+ <p attr="another quote">Here&rsquo;s a quote. Here’s a quote. &#x201c;Here’s a quote in quotes”.</p>
58
+
59
+ <p>Nullam sit amet enim. Suspendisse id velit vitae ligula volutpat condimentum. Aliquam erat volutpat. Sed quis velit. Nulla facilisi.</p>
60
+ <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi commodo, ipsum sed pharetra gravida, orci magna rhoncus neque, id pulvinar odio lorem non turpis. Nullam sit amet enim. Suspendisse id velit vitae ligula volutpat condimentum. Aliquam erat volutpat. Sed quis velit. Nulla facilisi. Nulla libero.</p>
61
+ <blockquote><p>“Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi commodo, ipsum sed pharetra gravida, orci magna rhoncus neque, id pulvinar odio lorem non turpis.”</p></blockquote>
62
+ <p>Aliquam erat volutpat. Sed quis velit. Nulla facilisi. Nulla libero.</p>
63
+
64
+ <h3>Link tests</h3>
65
+ <ul>
66
+ <li><a id="l01" href="/">Relative path to root</a></li>
67
+ <li><a id="l02" href="http://premailer.dialect.ca/">Absolute path to root</a></li>
68
+ <li><a id="l03" href="http://example.com/">Different domain</a></li>
69
+ <li><a id="l04" href="images/">Relative path to sub-directory</a></li>
70
+ <li><a id="l05" href="#relative">Link is not converted</a></li>
71
+ <li><a id="l06" href="http://example.com/test.html?cn=tf&amp;c=20&amp;ord=%%RANDOM%%">Funky ASP URL</a></li>
72
+ <li><a id="l07" href="?query=string">Appends tracking query string</a></li>
73
+ <li><a id="l08" href="{DONOTCONVERT}">Link is not converted</a></li>
74
+ <li><a id="l09" href="[DONOTCONVERT]">Link is not converted</a></li>
75
+ <li><a id="l10" href="<DONOTCONVERT>">Link is not converted</a></li>
76
+ <li><a id="l11" href="mailto:premailer@example.com">mailto link</a></li>
77
+ <li><a id="l12" href="ftp://example.com">FTP link</a></li>
78
+ <li><a id="l13" href="gopher://gopher.floodgap.com/1/fun/twitpher">Gopher link</a></li>
79
+ </ul>
80
+
81
+
82
+
83
+ <p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi commodo, ipsum sed pharetra gravida, orci magna rhoncus neque, id pulvinar odio lorem non turpis.</p>
84
+
85
+ <p>&nbsp;</p>
86
+
87
+
88
+ </td>
89
+ <td width="20">&#x00a0;</td>
90
+ </tr>
91
+ </table>
92
+
93
+ <p class="section"><img src="dots_end.png" alt="---" width="499" height="75"></p>
94
+ </td><!-- /#content -->
95
+ <td class="gutter" width="60">&#x00a0;</td>
96
+
97
+ <td class="frame" width="13">&#x00a0;</td>
98
+ </tr>
99
+
100
+ <tr>
101
+ <td class="frame" width="13">&#x00a0;</td>
102
+ <td colspan="4" width="620">
103
+ <table summary="Contact information" cellspacing="0" cellpadding="0" width="620">
104
+ <tr><td height="4" class="hairline">&#x00a0;</td></tr>
105
+ <tr><td height="34"class="contact">&#x00a0;</td></tr>
106
+ <tr>
107
+
108
+ <td align="center" class="contact" id="contact_info">
109
+ <p id="address">Premailer Test<br>
110
+ <a href="http://dialect.ca/?utm_source=Premailer&utm_medium=Test+Suite&utm_campaign=Premailer">by Dialect</a><br>
111
+ Vancouver Island, British Columbia<br>
112
+ 250 555.2222</p>
113
+ </td>
114
+
115
+ </tr>
116
+ <tr><td height="34"class="contact">&#x00a0;</td></tr>
117
+ <tr><td height="4" class="hairline">&#x00a0;</td></tr>
118
+ </table>
119
+ </td>
120
+ <td class="frame" width="13">&#x00a0;</td>
121
+ </tr>
122
+
123
+ <tr>
124
+ <td class="frame" width="13">&#x00a0;</td>
125
+
126
+ <td colspan="4" class="content" height="60">&#x00a0;</td>
127
+ <td class="frame" width="13">&#x00a0;</td>
128
+ </tr>
129
+
130
+
131
+
132
+
133
+
134
+ <tr><td height="13" colspan="6" class="frame">&#x00a0;</td></tr>
135
+ <tr><td height="22" colspan="6" >&#x00a0;</td></tr>
136
+ <tr><td id="credit" colspan="6">Newsletter communications by<br><a href="http://dialect.ca/dialogue/?utm_source=Dialogue&utm_medium=Credit&utm_campaign=South+Hollow"><img src="inc/dialect.png" alt="Dialect" width="60" height="35" border="0"></a><br><unsubscribe>Click here to unsubscribe</unsubscribe></td></tr>
137
+
138
+ </table>
139
+
140
+ </div>
141
+ </body>
142
+ </html>