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.
- data/CHANGELOG.rdoc +49 -0
- data/LICENSE.rdoc +42 -0
- data/README.rdoc +66 -0
- data/lib/premailer.rb +9 -0
- data/lib/premailer/html_to_plain_text.rb +58 -0
- data/lib/premailer/premailer.rb +393 -0
- data/misc/client_support.yaml +230 -0
- metadata +94 -0
data/CHANGELOG.rdoc
ADDED
@@ -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
|
data/LICENSE.rdoc
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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.
|
data/lib/premailer.rb
ADDED
@@ -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
|
+
|