packrat 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/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/packrat.rb +28 -0
- data/lib/packrat/view_helpers.rb +20 -0
- data/lib/premailer/html_to_plain_text.rb +74 -0
- data/lib/premailer/premailer.rb +409 -0
- data/packrat.gemspec +57 -0
- data/test/helper.rb +10 -0
- data/test/test_packrat.rb +7 -0
- metadata +87 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Mike Hansen
|
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.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= packrat
|
2
|
+
|
3
|
+
Packrat is a gem for merging all css rules to inline for html emails and files.
|
4
|
+
|
5
|
+
== Note on Patches/Pull Requests
|
6
|
+
|
7
|
+
* Fork the project.
|
8
|
+
* Make your feature addition or bug fix.
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
10
|
+
future version unintentionally.
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
12
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
+
* Send me a pull request. Bonus points for topic branches.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2010 Mike Hansen. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "packrat"
|
8
|
+
gem.summary = "Packrat is a gem for merging all css rules to inline for html emails and files."
|
9
|
+
gem.description = "Packrat is a gem for merging all css rules to inline for html emails and files."
|
10
|
+
gem.email = "indyjones805@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/mikehansen/packrat"
|
12
|
+
gem.authors = ["Mike Hansen"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/test_*.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :test => :check_dependencies
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "packrat #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/packrat.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'hpricot'
|
4
|
+
require 'css_parser'
|
5
|
+
require 'active_support'
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + "/premailer/html_to_plain_text"
|
8
|
+
require File.dirname(__FILE__) + "/premailer/premailer"
|
9
|
+
|
10
|
+
module Packrat
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def enable
|
14
|
+
enable_actionpack
|
15
|
+
end
|
16
|
+
|
17
|
+
def enable_actionpack
|
18
|
+
return if ActionView::Base.method_defined? :packrat_css
|
19
|
+
require 'packrat/view_helpers'
|
20
|
+
ActionView::Base.send :include, ViewHelpers
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
if defined? Rails
|
27
|
+
Packrat.enable_actionpack if defined? ActionController
|
28
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Packrat
|
2
|
+
module ViewHelpers
|
3
|
+
|
4
|
+
def packrat_css(&block)
|
5
|
+
c = capture(&block)
|
6
|
+
premailer = Premailer.new(StringIO.new(c), :local_file => false)
|
7
|
+
inlined = premailer.to_inline_css
|
8
|
+
|
9
|
+
if Rails::VERSION::MAJOR == 2
|
10
|
+
concat(inlined, proc.binding)
|
11
|
+
elsif Rails::VERSION::MAJOR == 3
|
12
|
+
inlined.html_safe
|
13
|
+
else
|
14
|
+
puts "Sorry, Packrat only works with Rails 2.x and Rails 3.x"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'text/reform'
|
2
|
+
require 'htmlentities'
|
3
|
+
|
4
|
+
# Support functions for Premailer
|
5
|
+
module HtmlToPlainText
|
6
|
+
|
7
|
+
# Returns the text in UTF-8 format with all HTML tags removed
|
8
|
+
#
|
9
|
+
# TODO:
|
10
|
+
# - add support for DL, OL
|
11
|
+
def convert_to_text(html, line_length = 65, from_charset = 'UTF-8')
|
12
|
+
r = Text::Reform.new(:trim => true,
|
13
|
+
:squeeze => false,
|
14
|
+
:break => Text::Reform.break_wrap)
|
15
|
+
|
16
|
+
txt = html
|
17
|
+
|
18
|
+
# decode HTML entities
|
19
|
+
he = HTMLEntities.new
|
20
|
+
txt = he.decode(txt)
|
21
|
+
|
22
|
+
# handle headings (H1-H6)
|
23
|
+
txt.gsub!(/[ \t]*<h([0-9]+)[^>]*>(.*)<\/h[0-9]+>/i) do |s|
|
24
|
+
hlevel = $1.to_i
|
25
|
+
# cleanup text inside of headings
|
26
|
+
htext = $2.gsub(/<\/?[^>]*>/i, '').strip
|
27
|
+
hlength = (htext.length > line_length ?
|
28
|
+
line_length :
|
29
|
+
htext.length)
|
30
|
+
|
31
|
+
case hlevel
|
32
|
+
when 1 # H1, asterisks above and below
|
33
|
+
('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength) + "\n"
|
34
|
+
when 2 # H1, dashes above and below
|
35
|
+
('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength) + "\n"
|
36
|
+
else # H3-H6, dashes below
|
37
|
+
htext + "\n" + ('-' * htext.length) + "\n"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# links
|
42
|
+
txt.gsub!(/<a.*href=\"([^\"]*)\"[^>]*>(.*)<\/a>/i) do |s|
|
43
|
+
$2.strip + ' ( ' + $1.strip + ' )'
|
44
|
+
end
|
45
|
+
|
46
|
+
# lists -- TODO: should handle ordered lists
|
47
|
+
txt.gsub!(/[\s]*(<li[^>]*>)[\s]*/i, '* ')
|
48
|
+
# list not followed by a newline
|
49
|
+
txt.gsub!(/<\/li>[\s]*(?![\n])/i, "\n")
|
50
|
+
|
51
|
+
# paragraphs and line breaks
|
52
|
+
txt.gsub!(/<\/p>/i, "\n\n")
|
53
|
+
txt.gsub!(/<br[\/ ]*>/i, "\n")
|
54
|
+
|
55
|
+
# strip remaining tags
|
56
|
+
txt.gsub!(/<\/?[^>]*>/, '')
|
57
|
+
|
58
|
+
# wrap text
|
59
|
+
txt = r.format(('[' * line_length), txt)
|
60
|
+
|
61
|
+
# remove linefeeds (\r\n and \r -> \n)
|
62
|
+
txt.gsub!(/\r\n?/, "\n")
|
63
|
+
|
64
|
+
# strip extra spaces
|
65
|
+
txt.gsub!(/\302\240+/, " ") # non-breaking spaces -> spaces
|
66
|
+
txt.gsub!(/\n[ \t]+/, "\n") # space at start of lines
|
67
|
+
txt.gsub!(/[ \t]+\n/, "\n") # space at end of lines
|
68
|
+
|
69
|
+
# no more than two consecutive newlines
|
70
|
+
txt.gsub!(/[\n]{3,}/, "\n\n")
|
71
|
+
|
72
|
+
txt.strip
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,409 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
#
|
3
|
+
# Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-09
|
4
|
+
#
|
5
|
+
# Premailer processes HTML and CSS to improve e-mail deliverability.
|
6
|
+
#
|
7
|
+
# Premailer's main function is to render all CSS as inline <tt>style</tt>
|
8
|
+
# attributes. It also converts relative links to absolute links and checks
|
9
|
+
# the 'safety' of CSS properties against a CSS support chart.
|
10
|
+
#
|
11
|
+
# = Example
|
12
|
+
# premailer = Premailer.new('http://example.com/myfile.html', :warn_level => Premailer::Warnings::SAFE)
|
13
|
+
#
|
14
|
+
# # Write the HTML output
|
15
|
+
# fout = File.open("output.html", "w")
|
16
|
+
# fout.puts premailer.to_inline_css
|
17
|
+
# fout.close
|
18
|
+
#
|
19
|
+
# # Write the plain-text output
|
20
|
+
# fout = File.open("ouput.txt", "w")
|
21
|
+
# fout.puts premailer.to_plain_text
|
22
|
+
# fout.close
|
23
|
+
#
|
24
|
+
# # List any CSS warnings
|
25
|
+
# puts premailer.warnings.length.to_s + ' warnings found'
|
26
|
+
# premailer.warnings.each do |w|
|
27
|
+
# puts "#{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# premailer = Premailer.new(html_file, :warn_level => Premailer::Warnings::SAFE)
|
31
|
+
# puts premailer.to_inline_css
|
32
|
+
class Premailer
|
33
|
+
include HtmlToPlainText
|
34
|
+
include CssParser
|
35
|
+
|
36
|
+
VERSION = '1.5.4'
|
37
|
+
|
38
|
+
CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
|
39
|
+
|
40
|
+
RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
|
41
|
+
|
42
|
+
# should also exclude :first-letter, etc...
|
43
|
+
|
44
|
+
# URI of the HTML file used
|
45
|
+
attr_reader :html_file
|
46
|
+
|
47
|
+
# processed HTML document (Hpricot)
|
48
|
+
attr_reader :processed_doc
|
49
|
+
|
50
|
+
# source HTML document (Hpricot)
|
51
|
+
attr_reader :doc
|
52
|
+
|
53
|
+
module Warnings
|
54
|
+
NONE = 0
|
55
|
+
SAFE = 1
|
56
|
+
POOR = 2
|
57
|
+
RISKY = 3
|
58
|
+
end
|
59
|
+
include Warnings
|
60
|
+
|
61
|
+
WARN_LABEL = %w(NONE SAFE POOR RISKY)
|
62
|
+
|
63
|
+
# Create a new Premailer object.
|
64
|
+
#
|
65
|
+
# +path+ is the path to the HTML file to process. Can be either the URL of a
|
66
|
+
# remote file or a local path.
|
67
|
+
#
|
68
|
+
# ==== Options
|
69
|
+
# [+line_length+] Line length used by to_plain_text. Boolean, default is 65.
|
70
|
+
# [+warn_level+] What level of CSS compatibility warnings to show (see Warnings).
|
71
|
+
# [+link_query_string+] A string to append to every <a href=""> link. Do not include the initial +?+.
|
72
|
+
# [+base_url+] Used to calculate absolute URLs for local files.
|
73
|
+
def initialize(path, options = {})
|
74
|
+
@options = {:warn_level => Warnings::SAFE,
|
75
|
+
:line_length => 65,
|
76
|
+
:link_query_string => nil,
|
77
|
+
:base_url => nil,
|
78
|
+
:local_file => true,
|
79
|
+
:remove_classes => false}.merge(options)
|
80
|
+
@html_file = path
|
81
|
+
|
82
|
+
if options[:local_file] == false
|
83
|
+
@is_local_file = false
|
84
|
+
elsif path =~ /^(http|https|ftp)\:\/\//i
|
85
|
+
@is_local_file = false
|
86
|
+
else
|
87
|
+
@is_local_file = true
|
88
|
+
end
|
89
|
+
|
90
|
+
@css_warnings = []
|
91
|
+
|
92
|
+
@css_parser = CssParser::Parser.new({:absolute_paths => true,
|
93
|
+
:import => true,
|
94
|
+
:io_exceptions => false
|
95
|
+
})
|
96
|
+
|
97
|
+
@doc, @html_charset = load_html(@html_file)
|
98
|
+
@processed_doc = @doc
|
99
|
+
|
100
|
+
if @is_local_file and @options[:base_url]
|
101
|
+
@processed_doc = convert_inline_links(@processed_doc, @options[:base_url])
|
102
|
+
elsif not @is_local_file
|
103
|
+
@processed_doc = convert_inline_links(@processed_doc, @html_file)
|
104
|
+
end
|
105
|
+
load_css_from_html!
|
106
|
+
end
|
107
|
+
|
108
|
+
# Array containing a hash of CSS warnings.
|
109
|
+
def warnings
|
110
|
+
return [] if @options[:warn_level] == Warnings::NONE
|
111
|
+
@css_warnings = check_client_support if @css_warnings.empty?
|
112
|
+
@css_warnings
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the original HTML as a string.
|
116
|
+
def to_s
|
117
|
+
@doc.to_html
|
118
|
+
end
|
119
|
+
|
120
|
+
# Converts the HTML document to a format suitable for plain-text e-mail.
|
121
|
+
#
|
122
|
+
# Returns a string.
|
123
|
+
def to_plain_text
|
124
|
+
html_src = ''
|
125
|
+
begin
|
126
|
+
html_src = @doc.search("body").innerHTML
|
127
|
+
rescue
|
128
|
+
html_src = @doc.to_html
|
129
|
+
end
|
130
|
+
convert_to_text(html_src, @options[:line_length], @html_charset)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Merge CSS into the HTML document.
|
134
|
+
#
|
135
|
+
# Returns a string.
|
136
|
+
def to_inline_css
|
137
|
+
doc = @processed_doc
|
138
|
+
unmergable_rules = CssParser::Parser.new
|
139
|
+
|
140
|
+
# Give all styles already in style attributes a specificity of 1000
|
141
|
+
# per http://www.w3.org/TR/CSS21/cascade.html#specificity
|
142
|
+
doc.search("*[@style]").each do |el|
|
143
|
+
el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
|
144
|
+
end
|
145
|
+
|
146
|
+
# Iterate through the rules and merge them into the HTML
|
147
|
+
@css_parser.each_selector(:all) do |selector, declaration, specificity|
|
148
|
+
# Save un-mergable rules separately
|
149
|
+
selector.gsub!(/:link([\s]|$)+/i, '')
|
150
|
+
|
151
|
+
# Convert element names to lower case
|
152
|
+
selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
|
153
|
+
|
154
|
+
if selector =~ RE_UNMERGABLE_SELECTORS
|
155
|
+
unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
|
156
|
+
else
|
157
|
+
|
158
|
+
doc.search(selector) do |el|
|
159
|
+
if el.elem?
|
160
|
+
# Add a style attribute or append to the existing one
|
161
|
+
block = "[SPEC=#{specificity}[#{declaration}]]"
|
162
|
+
el['style'] = (el.attributes['style'] ||= '') + ' ' + block
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Read <style> attributes and perform folding
|
169
|
+
doc.search("*[@style]").each do |el|
|
170
|
+
style = el.attributes['style'].to_s
|
171
|
+
|
172
|
+
declarations = []
|
173
|
+
|
174
|
+
style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
|
175
|
+
rs = RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
|
176
|
+
declarations << rs
|
177
|
+
end
|
178
|
+
|
179
|
+
# Perform style folding and save
|
180
|
+
merged = CssParser.merge(declarations)
|
181
|
+
|
182
|
+
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
183
|
+
end
|
184
|
+
|
185
|
+
doc = write_unmergable_css_rules(doc, unmergable_rules)
|
186
|
+
|
187
|
+
doc.search('*').remove_class if @options[:remove_classes]
|
188
|
+
|
189
|
+
@processed_doc = doc
|
190
|
+
|
191
|
+
doc.to_html
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
protected
|
196
|
+
# Load the HTML file and convert it into an Hpricot document.
|
197
|
+
#
|
198
|
+
# Returns an Hpricot document and a string with the HTML file's character set.
|
199
|
+
def load_html(path) # :nodoc:
|
200
|
+
if path.is_a?(IO) || path.is_a?(StringIO)
|
201
|
+
@html_file = "http://foo.bar"
|
202
|
+
Hpricot(path.read)
|
203
|
+
elsif @is_local_file
|
204
|
+
Hpricot(File.open(path, "r") {|f| f.read })
|
205
|
+
else
|
206
|
+
Hpricot(open(path))
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
|
211
|
+
def load_css_from_html! # :nodoc:
|
212
|
+
if tags = @doc.search("link[@rel='stylesheet'], style")
|
213
|
+
tags.each do |tag|
|
214
|
+
|
215
|
+
if tag.to_s.strip =~ /^\<link/i and tag.attributes['href'] and media_type_ok?(tag.attributes['media'])
|
216
|
+
|
217
|
+
link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
|
218
|
+
if @is_local_file
|
219
|
+
css_block = ''
|
220
|
+
begin
|
221
|
+
File.open(link_uri, "r") do |file|
|
222
|
+
while line = file.gets
|
223
|
+
css_block << line
|
224
|
+
end
|
225
|
+
end
|
226
|
+
@css_parser.add_block!(css_block, {:base_uri => @html_file})
|
227
|
+
rescue; end
|
228
|
+
else
|
229
|
+
@css_parser.load_uri!(link_uri)
|
230
|
+
end
|
231
|
+
|
232
|
+
elsif tag.to_s.strip =~ /^\<style/i
|
233
|
+
@css_parser.add_block!(tag.innerHTML, :base_uri => URI.parse(@html_file))
|
234
|
+
end
|
235
|
+
end
|
236
|
+
tags.remove
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def media_type_ok?(media_types) # :nodoc:
|
241
|
+
return true if media_types.nil? or media_types.empty?
|
242
|
+
return media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
|
243
|
+
rescue
|
244
|
+
return true
|
245
|
+
end
|
246
|
+
|
247
|
+
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
248
|
+
# and write it into the <tt>body</tt>.
|
249
|
+
#
|
250
|
+
# <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
251
|
+
#
|
252
|
+
# Returns an Hpricot document.
|
253
|
+
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
254
|
+
styles = ''
|
255
|
+
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
256
|
+
styles += "#{selector} { #{declarations} }\n"
|
257
|
+
end
|
258
|
+
|
259
|
+
unless styles.empty?
|
260
|
+
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
|
261
|
+
doc.search("head").append(style_tag)
|
262
|
+
end
|
263
|
+
doc
|
264
|
+
end
|
265
|
+
|
266
|
+
# Convert relative links to absolute links.
|
267
|
+
#
|
268
|
+
# Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
|
269
|
+
# as well as CSS <tt>url()</tt> declarations found in inline <tt>style</tt> attributes.
|
270
|
+
#
|
271
|
+
# <tt>doc</tt> is an Hpricot document and <tt>base_uri</tt> is either a string or a URI.
|
272
|
+
#
|
273
|
+
# Returns an Hpricot document.
|
274
|
+
def convert_inline_links(doc, base_uri) # :nodoc:
|
275
|
+
base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
|
276
|
+
|
277
|
+
append_qs = @options[:link_query_string] ||= ''
|
278
|
+
|
279
|
+
['href', 'src', 'background'].each do |attribute|
|
280
|
+
tags = doc.search("*[@#{attribute}]")
|
281
|
+
|
282
|
+
next if tags.empty?
|
283
|
+
|
284
|
+
tags.each do |tag|
|
285
|
+
# skip links that look like they have merge tags
|
286
|
+
# and mailto, ftp, etc...
|
287
|
+
if tag.attributes[attribute] =~ /^(\{|\[|<|\#|mailto:|ftp:|gopher:)/i
|
288
|
+
next
|
289
|
+
end
|
290
|
+
|
291
|
+
if tag.attributes[attribute] =~ /^http/i
|
292
|
+
begin
|
293
|
+
merged = URI.parse(tag.attributes[attribute])
|
294
|
+
rescue; next; end
|
295
|
+
else
|
296
|
+
begin
|
297
|
+
merged = Premailer.resolve_link(tag.attributes[attribute].to_s, base_uri)
|
298
|
+
rescue
|
299
|
+
begin
|
300
|
+
merged = Premailer.resolve_link(URI.escape(tag.attributes[attribute].to_s), base_uri)
|
301
|
+
rescue; end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# make sure 'merged' is a URI
|
306
|
+
merged = URI.parse(merged.to_s) unless merged.kind_of?(URI)
|
307
|
+
|
308
|
+
# only append a querystring to <a> tags
|
309
|
+
if tag.name =~ /^a$/i and not append_qs.empty?
|
310
|
+
if merged.query
|
311
|
+
merged.query = merged.query + '&' + append_qs
|
312
|
+
else
|
313
|
+
merged.query = append_qs
|
314
|
+
end
|
315
|
+
end
|
316
|
+
tag[attribute] = merged.to_s
|
317
|
+
|
318
|
+
end # end of each tag
|
319
|
+
end # end of each attrs
|
320
|
+
|
321
|
+
doc.search("*[@style]").each do |el|
|
322
|
+
el['style'] = CssParser.convert_uris(el.attributes['style'].to_s, base_uri)
|
323
|
+
end
|
324
|
+
doc
|
325
|
+
end
|
326
|
+
|
327
|
+
def self.escape_string(str) # :nodoc:
|
328
|
+
str.gsub(/"/, "'")
|
329
|
+
end
|
330
|
+
|
331
|
+
def self.resolve_link(path, base_path) # :nodoc:
|
332
|
+
resolved = nil
|
333
|
+
if base_path.kind_of?(URI)
|
334
|
+
resolved = base_path.merge(path)
|
335
|
+
return Premailer.canonicalize(resolved)
|
336
|
+
elsif base_path.kind_of?(String) and base_path =~ /^(http[s]?|ftp):\/\//i
|
337
|
+
resolved = URI.parse(base_path)
|
338
|
+
resolved = resolved.merge(path)
|
339
|
+
return Premailer.canonicalize(resolved)
|
340
|
+
else
|
341
|
+
|
342
|
+
return File.expand_path(path, File.dirname(base_path))
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# from http://www.ruby-forum.com/topic/140101
|
347
|
+
def self.canonicalize(uri) # :nodoc:
|
348
|
+
u = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
|
349
|
+
u.normalize!
|
350
|
+
newpath = u.path
|
351
|
+
while newpath.gsub!(%r{([^/]+)/\.\./?}) { |match|
|
352
|
+
$1 == '..' ? match : ''
|
353
|
+
} do end
|
354
|
+
newpath = newpath.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/')
|
355
|
+
u.path = newpath
|
356
|
+
u.to_s
|
357
|
+
end
|
358
|
+
|
359
|
+
# Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
|
360
|
+
def check_client_support # :nodoc:
|
361
|
+
@client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
|
362
|
+
|
363
|
+
warnings = []
|
364
|
+
properties = []
|
365
|
+
|
366
|
+
# Get a list off CSS properties
|
367
|
+
@processed_doc.search("*[@style]").each do |el|
|
368
|
+
style_url = el.attributes['style'].gsub(/([\w\-]+)[\s]*\:/i) do |s|
|
369
|
+
properties.push($1)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
properties.uniq!
|
374
|
+
|
375
|
+
property_support = @client_support['css_properties']
|
376
|
+
properties.each do |prop|
|
377
|
+
if property_support.include?(prop) and
|
378
|
+
property_support[prop].include?('support') and
|
379
|
+
property_support[prop]['support'] >= @options[:warn_level]
|
380
|
+
warnings.push({:message => "#{prop} CSS property",
|
381
|
+
:level => WARN_LABEL[property_support[prop]['support']],
|
382
|
+
:clients => property_support[prop]['unsupported_in'].join(', ')})
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
@client_support['attributes'].each do |attribute, data|
|
387
|
+
next unless data['support'] >= @options[:warn_level]
|
388
|
+
if @doc.search("*[@#{attribute}]").length > 0
|
389
|
+
warnings.push({:message => "#{attribute} HTML attribute",
|
390
|
+
:level => WARN_LABEL[property_support[prop]['support']],
|
391
|
+
:clients => property_support[prop]['unsupported_in'].join(', ')})
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
@client_support['elements'].each do |element, data|
|
396
|
+
next unless data['support'] >= @options[:warn_level]
|
397
|
+
if @doc.search("element").length > 0
|
398
|
+
warnings.push({:message => "#{element} HTML element",
|
399
|
+
:level => WARN_LABEL[property_support[prop]['support']],
|
400
|
+
:clients => property_support[prop]['unsupported_in'].join(', ')})
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
return warnings
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
|
409
|
+
|
data/packrat.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{packrat}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Mike Hansen"]
|
12
|
+
s.date = %q{2010-10-18}
|
13
|
+
s.description = %q{Packrat is a gem for merging all css rules to inline for html emails and files.}
|
14
|
+
s.email = %q{indyjones805@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"lib/packrat.rb",
|
27
|
+
"lib/packrat/view_helpers.rb",
|
28
|
+
"lib/premailer/html_to_plain_text.rb",
|
29
|
+
"lib/premailer/premailer.rb",
|
30
|
+
"packrat.gemspec",
|
31
|
+
"test/helper.rb",
|
32
|
+
"test/test_packrat.rb"
|
33
|
+
]
|
34
|
+
s.homepage = %q{http://github.com/mikehansen/packrat}
|
35
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
s.rubygems_version = %q{1.3.6}
|
38
|
+
s.summary = %q{Packrat is a gem for merging all css rules to inline for html emails and files.}
|
39
|
+
s.test_files = [
|
40
|
+
"test/helper.rb",
|
41
|
+
"test/test_packrat.rb"
|
42
|
+
]
|
43
|
+
|
44
|
+
if s.respond_to? :specification_version then
|
45
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
46
|
+
s.specification_version = 3
|
47
|
+
|
48
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
49
|
+
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
50
|
+
else
|
51
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
52
|
+
end
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: packrat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Mike Hansen
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-10-18 00:00:00 -05:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: thoughtbot-shoulda
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :development
|
31
|
+
version_requirements: *id001
|
32
|
+
description: Packrat is a gem for merging all css rules to inline for html emails and files.
|
33
|
+
email: indyjones805@gmail.com
|
34
|
+
executables: []
|
35
|
+
|
36
|
+
extensions: []
|
37
|
+
|
38
|
+
extra_rdoc_files:
|
39
|
+
- LICENSE
|
40
|
+
- README.rdoc
|
41
|
+
files:
|
42
|
+
- .document
|
43
|
+
- .gitignore
|
44
|
+
- LICENSE
|
45
|
+
- README.rdoc
|
46
|
+
- Rakefile
|
47
|
+
- VERSION
|
48
|
+
- lib/packrat.rb
|
49
|
+
- lib/packrat/view_helpers.rb
|
50
|
+
- lib/premailer/html_to_plain_text.rb
|
51
|
+
- lib/premailer/premailer.rb
|
52
|
+
- packrat.gemspec
|
53
|
+
- test/helper.rb
|
54
|
+
- test/test_packrat.rb
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: http://github.com/mikehansen/packrat
|
57
|
+
licenses: []
|
58
|
+
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options:
|
61
|
+
- --charset=UTF-8
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
version: "0"
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
requirements: []
|
79
|
+
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 1.3.6
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: Packrat is a gem for merging all css rules to inline for html emails and files.
|
85
|
+
test_files:
|
86
|
+
- test/helper.rb
|
87
|
+
- test/test_packrat.rb
|