premailer 1.7.3 → 1.7.8
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -1
- data/.jrubyrc +1 -0
- data/.travis.yml +9 -0
- data/.yardopts +10 -0
- data/Gemfile +12 -1
- data/{LICENSE.rdoc → LICENSE.md} +2 -2
- data/README.md +100 -0
- data/lib/premailer/adapter.rb +14 -10
- data/lib/premailer/adapter/hpricot.rb +22 -16
- data/lib/premailer/adapter/nokogiri.rb +46 -18
- data/lib/premailer/executor.rb +4 -0
- data/lib/premailer/html_to_plain_text.rb +28 -12
- data/lib/premailer/premailer.rb +135 -63
- data/lib/premailer/version.rb +4 -0
- data/premailer.gemspec +14 -5
- data/rakefile.rb +20 -25
- data/test/files/html_with_uri.html +9 -0
- data/test/future_tests.rb +1 -1
- data/test/helper.rb +34 -1
- data/test/test_adapter.rb +1 -1
- data/test/test_html_to_plain_text.rb +25 -2
- data/test/test_links.rb +45 -1
- data/test/test_misc.rb +50 -5
- data/test/test_premailer.rb +60 -39
- data/test/test_warnings.rb +1 -3
- metadata +165 -131
- data/README.rdoc +0 -85
data/.gitignore
CHANGED
data/.jrubyrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
cext.enabled=true
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
-
source
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gem 'css_parser', :git => 'git://github.com/alexdunae/css_parser.git'
|
4
|
+
gem 'webmock', :group => [:development, :test]
|
5
|
+
|
6
|
+
platforms :jruby do
|
7
|
+
gem 'jruby-openssl'
|
8
|
+
end
|
2
9
|
|
3
10
|
gemspec
|
11
|
+
|
12
|
+
gem "ripper", :group => :development, :platforms => :mri_18
|
13
|
+
|
14
|
+
gem "coveralls", :require => false, :platforms => [:mri_19, :mri_20], :group => :development
|
data/{LICENSE.rdoc → LICENSE.md}
RENAMED
@@ -1,6 +1,6 @@
|
|
1
|
-
|
1
|
+
# Premailer License
|
2
2
|
|
3
|
-
Copyright (c) 2007-
|
3
|
+
Copyright (c) 2007-2012, Alex Dunae. All rights reserved.
|
4
4
|
|
5
5
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
6
6
|
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
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 (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
|
+
File.open("output.html", "w") do |fout|
|
36
|
+
fout.puts premailer.to_inline_css
|
37
|
+
end
|
38
|
+
|
39
|
+
# Write the plain-text output
|
40
|
+
File.open("ouput.txt", "w") do |fout|
|
41
|
+
fout.puts premailer.to_plain_text
|
42
|
+
end
|
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 . 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
|
+
|
58
|
+
| CSS Attribute | Availability |
|
59
|
+
| ------------- | ------------ |
|
60
|
+
| -premailer-width | Available on `table`, `th` and `td` elements |
|
61
|
+
| -premailer-height | Available on `table`, `tr`, `th` and `td` elements |
|
62
|
+
| -premailer-cellpadding | Available on `table` elements |
|
63
|
+
| -premailer-cellspacing | Available on `table` elements |
|
64
|
+
|
65
|
+
Each of these CSS declarations will be copied to appropriate element's attribute.
|
66
|
+
|
67
|
+
For example
|
68
|
+
|
69
|
+
```css
|
70
|
+
table { -premailer-cellspacing: 5; -premailer-width: 500; }
|
71
|
+
```
|
72
|
+
|
73
|
+
will result in
|
74
|
+
|
75
|
+
```html
|
76
|
+
<table cellspacing='5' width='500'>
|
77
|
+
```
|
78
|
+
|
79
|
+
## Contributions
|
80
|
+
|
81
|
+
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.
|
82
|
+
|
83
|
+
A few areas that are particularly in need of love:
|
84
|
+
|
85
|
+
* Improved test coverage
|
86
|
+
* Move un-repeated background images defined in CSS for Outlook
|
87
|
+
|
88
|
+
## Credits and code
|
89
|
+
|
90
|
+
Thanks to [all the wonderful contributors](https://github.com/alexdunae/premailer/contributors) for their updates.
|
91
|
+
|
92
|
+
Thanks to [Greenhood + Company](http://www.greenhood.com/) for sponsoring some of the 1.5.6 updates,
|
93
|
+
and to [Campaign Monitor](http://www.campaignmonitor.com) for supporting the web interface.
|
94
|
+
|
95
|
+
The web interface can be found at [premailer.dialect.ca](http://premailer.dialect.ca).
|
96
|
+
|
97
|
+
The source code can be found on [GitHub](https://github.com/alexdunae/premailer).
|
98
|
+
|
99
|
+
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.
|
100
|
+
|
data/lib/premailer/adapter.rb
CHANGED
@@ -1,15 +1,16 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
# Manages the adapter classes. Currently supports:
|
4
|
-
#
|
5
|
-
# * nokogiri
|
6
|
-
# * hpricot
|
1
|
+
|
2
|
+
|
7
3
|
class Premailer
|
4
|
+
# Manages the adapter classes. Currently supports:
|
5
|
+
#
|
6
|
+
# * nokogiri
|
7
|
+
# * hpricot
|
8
8
|
module Adapter
|
9
9
|
|
10
10
|
autoload :Hpricot, 'premailer/adapter/hpricot'
|
11
11
|
autoload :Nokogiri, 'premailer/adapter/nokogiri'
|
12
12
|
|
13
|
+
# adapter to required file mapping.
|
13
14
|
REQUIREMENT_MAP = [
|
14
15
|
["hpricot", :hpricot],
|
15
16
|
["nokogiri", :nokogiri],
|
@@ -24,7 +25,8 @@ class Premailer
|
|
24
25
|
|
25
26
|
# The default adapter based on what you currently have loaded and
|
26
27
|
# installed. First checks to see if any adapters are already loaded,
|
27
|
-
# then
|
28
|
+
# then checks to see which are installed if none are loaded.
|
29
|
+
# @raise [RuntimeError] unless suitable adapter found.
|
28
30
|
def self.default
|
29
31
|
return :hpricot if defined?(::Hpricot)
|
30
32
|
return :nokogiri if defined?(::Nokogiri)
|
@@ -38,15 +40,17 @@ class Premailer
|
|
38
40
|
end
|
39
41
|
end
|
40
42
|
|
41
|
-
raise "No suitable adapter for Premailer was found, please install hpricot or nokogiri"
|
43
|
+
raise RuntimeError.new("No suitable adapter for Premailer was found, please install hpricot or nokogiri")
|
42
44
|
end
|
43
45
|
|
44
|
-
# Sets the
|
46
|
+
# Sets the adapter to use.
|
47
|
+
# @raise [ArgumentError] unless the adapter exists.
|
45
48
|
def self.use=(new_adapter)
|
46
49
|
@use = find(new_adapter)
|
47
50
|
end
|
48
51
|
|
49
|
-
# Returns an
|
52
|
+
# Returns an adapter.
|
53
|
+
# @raise [ArgumentError] unless the adapter exists.
|
50
54
|
def self.find(adapter)
|
51
55
|
return adapter if adapter.is_a?(Module)
|
52
56
|
|
@@ -2,11 +2,11 @@ require 'hpricot'
|
|
2
2
|
|
3
3
|
class Premailer
|
4
4
|
module Adapter
|
5
|
+
# Hpricot adapter
|
5
6
|
module Hpricot
|
6
7
|
|
7
8
|
# Merge CSS into the HTML document.
|
8
|
-
#
|
9
|
-
# Returns a string.
|
9
|
+
# @return [String] HTML.
|
10
10
|
def to_inline_css
|
11
11
|
doc = @processed_doc
|
12
12
|
@unmergable_rules = CssParser::Parser.new
|
@@ -29,16 +29,16 @@ class Premailer
|
|
29
29
|
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless @options[:preserve_styles]
|
30
30
|
else
|
31
31
|
begin
|
32
|
-
|
32
|
+
if selector =~ Premailer::RE_RESET_SELECTORS
|
33
33
|
# this is in place to preserve the MailChimp CSS reset: http://github.com/mailchimp/Email-Blueprints/
|
34
34
|
# however, this doesn't mean for testing pur
|
35
|
-
|
35
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
|
36
36
|
end
|
37
37
|
|
38
38
|
# Change single ID CSS selectors into xpath so that we can match more
|
39
39
|
# than one element. Added to work around dodgy generated code.
|
40
40
|
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
41
|
-
|
41
|
+
|
42
42
|
# convert attribute selectors to hpricot's format
|
43
43
|
selector.gsub!(/\[([\w]+)\]/, '[@\1]')
|
44
44
|
selector.gsub!(/\[([\w]+)([\=\~\^\$\*]+)([\w\s]+)\]/, '[@\1\2\'\3\']')
|
@@ -57,6 +57,11 @@ class Premailer
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
+
# Remove script tags
|
61
|
+
if @options[:remove_scripts]
|
62
|
+
doc.search("script").remove
|
63
|
+
end
|
64
|
+
|
60
65
|
# Read STYLE attributes and perform folding
|
61
66
|
doc.search("*[@style]").each do |el|
|
62
67
|
style = el.attributes['style'].to_s
|
@@ -74,13 +79,10 @@ class Premailer
|
|
74
79
|
# Duplicate CSS attributes as HTML attributes
|
75
80
|
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name)
|
76
81
|
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
|
77
|
-
el[html_att] = merged[css_att].gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
82
|
+
el[html_att] = merged[css_att].gsub(/url\('(.*)'\)/,'\1').gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
78
83
|
end
|
79
84
|
end
|
80
85
|
|
81
|
-
merged.create_dimensions_shorthand!
|
82
|
-
merged.create_border_shorthand!
|
83
|
-
|
84
86
|
# write the inline STYLE attribute
|
85
87
|
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
86
88
|
end
|
@@ -128,7 +130,7 @@ class Premailer
|
|
128
130
|
#
|
129
131
|
# <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
130
132
|
#
|
131
|
-
#
|
133
|
+
# @return [::Hpricot] a document.
|
132
134
|
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
133
135
|
styles = ''
|
134
136
|
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
@@ -137,7 +139,9 @@ class Premailer
|
|
137
139
|
|
138
140
|
unless styles.empty?
|
139
141
|
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
|
140
|
-
if
|
142
|
+
if head = doc.search('head')
|
143
|
+
head.append(style_tag)
|
144
|
+
elsif body = doc.search('body')
|
141
145
|
body.append(style_tag)
|
142
146
|
else
|
143
147
|
doc.inner_html= doc.inner_html << style_tag
|
@@ -151,7 +155,7 @@ class Premailer
|
|
151
155
|
#
|
152
156
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
153
157
|
#
|
154
|
-
#
|
158
|
+
# @return [String] Plain text.
|
155
159
|
def to_plain_text
|
156
160
|
html_src = ''
|
157
161
|
begin
|
@@ -163,24 +167,25 @@ class Premailer
|
|
163
167
|
end
|
164
168
|
|
165
169
|
|
166
|
-
#
|
170
|
+
# Gets the original HTML as a string.
|
171
|
+
# @return [String] HTML.
|
167
172
|
def to_s
|
168
173
|
@doc.to_original_html
|
169
174
|
end
|
170
175
|
|
171
176
|
# Load the HTML file and convert it into an Hpricot document.
|
172
177
|
#
|
173
|
-
#
|
178
|
+
# @return [::Hpricot] a document.
|
174
179
|
def load_html(input) # :nodoc:
|
175
180
|
thing = nil
|
176
181
|
|
177
182
|
# TODO: duplicate options
|
178
183
|
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
|
179
184
|
thing = input
|
180
|
-
|
185
|
+
elsif @is_local_file
|
181
186
|
@base_dir = File.dirname(input)
|
182
187
|
thing = File.open(input, 'r')
|
183
|
-
|
188
|
+
else
|
184
189
|
thing = open(input)
|
185
190
|
end
|
186
191
|
|
@@ -191,3 +196,4 @@ class Premailer
|
|
191
196
|
end
|
192
197
|
end
|
193
198
|
end
|
199
|
+
|
@@ -2,11 +2,12 @@ require 'nokogiri'
|
|
2
2
|
|
3
3
|
class Premailer
|
4
4
|
module Adapter
|
5
|
+
# Nokogiri adapter
|
5
6
|
module Nokogiri
|
6
7
|
|
7
8
|
# Merge CSS into the HTML document.
|
8
9
|
#
|
9
|
-
#
|
10
|
+
# @return [String] an HTML.
|
10
11
|
def to_inline_css
|
11
12
|
doc = @processed_doc
|
12
13
|
@unmergable_rules = CssParser::Parser.new
|
@@ -47,6 +48,11 @@ class Premailer
|
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
51
|
+
# Remove script tags
|
52
|
+
if @options[:remove_scripts]
|
53
|
+
doc.search("script").remove
|
54
|
+
end
|
55
|
+
|
50
56
|
# Read STYLE attributes and perform folding
|
51
57
|
doc.search("*[@style]").each do |el|
|
52
58
|
style = el.attributes['style'].to_s
|
@@ -64,15 +70,12 @@ class Premailer
|
|
64
70
|
# Duplicate CSS attributes as HTML attributes
|
65
71
|
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name)
|
66
72
|
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
|
67
|
-
el[html_att] = merged[css_att].gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
73
|
+
el[html_att] = merged[css_att].gsub(/url\('(.*)'\)/,'\1').gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
68
74
|
end
|
69
75
|
end
|
70
76
|
|
71
|
-
merged.create_dimensions_shorthand!
|
72
|
-
merged.create_border_shorthand!
|
73
|
-
|
74
77
|
# write the inline STYLE attribute
|
75
|
-
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
78
|
+
el['style'] = Premailer.escape_string(merged.declarations_to_s).split(';').map(&:strip).sort.join('; ')
|
76
79
|
end
|
77
80
|
|
78
81
|
doc = write_unmergable_css_rules(doc, @unmergable_rules)
|
@@ -109,7 +112,7 @@ class Premailer
|
|
109
112
|
@processed_doc = doc
|
110
113
|
if is_xhtml?
|
111
114
|
# we don't want to encode carriage returns
|
112
|
-
@processed_doc.to_xhtml(:encoding => nil).gsub(/&\#xD;/i, "\r")
|
115
|
+
@processed_doc.to_xhtml(:encoding => nil).gsub(/&\#(xD|13);/i, "\r")
|
113
116
|
else
|
114
117
|
@processed_doc.to_html
|
115
118
|
end
|
@@ -120,7 +123,7 @@ class Premailer
|
|
120
123
|
#
|
121
124
|
# <tt>doc</tt> is an Nokogiri document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
122
125
|
#
|
123
|
-
#
|
126
|
+
# @return [::Nokogiri::XML] a document.
|
124
127
|
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
125
128
|
styles = ''
|
126
129
|
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
@@ -129,13 +132,18 @@ class Premailer
|
|
129
132
|
|
130
133
|
unless styles.empty?
|
131
134
|
style_tag = "<style type=\"text/css\">\n#{styles}></style>"
|
132
|
-
if
|
133
|
-
doc.at_css('
|
135
|
+
if head = doc.search('head')
|
136
|
+
doc.at_css('head').add_child(::Nokogiri::XML.fragment(style_tag))
|
137
|
+
elsif body = doc.search('body')
|
138
|
+
if doc.at_css('body').children && !doc.at_css('body').children.empty?
|
139
|
+
doc.at_css('body').children.before(::Nokogiri::XML.fragment(style_tag))
|
140
|
+
else
|
141
|
+
doc.at_css('body').add_child(::Nokogiri::XML.fragment(style_tag))
|
142
|
+
end
|
134
143
|
else
|
135
144
|
doc.inner_html = style_tag += doc.inner_html
|
136
145
|
end
|
137
146
|
end
|
138
|
-
|
139
147
|
doc
|
140
148
|
end
|
141
149
|
|
@@ -144,7 +152,7 @@ class Premailer
|
|
144
152
|
#
|
145
153
|
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
146
154
|
#
|
147
|
-
#
|
155
|
+
# @return [String] a plain text.
|
148
156
|
def to_plain_text
|
149
157
|
html_src = ''
|
150
158
|
begin
|
@@ -155,7 +163,8 @@ class Premailer
|
|
155
163
|
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
156
164
|
end
|
157
165
|
|
158
|
-
#
|
166
|
+
# Gets the original HTML as a string.
|
167
|
+
# @return [String] HTML.
|
159
168
|
def to_s
|
160
169
|
if is_xhtml?
|
161
170
|
@doc.to_xhtml(:encoding => nil)
|
@@ -166,7 +175,7 @@ class Premailer
|
|
166
175
|
|
167
176
|
# Load the HTML file and convert it into an Nokogiri document.
|
168
177
|
#
|
169
|
-
#
|
178
|
+
# @return [::Nokogiri::XML] a document.
|
170
179
|
def load_html(input) # :nodoc:
|
171
180
|
thing = nil
|
172
181
|
|
@@ -185,15 +194,34 @@ class Premailer
|
|
185
194
|
end
|
186
195
|
|
187
196
|
return nil unless thing
|
188
|
-
|
189
197
|
doc = nil
|
190
198
|
|
199
|
+
# Handle HTML entities
|
200
|
+
if @options[:replace_html_entities] == true and thing.is_a?(String)
|
201
|
+
if RUBY_VERSION =~ /1.9/
|
202
|
+
html_entity_ruby_version = "1.9"
|
203
|
+
elsif RUBY_VERSION =~ /1.8/
|
204
|
+
html_entity_ruby_version = "1.8"
|
205
|
+
end
|
206
|
+
if html_entity_ruby_version
|
207
|
+
HTML_ENTITIES[html_entity_ruby_version].map do |entity, replacement|
|
208
|
+
thing.gsub! entity, replacement
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
191
212
|
# Default encoding is ASCII-8BIT (binary) per http://groups.google.com/group/nokogiri-talk/msg/0b81ef0dc180dc74
|
213
|
+
# However, we really don't want to hardcode this. ASCII-8BIG should be the default, but not the only option.
|
192
214
|
if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
|
193
|
-
thing = thing.force_encoding(
|
194
|
-
doc = ::Nokogiri::HTML(thing) {|c| c.recover }
|
215
|
+
thing = thing.force_encoding(@options[:input_encoding]).encode!
|
216
|
+
doc = ::Nokogiri::HTML(thing, nil, @options[:input_encoding]) {|c| c.recover }
|
195
217
|
else
|
196
|
-
|
218
|
+
default_encoding = RUBY_PLATFORM == 'java' ? nil : 'BINARY'
|
219
|
+
doc = ::Nokogiri::HTML(thing, nil, @options[:input_encoding] || default_encoding) {|c| c.recover }
|
220
|
+
end
|
221
|
+
|
222
|
+
# Fix for removing any CDATA tags inserted per https://github.com/sparklemotion/nokogiri/issues/311
|
223
|
+
doc.search("style").children.each do |child|
|
224
|
+
child.swap(child.text()) if child.cdata?
|
197
225
|
end
|
198
226
|
|
199
227
|
return doc
|