premailer 1.7.0 → 1.7.1
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/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:
|