premailer 1.7.3 → 1.7.8
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/.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
|