premailer 1.7.0 → 1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +1 -5
- data/lib/premailer.rb +0 -3
- data/lib/premailer/adapter.rb +39 -29
- data/lib/premailer/adapter/hpricot.rb +159 -156
- data/lib/premailer/adapter/nokogiri.rb +176 -172
- data/lib/premailer/premailer.rb +2 -3
- metadata +33 -18
data/README.rdoc
CHANGED
@@ -48,12 +48,8 @@ Download the Premailer gem from RubyGems.
|
|
48
48
|
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.
|
49
49
|
|
50
50
|
A few areas that are particularly in need of love:
|
51
|
-
*
|
52
|
-
There were unit tests but they were so funky that it was better to just strip them out.
|
53
|
-
* Create a binary file for easing command line use, allowing the output to be piped in *nix systems
|
54
|
-
* Test with Rails
|
51
|
+
* Improved test coverage
|
55
52
|
* Move un-repeated background images defined in CSS to <tt><td background=""></tt> for Outlook
|
56
|
-
* Correctly parse http://www.webstandards.org/files/acid2/test.html
|
57
53
|
|
58
54
|
=== Credits and code
|
59
55
|
|
data/lib/premailer.rb
CHANGED
@@ -4,8 +4,5 @@ require 'digest/md5'
|
|
4
4
|
require 'cgi'
|
5
5
|
require 'css_parser'
|
6
6
|
require File.expand_path(File.dirname(__FILE__) + '/premailer/adapter')
|
7
|
-
require File.expand_path(File.dirname(__FILE__) + '/premailer/adapter/hpricot')
|
8
|
-
require File.expand_path(File.dirname(__FILE__) + '/premailer/adapter/nokogiri')
|
9
|
-
|
10
7
|
require File.expand_path(File.dirname(__FILE__) + '/premailer/html_to_plain_text')
|
11
8
|
require File.expand_path(File.dirname(__FILE__) + '/premailer/premailer')
|
data/lib/premailer/adapter.rb
CHANGED
@@ -4,46 +4,56 @@
|
|
4
4
|
#
|
5
5
|
# * nokogiri
|
6
6
|
# * hpricot
|
7
|
-
|
8
|
-
|
7
|
+
class Premailer
|
8
|
+
module Adapter
|
9
9
|
|
10
|
-
|
10
|
+
autoload :Hpricot, 'premailer/adapter/hpricot'
|
11
|
+
autoload :Nokogiri, 'premailer/adapter/nokogiri'
|
12
|
+
|
13
|
+
REQUIREMENT_MAP = [
|
14
|
+
["hpricot", :hpricot],
|
15
|
+
["nokogiri", :nokogiri],
|
16
|
+
]
|
17
|
+
|
18
|
+
# Returns the adapter to use.
|
11
19
|
def self.use
|
12
|
-
@use
|
20
|
+
return @use if @use
|
21
|
+
self.use = self.default
|
22
|
+
@use
|
13
23
|
end
|
14
24
|
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
25
|
+
# The default adapter based on what you currently have loaded and
|
26
|
+
# installed. First checks to see if any adapters are already loaded,
|
27
|
+
# then ckecks to see which are installed if none are loaded.
|
28
|
+
def self.default
|
29
|
+
return :hpricot if defined?(::Hpricot)
|
30
|
+
return :nokogiri if defined?(::Nokogiri)
|
31
|
+
|
32
|
+
REQUIREMENT_MAP.each do |(library, adapter)|
|
33
|
+
begin
|
34
|
+
require library
|
35
|
+
return adapter
|
36
|
+
rescue LoadError
|
37
|
+
next
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
raise "No suitable adapter for Premailer was found, please install hpricot or nokogiri"
|
19
42
|
end
|
20
43
|
|
21
|
-
#
|
22
|
-
def self.
|
23
|
-
@
|
24
|
-
:nokogiri => { :class => Nokogiri, :require => "nokogiri" },
|
25
|
-
:hpricot => { :class => Hpricot, :require => "hpricot" },
|
26
|
-
}
|
44
|
+
# Sets the +adapter+ to use. Raises an +ArgumentError+ unless the +adapter+ exists.
|
45
|
+
def self.use=(new_adapter)
|
46
|
+
@use = find(new_adapter)
|
27
47
|
end
|
28
48
|
|
29
49
|
# Returns an +adapter+. Raises an +ArgumentError+ unless the +adapter+ exists.
|
30
50
|
def self.find(adapter)
|
31
|
-
|
32
|
-
load_adapter adapter
|
33
|
-
end
|
51
|
+
return adapter if adapter.is_a?(Module)
|
34
52
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
def self.validate_adapter!(adapter)
|
39
|
-
raise ArgumentError, "Invalid adapter: #{adapter}" unless adapters[adapter]
|
53
|
+
Premailer::Adapter.const_get("#{adapter.to_s.split('_').map{|s| s.capitalize}.join('')}")
|
54
|
+
rescue NameError
|
55
|
+
raise ArgumentError, "Invalid adapter: #{adapter}"
|
40
56
|
end
|
41
57
|
|
42
|
-
|
43
|
-
def self.load_adapter(adapter)
|
44
|
-
require adapters[adapter][:require]
|
45
|
-
[adapter, adapters[adapter][:class]]
|
46
|
-
rescue LoadError
|
47
|
-
puts "tried to use the #{adapter} adapter, but was unable to find the library in the LOAD_PATH."
|
48
|
-
end
|
58
|
+
end
|
49
59
|
end
|
@@ -1,180 +1,183 @@
|
|
1
|
+
require 'hpricot'
|
2
|
+
|
3
|
+
class Premailer
|
4
|
+
module Adapter
|
5
|
+
module Hpricot
|
6
|
+
|
7
|
+
# Merge CSS into the HTML document.
|
8
|
+
#
|
9
|
+
# Returns a string.
|
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
|
1
19
|
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
else
|
29
|
-
begin
|
30
|
-
# Change single ID CSS selectors into xpath so that we can match more
|
31
|
-
# than one element. Added to work around dodgy generated code.
|
32
|
-
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
33
|
-
|
34
|
-
doc.search(selector).each do |el|
|
35
|
-
if el.elem? and (el.name != 'head' and el.parent.name != 'head')
|
36
|
-
# Add a style attribute or append to the existing one
|
37
|
-
block = "[SPEC=#{specificity}[#{declaration}]]"
|
38
|
-
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
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
|
+
# Change single ID CSS selectors into xpath so that we can match more
|
33
|
+
# than one element. Added to work around dodgy generated code.
|
34
|
+
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
35
|
+
|
36
|
+
doc.search(selector).each do |el|
|
37
|
+
if el.elem? and (el.name != 'head' and el.parent.name != 'head')
|
38
|
+
# Add a style attribute or append to the existing one
|
39
|
+
block = "[SPEC=#{specificity}[#{declaration}]]"
|
40
|
+
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
41
|
+
end
|
42
|
+
end
|
43
|
+
rescue ::Hpricot::Error, RuntimeError, ArgumentError
|
44
|
+
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
45
|
+
next
|
39
46
|
end
|
40
47
|
end
|
41
|
-
rescue Hpricot::Error, RuntimeError, ArgumentError
|
42
|
-
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
43
|
-
next
|
44
48
|
end
|
45
|
-
end
|
46
|
-
end
|
47
49
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
declarations = []
|
50
|
+
# Read STYLE attributes and perform folding
|
51
|
+
doc.search("*[@style]").each do |el|
|
52
|
+
style = el.attributes['style'].to_s
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
54
|
+
declarations = []
|
55
|
+
|
56
|
+
style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
|
57
|
+
rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
|
58
|
+
declarations << rs
|
59
|
+
end
|
58
60
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
# Perform style folding
|
62
|
+
merged = CssParser.merge(declarations)
|
63
|
+
merged.expand_shorthand!
|
64
|
+
|
65
|
+
# Duplicate CSS attributes as HTML attributes
|
66
|
+
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name)
|
67
|
+
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
|
68
|
+
el[html_att] = merged[css_att].gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
merged.create_dimensions_shorthand!
|
73
|
+
|
74
|
+
# write the inline STYLE attribute
|
75
|
+
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
67
76
|
end
|
68
|
-
end
|
69
|
-
|
70
|
-
merged.create_dimensions_shorthand!
|
71
77
|
|
72
|
-
|
73
|
-
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
74
|
-
end
|
78
|
+
doc = write_unmergable_css_rules(doc, @unmergable_rules)
|
75
79
|
|
76
|
-
|
80
|
+
if @options[:remove_classes] or @options[:remove_comments]
|
81
|
+
doc.search('*').each do |el|
|
82
|
+
if el.comment? and @options[:remove_comments]
|
83
|
+
lst = el.parent.children
|
84
|
+
el.parent = nil
|
85
|
+
lst.delete(el)
|
86
|
+
elsif el.elem?
|
87
|
+
el.remove_attribute('class') if @options[:remove_classes]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
77
91
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
92
|
+
if @options[:remove_ids]
|
93
|
+
# find all anchor's targets and hash them
|
94
|
+
targets = []
|
95
|
+
doc.search("a[@href^='#']").each do |el|
|
96
|
+
target = el.get_attribute('href')[1..-1]
|
97
|
+
targets << target
|
98
|
+
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
|
99
|
+
end
|
100
|
+
# hash ids that are links target, delete others
|
101
|
+
doc.search("*[@id]").each do |el|
|
102
|
+
id = el.get_attribute('id')
|
103
|
+
if targets.include?(id)
|
104
|
+
el.set_attribute('id', Digest::MD5.hexdigest(id))
|
105
|
+
else
|
106
|
+
el.remove_attribute('id')
|
107
|
+
end
|
108
|
+
end
|
86
109
|
end
|
87
|
-
end
|
88
|
-
end
|
89
110
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
doc.search("a[@href^='#']").each do |el|
|
94
|
-
target = el.get_attribute('href')[1..-1]
|
95
|
-
targets << target
|
96
|
-
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
|
111
|
+
@processed_doc = doc
|
112
|
+
|
113
|
+
@processed_doc.to_original_html
|
97
114
|
end
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
115
|
+
|
116
|
+
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
117
|
+
# and write it into the <tt>body</tt>.
|
118
|
+
#
|
119
|
+
# <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
120
|
+
#
|
121
|
+
# Returns an Hpricot document.
|
122
|
+
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
123
|
+
if head = doc.search('head')
|
124
|
+
styles = ''
|
125
|
+
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
126
|
+
styles += "#{selector} { #{declarations} }\n"
|
127
|
+
end
|
128
|
+
|
129
|
+
unless styles.empty?
|
130
|
+
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
|
131
|
+
head.html.empty? ? head.inner_html(style_tag) : head.append(style_tag)
|
132
|
+
end
|
103
133
|
else
|
104
|
-
|
134
|
+
$stderr.puts "Unable to write unmergable CSS rules: no <head> was found" if @options[:verbose]
|
105
135
|
end
|
136
|
+
doc
|
106
137
|
end
|
107
|
-
end
|
108
138
|
|
109
|
-
@processed_doc = doc
|
110
139
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
styles += "#{selector} { #{declarations} }\n"
|
125
|
-
end
|
126
|
-
|
127
|
-
unless styles.empty?
|
128
|
-
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
|
129
|
-
head.html.empty? ? head.inner_html(style_tag) : head.append(style_tag)
|
140
|
+
# Converts the HTML document to a format suitable for plain-text e-mail.
|
141
|
+
#
|
142
|
+
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
143
|
+
#
|
144
|
+
# Returns a string.
|
145
|
+
def to_plain_text
|
146
|
+
html_src = ''
|
147
|
+
begin
|
148
|
+
html_src = @doc.search("body").inner_html
|
149
|
+
rescue; end
|
150
|
+
|
151
|
+
html_src = @doc.to_html unless html_src and not html_src.empty?
|
152
|
+
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
130
153
|
end
|
131
|
-
else
|
132
|
-
$stderr.puts "Unable to write unmergable CSS rules: no <head> was found" if @options[:verbose]
|
133
|
-
end
|
134
|
-
doc
|
135
|
-
end
|
136
154
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
# Returns the original HTML as a string.
|
155
|
-
def to_s
|
156
|
-
@doc.to_original_html
|
157
|
-
end
|
158
|
-
|
159
|
-
# Load the HTML file and convert it into an Hpricot document.
|
160
|
-
#
|
161
|
-
# Returns an Hpricot document.
|
162
|
-
def load_html(input) # :nodoc:
|
163
|
-
thing = nil
|
164
|
-
|
165
|
-
# TODO: duplicate options
|
166
|
-
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
|
167
|
-
thing = input
|
155
|
+
|
156
|
+
# Returns the original HTML as a string.
|
157
|
+
def to_s
|
158
|
+
@doc.to_original_html
|
159
|
+
end
|
160
|
+
|
161
|
+
# Load the HTML file and convert it into an Hpricot document.
|
162
|
+
#
|
163
|
+
# Returns an Hpricot document.
|
164
|
+
def load_html(input) # :nodoc:
|
165
|
+
thing = nil
|
166
|
+
|
167
|
+
# TODO: duplicate options
|
168
|
+
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
|
169
|
+
thing = input
|
168
170
|
elsif @is_local_file
|
169
|
-
|
170
|
-
|
171
|
+
@base_dir = File.dirname(input)
|
172
|
+
thing = File.open(input, 'r')
|
171
173
|
else
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
174
|
+
thing = open(input)
|
175
|
+
end
|
176
|
+
|
177
|
+
# TODO: deal with Hpricot seg faults on empty input
|
178
|
+
thing ? Hpricot(thing) : nil
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
180
183
|
end
|
@@ -1,199 +1,203 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class Premailer
|
4
|
+
module Adapter
|
5
|
+
module Nokogiri
|
6
|
+
|
7
|
+
# Merge CSS into the HTML document.
|
8
|
+
#
|
9
|
+
# Returns a string.
|
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
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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 }
|
21
24
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
+
# Change single ID CSS selectors into xpath so that we can match more
|
33
|
+
# than one element. Added to work around dodgy generated code.
|
34
|
+
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
35
|
+
|
36
|
+
doc.search(selector).each do |el|
|
37
|
+
if el.elem? and (el.name != 'head' and el.parent.name != 'head')
|
38
|
+
# Add a style attribute or append to the existing one
|
39
|
+
block = "[SPEC=#{specificity}[#{declaration}]]"
|
40
|
+
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
41
|
+
end
|
38
42
|
end
|
43
|
+
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
|
44
|
+
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
45
|
+
next
|
39
46
|
end
|
40
|
-
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
|
41
|
-
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
42
|
-
next
|
43
47
|
end
|
44
48
|
end
|
45
|
-
end
|
46
49
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
declarations = []
|
50
|
+
# Read STYLE attributes and perform folding
|
51
|
+
doc.search("*[@style]").each do |el|
|
52
|
+
style = el.attributes['style'].to_s
|
52
53
|
|
53
|
-
|
54
|
-
rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
|
55
|
-
declarations << rs
|
56
|
-
end
|
54
|
+
declarations = []
|
57
55
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
# Duplicate CSS attributes as HTML attributes
|
63
|
-
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name)
|
64
|
-
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
|
65
|
-
el[html_att] = merged[css_att].gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
56
|
+
style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
|
57
|
+
rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
|
58
|
+
declarations << rs
|
66
59
|
end
|
60
|
+
|
61
|
+
# Perform style folding
|
62
|
+
merged = CssParser.merge(declarations)
|
63
|
+
merged.expand_shorthand!
|
64
|
+
|
65
|
+
# Duplicate CSS attributes as HTML attributes
|
66
|
+
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name)
|
67
|
+
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
|
68
|
+
el[html_att] = merged[css_att].gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
merged.create_dimensions_shorthand!
|
73
|
+
|
74
|
+
# write the inline STYLE attribute
|
75
|
+
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
67
76
|
end
|
68
|
-
|
69
|
-
merged.create_dimensions_shorthand!
|
70
77
|
|
71
|
-
|
72
|
-
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
73
|
-
end
|
78
|
+
doc = write_unmergable_css_rules(doc, @unmergable_rules)
|
74
79
|
|
75
|
-
|
80
|
+
if @options[:remove_classes] or @options[:remove_comments]
|
81
|
+
doc.traverse do |el|
|
82
|
+
if el.comment? and @options[:remove_comments]
|
83
|
+
el.remove
|
84
|
+
elsif el.element?
|
85
|
+
el.remove_attribute('class') if @options[:remove_classes]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
76
89
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
90
|
+
if @options[:remove_ids]
|
91
|
+
# find all anchor's targets and hash them
|
92
|
+
targets = []
|
93
|
+
doc.search("a[@href^='#']").each do |el|
|
94
|
+
target = el.get_attribute('href')[1..-1]
|
95
|
+
targets << target
|
96
|
+
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
|
83
97
|
end
|
84
|
-
|
85
|
-
|
98
|
+
# hash ids that are links target, delete others
|
99
|
+
doc.search("*[@id]").each do |el|
|
100
|
+
id = el.get_attribute('id')
|
101
|
+
if targets.include?(id)
|
102
|
+
el.set_attribute('id', Digest::MD5.hexdigest(id))
|
103
|
+
else
|
104
|
+
el.remove_attribute('id')
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
86
108
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
targets << target
|
93
|
-
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
|
109
|
+
@processed_doc = doc
|
110
|
+
if is_xhtml?
|
111
|
+
@processed_doc.to_xhtml
|
112
|
+
else
|
113
|
+
@processed_doc.to_html
|
94
114
|
end
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
115
|
+
end
|
116
|
+
|
117
|
+
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
118
|
+
# and write it into the <tt>body</tt>.
|
119
|
+
#
|
120
|
+
# <tt>doc</tt> is an Nokogiri document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
121
|
+
#
|
122
|
+
# Returns an Nokogiri document.
|
123
|
+
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
124
|
+
if head = doc.at('head')
|
125
|
+
styles = ''
|
126
|
+
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
127
|
+
styles += "#{selector} { #{declarations} }\n"
|
102
128
|
end
|
129
|
+
|
130
|
+
unless styles.empty?
|
131
|
+
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
|
132
|
+
|
133
|
+
head.add_child(style_tag)
|
134
|
+
end
|
135
|
+
else
|
136
|
+
$stderr.puts "Unable to write unmergable CSS rules: no <head> was found" if @options[:verbose]
|
103
137
|
end
|
138
|
+
doc
|
104
139
|
end
|
105
140
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
|
129
|
-
|
130
|
-
head.add_child(style_tag)
|
141
|
+
|
142
|
+
# Converts the HTML document to a format suitable for plain-text e-mail.
|
143
|
+
#
|
144
|
+
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
145
|
+
#
|
146
|
+
# Returns a string.
|
147
|
+
def to_plain_text
|
148
|
+
html_src = ''
|
149
|
+
begin
|
150
|
+
html_src = @doc.at("body").inner_html
|
151
|
+
rescue; end
|
152
|
+
|
153
|
+
html_src = @doc.to_html unless html_src and not html_src.empty?
|
154
|
+
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns the original HTML as a string.
|
158
|
+
def to_s
|
159
|
+
if is_xhtml?
|
160
|
+
@doc.to_xhtml
|
161
|
+
else
|
162
|
+
@doc.to_html
|
131
163
|
end
|
132
|
-
else
|
133
|
-
$stderr.puts "Unable to write unmergable CSS rules: no <head> was found" if @options[:verbose]
|
134
164
|
end
|
135
|
-
doc
|
136
|
-
end
|
137
165
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
html_src = @doc.at("body").inner_html
|
148
|
-
rescue; end
|
149
|
-
|
150
|
-
html_src = @doc.to_html unless html_src and not html_src.empty?
|
151
|
-
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
152
|
-
end
|
153
|
-
|
154
|
-
# Returns the original HTML as a string.
|
155
|
-
def to_s
|
156
|
-
if is_xhtml?
|
157
|
-
@doc.to_xhtml
|
158
|
-
else
|
159
|
-
@doc.to_html
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# Load the HTML file and convert it into an Nokogiri document.
|
164
|
-
#
|
165
|
-
# Returns an Nokogiri document.
|
166
|
-
def load_html(input) # :nodoc:
|
167
|
-
thing = nil
|
168
|
-
|
169
|
-
# TODO: duplicate options
|
170
|
-
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
|
171
|
-
thing = input
|
166
|
+
# Load the HTML file and convert it into an Nokogiri document.
|
167
|
+
#
|
168
|
+
# Returns an Nokogiri document.
|
169
|
+
def load_html(input) # :nodoc:
|
170
|
+
thing = nil
|
171
|
+
|
172
|
+
# TODO: duplicate options
|
173
|
+
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
|
174
|
+
thing = input
|
172
175
|
elsif @is_local_file
|
173
|
-
|
174
|
-
|
176
|
+
@base_dir = File.dirname(input)
|
177
|
+
thing = File.open(input, 'r')
|
175
178
|
else
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
179
|
+
thing = open(input)
|
180
|
+
end
|
181
|
+
|
182
|
+
if thing.respond_to?(:read)
|
183
|
+
thing = thing.read
|
184
|
+
end
|
185
|
+
|
186
|
+
return nil unless thing
|
187
|
+
|
188
|
+
doc = nil
|
189
|
+
|
190
|
+
# Default encoding is ASCII-8BIT (binary) per http://groups.google.com/group/nokogiri-talk/msg/0b81ef0dc180dc74
|
191
|
+
if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
|
192
|
+
thing = thing.force_encoding('ASCII-8BIT').encode!
|
193
|
+
doc = ::Nokogiri::HTML(thing) {|c| c.noent.recover }
|
194
|
+
else
|
195
|
+
doc = ::Nokogiri::HTML(thing, nil, 'ASCII-8BIT') {|c| c.noent.recover }
|
196
|
+
end
|
197
|
+
|
198
|
+
return doc
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|
199
203
|
end
|
data/lib/premailer/premailer.rb
CHANGED
@@ -33,7 +33,7 @@ class Premailer
|
|
33
33
|
include HtmlToPlainText
|
34
34
|
include CssParser
|
35
35
|
|
36
|
-
VERSION = '1.7.
|
36
|
+
VERSION = '1.7.1'
|
37
37
|
|
38
38
|
CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
|
39
39
|
|
@@ -151,8 +151,7 @@ class Premailer
|
|
151
151
|
:io_exceptions => @options[:io_exceptions]
|
152
152
|
})
|
153
153
|
|
154
|
-
@
|
155
|
-
@adapter_name, @adapter_class = Adapter.find @adapter_name
|
154
|
+
@adapter_class = Adapter.find @options[:adapter]
|
156
155
|
|
157
156
|
self.class.send(:include, @adapter_class)
|
158
157
|
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 1
|
7
7
|
- 7
|
8
|
-
-
|
9
|
-
version: 1.7.
|
8
|
+
- 1
|
9
|
+
version: 1.7.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Alex Dunae
|
@@ -14,11 +14,11 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-
|
17
|
+
date: 2011-04-01 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
|
-
name:
|
21
|
+
name: css_parser
|
22
22
|
prerelease: false
|
23
23
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
24
|
none: false
|
@@ -26,14 +26,14 @@ dependencies:
|
|
26
26
|
- - ">="
|
27
27
|
- !ruby/object:Gem::Version
|
28
28
|
segments:
|
29
|
-
-
|
30
|
-
-
|
31
|
-
-
|
32
|
-
version:
|
29
|
+
- 1
|
30
|
+
- 1
|
31
|
+
- 9
|
32
|
+
version: 1.1.9
|
33
33
|
type: :runtime
|
34
34
|
version_requirements: *id001
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
|
-
name:
|
36
|
+
name: htmlentities
|
37
37
|
prerelease: false
|
38
38
|
requirement: &id002 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
@@ -41,14 +41,14 @@ dependencies:
|
|
41
41
|
- - ">="
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
segments:
|
44
|
-
-
|
45
|
-
-
|
46
|
-
-
|
47
|
-
version:
|
44
|
+
- 4
|
45
|
+
- 0
|
46
|
+
- 0
|
47
|
+
version: 4.0.0
|
48
48
|
type: :runtime
|
49
49
|
version_requirements: *id002
|
50
50
|
- !ruby/object:Gem::Dependency
|
51
|
-
name:
|
51
|
+
name: hpricot
|
52
52
|
prerelease: false
|
53
53
|
requirement: &id003 !ruby/object:Gem::Requirement
|
54
54
|
none: false
|
@@ -56,12 +56,27 @@ dependencies:
|
|
56
56
|
- - ">="
|
57
57
|
- !ruby/object:Gem::Version
|
58
58
|
segments:
|
59
|
-
- 4
|
60
|
-
- 0
|
61
59
|
- 0
|
62
|
-
|
63
|
-
|
60
|
+
- 8
|
61
|
+
- 3
|
62
|
+
version: 0.8.3
|
63
|
+
type: :development
|
64
64
|
version_requirements: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: nokogiri
|
67
|
+
prerelease: false
|
68
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
segments:
|
74
|
+
- 1
|
75
|
+
- 4
|
76
|
+
- 4
|
77
|
+
version: 1.4.4
|
78
|
+
type: :development
|
79
|
+
version_requirements: *id004
|
65
80
|
description: Improve the rendering of HTML emails by making CSS inline, converting links and warning about unsupported code.
|
66
81
|
email: code@dunae.ca
|
67
82
|
executables:
|