premailer 1.5.2

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.
@@ -0,0 +1,49 @@
1
+ = Premailer CHANGELOG
2
+
3
+ == Version 1.5.2
4
+ * released to GitHub
5
+ * fixed handling of mailto links
6
+ * various minor updates
7
+
8
+ == Version 1.5.1
9
+ * 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
10
+ * bugfix (http://code.google.com/p/premailer/issues/detail?id=4) thanks to Dave Holmes
11
+
12
+ == Version 1.5.0
13
+ * preview release of Ruby gem
14
+
15
+ == Version 1.4
16
+ * incremental parsing improvements
17
+ * respect <tt>@media</tt> rule (http://www.w3.org/TR/CSS21/media.html#at-media-rule)
18
+ * better quote escaping
19
+
20
+ == Version 1.3
21
+ * separate CSS parser into its own library
22
+ * handle <tt>background: red url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4%2F58BAAT%2FAf9jgNErAAAAAElFTkSuQmCC);</tt>
23
+ * preserve <tt>:hover</tt> etc... in head styles
24
+
25
+ == Version 1.2
26
+ * respect <tt>LINK</tt> media types
27
+ * better style folding
28
+ * incremental parsing improvements
29
+
30
+ == Version 1.1
31
+ * proper calculation of selector specificity per CSS 2.1 spec
32
+ * support for <tt>@import</tt>
33
+ * preliminary support for shorthand CSS properties (<tt>margin</tt>, <tt>padding</tt>)
34
+ * preliminary separation of CSS parser
35
+
36
+ == Version 1.0
37
+ * ported web interface to eRuby
38
+ * incremental parsing improvements
39
+
40
+ == Version 0.9
41
+ * initial proof-of-concept
42
+ * PHP web version
43
+
44
+ == TODO: Future
45
+ * complete shorthand properties support (<tt>border-width</tt>, <tt>font</tt>, <tt>background</tt>)
46
+ * UTF-8 and other charsets (test page: http://kianga.kcore.de/2004/09/21/utf8_test)
47
+ * make warnings for <tt>border</tt> match <tt>border-left</tt>, etc...
48
+ * Integrate CSS validator
49
+ * Remove unused classes and IDs
@@ -0,0 +1,42 @@
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.
@@ -0,0 +1,66 @@
1
+ = Premailer README
2
+
3
+ === What is this?
4
+
5
+ For the best HTML e-mail delivery results, CSS should be inline. This is a
6
+ huge pain and a simple newsletter becomes un-managable very quickly. This
7
+ script is my solution.
8
+
9
+ * CSS styles are converted to inline style attributes
10
+ Checks style and link[rel=stylesheet] tags and preserves existing inline attributes
11
+ * Relative paths are converted to absolute paths
12
+ Checks links in href, src and CSS url('')
13
+ * CSS properties are checked against e-mail client capabilities
14
+ Based on the Email Standards Project's guides
15
+ * A plain text version is created
16
+ Optional
17
+
18
+
19
+ === Installation
20
+
21
+ Download the Premailer gem from GemCutter.
22
+
23
+ gem sources -a http://gemcutter.org
24
+ sudo gem install premailer
25
+
26
+ === Example
27
+ premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)
28
+
29
+ # Write the HTML output
30
+ fout = File.open("output.html", "w")
31
+ fout.puts premailer.to_inline_css
32
+ fout.close
33
+
34
+ # Write the plain-text output
35
+ fout = File.open("ouput.txt", "w")
36
+ fout.puts premailer.to_plain_text
37
+ fout.close
38
+
39
+ # Output any CSS warnings
40
+ premailer.warnings.each do |w|
41
+ puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
42
+ end
43
+
44
+ === Contributions
45
+
46
+ Contributions are most welcome. Premailer was rotting away in a private SVN repository for too long and could use some TLC. Pull and patch to your heart's content.
47
+
48
+ A few areas that are particularly in need of love:
49
+ * Testing suite
50
+ 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
+ * Create a binary file for easing command line use, allowing the output to be piped in *nix systems
53
+ * Ruby 1.9 testing
54
+ * Test with Rails
55
+ * Move un-repeated background images defined in CSS to <tt><td background=""></tt> for Outlook
56
+ * Correctly parse http://www.webstandards.org/files/acid2/test.html
57
+
58
+ === Credits and code
59
+
60
+ Premailer is written in Ruby.
61
+
62
+ The web interface can be found at http://premailer.dialect.ca/ .
63
+
64
+ The source code can be found at http://github.com/alexdunae/premailer .
65
+
66
+ Written by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-2009.
@@ -0,0 +1,9 @@
1
+ # Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-09
2
+
3
+ require 'yaml'
4
+ require 'open-uri'
5
+ require 'hpricot'
6
+ require 'css_parser'
7
+
8
+ require File.dirname(__FILE__) + "/premailer/html_to_plain_text"
9
+ require File.dirname(__FILE__) + "/premailer/premailer"
@@ -0,0 +1,58 @@
1
+ require 'text/reform'
2
+ require 'htmlentities'
3
+
4
+ # Support functions for Premailer
5
+ module HtmlToPlainText
6
+
7
+ # Returns the text in UTF-8 format with all HTML tags removed
8
+ #
9
+ # TODO:
10
+ # - add support for DL, OL
11
+ def convert_to_text(html, line_length, from_charset = 'UTF-8')
12
+ r = Text::Reform.new(:trim => true,
13
+ :squeeze => false,
14
+ :break => Text::Reform.break_wrap)
15
+
16
+ txt = html
17
+
18
+ he = HTMLEntities.new # decode HTML entities
19
+
20
+ txt = he.decode(txt)
21
+
22
+ txt.gsub!(/<h([0-9]+)[^>]*>(.*)<\/h[0-9]+>/i) do |s| # handle headings
23
+ hlevel = $1.to_i
24
+ htext = $2.gsub(/<\/?[^>]*>/i, '') # remove tags inside headings
25
+ hlength = (htext.length > line_length ?
26
+ line_length :
27
+ htext.length)
28
+
29
+ case hlevel
30
+ when 1 # H1
31
+ ('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength) + "\n"
32
+ when 2 # H2
33
+ ('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength) + "\n"
34
+ else # H3-H6 are styled the same
35
+ htext + "\n" + ('-' * htext.length) + "\n"
36
+ end
37
+ end
38
+
39
+ txt.gsub!(/<a.*href=\"([^\"]*)\"[^>]*>(.*)<\/a>/i) do |s| # links
40
+ $2 + ' [' + $1 + ']'
41
+ end
42
+
43
+ txt.gsub!(/(<li[\s]+[^>]*>|<li>)/i, ' * ') # unordered LIsts
44
+ txt.gsub!(/<\/p>/i, "\n\n") # paragraphs
45
+
46
+ txt.gsub!(/<\/?[^>]*>/, '') # strip remaining tags
47
+ txt.gsub!(/\A[\s]+|[\s]+\Z|^[ \t]+/m, '') # strip extra spaces
48
+ txt.gsub!(/[\n]{3,}/m, "\n\n") # tighten line breaks
49
+
50
+ txt = r.format(('[' * line_length), txt) # wrap text
51
+ txt.gsub!(/^[\*][\s]/m, ' * ') # add spaces back to lists
52
+
53
+ txt.gsub!(/^\s+$/, "\n") # \r\n and \r -> \n
54
+ txt.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
55
+ txt.gsub!(/[\n]{3,}/, "\n")
56
+ txt
57
+ end
58
+ end
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-09
4
+ #
5
+ # Premailer processes HTML and CSS to improve e-mail deliverability.
6
+ #
7
+ # Premailer's main function is to render all CSS as inline <tt>style</tt>
8
+ # attributes. It also converts relative links to absolute links and checks
9
+ # the 'safety' of CSS properties against a CSS support chart.
10
+ #
11
+ # = Example
12
+ # premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)
13
+ #
14
+ # # Write the HTML output
15
+ # fout = File.open("output.html", "w")
16
+ # fout.puts premailer.to_inline_css
17
+ # fout.close
18
+ #
19
+ # # Write the plain-text output
20
+ # fout = File.open("ouput.txt", "w")
21
+ # fout.puts premailer.to_plain_text
22
+ # fout.close
23
+ #
24
+ # # List any CSS warnings
25
+ # puts premailer.warnings.length.to_s + ' warnings found'
26
+ # premailer.warnings.each do |w|
27
+ # puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
28
+ # end
29
+ #
30
+ # premailer = Premailer.new(html_file, :warn_level => Premailer::Warnings::SAFE)
31
+ # puts premailer.to_inline_css
32
+ class Premailer
33
+ include HtmlToPlainText
34
+ include CssParser
35
+
36
+ VERSION = '1.5.2'
37
+
38
+ CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
39
+
40
+ RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
41
+
42
+ # should also exclude :first-letter, etc...
43
+
44
+ # URI of the HTML file used
45
+ attr_reader :html_file
46
+
47
+ module Warnings
48
+ NONE = 0
49
+ SAFE = 1
50
+ POOR = 2
51
+ RISKY = 3
52
+ end
53
+ include Warnings
54
+
55
+ WARN_LABEL = %w(NONE SAFE POOR RISKY)
56
+
57
+ # Create a new Premailer object.
58
+ #
59
+ # +path+ is the path to the HTML file to process. Can be either the URL of a
60
+ # remote file or a local path.
61
+ #
62
+ # ==== Options
63
+ # [+line_length+] Line length used by to_plain_text. Boolean, default is 65.
64
+ # [+warn_level+] What level of CSS compatibility warnings to show (see Warnings).
65
+ # [+link_query_string+] A string to append to every <a href=""> link. Do not include the initial +?+.
66
+ # [+base_url+] Used to calculate absolute URLs for local files.
67
+ def initialize(path, options = {})
68
+ @options = {:warn_level => Warnings::SAFE,
69
+ :line_length => 65,
70
+ :link_query_string => nil,
71
+ :base_url => nil,
72
+ :remove_classes => false}.merge(options)
73
+ @html_file = path
74
+
75
+ @is_local_file = true
76
+ if path =~ /^(http|https|ftp)\:\/\//i
77
+ @is_local_file = false
78
+ end
79
+
80
+ @css_warnings = []
81
+
82
+ @css_parser = CssParser::Parser.new({:absolute_paths => true,
83
+ :import => true,
84
+ :io_exceptions => false
85
+ })
86
+
87
+ @doc, @html_charset = load_html(@html_file)
88
+
89
+ if @is_local_file and @options[:base_url]
90
+ @doc = convert_inline_links(@doc, @options[:base_url])
91
+ elsif not @is_local_file
92
+ @doc = convert_inline_links(@doc, @html_file)
93
+ end
94
+ load_css_from_html!
95
+ end
96
+
97
+ # Array containing a hash of CSS warnings.
98
+ def warnings
99
+ return [] if @options[:warn_level] == Warnings::NONE
100
+ @css_warnings = check_client_support if @css_warnings.empty?
101
+ @css_warnings
102
+ end
103
+
104
+ # Returns the original HTML as a string.
105
+ def to_s
106
+ @doc.to_html
107
+ end
108
+
109
+ # Converts the HTML document to a format suitable for plain-text e-mail.
110
+ #
111
+ # Returns a string.
112
+ def to_plain_text
113
+ html_src = ''
114
+ begin
115
+ html_src = @doc.search("body").innerHTML
116
+ rescue
117
+ html_src = @doc.to_html
118
+ end
119
+ convert_to_text(html_src, @options[:line_length], @html_charset)
120
+ end
121
+
122
+ # Merge CSS into the HTML document.
123
+ #
124
+ # Returns a string.
125
+ def to_inline_css
126
+ doc = @doc
127
+ unmergable_rules = CssParser::Parser.new
128
+
129
+ # Give all styles already in style attributes a specificity of 1000
130
+ # per http://www.w3.org/TR/CSS21/cascade.html#specificity
131
+ doc.search("*[@style]").each do |el|
132
+ el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
133
+ end
134
+
135
+ # Iterate through the rules and merge them into the HTML
136
+ @css_parser.each_selector(:all) do |selector, declaration, specificity|
137
+ # Save un-mergable rules separately
138
+ selector.gsub!(/:link([\s]|$)+/i, '')
139
+
140
+ # Convert element names to lower case
141
+ selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
142
+
143
+ if selector =~ RE_UNMERGABLE_SELECTORS
144
+ unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
145
+ else
146
+
147
+ doc.search(selector) do |el|
148
+ if el.elem?
149
+ # Add a style attribute or append to the existing one
150
+ block = "[SPEC=#{specificity}[#{declaration}]]"
151
+ el['style'] = (el.attributes['style'] ||= '') + ' ' + block
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ # Read <style> attributes and perform folding
158
+ doc.search("*[@style]").each do |el|
159
+ style = el.attributes['style'].to_s
160
+
161
+ declarations = []
162
+
163
+ style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
164
+ rs = RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
165
+ declarations << rs
166
+ end
167
+
168
+ # Perform style folding and save
169
+ merged = CssParser.merge(declarations)
170
+
171
+ el['style'] = Premailer.escape_string(merged.declarations_to_s)
172
+ end
173
+
174
+ doc = write_unmergable_css_rules(doc, unmergable_rules)
175
+
176
+ doc.search('*').remove_class if @options[:remove_classes]
177
+
178
+ doc.to_html
179
+ end
180
+
181
+
182
+ protected
183
+ # Load the HTML file and convert it into an Hpricot document.
184
+ #
185
+ # Returns an Hpricot document and a string with the HTML file's character set.
186
+ def load_html(path) # :nodoc:
187
+ if @is_local_file
188
+ Hpricot(File.open(path, "r") {|f| f.read })
189
+ else
190
+ Hpricot(open(path))
191
+ end
192
+ end
193
+
194
+ # Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
195
+ def load_css_from_html! # :nodoc:
196
+ if tags = @doc.search("link[@rel='stylesheet'], style")
197
+ tags.each do |tag|
198
+
199
+ if tag.to_s.strip =~ /^\<link/i and tag.attributes['href'] and media_type_ok?(tag.attributes['media'])
200
+
201
+ link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
202
+ if @is_local_file
203
+ css_block = ''
204
+ begin
205
+ File.open(link_uri, "r") do |file|
206
+ while line = file.gets
207
+ css_block << line
208
+ end
209
+ end
210
+ @css_parser.add_block!(css_block, {:base_uri => @html_file})
211
+ rescue; end
212
+ else
213
+ @css_parser.load_uri!(link_uri)
214
+ end
215
+
216
+ elsif tag.to_s.strip =~ /^\<style/i
217
+ @css_parser.add_block!(tag.innerHTML, :base_uri => URI.parse(@html_file))
218
+ end
219
+ end
220
+ tags.remove
221
+ end
222
+ end
223
+
224
+ def media_type_ok?(media_types) # :nodoc:
225
+ return media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
226
+ rescue
227
+ return true
228
+ end
229
+
230
+ # Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
231
+ # and write it into the <tt>body</tt>.
232
+ #
233
+ # <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
234
+ #
235
+ # Returns an Hpricot document.
236
+ def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
237
+ styles = ''
238
+ unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
239
+ styles += "#{selector} { #{declarations} }\n"
240
+ end
241
+
242
+ unless styles.empty?
243
+ style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
244
+ doc.search("head").append(style_tag)
245
+ end
246
+ doc
247
+ end
248
+
249
+ # Convert relative links to absolute links.
250
+ #
251
+ # Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
252
+ # as well as CSS <tt>url()</tt> declarations found in inline <tt>style</tt> attributes.
253
+ #
254
+ # <tt>doc</tt> is an Hpricot document and <tt>base_uri</tt> is either a string or a URI.
255
+ #
256
+ # Returns an Hpricot document.
257
+ def convert_inline_links(doc, base_uri) # :nodoc:
258
+ base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
259
+
260
+ append_qs = @options[:link_query_string] ||= ''
261
+
262
+ ['href', 'src', 'background'].each do |attribute|
263
+ tags = doc.search("*[@#{attribute}]")
264
+
265
+ next if tags.empty?
266
+
267
+ tags.each do |tag|
268
+
269
+ # skip links that look like they have merge tags
270
+ # and mailto, ftp, etc...
271
+ if tag.attributes[attribute] =~ /^(\{|\[|<|\#|mailto:|ftp:|gopher:)/i
272
+ next
273
+ end
274
+
275
+ if tag.attributes[attribute] =~ /^http/i
276
+ begin
277
+ merged = URI.parse(tag.attributes[attribute])
278
+ rescue; next; end
279
+ else
280
+ begin
281
+ merged = Premailer.resolve_link(tag.attributes[attribute].to_s, base_uri)
282
+ rescue
283
+ begin
284
+ merged = Premailer.resolve_link(URI.escape(tag.attributes[attribute].to_s), base_uri)
285
+ rescue; end
286
+ end
287
+ end
288
+
289
+ # make sure 'merged' is a URI
290
+ merged = URI.parse(merged.to_s) unless merged.kind_of?(URI)
291
+
292
+ # only append a querystring to <a> tags
293
+ if tag.name =~ /^a$/i and not append_qs.empty?
294
+ if merged.query
295
+ merged.query = merged.query + '&' + append_qs
296
+ else
297
+ merged.query = append_qs
298
+ end
299
+ end
300
+ tag[attribute] = merged.to_s
301
+
302
+ end # end of each tag
303
+ end # end of each attrs
304
+
305
+ doc.search("*[@style]").each do |el|
306
+ el['style'] = CssParser.convert_uris(el.attributes['style'].to_s, base_uri)
307
+ end
308
+ doc
309
+ end
310
+
311
+ def self.escape_string(str) # :nodoc:
312
+ str.gsub(/"/, "'")
313
+ end
314
+
315
+ def self.resolve_link(path, base_path) # :nodoc:
316
+ resolved = nil
317
+ if base_path.kind_of?(URI)
318
+ resolved = base_path.merge(path)
319
+ return Premailer.canonicalize(resolved)
320
+ elsif base_path.kind_of?(String) and base_path =~ /^(http[s]?|ftp):\/\//i
321
+ resolved = URI.parse(base_path)
322
+ resolved = resolved.merge(path)
323
+ return Premailer.canonicalize(resolved)
324
+ else
325
+
326
+ return File.expand_path(path, File.dirname(base_path))
327
+ end
328
+ end
329
+
330
+ # from http://www.ruby-forum.com/topic/140101
331
+ def self.canonicalize(uri) # :nodoc:
332
+ u = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
333
+ u.normalize!
334
+ newpath = u.path
335
+ while newpath.gsub!(%r{([^/]+)/\.\./?}) { |match|
336
+ $1 == '..' ? match : ''
337
+ } do end
338
+ newpath = newpath.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/')
339
+ u.path = newpath
340
+ u.to_s
341
+ end
342
+
343
+ # Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
344
+ def check_client_support # :nodoc:
345
+ @client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
346
+
347
+ warnings = []
348
+ properties = []
349
+
350
+ # Get a list off CSS properties
351
+ @doc.search("*[@style]").each do |el|
352
+ style_url = el.attributes['style'].gsub(/([\w\-]+)[\s]*\:/i) do |s|
353
+ properties.push($1)
354
+ end
355
+ end
356
+
357
+ properties.uniq!
358
+
359
+ property_support = @client_support['css_properties']
360
+ properties.each do |prop|
361
+ if property_support.include?(prop) and
362
+ property_support[prop].include?('support') and
363
+ property_support[prop]['support'] >= @options[:warn_level]
364
+ warnings.push({:message => "#{prop} CSS property",
365
+ :level => WARN_LABEL[property_support[prop]['support']],
366
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
367
+ end
368
+ end
369
+
370
+ @client_support['attributes'].each do |attribute, data|
371
+ next unless data['support'] >= @options[:warn_level]
372
+ if @doc.search("*[@#{attribute}]").length > 0
373
+ warnings.push({:message => "#{attribute} HTML attribute",
374
+ :level => WARN_LABEL[property_support[prop]['support']],
375
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
376
+ end
377
+ end
378
+
379
+ @client_support['elements'].each do |element, data|
380
+ next unless data['support'] >= @options[:warn_level]
381
+ if @doc.search("element").length > 0
382
+ warnings.push({:message => "#{element} HTML element",
383
+ :level => WARN_LABEL[property_support[prop]['support']],
384
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
385
+ end
386
+ end
387
+
388
+ return warnings
389
+ end
390
+ end
391
+
392
+
393
+
@@ -0,0 +1,230 @@
1
+ # Capabilities of e-mail clients
2
+ #
3
+ # Sources
4
+ # * http://campaignmonitor.com/css/
5
+ # * http://www.campaignmonitor.com/blog/archives/2007/04/a_guide_to_css_support_in_emai_2.html
6
+ # * http://www.campaignmonitor.com/blog/archives/2007/11/do_image_maps_work_in_html_ema.html
7
+ # * http://www.campaignmonitor.com/blog/archives/2007/11/how_forms_perform_in_html_emai.html
8
+ # * http://www.xavierfrenette.com/articles/css-support-in-webmail/
9
+ # * http://www.email-standards.org/
10
+ # Updated 2008-08-26
11
+ #
12
+ # Support: 1 = SAFE, 2 = POOR, 3 = RISKY
13
+ elements:
14
+ map:
15
+ support: 2
16
+ unsupported_in: [GMail]
17
+ area:
18
+ support: 2
19
+ unsupported_in: [GMail]
20
+ form:
21
+ support: 3
22
+ unsupported_in: [Mobile Me, Old Yahoo, AOL, Live Mail, Outlook 07, Outlook 03]
23
+ link:
24
+ support: 2
25
+ unsupported_in: [GMail, Hotmail, Old Yahoo]
26
+ attributes:
27
+ ismap:
28
+ support: 2
29
+ unsupported_in: [GMail]
30
+ css_properties:
31
+ color:
32
+ unsupported_in: [Eudora]
33
+ support_level: 92%
34
+ support: 1
35
+ font-size:
36
+ unsupported_in: [Eudora]
37
+ support_level: 92%
38
+ support: 1
39
+ font-style:
40
+ unsupported_in: [Eudora]
41
+ support_level: 92%
42
+ support: 1
43
+ font-weight:
44
+ unsupported_in: [Eudora]
45
+ support_level: 92%
46
+ support: 1
47
+ text-align:
48
+ unsupported_in: [Eudora]
49
+ support_level: 92%
50
+ support: 1
51
+ text-decoration:
52
+ unsupported_in: [Eudora]
53
+ support_level: 92%
54
+ support: 1
55
+ background-color:
56
+ unsupported_in: [Notes 6, Eudora]
57
+ support_level: 85%
58
+ support: 2
59
+ border: &border_shorthand
60
+ unsupported_in: [Notes 6, Eudora]
61
+ support_level: 85%
62
+ support: 2
63
+ border-bottom: *border_shorthand
64
+ border-left: *border_shorthand
65
+ border-right: *border_shorthand
66
+ border-top: *border_shorthand
67
+ display:
68
+ unsupported_in: [Outlook 07, Eudora]
69
+ support_level: 85%
70
+ support: 2
71
+ font-family:
72
+ unsupported_in: [Eudora, Old GMail, New GMail]
73
+ support_level: 92%
74
+ support: 2
75
+ font-variant:
76
+ unsupported_in: [Notes 6, Eudora]
77
+ support_level: 85%
78
+ support: 2
79
+ letter-spacing:
80
+ unsupported_in: [Notes 6, Eudora]
81
+ support_level: 85%
82
+ support: 2
83
+ line-height:
84
+ unsupported_in: [Notes 6, Eudora]
85
+ support_level: 85%
86
+ support: 2
87
+ padding: &padding_shorthand
88
+ unsupported_in: [Notes 6, Eudora]
89
+ support_level: 85%
90
+ support: 2
91
+ padding-bottom: *padding_shorthand
92
+ padding-left: *padding_shorthand
93
+ padding-right: *padding_shorthand
94
+ padding-top: *padding_shorthand
95
+ table-layout:
96
+ unsupported_in: [Notes 6, Eudora]
97
+ support_level: 85%
98
+ support: 2
99
+ text-indent:
100
+ unsupported_in: [Notes 6, Eudora]
101
+ support_level: 85%
102
+ support: 2
103
+ text-transform:
104
+ unsupported_in: [Notes 6, Eudora]
105
+ support_level: 85%
106
+ support: 2
107
+ border-collapse:
108
+ unsupported_in: [Entourage 2004, Notes 6, Eudora]
109
+ support_level: 77%
110
+ support: 3
111
+ clear:
112
+ unsupported_in: [Outlook 07, Notes 6, Eudora]
113
+ support_level: 77%
114
+ support: 3
115
+ direction:
116
+ unsupported_in: [Outlook 07, Entourage 2004, Eudora, New GMail]
117
+ support_level: 77%
118
+ support: 3
119
+ float:
120
+ unsupported_in: [Outlook 07, Eudora, Old GMail]
121
+ support_level: 85%
122
+ support: 3
123
+ vertical-align:
124
+ unsupported_in: [Outlook 07, Notes 6, Eudora]
125
+ support_level: 77%
126
+ support: 3
127
+ width:
128
+ unsupported_in: [Outlook 07, Notes 6, Eudora]
129
+ support_level: 77%
130
+ support: 3
131
+ word-spacing:
132
+ unsupported_in: [Outlook 07, Notes 6, Eudora]
133
+ support_level: 77%
134
+ support: 3
135
+ height:
136
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail]
137
+ support_level: 77%
138
+ support: 3
139
+ list-style-type:
140
+ unsupported_in: [Outlook 07, Eudora, Hotmail]
141
+ support_level: 85%
142
+ support: 3
143
+ overflow:
144
+ unsupported_in: [Outlook 07, Entourage 2004, Notes 6, Eudora]
145
+ support_level: 69%
146
+ support: 3
147
+ visibility:
148
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail, New GMail, aolWeb]
149
+ support_level: 77%
150
+ support: 3
151
+ white-space:
152
+ unsupported_in: [Outlook 03, Windows Mail, AOL 9, AOL 10, Notes 6, Eudora, Mobile Me]
153
+ support_level: 54%
154
+ support: 3
155
+ background-image:
156
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail, New GMail, Live Mail]
157
+ support_level: 77%
158
+ support: 3
159
+ background-repeat:
160
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail, New GMail, Live Mail]
161
+ support_level: 77%
162
+ support: 3
163
+ clip:
164
+ unsupported_in: [Outlook 07, Notes 6, Eudora, New Yahoo, New GMail, Live Mail, Mobile Me]
165
+ support_level: 77%
166
+ support: 3
167
+ cursor:
168
+ unsupported_in: [Outlook 07, Entourage 2004, Notes 6, Eudora, Old GMail, New GMail]
169
+ support_level: 69%
170
+ support: 3
171
+ list-style-image:
172
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old GMail, New GMail, Live Mail]
173
+ support_level: 77%
174
+ support: 3
175
+ list-style-position:
176
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old Yahoo, Hotmail]
177
+ support_level: 77%
178
+ support: 3
179
+ margin: &margin_shorthand
180
+ unsupported_in: [AOL 9, Notes 6, Eudora, Live Mail, Hotmail]
181
+ support_level: 77%
182
+ support: 3
183
+ margin-bottom: *margin_shorthand
184
+ margin-left: *margin_shorthand
185
+ margin-right: *margin_shorthand
186
+ margin-top: *margin_shorthand
187
+ z-index:
188
+ unsupported_in: [Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
189
+ support_level: 85%
190
+ support: 3
191
+ left:
192
+ unsupported_in: [Outlook 07, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
193
+ support_level: 77%
194
+ support: 3
195
+ right:
196
+ unsupported_in: [Outlook 07, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
197
+ support_level: 77%
198
+ support: 3
199
+ top:
200
+ unsupported_in: [Outlook 07, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
201
+ support_level: 77%
202
+ support: 3
203
+ background-position:
204
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old Yahoo, Old GMail, New GMail, Live Mail, Hotmail]
205
+ support_level: 77%
206
+ support: 3
207
+ border-spacing:
208
+ unsupported_in: [Outlook 03, Outlook 07, Windows Mail, Entourage 2004, AOL 10, Notes 6, Eudora, Live Mail, Hotmail]
209
+ support_level: 46%
210
+ support: 3
211
+ bottom:
212
+ unsupported_in: [Outlook 07, AOL 9, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail]
213
+ support_level: 69%
214
+ support: 3
215
+ empty-cells:
216
+ unsupported_in: [Outlook 03, Outlook 07, Windows Mail, Entourage 2004, AOL 9, AOL 10, Notes 6, Eudora, Hotmail]
217
+ support_level: 38%
218
+ support: 3
219
+ position:
220
+ unsupported_in: [Outlook 07, Notes 6, Eudora, Old Yahoo, New Yahoo, Old GMail, New GMail, Live Mail, Hotmail, Mobile Me]
221
+ support_level: 77%
222
+ support: 3
223
+ caption-side:
224
+ unsupported_in: [Outlook 03, Outlook 07, Windows Mail, Mac Mail, Entourage 2004, Entourage 2008, AOL 9, AOL 10, AOL Desktop for Mac, Notes 6, Eudora, New Yahoo, Hotmail]
225
+ support_level: 15%
226
+ support: 3
227
+ opacity:
228
+ unsupported_in: [Outlook 03, Outlook 07, Windows Mail, Entourage 2004, Notes 6, Eudora, New Yahoo, Old GMail, New GMail, Live Mail, Hotmail]
229
+ support_level: 54%
230
+ support: 3
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: premailer
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.2
5
+ platform: ruby
6
+ authors:
7
+ - Alex Dunae
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-27 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.6"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: css_parser
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.0
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: text-reform
37
+ 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:
45
+ description: Improve the rendering of HTML emails by making CSS inline, converting links and warning about unsupported code.
46
+ email: code@dunae.ca
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files: []
52
+
53
+ files:
54
+ - README.rdoc
55
+ - CHANGELOG.rdoc
56
+ - LICENSE.rdoc
57
+ - lib/premailer.rb
58
+ - lib/premailer/premailer.rb
59
+ - lib/premailer/html_to_plain_text.rb
60
+ - misc/client_support.yaml
61
+ has_rdoc: true
62
+ homepage: http://premailer.dialect.ca/
63
+ licenses: []
64
+
65
+ post_install_message:
66
+ rdoc_options:
67
+ - --all
68
+ - --inline-source
69
+ - --line-numbers
70
+ - --charset
71
+ - utf-8
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: "0"
85
+ version:
86
+ requirements: []
87
+
88
+ rubyforge_project:
89
+ rubygems_version: 1.3.5
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Preflight for HTML e-mail.
93
+ test_files: []
94
+