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 CHANGED
@@ -4,4 +4,8 @@ Gemfile.lock
4
4
  bin/*.html
5
5
  html/
6
6
  vendor/
7
- rdoc/
7
+ doc/
8
+ .yardoc/
9
+ *.sw?
10
+ pkg/
11
+ *.sublime-*
@@ -0,0 +1 @@
1
+ cext.enabled=true
@@ -0,0 +1,9 @@
1
+ notifications:
2
+ disabled: true
3
+ rvm:
4
+ - 1.8.7
5
+ - 1.9.2
6
+ - 1.9.3
7
+ - 2.0.0
8
+ - jruby
9
+ - ree
@@ -0,0 +1,10 @@
1
+ --markup markdown
2
+ --markup-provider redcarpet
3
+ --charset utf-8
4
+ --no-private
5
+ --readme README.md
6
+ --title "Premailer Documentation"
7
+ --plugin redcarpet-ext
8
+ -
9
+ README.md
10
+ LICENSE.md
data/Gemfile CHANGED
@@ -1,3 +1,14 @@
1
- source :rubygems
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
@@ -1,6 +1,6 @@
1
- = Premailer License
1
+ # Premailer License
2
2
 
3
- Copyright (c) 2007-2011, Alex Dunae. All rights reserved.
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
 
@@ -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
+
@@ -1,15 +1,16 @@
1
- # = HTTPI::Adapter
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 ckecks to see which are installed if none are loaded.
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 +adapter+ to use. Raises an +ArgumentError+ unless the +adapter+ exists.
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 +adapter+. Raises an +ArgumentError+ unless the +adapter+ exists.
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
- if selector =~ Premailer::RE_RESET_SELECTORS
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
- @unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless !@options[:preserve_reset]
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
- # Returns an Hpricot document.
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 body = doc.search('body')
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
- # Returns a string.
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
- # Returns the original HTML as a string.
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
- # Returns an Hpricot document.
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
- elsif @is_local_file
185
+ elsif @is_local_file
181
186
  @base_dir = File.dirname(input)
182
187
  thing = File.open(input, 'r')
183
- else
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
- # Returns a string.
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
- # Returns an Nokogiri document.
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 body = doc.search('body')
133
- doc.at_css('body').children.first.before(style_tag)
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
- # Returns a string.
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
- # Returns the original HTML as a string.
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
- # Returns an Nokogiri document.
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('ASCII-8BIT').encode!
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
- doc = ::Nokogiri::HTML(thing, nil, @options[:inputencoding] || 'BINARY') {|c| c.recover }
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