regru-premailer 1.7.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .DS_Store
2
+ *.gem
3
+ Gemfile.lock
4
+ bin/*.html
5
+ html/
6
+ vendor/
7
+ doc/
8
+ .yardoc/
9
+ *.sw?
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ notifications:
2
+ disabled: true
3
+ rvm:
4
+ - 1.8.7
5
+ - 1.9.2
6
+ - 1.9.3
7
+ - jruby
8
+ - ree
data/.yardopts ADDED
@@ -0,0 +1,9 @@
1
+ --markup markdown
2
+ --markup-provider redcarpet
3
+ --charset utf-8
4
+ --no-private
5
+ --readme README.md
6
+ --title "Premailer Documentation"
7
+ -
8
+ README.md
9
+ LICENSE.md
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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This binary used in rubygems environment only as part of installed gem
4
+
5
+ require 'rubygems'
6
+ require 'premailer/executor'
7
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'premailer'
data/lib/premailer.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'yaml'
2
+ require 'open-uri'
3
+ require 'digest/md5'
4
+ require 'cgi'
5
+ require 'css_parser'
6
+
7
+ require 'premailer/adapter'
8
+ require 'premailer/html_to_plain_text'
9
+ require 'premailer/premailer'
10
+
@@ -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
+