mikedamage-premailer-plus 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Mike Green
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,41 @@
1
+ h1. premailer-plus
2
+
3
+ |*By:*||Mike Green ("mike.is.green@gmail.com":mailto:mike.is.green@gmail.com)|
4
+ |*Original Premailer By:*||Alex Dunae ("http://www.dunae.ca":http://www.dunae.ca)|
5
+
6
+ h2. Background
7
+
8
+ Premailer Plus is my set of additions and corrections to Alex Dunae's excellent "Premailer":http://code.dunae.ca/premailer.web/ application. Premailer is "Preflight for HTML email", i.e. it takes your HTML code, and makes it compatible with most email clients. Email clients aren't like web browsers; their support for CSS is spotty and erratic, and they don't always render your code the way you'd expect. Premailer takes your CSS and moves it inline to each matched HTML element, and it displays warnings if you use code that's not well supported by email clients. "Campaign Monitor":http://www.campaignmonitor.com sponsors and uses Premailer to get your code ready for mass emails. It's a one of a kind, indispensible service, but I've always had a few gripes about the Premailer Rubygem, so I finally decided to fork the code and apply my fixes and improvements.
9
+
10
+ h3. Problems with "Premailer Classic"
11
+
12
+ I downloaded the Premailer gem so that I could rock the excellence of Premailer on my Mac instead of having to rely on an external service. There's even a TextMate bundle available that lets you Premailer you code right in the editor. But when I downloaded it, I found one glaring problem: *When you run Premailer from the command line, it throws an error when you give it a local file instead of a URL.* Kind of defeats the purpose.
13
+
14
+ h3. Improvements in Premailer Plus
15
+
16
+ * I fixed the command line script so that it no longer throws errors when you feed it a local file
17
+ * New option to shorten the URLs of links in your plain text version
18
+ ** "Bit.ly":http://bit.ly
19
+ * More to come!
20
+
21
+ h2. Installation
22
+
23
+ The easiest way to install Premailer Plus is to run the following code on your OS X or Linux terminal (should work on Windows too, but I'm not sure):
24
+
25
+ <pre>
26
+ $ sudo gem install mikedamage-premailer-plus
27
+ </pre>
28
+
29
+ You can also clone this repository and run @sudo rake install@ from the project root.
30
+
31
+ h2. Dependencies
32
+
33
+ * "RubyGems":http://rubygems.rubyforge.org
34
+ * "Hpricot":http://wiki.github.com/why/hpricot
35
+ * "css-parser":http://code.dunae.ca/css_parser/
36
+ * "text-reform":http://rubyforge.org/projects/text-format/
37
+ * "Bitly4r":http://wiki.cantremember.com/Bitly4R/HomePage
38
+
39
+ h2. Copyright
40
+
41
+ Copyright (C) 2009 Mike Green. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "premailer-plus"
8
+ gem.summary = %Q{Miscellaneous improvements and fixes to Alex Dunae's Premailer gem}
9
+ gem.email = "mike.is.green@gmail.com"
10
+ gem.homepage = "http://github.com/mikedamage/premailer-plus"
11
+ gem.authors = ["Mike Green"]
12
+
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ rescue LoadError
16
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+
40
+ task :default => :test
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ if File.exist?('VERSION.yml')
45
+ config = YAML.load(File.read('VERSION.yml'))
46
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
47
+ else
48
+ version = ""
49
+ end
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "premailer-plus #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
56
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # = Premailer Plus
4
+ #
5
+ # Miscellaneous improvements to Alex Dunae's Premailer gem
6
+
7
+ require "optparse"
8
+ require "optparse/time"
9
+ require "ostruct"
10
+ require "pathname"
11
+ require File.join(File.dirname(__FILE__), "../lib/premailer")
12
+
13
+ class PremailerApp
14
+
15
+
16
+ def initialize(args)
17
+ @args = args
18
+ @config = OpenStruct.new({
19
+ :plaintext => false,
20
+ :shorten_urls => false,
21
+ :base_url => '',
22
+ :infile => nil,
23
+ :outfile => '',
24
+ :querystring => '',
25
+ :warnings => false,
26
+ :verbose => true
27
+ })
28
+ end
29
+
30
+ def run
31
+ if parse_options and get_arguments
32
+ @pm_opts = {}
33
+ @pm_opts.merge!({:base_url => @config.base_url}) unless @config.base_url.empty?
34
+ @pm_opts.merge!({:link_query_string => @config.querystring}) unless @config.querystring.empty?
35
+ @pm_opts.merge!({:shorten_urls => true}) if @config.shorten_urls
36
+ @premailer = Premailer.new(@config.infile.to_s, @pm_opts)
37
+
38
+ write_premail_html
39
+ write_plain_text if @config.plaintext
40
+ show_warnings if @config.warnings
41
+ else
42
+ puts "Invalid options/arguments!"
43
+ exit 1
44
+ end
45
+ end
46
+
47
+ private
48
+ def parse_options
49
+ opts = OptionParser.new
50
+ opts.banner = DATA.read.gsub!(/FILE/, File.basename(__FILE__))
51
+ opts.on("-p", "--plaintext", "Output a plain-text version of the email in addition to HTML") { @config.plaintext = true }
52
+ opts.on("-s", "--shorten-urls", "Shorten URLs with Bit.ly in the plain-text version") { @config.shorten_urls = true }
53
+ opts.on("-b", "--baseurl URL", "Prepend this URL to links if your input is a local file.") {|url| @config.base_url = url }
54
+ opts.on("-q", "--querystring STRING", "Append this query string to link URLs") {|string| @config.querystring = string }
55
+ opts.on("-v", "--verbose", "Display status information during the pre-mailing process") { @config.verbose = true }
56
+ opts.on("-w", "--warnings", "Display CSS support warnings") { @config.warnings = true }
57
+ opts.on("-h", "--help", "Show this information") { puts opts; exit 0; }
58
+ true if opts.parse!(@args) rescue return false
59
+ end
60
+
61
+ def get_arguments
62
+ if @args.nitems >= 2
63
+ @config.infile = Pathname.new(@args.shift)
64
+ @config.outfile = Pathname.new(@args.shift)
65
+ return true
66
+ else
67
+ return false
68
+ end
69
+ end
70
+
71
+ def write_premail_html
72
+ puts "Converting HTML to inline CSS and saving as #{@config.outfile.basename.to_s}..." if @config.verbose
73
+ @config.outfile.open("w") do |file|
74
+ file.write(@premailer.to_inline_css)
75
+ end
76
+ puts "Done." if @config.verbose
77
+ end
78
+
79
+ def write_plain_text
80
+ textfile = Pathname.new(File.join(@config.outfile.dirname.to_s, @config.outfile.basename.to_s.gsub(/\..+$/, ".txt")))
81
+ puts "Saving a plain-text version of your email as #{@config.outfile.basename.to_s.gsub(/\.html$/, '.txt')}..." if @config.verbose
82
+ puts "Shortening link URLs with Bit.ly..." if @config.verbose and @config.shorten_urls
83
+ textfile.open("w") do |file|
84
+ file.write(@premailer.to_plain_text)
85
+ end
86
+ puts "Done." if @config.verbose
87
+ end
88
+
89
+ def show_warnings
90
+ puts "#{@premailer.warnings.nitems.to_s} warnings found:"
91
+ @premailer.warnings.each do |warning|
92
+ puts warning
93
+ end
94
+ end
95
+ end
96
+
97
+ app = PremailerApp.new(ARGV)
98
+ app.run
99
+
100
+ __END__
101
+ Premailer Plus
102
+ -----------------
103
+
104
+ Usage:
105
+ FILE [options] inputfile outputfile
106
+ "inputfile" can be either a local file or a URL
107
+
108
+ Options:
@@ -0,0 +1,61 @@
1
+ require 'text/reform'
2
+ require 'htmlentities'
3
+ require 'bitly4r'
4
+
5
+ # Support functions for Premailer
6
+ module HtmlToPlainText
7
+
8
+ # Returns the text in UTF-8 format with all HTML tags removed
9
+ #
10
+ # TODO:
11
+ # - add support for DL, OL
12
+ def convert_to_text(html, line_length, from_charset = 'UTF-8', shorten = false)
13
+ r = Text::Reform.new(:trim => true,
14
+ :squeeze => false,
15
+ :break => Text::Reform.break_wrap)
16
+
17
+ txt = html
18
+
19
+ bitly = shorten ? Bitly4R.Keyed("mikedamage", "R_abb45e99634386334b7ed6c8d081e80e") : nil
20
+
21
+ he = HTMLEntities.new # decode HTML entities
22
+
23
+ txt = he.decode(txt)
24
+
25
+ txt.gsub!(/<h([0-9]+)[^>]*>(.*)<\/h[0-9]+>/i) do |s| # handle headings
26
+ hlevel = $1.to_i
27
+ htext = $2.gsub(/<\/?[^>]*>/i, '') # remove tags inside headings
28
+ hlength = (htext.length > line_length ?
29
+ line_length :
30
+ htext.length)
31
+
32
+ case hlevel
33
+ when 1 # H1
34
+ ('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength) + "\n"
35
+ when 2 # H2
36
+ ('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength) + "\n"
37
+ else # H3-H6 are styled the same
38
+ htext + "\n" + ('-' * htext.length) + "\n"
39
+ end
40
+ end
41
+
42
+ txt.gsub!(/<a.*href=\"([^\"]*)\"[^>]*>(.*)<\/a>/i) do |s| # links
43
+ if bitly
44
+ $2 + ' [' + bitly.shorten($1) + ']'
45
+ else
46
+ $2 + ' [' + $1 + ']'
47
+ end
48
+ end
49
+
50
+ txt.gsub!(/(<li[\s]+[^>]*>|<li>)/i, ' * ') # unordered LIsts
51
+ txt.gsub!(/<\/p>/i, "\n\n") # paragraphs
52
+
53
+ txt.gsub!(/<\/?[^>]*>/, '') # strip remaining tags
54
+ txt.gsub!(/\A[\s]+|[\s]+\Z|^[ \t]+/m, '') # strip extra spaces
55
+ txt.gsub!(/[\n]{3,}/m, "\n\n") # tighten line breaks
56
+
57
+ txt = r.format(('[' * line_length), txt) # wrap text
58
+ txt.gsub!(/^[\*][\s]/m, ' * ') # add spaces back to lists
59
+ txt
60
+ end
61
+ end
data/lib/premailer.rb ADDED
@@ -0,0 +1,396 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008
4
+ # Version 1.5.0
5
+
6
+ ENV["GEM_PATH"] = "/Library/Ruby/Gems/1.8"
7
+
8
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__), ''))
9
+
10
+ require 'rubygems'
11
+ require 'yaml'
12
+ require 'open-uri'
13
+ require 'hpricot'
14
+ require 'css_parser'
15
+
16
+ require 'html_to_plain_text'
17
+
18
+ # Premailer processes HTML and CSS to improve e-mail deliverability.
19
+ #
20
+ # Premailer's main function is to render all CSS as inline <tt>style</tt> attributes using
21
+ # the CssParser. It can also convert relative links to absolute links and check the 'safety' of
22
+ # CSS properties against a CSS support chart.
23
+ #
24
+ # = Example
25
+ #
26
+ # premailer = Premailer.new(html_file, :warn_level => Premailer::Warnings::SAFE)
27
+ # premailer.parse!
28
+ # puts premailer.warnings.length.to_s + ' warnings found'
29
+ class Premailer
30
+ include HtmlToPlainText
31
+ include CssParser
32
+
33
+ CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../misc/client_support.yaml'
34
+
35
+ RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
36
+
37
+ # should also exclude :first-letter, etc...
38
+
39
+ # URI of the HTML file used
40
+ attr_reader :html_file
41
+
42
+ module Warnings
43
+ NONE = 0
44
+ SAFE = 1
45
+ POOR = 2
46
+ RISKY = 3
47
+ end
48
+ include Warnings
49
+
50
+ WARN_LABEL = %w(NONE SAFE POOR RISKY)
51
+
52
+ # Create a new Premailer object.
53
+ #
54
+ # +uri+ is the URL of the HTML file to process. Should be a string.
55
+ #
56
+ # ==== Options
57
+ # [+line_length+] Line length used by to_plain_text. Boolean, default is 65.
58
+ # [+warn_level+] What level of CSS compatibility warnings to show (see Warnings).
59
+ # [+link_query_string+] A string to append to every <a href=""> link.
60
+ def initialize(uri, options = {})
61
+ @options = {:warn_level => Warnings::SAFE, :line_length => 65, :link_query_string => nil, :base_url => nil, :shorten_urls => false}.merge(options)
62
+ @html_file = uri
63
+
64
+
65
+ @is_local_file = true
66
+ if uri =~ /^(http|https|ftp)\:\/\//i
67
+ @is_local_file = false
68
+ end
69
+
70
+
71
+ @css_warnings = []
72
+
73
+ @css_parser = CssParser::Parser.new({:absolute_paths => true,
74
+ :import => true,
75
+ :io_exceptions => false
76
+ })
77
+
78
+ @doc, @html_charset = load_html(@html_file)
79
+
80
+ if @is_local_file and @options[:base_url]
81
+ @doc = convert_inline_links(@doc, @options[:base_url])
82
+ elsif not @is_local_file
83
+ @doc = convert_inline_links(@doc, @html_file)
84
+ end
85
+ load_css_from_html!
86
+ end
87
+
88
+ # Array containing a hash of CSS warnings.
89
+ def warnings
90
+ return [] if @options[:warn_level] == Warnings::NONE
91
+ @css_warnings = check_client_support if @css_warnings.empty?
92
+ @css_warnings
93
+ end
94
+
95
+ # Returns the original HTML as a string.
96
+ def to_s
97
+ @doc.to_html
98
+ end
99
+
100
+ # Returns the document with all HTML tags removed.
101
+ def to_plain_text
102
+ html_src = ''
103
+ begin
104
+ html_src = @doc.search("body").innerHTML
105
+ rescue
106
+ html_src = @doc.to_html
107
+ end
108
+ if @options[:shorten_urls]
109
+ convert_to_text(html_src, @options[:line_length], @html_charset, true)
110
+ else
111
+ convert_to_text(html_src, @options[:line_length], @html_charset, false)
112
+ end
113
+ end
114
+
115
+ # Merge CSS into the HTML document.
116
+ #
117
+ # Returns a string.
118
+ def to_inline_css
119
+ doc = @doc
120
+ unmergable_rules = CssParser::Parser.new
121
+
122
+ # Give all styles already in style attributes a specificity of 1000
123
+ # per http://www.w3.org/TR/CSS21/cascade.html#specificity
124
+ doc.search("*[@style]").each do |el|
125
+ el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
126
+ end
127
+
128
+ # Iterate through the rules and merge them into the HTML
129
+ @css_parser.each_selector(:all) do |selector, declaration, specificity|
130
+ # Save un-mergable rules separately
131
+ selector.gsub!(/:link([\s]|$)+/i, '')
132
+
133
+ # Convert element names to lower case
134
+ selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
135
+
136
+ if selector =~ RE_UNMERGABLE_SELECTORS
137
+ unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
138
+ else
139
+
140
+ doc.search(selector) do |el|
141
+ if el.elem?
142
+ # Add a style attribute or append to the existing one
143
+ block = "[SPEC=#{specificity}[#{declaration}]]"
144
+ el['style'] = (el.attributes['style'] ||= '') + ' ' + block
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ # Read <style> attributes and perform folding
151
+ doc.search("*[@style]").each do |el|
152
+ style = el.attributes['style'].to_s
153
+
154
+ declarations = []
155
+
156
+ style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
157
+ rs = RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
158
+ declarations << rs
159
+ end
160
+
161
+ # Perform style folding and save
162
+ merged = CssParser.merge(declarations)
163
+
164
+ el['style'] = Premailer.escape_string(merged.declarations_to_s)
165
+ end
166
+
167
+ doc = write_unmergable_css_rules(doc, unmergable_rules)
168
+
169
+ #doc = add_body_imposter(doc)
170
+
171
+ doc.to_html
172
+ end
173
+
174
+
175
+ protected
176
+ # Load the HTML file and convert it into an Hpricot document.
177
+ #
178
+ # Returns an Hpricot document and a string with the HTML file's character set.
179
+ def load_html(uri)
180
+ if @is_local_file
181
+ Hpricot(File.open(uri, "r") {|f| f.read })
182
+ else
183
+ Hpricot(open(uri))
184
+ end
185
+ end
186
+
187
+ # Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
188
+ def load_css_from_html!
189
+ if tags = @doc.search("link[@rel='stylesheet'], style")
190
+ tags.each do |tag|
191
+ if tag.to_s.strip =~ /^\<link/i and tag.attributes['href']
192
+ if media_type_ok?(tag.attributes['media'])
193
+ link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
194
+ if @is_local_file
195
+ css_block = ''
196
+ File.open(link_uri, "r") do |file|
197
+ while line = file.gets
198
+ css_block << line
199
+ end
200
+ end
201
+ @css_parser.add_block!(css_block, {:base_uri => @html_file})
202
+ else
203
+ @css_parser.load_uri!(link_uri)
204
+ end
205
+ end
206
+ elsif tag.to_s.strip =~ /^\<style/i
207
+ @css_parser.add_block!(tag.innerHTML, :base_uri => URI.parse(@html_file))
208
+ end
209
+ end
210
+ tags.remove
211
+ end
212
+ end
213
+
214
+ def media_type_ok?(media_types)
215
+ return media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
216
+ rescue
217
+ return true
218
+ end
219
+
220
+ # Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
221
+ # and write it into the <tt>body</tt>.
222
+ #
223
+ # <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
224
+ #
225
+ # Returns an Hpricot document.
226
+ def write_unmergable_css_rules(doc, unmergable_rules)
227
+ styles = ''
228
+ unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
229
+ styles += "#{selector} { #{declarations} }\n"
230
+ end
231
+ unless styles.empty?
232
+ style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
233
+ doc.search("head").append(style_tag)
234
+ end
235
+ doc
236
+ end
237
+
238
+ # Convert relative links to absolute links.
239
+ #
240
+ # Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
241
+ # as well as CSS <tt>url()</tt> declarations found in inline <tt>style</tt> attributes.
242
+ #
243
+ # <tt>doc</tt> is an Hpricot document and <tt>base_uri</tt> is either a string or a URI.
244
+ #
245
+ # Returns an Hpricot document.
246
+ def convert_inline_links(doc, base_uri)
247
+ base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
248
+
249
+ ['href', 'src', 'background'].each do |attribute|
250
+
251
+ tags = doc.search("*[@#{attribute}]")
252
+ append_qs = @options[:link_query_string] ||= ''
253
+ unless tags.empty?
254
+ tags.each do |tag|
255
+ unless tag.attributes[attribute] =~ /^(\{|\[|<|\#)/i
256
+ if tag.attributes[attribute] =~ /^http/i
257
+ begin
258
+ merged = URI.parse(tag.attributes[attribute])
259
+ rescue
260
+ next
261
+ end
262
+ else
263
+ begin
264
+ merged = Premailer.resolve_link(tag.attributes[attribute].to_s, base_uri.merge)
265
+ rescue
266
+ begin
267
+ merged = Premailer.resolve_link(URI.escape(tag.attributes[attribute].to_s), base_uri.merge)
268
+ # merged = base_uri.merge(URI.escape(tag.attributes[attribute].to_s))
269
+ rescue; end
270
+ end
271
+ end # end of relative urls only
272
+
273
+ if tag.name =~ /^a$/i and not append_qs.empty?
274
+ if merged.query
275
+ merged.query = merged.query + '&' + append_qs
276
+ else
277
+ merged.query = append_qs
278
+ end
279
+ end
280
+ tag[attribute] = merged
281
+ #puts merged.inspect
282
+ end # end of skipping special chars
283
+
284
+
285
+ end # end of each tag
286
+ end # end of empty
287
+ end # end of attrs
288
+
289
+ doc.search("*[@style]").each do |el|
290
+ el['style'] = CssParser.convert_uris(el.attributes['style'].to_s, base_uri)
291
+ end
292
+ doc
293
+ end
294
+
295
+ def self.escape_string(str)
296
+ str.gsub(/"/, "'")
297
+ end
298
+
299
+ def self.resolve_link(path, base_path)
300
+ if base_path.kind_of?(URI)
301
+ base_path.merge!(path)
302
+ return Premailer.canonicalize(base_path)
303
+ elsif base_path.kind_of?(String) and base_path =~ /^(http[s]?|ftp):\/\//i
304
+ base_uri = URI.parse(base_path)
305
+ base_uri.merge!(path)
306
+ return Premailer.canonicalize(base_uri)
307
+ else
308
+
309
+ return File.expand_path(path, File.dirname(base_path))
310
+ end
311
+ end
312
+
313
+ # from http://www.ruby-forum.com/topic/140101
314
+ def self.canonicalize(uri)
315
+ u = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
316
+ u.normalize!
317
+ newpath = u.path
318
+ while newpath.gsub!(%r{([^/]+)/\.\./?}) { |match|
319
+ $1 == '..' ? match : ''
320
+ } do end
321
+ newpath = newpath.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/')
322
+ u.path = newpath
323
+ u.to_s
324
+ end
325
+
326
+
327
+
328
+ def add_body_imposter(doc)
329
+ newdoc = doc
330
+ if body_tag = newdoc.at("body") and body_tag.attributes["style"]
331
+ body_html = body_tag.inner_html
332
+ body_tag.inner_html = "\n<div id=\"premailer_body_wrapper\">\n#{body_html}\n</div>\n"
333
+ if body_tag.attributes["style"]
334
+ newdoc.at("#premailer_body_wrapper")["style"] = body_tag.attributes["style"].to_s
335
+ newdoc.at("body")["style"] = "margin: 0; padding: 0;"
336
+ end
337
+
338
+ end
339
+ return newdoc
340
+ rescue
341
+ return doc
342
+ end
343
+
344
+
345
+ # Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
346
+ def check_client_support
347
+ @client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
348
+
349
+ warnings = []
350
+ properties = []
351
+
352
+ # Get a list off CSS properties
353
+ @doc.search("*[@style]").each do |el|
354
+ style_url = el.attributes['style'].gsub(/([\w\-]+)[\s]*\:/i) do |s|
355
+ properties.push($1)
356
+ end
357
+ end
358
+
359
+ properties.uniq!
360
+
361
+ property_support = @client_support['css_properties']
362
+ properties.each do |prop|
363
+ if property_support.include?(prop) and property_support[prop]['support'] >= @options[:warn_level]
364
+ warnings.push({:message => "#{prop} CSS property",
365
+ :level => WARN_LABEL[property_support[prop]['support']],
366
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
367
+ end
368
+ end
369
+
370
+ @client_support['attributes'].each do |attribute, data|
371
+ next unless data['support'] >= @options[:warn_level]
372
+ if @doc.search("*[@#{attribute}]").length > 0
373
+ warnings.push({:message => "#{attribute} HTML attribute",
374
+ :level => WARN_LABEL[property_support[prop]['support']],
375
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
376
+ end
377
+ end
378
+
379
+ @client_support['elements'].each do |element, data|
380
+ next unless data['support'] >= @options[:warn_level]
381
+ if @doc.search("element").length > 0
382
+ warnings.push({:message => "#{element} HTML element",
383
+ :level => WARN_LABEL[property_support[prop]['support']],
384
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
385
+ end
386
+ end
387
+
388
+
389
+
390
+
391
+
392
+ return warnings
393
+ end
394
+ end
395
+
396
+