regru-premailer 1.7.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/.travis.yml +8 -0
- data/.yardopts +9 -0
- data/Gemfile +11 -0
- data/LICENSE.md +11 -0
- data/README.md +103 -0
- data/bin/premailer +7 -0
- data/init.rb +1 -0
- data/lib/premailer.rb +10 -0
- data/lib/premailer/adapter.rb +63 -0
- data/lib/premailer/adapter/hpricot.rb +197 -0
- data/lib/premailer/adapter/nokogiri.rb +221 -0
- data/lib/premailer/executor.rb +100 -0
- data/lib/premailer/html_to_plain_text.rb +105 -0
- data/lib/premailer/premailer.rb +549 -0
- data/local-premailer +9 -0
- data/misc/client_support.yaml +230 -0
- data/premailer.gemspec +22 -0
- data/rakefile.rb +71 -0
- data/test/files/base.html +142 -0
- data/test/files/chars.html +6 -0
- data/test/files/contact_bg.png +0 -0
- data/test/files/dialect.png +0 -0
- data/test/files/dots_end.png +0 -0
- data/test/files/dots_h.gif +0 -0
- data/test/files/html4.html +12 -0
- data/test/files/html_with_uri.html +9 -0
- data/test/files/import.css +13 -0
- data/test/files/inc/2009-placeholder.png +0 -0
- data/test/files/iso-8859-2.html +1 -0
- data/test/files/iso-8859-5.html +8 -0
- data/test/files/no_css.html +11 -0
- data/test/files/noimport.css +13 -0
- data/test/files/styles.css +106 -0
- data/test/files/xhtml.html +11 -0
- data/test/future_tests.rb +50 -0
- data/test/helper.rb +40 -0
- data/test/test_adapter.rb +29 -0
- data/test/test_html_to_plain_text.rb +155 -0
- data/test/test_links.rb +185 -0
- data/test/test_misc.rb +278 -0
- data/test/test_premailer.rb +277 -0
- data/test/test_warnings.rb +95 -0
- metadata +231 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
source :rubygems
|
2
|
+
gem 'css_parser', :git => 'git://github.com/alexdunae/css_parser.git'
|
3
|
+
gem 'webmock', :group => [:development, :test]
|
4
|
+
|
5
|
+
platforms :jruby do
|
6
|
+
gem 'jruby-openssl'
|
7
|
+
end
|
8
|
+
|
9
|
+
gemspec
|
10
|
+
|
11
|
+
gem "ripper", :group => :development, :platforms => :mri_18
|
data/LICENSE.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Premailer License
|
2
|
+
|
3
|
+
Copyright (c) 2007-2012, Alex Dunae. All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
8
|
+
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
9
|
+
* Neither the name of Premailer, Alex Dunae nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
10
|
+
|
11
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
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 <tt>style</tt> and <tt>link[rel=stylesheet]</tt> tags and preserves existing inline attributes
|
11
|
+
* Relative paths are converted to absolute paths
|
12
|
+
- Checks links in <tt>href</tt>, <tt>src</tt> and CSS <tt>url('')</tt>
|
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 (optional)
|
16
|
+
|
17
|
+
## Premailer 2.0 is coming
|
18
|
+
|
19
|
+
I'm looking for input on a version 2.0 update to Premailer. PLease visit the [Premailer 2.0 Planning Page](https://github.com/alexdunae/premailer/wiki/Premailer-2.0-Planning) and give me your feedback.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Download the Premailer gem from RubyGems.
|
24
|
+
|
25
|
+
```bash
|
26
|
+
gem install premailer
|
27
|
+
```
|
28
|
+
|
29
|
+
## Example
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)
|
33
|
+
|
34
|
+
# Write the HTML output
|
35
|
+
fout = File.open("output.html", "w")
|
36
|
+
fout.puts premailer.to_inline_css
|
37
|
+
fout.close
|
38
|
+
|
39
|
+
# Write the plain-text output
|
40
|
+
fout = File.open("ouput.txt", "w")
|
41
|
+
fout.puts premailer.to_plain_text
|
42
|
+
fout.close
|
43
|
+
|
44
|
+
# Output any CSS warnings
|
45
|
+
premailer.warnings.each do |w|
|
46
|
+
puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
## Ruby Compatibility
|
51
|
+
|
52
|
+
Premailer is tested on Ruby 1.8.7, Ruby 1.9.2 and Ruby 1.9.3 (preview 1). It also works on REE. JRuby support is close; contributors are welcome. Checkout the latest build status on the [Travis CI dashboard](http://travis-ci.org/#!/alexdunae/premailer).
|
53
|
+
|
54
|
+
## Premailer-specific CSS
|
55
|
+
|
56
|
+
Premailer looks for a few CSS attributes that make working with tables a bit easier.
|
57
|
+
<dl>
|
58
|
+
<dt>-premailer-width</dt>
|
59
|
+
<dd>Available on <tt>table</tt>, <tt>th</tt> and <tt>td</tt> elements</dd>
|
60
|
+
<dt>-premailer-height</dt>
|
61
|
+
<dd>Available on <tt>table</tt>, <tt>tr</tt>, <tt>th</tt> and <tt>td</tt> elements</dd>
|
62
|
+
<dt>-premailer-cellpadding</dt>
|
63
|
+
<dd>Available on <tt>table</tt> elements</dd>
|
64
|
+
<dt>-premailer-cellspacing</dt>
|
65
|
+
<dd>Available on <tt>table</tt> elements</dd>
|
66
|
+
</dl>
|
67
|
+
|
68
|
+
Each of these CSS declarations will be copied to appropriate element's attribute.
|
69
|
+
|
70
|
+
For example
|
71
|
+
|
72
|
+
```css
|
73
|
+
table { -premailer-cellspacing: 5; -premailer-width: 500;}
|
74
|
+
```
|
75
|
+
|
76
|
+
will result in
|
77
|
+
|
78
|
+
```html
|
79
|
+
<table cellspacing='5' width='500'>
|
80
|
+
```
|
81
|
+
|
82
|
+
## Contributions
|
83
|
+
|
84
|
+
Contributions are most welcome. Premailer was rotting away in a private SVN repository for too long and could use some TLC. Fork and patch to your heart's content. Please don't increment the version numbers, though.
|
85
|
+
|
86
|
+
A few areas that are particularly in need of love:
|
87
|
+
|
88
|
+
* Improved test coverage
|
89
|
+
* Move un-repeated background images defined in CSS for Outlook
|
90
|
+
|
91
|
+
## Credits and code
|
92
|
+
|
93
|
+
Thanks to [all the wonderful contributors](https://github.com/alexdunae/premailer/contributors) for their updates.
|
94
|
+
|
95
|
+
Thanks to [Greenhood + Company](http://www.greenhood.com/) for sponsoring some of the 1.5.6 updates,
|
96
|
+
and to [Campaign Monitor](http://www.campaignmonitor.com) for supporting the web interface.
|
97
|
+
|
98
|
+
The web interface can be found at [premailer.dialect.ca](http://premailer.dialect.ca).
|
99
|
+
|
100
|
+
The source code can be found on [GitHub](https://github.com/alexdunae/premailer).
|
101
|
+
|
102
|
+
Copyright by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-2012. See [LICENSE.md](https://github.com/alexdunae/premailer/blob/master/LICENSE.md) for license details.
|
103
|
+
|
data/bin/premailer
ADDED
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'premailer'
|
data/lib/premailer.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Premailer
|
4
|
+
# Manages the adapter classes. Currently supports:
|
5
|
+
#
|
6
|
+
# * nokogiri
|
7
|
+
# * hpricot
|
8
|
+
module Adapter
|
9
|
+
|
10
|
+
autoload :Hpricot, 'premailer/adapter/hpricot'
|
11
|
+
autoload :Nokogiri, 'premailer/adapter/nokogiri'
|
12
|
+
|
13
|
+
# adapter to required file mapping.
|
14
|
+
REQUIREMENT_MAP = [
|
15
|
+
["hpricot", :hpricot],
|
16
|
+
["nokogiri", :nokogiri],
|
17
|
+
]
|
18
|
+
|
19
|
+
# Returns the adapter to use.
|
20
|
+
def self.use
|
21
|
+
return @use if @use
|
22
|
+
self.use = self.default
|
23
|
+
@use
|
24
|
+
end
|
25
|
+
|
26
|
+
# The default adapter based on what you currently have loaded and
|
27
|
+
# installed. First checks to see if any adapters are already loaded,
|
28
|
+
# then checks to see which are installed if none are loaded.
|
29
|
+
# @raise [RuntimeError] unless suitable adapter found.
|
30
|
+
def self.default
|
31
|
+
return :hpricot if defined?(::Hpricot)
|
32
|
+
return :nokogiri if defined?(::Nokogiri)
|
33
|
+
|
34
|
+
REQUIREMENT_MAP.each do |(library, adapter)|
|
35
|
+
begin
|
36
|
+
require library
|
37
|
+
return adapter
|
38
|
+
rescue LoadError
|
39
|
+
next
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
raise RuntimeError.new("No suitable adapter for Premailer was found, please install hpricot or nokogiri")
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sets the adapter to use.
|
47
|
+
# @raise [ArgumentError] unless the adapter exists.
|
48
|
+
def self.use=(new_adapter)
|
49
|
+
@use = find(new_adapter)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns an adapter.
|
53
|
+
# @raise [ArgumentError] unless the adapter exists.
|
54
|
+
def self.find(adapter)
|
55
|
+
return adapter if adapter.is_a?(Module)
|
56
|
+
|
57
|
+
Premailer::Adapter.const_get("#{adapter.to_s.split('_').map{|s| s.capitalize}.join('')}")
|
58
|
+
rescue NameError
|
59
|
+
raise ArgumentError, "Invalid adapter: #{adapter}"
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'hpricot'
|
2
|
+
|
3
|
+
class Premailer
|
4
|
+
module Adapter
|
5
|
+
# Hpricot adapter
|
6
|
+
module Hpricot
|
7
|
+
|
8
|
+
# Merge CSS into the HTML document.
|
9
|
+
# @return [String] HTML.
|
10
|
+
def to_inline_css
|
11
|
+
doc = @processed_doc
|
12
|
+
@unmergable_rules = CssParser::Parser.new
|
13
|
+
|
14
|
+
# Give all styles already in style attributes a specificity of 1000
|
15
|
+
# per http://www.w3.org/TR/CSS21/cascade.html#specificity
|
16
|
+
doc.search("*[@style]").each do |el|
|
17
|
+
el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Iterate through the rules and merge them into the HTML
|
21
|
+
@css_parser.each_selector(:all) do |selector, declaration, specificity|
|
22
|
+
# Save un-mergable rules separately
|
23
|
+
selector.gsub!(/:link([\s]*)+/i) {|m| $1 }
|
24
|
+
|
25
|
+
# Convert element names to lower case
|
26
|
+
selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
|
27
|
+
|
28
|
+
if selector =~ Premailer::RE_UNMERGABLE_SELECTORS
|
29
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless @options[:preserve_styles]
|
30
|
+
else
|
31
|
+
begin
|
32
|
+
if selector =~ Premailer::RE_RESET_SELECTORS
|
33
|
+
# this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
|
34
|
+
# however, this doesn't mean for testing pur
|
35
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Change single ID CSS selectors into xpath so that we can match more
|
39
|
+
# than one element. Added to work around dodgy generated code.
|
40
|
+
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
41
|
+
|
42
|
+
# convert attribute selectors to hpricot's format
|
43
|
+
selector.gsub!(/\[([\w]+)\]/, '[@\1]')
|
44
|
+
selector.gsub!(/\[([\w]+)([\=\~\^\$\*]+)([\w\s]+)\]/, '[@\1\2\'\3\']')
|
45
|
+
|
46
|
+
doc.search(selector).each do |el|
|
47
|
+
if el.elem? and (el.name != 'head' and el.parent.name != 'head')
|
48
|
+
# Add a style attribute or append to the existing one
|
49
|
+
block = "[SPEC=#{specificity}[#{declaration}]]"
|
50
|
+
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
51
|
+
end
|
52
|
+
end
|
53
|
+
rescue ::Hpricot::Error, RuntimeError, ArgumentError
|
54
|
+
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
55
|
+
next
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Remove script tags
|
61
|
+
if @options[:remove_scripts]
|
62
|
+
doc.search("script").remove
|
63
|
+
end
|
64
|
+
|
65
|
+
# Read STYLE attributes and perform folding
|
66
|
+
doc.search("*[@style]").each do |el|
|
67
|
+
style = el.attributes['style'].to_s
|
68
|
+
|
69
|
+
declarations = []
|
70
|
+
|
71
|
+
style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
|
72
|
+
rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
|
73
|
+
declarations << rs
|
74
|
+
end
|
75
|
+
# Perform style folding
|
76
|
+
merged = CssParser.merge(declarations)
|
77
|
+
merged.expand_shorthand!
|
78
|
+
|
79
|
+
# Duplicate CSS attributes as HTML attributes
|
80
|
+
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name)
|
81
|
+
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
|
82
|
+
el[html_att] = merged[css_att].gsub(/url\('(.*)'\)/,'\1').gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# write the inline STYLE attribute
|
87
|
+
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
88
|
+
end
|
89
|
+
|
90
|
+
doc = write_unmergable_css_rules(doc, @unmergable_rules)
|
91
|
+
|
92
|
+
if @options[:remove_classes] or @options[:remove_comments]
|
93
|
+
doc.search('*').each do |el|
|
94
|
+
if el.comment? and @options[:remove_comments]
|
95
|
+
lst = el.parent.children
|
96
|
+
el.parent = nil
|
97
|
+
lst.delete(el)
|
98
|
+
elsif el.elem?
|
99
|
+
el.remove_attribute('class') if @options[:remove_classes]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
if @options[:remove_ids]
|
105
|
+
# find all anchor's targets and hash them
|
106
|
+
targets = []
|
107
|
+
doc.search("a[@href^='#']").each do |el|
|
108
|
+
target = el.get_attribute('href')[1..-1]
|
109
|
+
targets << target
|
110
|
+
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
|
111
|
+
end
|
112
|
+
# hash ids that are links target, delete others
|
113
|
+
doc.search("*[@id]").each do |el|
|
114
|
+
id = el.get_attribute('id')
|
115
|
+
if targets.include?(id)
|
116
|
+
el.set_attribute('id', Digest::MD5.hexdigest(id))
|
117
|
+
else
|
118
|
+
el.remove_attribute('id')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
@processed_doc = doc
|
124
|
+
|
125
|
+
@processed_doc.to_original_html
|
126
|
+
end
|
127
|
+
|
128
|
+
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
129
|
+
# and write it into the <tt>body</tt>.
|
130
|
+
#
|
131
|
+
# <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
132
|
+
#
|
133
|
+
# @return [::Hpricot] a document.
|
134
|
+
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
135
|
+
styles = ''
|
136
|
+
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
137
|
+
styles += "#{selector} { #{declarations} }\n"
|
138
|
+
end
|
139
|
+
|
140
|
+
unless styles.empty?
|
141
|
+
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
|
142
|
+
if body = doc.search('body')
|
143
|
+
body.append(style_tag)
|
144
|
+
else
|
145
|
+
doc.inner_html= doc.inner_html << style_tag
|
146
|
+
end
|
147
|
+
end
|
148
|
+
doc
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
# Converts the HTML document to a format suitable for plain-text e-mail.
|
153
|
+
#
|
154
|
+
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
155
|
+
#
|
156
|
+
# @return [String] Plain text.
|
157
|
+
def to_plain_text
|
158
|
+
html_src = ''
|
159
|
+
begin
|
160
|
+
html_src = @doc.search("body").inner_html
|
161
|
+
rescue; end
|
162
|
+
|
163
|
+
html_src = @doc.to_html unless html_src and not html_src.empty?
|
164
|
+
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
# Gets the original HTML as a string.
|
169
|
+
# @return [String] HTML.
|
170
|
+
def to_s
|
171
|
+
@doc.to_original_html
|
172
|
+
end
|
173
|
+
|
174
|
+
# Load the HTML file and convert it into an Hpricot document.
|
175
|
+
#
|
176
|
+
# @return [::Hpricot] a document.
|
177
|
+
def load_html(input) # :nodoc:
|
178
|
+
thing = nil
|
179
|
+
|
180
|
+
# TODO: duplicate options
|
181
|
+
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
|
182
|
+
thing = input
|
183
|
+
elsif @is_local_file
|
184
|
+
@base_dir = File.dirname(input)
|
185
|
+
thing = File.open(input, 'r')
|
186
|
+
else
|
187
|
+
thing = open(input)
|
188
|
+
end
|
189
|
+
|
190
|
+
# TODO: deal with Hpricot seg faults on empty input
|
191
|
+
thing ? Hpricot(thing) : nil
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|