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 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