premailer-plus 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,8 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ ._*
7
+ *~
8
+ *.tmproj
data/LICENSE ADDED
@@ -0,0 +1,42 @@
1
+ Premailer License
2
+
3
+ Copyright (c) 2007 Alex Dunae
4
+
5
+ Premailer is copyrighted free software by Alex Dunae (http://dunae.ca/).
6
+ You can redistribute it and/or modify it under the conditions below:
7
+
8
+ 1. You may make and give away verbatim copies of the source form of the
9
+ software without restriction, provided that you duplicate all of the
10
+ original copyright notices and associated disclaimers.
11
+
12
+ 2. You may modify your copy of the software in any way, provided that
13
+ you do at least ONE of the following:
14
+
15
+ a) place your modifications in the Public Domain or otherwise
16
+ make them Freely Available, such as by posting said
17
+ modifications to the internet or an equivalent medium, or by
18
+ allowing the author to include your modifications in the software.
19
+
20
+ b) use the modified software only within your corporation or
21
+ organization.
22
+
23
+ c) rename any non-standard executables so the names do not conflict
24
+ with standard executables, which must also be provided.
25
+
26
+ d) make other distribution arrangements with the author.
27
+
28
+ 3. You may modify and include the part of the software into any other
29
+ software (possibly commercial) as long as clear acknowledgement and
30
+ a link back to the original software (http://code.dunae.ca/premailer.web/)
31
+ is provided.
32
+
33
+ 5. The scripts and library files supplied as input to or produced as
34
+ output from the software do not automatically fall under the
35
+ copyright of the software, but belong to whomever generated them,
36
+ and may be sold commercially, and may be aggregated with this
37
+ software.
38
+
39
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
40
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
41
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
42
+ PURPOSE.
@@ -0,0 +1,43 @@
1
+ h1. premailer-plus
2
+
3
+ |*Misc. improvements 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
+ Check out the Premailer web application at http://code.dunae.ca/premailer.web
7
+
8
+ h2. Background
9
+
10
+ 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.
11
+
12
+ h3. Problems with "Premailer Classic"
13
+
14
+ 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.
15
+
16
+ h3. Improvements in Premailer Plus
17
+
18
+ * I fixed the command line script so that it no longer throws errors when you feed it a local file
19
+ * New option to shorten the URLs of links in your plain text version
20
+ ** "Bit.ly":http://bit.ly
21
+ * More to come!
22
+
23
+ h2. Installation
24
+
25
+ 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):
26
+
27
+ <pre>
28
+ $ sudo gem install mikedamage-premailer-plus
29
+ </pre>
30
+
31
+ You can also clone this repository and run @rake build@ from the project root. Then run @sudo gem install ./pkg/premailer-plus*.gem@
32
+
33
+ h2. Dependencies
34
+
35
+ * "RubyGems":http://rubygems.rubyforge.org
36
+ * "Hpricot":http://wiki.github.com/why/hpricot
37
+ * "css-parser":http://code.dunae.ca/css_parser/
38
+ * "text-reform":http://rubyforge.org/projects/text-format/
39
+ * "Bitly4r":http://wiki.cantremember.com/Bitly4R/HomePage
40
+
41
+ h2. Copyright
42
+
43
+ Since I only improved Premailer's handling of local files and gave it the ability to shorten URL's, the copyright to this code is still held by Alex Dunae. I am only modifying and redistributing his code in compliance with the terms of Premailer's license.
@@ -0,0 +1,62 @@
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.add_dependency("hpricot", ">= 0.8.1")
14
+ gem.add_dependency("htmlentities", ">= 4.1.0")
15
+ gem.add_dependency("css_parser", ">= 0.9.0")
16
+ gem.add_dependency("text-reform", ">= 0.2.0")
17
+
18
+ Jeweler::GemcutterTasks.new
19
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
+ end
21
+ rescue LoadError
22
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
23
+ end
24
+
25
+ require 'rake/testtask'
26
+ Rake::TestTask.new(:test) do |test|
27
+ test.libs << 'lib' << 'test'
28
+ test.pattern = 'test/**/*_test.rb'
29
+ test.verbose = true
30
+ end
31
+
32
+ begin
33
+ require 'rcov/rcovtask'
34
+ Rcov::RcovTask.new do |test|
35
+ test.libs << 'test'
36
+ test.pattern = 'test/**/*_test.rb'
37
+ test.verbose = true
38
+ end
39
+ rescue LoadError
40
+ task :rcov do
41
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
42
+ end
43
+ end
44
+
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/rdoctask'
49
+ Rake::RDocTask.new do |rdoc|
50
+ if File.exist?('VERSION.yml')
51
+ config = YAML.load(File.read('VERSION.yml'))
52
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
53
+ else
54
+ version = ""
55
+ end
56
+
57
+ rdoc.rdoc_dir = 'rdoc'
58
+ rdoc.title = "premailer-plus #{version}"
59
+ rdoc.rdoc_files.include('README*')
60
+ rdoc.rdoc_files.include('lib/**/*.rb')
61
+ end
62
+
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 3
@@ -0,0 +1,107 @@
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
+ BANNER = <<-END
15
+ Premailer Plus
16
+ -----------------
17
+
18
+ Usage:
19
+ FILE [options] inputfile outputfile
20
+ "inputfile" can be either a local file or a URL
21
+
22
+ Options:
23
+ END
24
+
25
+ def initialize(args)
26
+ @args = args
27
+ @config = OpenStruct.new({
28
+ :plaintext => false,
29
+ :shorten_urls => false,
30
+ :base_url => '',
31
+ :infile => nil,
32
+ :outfile => '',
33
+ :querystring => '',
34
+ :warnings => false,
35
+ :verbose => true
36
+ })
37
+ end
38
+
39
+ def run
40
+ if parse_options and get_arguments
41
+ @pm_opts = {}
42
+ @pm_opts.merge!({:base_url => @config.base_url}) unless @config.base_url.empty?
43
+ @pm_opts.merge!({:link_query_string => @config.querystring}) unless @config.querystring.empty?
44
+ @pm_opts.merge!({:shorten_urls => true}) if @config.shorten_urls
45
+ @premailer = Premailer.new(@config.infile.to_s, @pm_opts)
46
+
47
+ write_premail_html
48
+ write_plain_text if @config.plaintext
49
+ show_warnings if @config.warnings
50
+ else
51
+ puts "Invalid options/arguments!"
52
+ exit 1
53
+ end
54
+ end
55
+
56
+ private
57
+ def parse_options
58
+ opts = OptionParser.new
59
+ opts.banner = BANNER.gsub!(/FILE/, File.basename(__FILE__))
60
+ opts.on("-p", "--plaintext", "Output a plain-text version of the email in addition to HTML") { @config.plaintext = true }
61
+ opts.on("-s", "--shorten-urls", "Shorten URLs with Bit.ly in the plain-text version") { @config.shorten_urls = true }
62
+ opts.on("-b", "--baseurl URL", "Prepend this URL to links") {|url| @config.base_url = url }
63
+ opts.on("-q", "--querystring STRING", "Append this query string to link URLs") {|string| @config.querystring = string }
64
+ opts.on("-v", "--verbose", "Display status information during the pre-mailing process") { @config.verbose = true }
65
+ opts.on("-w", "--warnings", "Display CSS support warnings") { @config.warnings = true }
66
+ opts.on("-h", "--help", "Show this information") { puts opts; exit 0; }
67
+ true if opts.parse!(@args) rescue return false
68
+ end
69
+
70
+ def get_arguments
71
+ if @args.nitems >= 2
72
+ @config.infile = Pathname.new(@args.shift)
73
+ @config.outfile = Pathname.new(@args.shift)
74
+ return true
75
+ else
76
+ return false
77
+ end
78
+ end
79
+
80
+ def write_premail_html
81
+ puts "Converting HTML to inline CSS and saving as #{@config.outfile.basename.to_s}..." if @config.verbose
82
+ @config.outfile.open("w") do |file|
83
+ file.write(@premailer.to_inline_css)
84
+ end
85
+ puts "Done." if @config.verbose
86
+ end
87
+
88
+ def write_plain_text
89
+ textfile = Pathname.new(File.join(@config.outfile.dirname.to_s, @config.outfile.basename.to_s.gsub(/\..+$/, ".txt")))
90
+ puts "Saving a plain-text version of your email as #{@config.outfile.basename.to_s.gsub(/\.html$/, '.txt')}..." if @config.verbose
91
+ puts "Shortening link URLs with Bit.ly..." if @config.verbose and @config.shorten_urls
92
+ textfile.open("w") do |file|
93
+ file.write(@premailer.to_plain_text)
94
+ end
95
+ puts "Done." if @config.verbose
96
+ end
97
+
98
+ def show_warnings
99
+ puts "#{@premailer.warnings.nitems.to_s} warnings found:"
100
+ @premailer.warnings.each do |warning|
101
+ puts warning
102
+ end
103
+ end
104
+ end
105
+
106
+ app = PremailerApp.new(ARGV)
107
+ app.run
@@ -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
@@ -0,0 +1,401 @@
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
+ require 'tempfile'
16
+
17
+ require 'html_to_plain_text'
18
+
19
+ # Premailer processes HTML and CSS to improve e-mail deliverability.
20
+ #
21
+ # Premailer's main function is to render all CSS as inline <tt>style</tt> attributes using
22
+ # the CssParser. It can also convert relative links to absolute links and check the 'safety' of
23
+ # CSS properties against a CSS support chart.
24
+ #
25
+ # = Example
26
+ #
27
+ # premailer = Premailer.new(html_file, :warn_level => Premailer::Warnings::SAFE)
28
+ # premailer.parse!
29
+ # puts premailer.warnings.length.to_s + ' warnings found'
30
+ class Premailer
31
+ include HtmlToPlainText
32
+ include CssParser
33
+
34
+ CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../misc/client_support.yaml'
35
+
36
+ RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
37
+
38
+ # should also exclude :first-letter, etc...
39
+
40
+ # URI of the HTML file used
41
+ attr_reader :html_file
42
+
43
+ module Warnings
44
+ NONE = 0
45
+ SAFE = 1
46
+ POOR = 2
47
+ RISKY = 3
48
+ end
49
+ include Warnings
50
+
51
+ WARN_LABEL = %w(NONE SAFE POOR RISKY)
52
+
53
+ # Create a new Premailer object.
54
+ #
55
+ # +uri+ is the URL of the HTML file to process. Should be a string.
56
+ #
57
+ # ==== Options
58
+ # [+line_length+] Line length used by to_plain_text. Boolean, default is 65.
59
+ # [+warn_level+] What level of CSS compatibility warnings to show (see Warnings).
60
+ # [+link_query_string+] A string to append to every <a href=""> link.
61
+ def initialize(uri, options = {})
62
+ @options = {:warn_level => Warnings::SAFE, :line_length => 65, :link_query_string => nil, :base_url => nil, :shorten_urls => false}.merge(options)
63
+ @html_file = uri
64
+
65
+
66
+ @is_local_file = true
67
+ if uri =~ /^(http|https|ftp)\:\/\//i
68
+ @is_local_file = false
69
+ end
70
+
71
+
72
+ @css_warnings = []
73
+
74
+ @css_parser = CssParser::Parser.new({:absolute_paths => true,
75
+ :import => true,
76
+ :io_exceptions => false
77
+ })
78
+
79
+ @doc, @html_charset = load_html(@html_file)
80
+
81
+ if @is_local_file and @options[:base_url]
82
+ @doc = convert_inline_links(@doc, @options[:base_url])
83
+ elsif not @is_local_file
84
+ @doc = convert_inline_links(@doc, @html_file)
85
+ end
86
+ load_css_from_html!
87
+ end
88
+
89
+ # Array containing a hash of CSS warnings.
90
+ def warnings
91
+ return [] if @options[:warn_level] == Warnings::NONE
92
+ @css_warnings = check_client_support if @css_warnings.empty?
93
+ @css_warnings
94
+ end
95
+
96
+ # Returns the original HTML as a string.
97
+ def to_s
98
+ @doc.to_html
99
+ end
100
+
101
+ # Returns the document with all HTML tags removed.
102
+ def to_plain_text
103
+ html_src = ''
104
+ begin
105
+ html_src = @doc.search("body").innerHTML
106
+ rescue
107
+ html_src = @doc.to_html
108
+ end
109
+ if @options[:shorten_urls]
110
+ convert_to_text(html_src, @options[:line_length], @html_charset, true)
111
+ else
112
+ convert_to_text(html_src, @options[:line_length], @html_charset, false)
113
+ end
114
+ end
115
+
116
+ # Merge CSS into the HTML document.
117
+ #
118
+ # Returns a string.
119
+ def to_inline_css
120
+ doc = @doc
121
+ unmergable_rules = CssParser::Parser.new
122
+
123
+ # Give all styles already in style attributes a specificity of 1000
124
+ # per http://www.w3.org/TR/CSS21/cascade.html#specificity
125
+ doc.search("*[@style]").each do |el|
126
+ el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
127
+ end
128
+
129
+ # Iterate through the rules and merge them into the HTML
130
+ @css_parser.each_selector(:all) do |selector, declaration, specificity|
131
+ # Save un-mergable rules separately
132
+ selector.gsub!(/:link([\s]|$)+/i, '')
133
+
134
+ # Convert element names to lower case
135
+ selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
136
+
137
+ if selector =~ RE_UNMERGABLE_SELECTORS
138
+ unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
139
+ else
140
+
141
+ doc.search(selector) do |el|
142
+ if el.elem?
143
+ # Add a style attribute or append to the existing one
144
+ block = "[SPEC=#{specificity}[#{declaration}]]"
145
+ el['style'] = (el.attributes['style'] ||= '') + ' ' + block
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ # Read <style> attributes and perform folding
152
+ doc.search("*[@style]").each do |el|
153
+ style = el.attributes['style'].to_s
154
+
155
+ declarations = []
156
+
157
+ style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
158
+ rs = RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
159
+ declarations << rs
160
+ end
161
+
162
+ # Perform style folding and save
163
+ merged = CssParser.merge(declarations)
164
+
165
+ el['style'] = Premailer.escape_string(merged.declarations_to_s)
166
+ end
167
+
168
+ doc = write_unmergable_css_rules(doc, unmergable_rules)
169
+
170
+ #doc = add_body_imposter(doc)
171
+
172
+ doc.to_html
173
+ end
174
+
175
+
176
+ protected
177
+ # Load the HTML file and convert it into an Hpricot document.
178
+ #
179
+ # Returns an Hpricot document and a string with the HTML file's character set.
180
+ def load_html(uri)
181
+ if @is_local_file
182
+ Hpricot(File.open(uri, "r") {|f| f.read })
183
+ else
184
+ @temp_file = Tempfile.new("premailer_plus")
185
+ @temp_file.write(open(uri).read)
186
+ @temp_file.close
187
+ @is_local_file = true
188
+ Hpricot(@temp_file.open.read)
189
+ end
190
+ end
191
+
192
+ # Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
193
+ def load_css_from_html!
194
+ if tags = @doc.search("link[@rel='stylesheet'], style")
195
+ tags.each do |tag|
196
+ if tag.to_s.strip =~ /^\<link/i and tag.attributes['href']
197
+ if media_type_ok?(tag.attributes['media'])
198
+ link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
199
+ if @is_local_file
200
+ css_block = ''
201
+ File.open(link_uri, "r") do |file|
202
+ while line = file.gets
203
+ css_block << line
204
+ end
205
+ end
206
+ @css_parser.add_block!(css_block, {:base_uri => @html_file})
207
+ else
208
+ @css_parser.load_uri!(link_uri)
209
+ end
210
+ end
211
+ elsif tag.to_s.strip =~ /^\<style/i
212
+ @css_parser.add_block!(tag.innerHTML, :base_uri => URI.parse(@html_file))
213
+ end
214
+ end
215
+ tags.remove
216
+ end
217
+ end
218
+
219
+ def media_type_ok?(media_types)
220
+ return media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
221
+ rescue
222
+ return true
223
+ end
224
+
225
+ # Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
226
+ # and write it into the <tt>body</tt>.
227
+ #
228
+ # <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
229
+ #
230
+ # Returns an Hpricot document.
231
+ def write_unmergable_css_rules(doc, unmergable_rules)
232
+ styles = ''
233
+ unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
234
+ styles += "#{selector} { #{declarations} }\n"
235
+ end
236
+ unless styles.empty?
237
+ style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
238
+ doc.search("head").append(style_tag)
239
+ end
240
+ doc
241
+ end
242
+
243
+ # Convert relative links to absolute links.
244
+ #
245
+ # Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
246
+ # as well as CSS <tt>url()</tt> declarations found in inline <tt>style</tt> attributes.
247
+ #
248
+ # <tt>doc</tt> is an Hpricot document and <tt>base_uri</tt> is either a string or a URI.
249
+ #
250
+ # Returns an Hpricot document.
251
+ def convert_inline_links(doc, base_uri)
252
+ base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
253
+
254
+ ['href', 'src', 'background'].each do |attribute|
255
+
256
+ tags = doc.search("*[@#{attribute}]")
257
+ append_qs = @options[:link_query_string] ||= ''
258
+ unless tags.empty?
259
+ tags.each do |tag|
260
+ unless tag.attributes[attribute] =~ /^(\{|\[|<|\#)/i
261
+ if tag.attributes[attribute] =~ /^http/i
262
+ begin
263
+ merged = URI.parse(tag.attributes[attribute])
264
+ rescue
265
+ next
266
+ end
267
+ else
268
+ begin
269
+ merged = Premailer.resolve_link(tag.attributes[attribute].to_s, base_uri.merge)
270
+ rescue
271
+ begin
272
+ merged = Premailer.resolve_link(URI.escape(tag.attributes[attribute].to_s), base_uri.merge)
273
+ # merged = base_uri.merge(URI.escape(tag.attributes[attribute].to_s))
274
+ rescue; end
275
+ end
276
+ end # end of relative urls only
277
+
278
+ if tag.name =~ /^a$/i and not append_qs.empty?
279
+ if merged.query
280
+ merged.query = merged.query + '&' + append_qs
281
+ else
282
+ merged.query = append_qs
283
+ end
284
+ end
285
+ tag[attribute] = merged
286
+ #puts merged.inspect
287
+ end # end of skipping special chars
288
+
289
+
290
+ end # end of each tag
291
+ end # end of empty
292
+ end # end of attrs
293
+
294
+ doc.search("*[@style]").each do |el|
295
+ el['style'] = CssParser.convert_uris(el.attributes['style'].to_s, base_uri)
296
+ end
297
+ doc
298
+ end
299
+
300
+ def self.escape_string(str)
301
+ str.gsub(/"/, "'")
302
+ end
303
+
304
+ def self.resolve_link(path, base_path)
305
+ if base_path.kind_of?(URI)
306
+ base_path.merge!(path)
307
+ return Premailer.canonicalize(base_path)
308
+ elsif base_path.kind_of?(String) and base_path =~ /^(http[s]?|ftp):\/\//i
309
+ base_uri = URI.parse(base_path)
310
+ base_uri.merge!(path)
311
+ return Premailer.canonicalize(base_uri)
312
+ else
313
+
314
+ return File.expand_path(path, File.dirname(base_path))
315
+ end
316
+ end
317
+
318
+ # from http://www.ruby-forum.com/topic/140101
319
+ def self.canonicalize(uri)
320
+ u = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
321
+ u.normalize!
322
+ newpath = u.path
323
+ while newpath.gsub!(%r{([^/]+)/\.\./?}) { |match|
324
+ $1 == '..' ? match : ''
325
+ } do end
326
+ newpath = newpath.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/')
327
+ u.path = newpath
328
+ u.to_s
329
+ end
330
+
331
+
332
+
333
+ def add_body_imposter(doc)
334
+ newdoc = doc
335
+ if body_tag = newdoc.at("body") and body_tag.attributes["style"]
336
+ body_html = body_tag.inner_html
337
+ body_tag.inner_html = "\n<div id=\"premailer_body_wrapper\">\n#{body_html}\n</div>\n"
338
+ if body_tag.attributes["style"]
339
+ newdoc.at("#premailer_body_wrapper")["style"] = body_tag.attributes["style"].to_s
340
+ newdoc.at("body")["style"] = "margin: 0; padding: 0;"
341
+ end
342
+
343
+ end
344
+ return newdoc
345
+ rescue
346
+ return doc
347
+ end
348
+
349
+
350
+ # Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
351
+ def check_client_support
352
+ @client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
353
+
354
+ warnings = []
355
+ properties = []
356
+
357
+ # Get a list off CSS properties
358
+ @doc.search("*[@style]").each do |el|
359
+ style_url = el.attributes['style'].gsub(/([\w\-]+)[\s]*\:/i) do |s|
360
+ properties.push($1)
361
+ end
362
+ end
363
+
364
+ properties.uniq!
365
+
366
+ property_support = @client_support['css_properties']
367
+ properties.each do |prop|
368
+ if property_support.include?(prop) and property_support[prop]['support'] >= @options[:warn_level]
369
+ warnings.push({:message => "#{prop} CSS property",
370
+ :level => WARN_LABEL[property_support[prop]['support']],
371
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
372
+ end
373
+ end
374
+
375
+ @client_support['attributes'].each do |attribute, data|
376
+ next unless data['support'] >= @options[:warn_level]
377
+ if @doc.search("*[@#{attribute}]").length > 0
378
+ warnings.push({:message => "#{attribute} HTML attribute",
379
+ :level => WARN_LABEL[property_support[prop]['support']],
380
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
381
+ end
382
+ end
383
+
384
+ @client_support['elements'].each do |element, data|
385
+ next unless data['support'] >= @options[:warn_level]
386
+ if @doc.search("element").length > 0
387
+ warnings.push({:message => "#{element} HTML element",
388
+ :level => WARN_LABEL[property_support[prop]['support']],
389
+ :clients => property_support[prop]['unsupported_in'].join(', ')})
390
+ end
391
+ end
392
+
393
+
394
+
395
+
396
+
397
+ return warnings
398
+ end
399
+ end
400
+
401
+