premailer 1.6.2 → 1.7.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/README.rdoc +9 -6
- data/bin/premailer +1 -1
- data/lib/premailer.rb +5 -1
- data/lib/premailer/adapter.rb +49 -0
- data/lib/premailer/adapter/hpricot.rb +180 -0
- data/lib/premailer/adapter/nokogiri.rb +199 -0
- data/lib/premailer/html_to_plain_text.rb +19 -15
- data/lib/premailer/premailer.rb +117 -242
- metadata +9 -6
data/README.rdoc
CHANGED
@@ -15,10 +15,13 @@ script is my solution.
|
|
15
15
|
* A plain text version is created
|
16
16
|
Optional
|
17
17
|
|
18
|
+
=== Premailer 2.0 is coming
|
19
|
+
|
20
|
+
I'm looking for input on a version 2.0 update to Premailer. PLease visit the {Premailer 2.0 Planning Page}[https://github.com/alexdunae/premailer/wiki/Premailer-2.0-Planning] and give me your feedback.
|
18
21
|
|
19
22
|
=== Installation
|
20
23
|
|
21
|
-
Download the Premailer gem from
|
24
|
+
Download the Premailer gem from RubyGems.
|
22
25
|
|
23
26
|
sudo gem install premailer
|
24
27
|
|
@@ -42,7 +45,7 @@ Download the Premailer gem from GemCutter.
|
|
42
45
|
|
43
46
|
=== Contributions
|
44
47
|
|
45
|
-
Contributions are most welcome. Premailer was rotting away in a private SVN repository for too long and could use some TLC.
|
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.
|
46
49
|
|
47
50
|
A few areas that are particularly in need of love:
|
48
51
|
* Testing suite
|
@@ -54,13 +57,13 @@ A few areas that are particularly in need of love:
|
|
54
57
|
|
55
58
|
=== Credits and code
|
56
59
|
|
57
|
-
Thanks to {all the wonderful contributors}[
|
60
|
+
Thanks to {all the wonderful contributors}[https://github.com/alexdunae/premailer/contributors] for their updates.
|
58
61
|
|
59
62
|
Thanks to {Greenhood + Company}[http://www.greenhood.com/] for sponsoring some of the 1.5.6 updates,
|
60
63
|
and to {Campaign Monitor}[http://www.campaignmonitor.com] for supporting the web interface.
|
61
64
|
|
62
|
-
The web interface can be found at http://premailer.dialect.ca
|
65
|
+
The web interface can be found at {premailer.dialect.ca}[http://premailer.dialect.ca].
|
63
66
|
|
64
|
-
The source code can be found
|
67
|
+
The source code can be found on {GitHub}[https://github.com/alexdunae/premailer].
|
65
68
|
|
66
|
-
|
69
|
+
Copyright by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-2011. See LICENSE.rdoc for license details.
|
data/bin/premailer
CHANGED
@@ -31,7 +31,7 @@ opts = OptionParser.new do |opts|
|
|
31
31
|
mode = v
|
32
32
|
end
|
33
33
|
|
34
|
-
opts.on("-b", "--base-url", String, "Base URL, useful for local files") do |v|
|
34
|
+
opts.on("-b", "--base-url STRING", String, "Base URL, useful for local files") do |v|
|
35
35
|
options[:base_url] = v
|
36
36
|
end
|
37
37
|
|
data/lib/premailer.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
require 'open-uri'
|
3
|
+
require 'digest/md5'
|
3
4
|
require 'cgi'
|
4
|
-
require 'hpricot'
|
5
5
|
require 'css_parser'
|
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
|
+
|
6
10
|
require File.expand_path(File.dirname(__FILE__) + '/premailer/html_to_plain_text')
|
7
11
|
require File.expand_path(File.dirname(__FILE__) + '/premailer/premailer')
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# = HTTPI::Adapter
|
2
|
+
#
|
3
|
+
# Manages the adapter classes. Currently supports:
|
4
|
+
#
|
5
|
+
# * nokogiri
|
6
|
+
# * hpricot
|
7
|
+
module Adapter
|
8
|
+
DEFAULT = :hpricot
|
9
|
+
|
10
|
+
# Returns the adapter to use. Defaults to <tt>Adapter::</tt>.
|
11
|
+
def self.use
|
12
|
+
@use ||= DEFAULT
|
13
|
+
end
|
14
|
+
|
15
|
+
# Sets the +adapter+ to use. Raises an +ArgumentError+ unless the +adapter+ exists.
|
16
|
+
def self.use=(adapter)
|
17
|
+
validate_adapter! adapter
|
18
|
+
@use = adapter
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a memoized +Hash+ of adapters.
|
22
|
+
def self.adapters
|
23
|
+
@adapters ||= {
|
24
|
+
:nokogiri => { :class => Nokogiri, :require => "nokogiri" },
|
25
|
+
:hpricot => { :class => Hpricot, :require => "hpricot" },
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns an +adapter+. Raises an +ArgumentError+ unless the +adapter+ exists.
|
30
|
+
def self.find(adapter)
|
31
|
+
validate_adapter! adapter
|
32
|
+
load_adapter adapter
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Raises an +ArgumentError+ unless the +adapter+ exists.
|
38
|
+
def self.validate_adapter!(adapter)
|
39
|
+
raise ArgumentError, "Invalid adapter: #{adapter}" unless adapters[adapter]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Tries to load and return the given +adapter+ name and class and falls back to the +FALLBACK+ adapter.
|
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
|
49
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
|
2
|
+
module Adapter
|
3
|
+
module Hpricot
|
4
|
+
|
5
|
+
# Merge CSS into the HTML document.
|
6
|
+
#
|
7
|
+
# Returns a string.
|
8
|
+
def to_inline_css
|
9
|
+
doc = @processed_doc
|
10
|
+
@unmergable_rules = CssParser::Parser.new
|
11
|
+
|
12
|
+
# Give all styles already in style attributes a specificity of 1000
|
13
|
+
# per http://www.w3.org/TR/CSS21/cascade.html#specificity
|
14
|
+
doc.search("*[@style]").each do |el|
|
15
|
+
el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
|
16
|
+
end
|
17
|
+
|
18
|
+
# Iterate through the rules and merge them into the HTML
|
19
|
+
@css_parser.each_selector(:all) do |selector, declaration, specificity|
|
20
|
+
# Save un-mergable rules separately
|
21
|
+
selector.gsub!(/:link([\s]*)+/i) {|m| $1 }
|
22
|
+
|
23
|
+
# Convert element names to lower case
|
24
|
+
selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
|
25
|
+
|
26
|
+
if selector =~ Premailer::RE_UNMERGABLE_SELECTORS
|
27
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless @options[:preserve_styles]
|
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
|
39
|
+
end
|
40
|
+
end
|
41
|
+
rescue Hpricot::Error, RuntimeError, ArgumentError
|
42
|
+
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
43
|
+
next
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Read STYLE attributes and perform folding
|
49
|
+
doc.search("*[@style]").each do |el|
|
50
|
+
style = el.attributes['style'].to_s
|
51
|
+
|
52
|
+
declarations = []
|
53
|
+
|
54
|
+
style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
|
55
|
+
rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
|
56
|
+
declarations << rs
|
57
|
+
end
|
58
|
+
|
59
|
+
# Perform style folding
|
60
|
+
merged = CssParser.merge(declarations)
|
61
|
+
merged.expand_shorthand!
|
62
|
+
|
63
|
+
# Duplicate CSS attributes as HTML attributes
|
64
|
+
if Premailer::RELATED_ATTRIBUTES.has_key?(el.name)
|
65
|
+
Premailer::RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
|
66
|
+
el[html_att] = merged[css_att].gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
merged.create_dimensions_shorthand!
|
71
|
+
|
72
|
+
# write the inline STYLE attribute
|
73
|
+
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
74
|
+
end
|
75
|
+
|
76
|
+
doc = write_unmergable_css_rules(doc, @unmergable_rules)
|
77
|
+
|
78
|
+
if @options[:remove_classes] or @options[:remove_comments]
|
79
|
+
doc.search('*').each do |el|
|
80
|
+
if el.comment? and @options[:remove_comments]
|
81
|
+
lst = el.parent.children
|
82
|
+
el.parent = nil
|
83
|
+
lst.delete(el)
|
84
|
+
elsif el.elem?
|
85
|
+
el.remove_attribute('class') if @options[:remove_classes]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
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))
|
97
|
+
end
|
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
|
108
|
+
|
109
|
+
@processed_doc = doc
|
110
|
+
|
111
|
+
@processed_doc.to_original_html
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
115
|
+
# and write it into the <tt>body</tt>.
|
116
|
+
#
|
117
|
+
# <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
118
|
+
#
|
119
|
+
# Returns an Hpricot document.
|
120
|
+
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
121
|
+
if head = doc.search('head')
|
122
|
+
styles = ''
|
123
|
+
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
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)
|
130
|
+
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
|
+
|
137
|
+
|
138
|
+
# Converts the HTML document to a format suitable for plain-text e-mail.
|
139
|
+
#
|
140
|
+
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
141
|
+
#
|
142
|
+
# Returns a string.
|
143
|
+
def to_plain_text
|
144
|
+
html_src = ''
|
145
|
+
begin
|
146
|
+
html_src = @doc.search("body").inner_html
|
147
|
+
rescue; end
|
148
|
+
|
149
|
+
html_src = @doc.to_html unless html_src and not html_src.empty?
|
150
|
+
convert_to_text(html_src, @options[:line_length], @html_encoding)
|
151
|
+
end
|
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
|
168
|
+
elsif @is_local_file
|
169
|
+
@base_dir = File.dirname(input)
|
170
|
+
thing = File.open(input, 'r')
|
171
|
+
else
|
172
|
+
thing = open(input)
|
173
|
+
end
|
174
|
+
|
175
|
+
# TODO: deal with Hpricot seg faults on empty input
|
176
|
+
thing ? Hpricot(thing) : nil
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module Adapter
|
2
|
+
module Nokogiri
|
3
|
+
|
4
|
+
# Merge CSS into the HTML document.
|
5
|
+
#
|
6
|
+
# Returns a string.
|
7
|
+
def to_inline_css
|
8
|
+
doc = @processed_doc
|
9
|
+
@unmergable_rules = CssParser::Parser.new
|
10
|
+
|
11
|
+
# Give all styles already in style attributes a specificity of 1000
|
12
|
+
# per http://www.w3.org/TR/CSS21/cascade.html#specificity
|
13
|
+
doc.search("*[@style]").each do |el|
|
14
|
+
el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
|
15
|
+
end
|
16
|
+
|
17
|
+
# Iterate through the rules and merge them into the HTML
|
18
|
+
@css_parser.each_selector(:all) do |selector, declaration, specificity|
|
19
|
+
# Save un-mergable rules separately
|
20
|
+
selector.gsub!(/:link([\s]*)+/i) {|m| $1 }
|
21
|
+
|
22
|
+
# Convert element names to lower case
|
23
|
+
selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
|
24
|
+
|
25
|
+
if selector =~ Premailer::RE_UNMERGABLE_SELECTORS
|
26
|
+
@unmergable_rules.add_rule_set!(CssParser::RuleSet.new(selector, declaration)) unless @options[:preserve_styles]
|
27
|
+
else
|
28
|
+
begin
|
29
|
+
# Change single ID CSS selectors into xpath so that we can match more
|
30
|
+
# than one element. Added to work around dodgy generated code.
|
31
|
+
selector.gsub!(/\A\#([\w_\-]+)\Z/, '*[@id=\1]')
|
32
|
+
|
33
|
+
doc.search(selector).each do |el|
|
34
|
+
if el.elem? and (el.name != 'head' and el.parent.name != 'head')
|
35
|
+
# Add a style attribute or append to the existing one
|
36
|
+
block = "[SPEC=#{specificity}[#{declaration}]]"
|
37
|
+
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
38
|
+
end
|
39
|
+
end
|
40
|
+
rescue ::Nokogiri::SyntaxError, RuntimeError, ArgumentError
|
41
|
+
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
42
|
+
next
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Read STYLE attributes and perform folding
|
48
|
+
doc.search("*[@style]").each do |el|
|
49
|
+
style = el.attributes['style'].to_s
|
50
|
+
|
51
|
+
declarations = []
|
52
|
+
|
53
|
+
style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
|
54
|
+
rs = CssParser::RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
|
55
|
+
declarations << rs
|
56
|
+
end
|
57
|
+
|
58
|
+
# Perform style folding
|
59
|
+
merged = CssParser.merge(declarations)
|
60
|
+
merged.expand_shorthand!
|
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?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
merged.create_dimensions_shorthand!
|
70
|
+
|
71
|
+
# write the inline STYLE attribute
|
72
|
+
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
73
|
+
end
|
74
|
+
|
75
|
+
doc = write_unmergable_css_rules(doc, @unmergable_rules)
|
76
|
+
|
77
|
+
if @options[:remove_classes] or @options[:remove_comments]
|
78
|
+
doc.traverse do |el|
|
79
|
+
if el.comment? and @options[:remove_comments]
|
80
|
+
el.remove
|
81
|
+
elsif el.element?
|
82
|
+
el.remove_attribute('class') if @options[:remove_classes]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if @options[:remove_ids]
|
88
|
+
# find all anchor's targets and hash them
|
89
|
+
targets = []
|
90
|
+
doc.search("a[@href^='#']").each do |el|
|
91
|
+
target = el.get_attribute('href')[1..-1]
|
92
|
+
targets << target
|
93
|
+
el.set_attribute('href', "#" + Digest::MD5.hexdigest(target))
|
94
|
+
end
|
95
|
+
# hash ids that are links target, delete others
|
96
|
+
doc.search("*[@id]").each do |el|
|
97
|
+
id = el.get_attribute('id')
|
98
|
+
if targets.include?(id)
|
99
|
+
el.set_attribute('id', Digest::MD5.hexdigest(id))
|
100
|
+
else
|
101
|
+
el.remove_attribute('id')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
@processed_doc = doc
|
107
|
+
if is_xhtml?
|
108
|
+
@processed_doc.to_xhtml
|
109
|
+
else
|
110
|
+
@processed_doc.to_html
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
115
|
+
# and write it into the <tt>body</tt>.
|
116
|
+
#
|
117
|
+
# <tt>doc</tt> is an Nokogiri document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
118
|
+
#
|
119
|
+
# Returns an Nokogiri document.
|
120
|
+
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
121
|
+
if head = doc.at('head')
|
122
|
+
styles = ''
|
123
|
+
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
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
|
+
|
130
|
+
head.add_child(style_tag)
|
131
|
+
end
|
132
|
+
else
|
133
|
+
$stderr.puts "Unable to write unmergable CSS rules: no <head> was found" if @options[:verbose]
|
134
|
+
end
|
135
|
+
doc
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
# Converts the HTML document to a format suitable for plain-text e-mail.
|
140
|
+
#
|
141
|
+
# If present, uses the <body> element as its base; otherwise uses the whole document.
|
142
|
+
#
|
143
|
+
# Returns a string.
|
144
|
+
def to_plain_text
|
145
|
+
html_src = ''
|
146
|
+
begin
|
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
|
172
|
+
elsif @is_local_file
|
173
|
+
@base_dir = File.dirname(input)
|
174
|
+
thing = File.open(input, 'r')
|
175
|
+
else
|
176
|
+
thing = open(input)
|
177
|
+
end
|
178
|
+
|
179
|
+
if thing.respond_to?(:read)
|
180
|
+
thing = thing.read
|
181
|
+
end
|
182
|
+
|
183
|
+
return nil unless thing
|
184
|
+
|
185
|
+
doc = nil
|
186
|
+
|
187
|
+
# Default encoding is ASCII-8BIT (binary) per http://groups.google.com/group/nokogiri-talk/msg/0b81ef0dc180dc74
|
188
|
+
if thing.is_a?(String) and RUBY_VERSION =~ /1.9/
|
189
|
+
thing = thing.force_encoding('ASCII-8BIT').encode!
|
190
|
+
doc = ::Nokogiri::HTML(thing) {|c| c.noent.recover }
|
191
|
+
else
|
192
|
+
doc = ::Nokogiri::HTML(thing, nil, 'ASCII-8BIT') {|c| c.noent.recover }
|
193
|
+
end
|
194
|
+
|
195
|
+
return doc
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
end
|
@@ -6,15 +6,10 @@ module HtmlToPlainText
|
|
6
6
|
|
7
7
|
# Returns the text in UTF-8 format with all HTML tags removed
|
8
8
|
#
|
9
|
-
# TODO:
|
10
|
-
# - add support for DL, OL
|
9
|
+
# TODO: add support for DL, OL
|
11
10
|
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
11
|
txt = html
|
17
|
-
|
12
|
+
|
18
13
|
# decode HTML entities
|
19
14
|
he = HTMLEntities.new
|
20
15
|
txt = he.decode(txt)
|
@@ -24,7 +19,7 @@ module HtmlToPlainText
|
|
24
19
|
txt.gsub!(/[\s]*<h([1-6]+)[^>]*>[\s]*(.*)[\s]*<\/h[1-6]+>/i) do |s|
|
25
20
|
hlevel = $1.to_i
|
26
21
|
|
27
|
-
htext = $2
|
22
|
+
htext = $2
|
28
23
|
htext.gsub!(/<br[\s]*\/?>/i, "\n") # handle <br>s
|
29
24
|
htext.gsub!(/<\/?[^>]*>/i, '') # strip tags
|
30
25
|
|
@@ -41,10 +36,13 @@ module HtmlToPlainText
|
|
41
36
|
else # H3-H6, dashes below
|
42
37
|
htext = htext + "\n" + ('-' * hlength)
|
43
38
|
end
|
44
|
-
|
39
|
+
|
45
40
|
"\n\n" + htext + "\n\n"
|
46
41
|
end
|
47
42
|
|
43
|
+
# wrap spans
|
44
|
+
txt.gsub!(/(<\/span>)[\s]+(<span)/mi, '\1 \2')
|
45
|
+
|
48
46
|
# links
|
49
47
|
txt.gsub!(/<a.*href=\"([^\"]*)\"[^>]*>(.*)<\/a>/i) do |s|
|
50
48
|
$2.strip + ' ( ' + $1.strip + ' )'
|
@@ -54,20 +52,19 @@ module HtmlToPlainText
|
|
54
52
|
txt.gsub!(/[\s]*(<li[^>]*>)[\s]*/i, '* ')
|
55
53
|
# list not followed by a newline
|
56
54
|
txt.gsub!(/<\/li>[\s]*(?![\n])/i, "\n")
|
57
|
-
|
55
|
+
|
58
56
|
# paragraphs and line breaks
|
59
57
|
txt.gsub!(/<\/p>/i, "\n\n")
|
60
58
|
txt.gsub!(/<br[\/ ]*>/i, "\n")
|
61
|
-
|
59
|
+
|
62
60
|
# strip remaining tags
|
63
61
|
txt.gsub!(/<\/?[^>]*>/, '')
|
64
62
|
|
65
|
-
|
66
|
-
|
67
|
-
|
63
|
+
txt = word_wrap(txt, line_length)
|
64
|
+
|
68
65
|
# remove linefeeds (\r\n and \r -> \n)
|
69
66
|
txt.gsub!(/\r\n?/, "\n")
|
70
|
-
|
67
|
+
|
71
68
|
# strip extra spaces
|
72
69
|
txt.gsub!(/\302\240+/, " ") # non-breaking spaces -> spaces
|
73
70
|
txt.gsub!(/\n[ \t]+/, "\n") # space at start of lines
|
@@ -78,4 +75,11 @@ module HtmlToPlainText
|
|
78
75
|
|
79
76
|
txt.strip
|
80
77
|
end
|
78
|
+
|
79
|
+
# Taken from Rails' word_wrap helper (http://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-word_wrap)
|
80
|
+
def word_wrap(txt, line_length)
|
81
|
+
txt.split("\n").collect do |line|
|
82
|
+
line.length > line_length ? line.gsub(/(.{1,#{line_length}})(\s+|$)/, "\\1\n").strip : line
|
83
|
+
end * "\n"
|
84
|
+
end
|
81
85
|
end
|
data/lib/premailer/premailer.rb
CHANGED
@@ -4,8 +4,8 @@
|
|
4
4
|
#
|
5
5
|
# Premailer processes HTML and CSS to improve e-mail deliverability.
|
6
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
|
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
9
|
# the 'safety' of CSS properties against a CSS support chart.
|
10
10
|
#
|
11
11
|
# = Example
|
@@ -33,17 +33,17 @@ class Premailer
|
|
33
33
|
include HtmlToPlainText
|
34
34
|
include CssParser
|
35
35
|
|
36
|
-
VERSION = '1.
|
36
|
+
VERSION = '1.7.0'
|
37
37
|
|
38
38
|
CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
|
39
39
|
|
40
40
|
RE_UNMERGABLE_SELECTORS = /(\:(visited|active|hover|focus|after|before|selection|target|first\-(line|letter))|^\@)/i
|
41
|
-
|
41
|
+
|
42
42
|
# list of CSS attributes that can be rendered as HTML attributes
|
43
43
|
#
|
44
44
|
# TODO: too much repetition
|
45
45
|
# TODO: background=""
|
46
|
-
RELATED_ATTRIBUTES = {
|
46
|
+
RELATED_ATTRIBUTES = {
|
47
47
|
'h1' => {'text-align' => 'align'},
|
48
48
|
'h2' => {'text-align' => 'align'},
|
49
49
|
'h3' => {'text-align' => 'align'},
|
@@ -63,18 +63,20 @@ class Premailer
|
|
63
63
|
|
64
64
|
# URI of the HTML file used
|
65
65
|
attr_reader :html_file
|
66
|
-
|
66
|
+
|
67
67
|
# base URL used to resolve links
|
68
68
|
attr_reader :base_url
|
69
69
|
|
70
70
|
# base directory used to resolve links for local files
|
71
71
|
attr_reader :base_dir
|
72
72
|
|
73
|
-
|
74
|
-
|
73
|
+
# unmergeable CSS rules to be preserved in the head (CssParser)
|
74
|
+
attr_reader :unmergable_rules
|
75
|
+
|
76
|
+
# processed HTML document (Hpricot/Nokogiri)
|
75
77
|
attr_reader :processed_doc
|
76
|
-
|
77
|
-
# source HTML document (Hpricot)
|
78
|
+
|
79
|
+
# source HTML document (Hpricot/Nokogiri)
|
78
80
|
attr_reader :doc
|
79
81
|
|
80
82
|
module Warnings
|
@@ -89,8 +91,8 @@ class Premailer
|
|
89
91
|
|
90
92
|
# Create a new Premailer object.
|
91
93
|
#
|
92
|
-
# +html+ is the HTML data to process. It can be either an IO object, the URL of a
|
93
|
-
# remote file, a local path or a raw HTML string. If passing an HTML string you
|
94
|
+
# +html+ is the HTML data to process. It can be either an IO object, the URL of a
|
95
|
+
# remote file, a local path or a raw HTML string. If passing an HTML string you
|
94
96
|
# must set the +:with_html_string+ option to +true+.
|
95
97
|
#
|
96
98
|
# ==== Options
|
@@ -100,24 +102,33 @@ class Premailer
|
|
100
102
|
# [+base_url+] Used to calculate absolute URLs for local files.
|
101
103
|
# [+css+] Manually specify a CSS stylesheet.
|
102
104
|
# [+css_to_attributes+] Copy related CSS attributes into HTML attributes (e.g. +background-color+ to +bgcolor+)
|
105
|
+
# [+css_string+] Pass CSS as a string
|
106
|
+
# [+remove_ids+] Remove ID attributes whenever possible and convert IDs used as anchors to hashed to avoid collisions in webmail programs. Default is +false+.
|
107
|
+
# [+remove_classes+] Remove class attributes. Default is +false+.
|
108
|
+
# [+remove_comments+] Remove html comments. Default is +false+.
|
103
109
|
# [+preserve_styles+] Whether to preserve any <tt>link rel=stylesheet</tt> and <tt>style</tt> elements. Default is +false+.
|
104
110
|
# [+with_html_string+] Whether the +html+ param should be treated as a raw string.
|
105
111
|
# [+verbose+] Whether to print errors and warnings to <tt>$stderr</tt>. Default is +false+.
|
112
|
+
# [+adapter+] Which HTML parser to use, either <tt>:nokogiri</tt> or <tt>:hpricot</tt>. Default is <tt>:hpricot</tt>.
|
106
113
|
def initialize(html, options = {})
|
107
|
-
@options = {:warn_level => Warnings::SAFE,
|
108
|
-
:line_length => 65,
|
109
|
-
:link_query_string => nil,
|
114
|
+
@options = {:warn_level => Warnings::SAFE,
|
115
|
+
:line_length => 65,
|
116
|
+
:link_query_string => nil,
|
110
117
|
:base_url => nil,
|
111
118
|
:remove_classes => false,
|
119
|
+
:remove_ids => false,
|
120
|
+
:remove_comments => false,
|
112
121
|
:css => [],
|
113
122
|
:css_to_attributes => true,
|
114
123
|
:with_html_string => false,
|
124
|
+
:css_string => nil,
|
115
125
|
:preserve_styles => false,
|
116
126
|
:verbose => false,
|
117
127
|
:debug => false,
|
118
|
-
:io_exceptions => false
|
128
|
+
:io_exceptions => false,
|
129
|
+
:adapter => Adapter.use}.merge(options)
|
119
130
|
|
120
|
-
@html_file = html
|
131
|
+
@html_file = html
|
121
132
|
@is_local_file = @options[:with_html_string] || Premailer.local_data?(html)
|
122
133
|
|
123
134
|
@css_files = @options[:css]
|
@@ -126,6 +137,7 @@ class Premailer
|
|
126
137
|
|
127
138
|
@base_url = nil
|
128
139
|
@base_dir = nil
|
140
|
+
@unmergable_rules = nil
|
129
141
|
|
130
142
|
if @options[:base_url]
|
131
143
|
@base_url = URI.parse(@options.delete(:base_url))
|
@@ -138,10 +150,14 @@ class Premailer
|
|
138
150
|
:import => true,
|
139
151
|
:io_exceptions => @options[:io_exceptions]
|
140
152
|
})
|
141
|
-
|
153
|
+
|
154
|
+
@adapter_name = @options[:adapter]
|
155
|
+
@adapter_name, @adapter_class = Adapter.find @adapter_name
|
156
|
+
|
157
|
+
self.class.send(:include, @adapter_class)
|
158
|
+
|
142
159
|
@doc = load_html(@html_file)
|
143
|
-
|
144
|
-
@html_charset = nil # @doc.encoding || nil
|
160
|
+
|
145
161
|
@processed_doc = @doc
|
146
162
|
@processed_doc = convert_inline_links(@processed_doc, @base_url) if @base_url
|
147
163
|
if options[:link_query_string]
|
@@ -157,141 +173,8 @@ class Premailer
|
|
157
173
|
@css_warnings = check_client_support if @css_warnings.empty?
|
158
174
|
@css_warnings
|
159
175
|
end
|
160
|
-
|
161
|
-
# Returns the original HTML as a string.
|
162
|
-
def to_s
|
163
|
-
@doc.to_original_html
|
164
|
-
end
|
165
|
-
|
166
|
-
# Converts the HTML document to a format suitable for plain-text e-mail.
|
167
|
-
#
|
168
|
-
# Returns a string.
|
169
|
-
def to_plain_text
|
170
|
-
html_src = ''
|
171
|
-
begin
|
172
|
-
html_src = @doc.search("body").inner_html
|
173
|
-
rescue
|
174
|
-
html_src = @doc.to_html
|
175
|
-
end
|
176
|
-
convert_to_text(html_src, @options[:line_length], @html_charset)
|
177
|
-
end
|
178
|
-
|
179
|
-
# Merge CSS into the HTML document.
|
180
|
-
#
|
181
|
-
# Returns a string.
|
182
|
-
def to_inline_css
|
183
|
-
doc = @processed_doc
|
184
|
-
unmergable_rules = CssParser::Parser.new
|
185
|
-
|
186
|
-
# Give all styles already in style attributes a specificity of 1000
|
187
|
-
# per http://www.w3.org/TR/CSS21/cascade.html#specificity
|
188
|
-
doc.search("*[@style]").each do |el|
|
189
|
-
el['style'] = '[SPEC=1000[' + el.attributes['style'] + ']]'
|
190
|
-
end
|
191
|
-
|
192
|
-
# Iterate through the rules and merge them into the HTML
|
193
|
-
@css_parser.each_selector(:all) do |selector, declaration, specificity|
|
194
|
-
# Save un-mergable rules separately
|
195
|
-
selector.gsub!(/:link([\s]*)+/i) {|m| $1 }
|
196
|
-
|
197
|
-
# Convert element names to lower case
|
198
|
-
selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
|
199
|
-
|
200
|
-
if selector =~ RE_UNMERGABLE_SELECTORS
|
201
|
-
unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration)) unless @options[:preserve_styles]
|
202
|
-
else
|
203
|
-
begin
|
204
|
-
doc.search(selector).each do |el|
|
205
|
-
if el.elem? and (el.name != 'head' and el.parent.name != 'head')
|
206
|
-
# Add a style attribute or append to the existing one
|
207
|
-
block = "[SPEC=#{specificity}[#{declaration}]]"
|
208
|
-
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
209
|
-
end
|
210
|
-
end
|
211
|
-
rescue Hpricot::Error, RuntimeError, ArgumentError
|
212
|
-
$stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
|
213
|
-
next
|
214
|
-
end
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
# Read STYLE attributes and perform folding
|
219
|
-
doc.search("*[@style]").each do |el|
|
220
|
-
style = el.attributes['style'].to_s
|
221
|
-
|
222
|
-
declarations = []
|
223
|
-
|
224
|
-
style.scan(/\[SPEC\=([\d]+)\[(.[^\]\]]*)\]\]/).each do |declaration|
|
225
|
-
rs = RuleSet.new(nil, declaration[1].to_s, declaration[0].to_i)
|
226
|
-
declarations << rs
|
227
|
-
end
|
228
|
-
|
229
|
-
# Perform style folding
|
230
|
-
merged = CssParser.merge(declarations)
|
231
|
-
merged.expand_shorthand!
|
232
|
-
|
233
|
-
#if @options[:prefer_cellpadding] and (el.name == 'td' or el.name == 'th') and el['cellpadding'].nil?
|
234
|
-
# if cellpadding = equivalent_cellpadding(merged)
|
235
|
-
# el['cellpadding'] = cellpadding
|
236
|
-
# merged['padding-left'] = nil
|
237
|
-
# merged['padding-right'] = nil
|
238
|
-
# merged['padding-top'] = nil
|
239
|
-
# merged['padding-bottom'] = nil
|
240
|
-
# end
|
241
|
-
#end
|
242
|
-
|
243
|
-
# Duplicate CSS attributes as HTML attributes
|
244
|
-
if RELATED_ATTRIBUTES.has_key?(el.name)
|
245
|
-
RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
|
246
|
-
el[html_att] = merged[css_att].gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
merged.create_dimensions_shorthand!
|
251
|
-
|
252
|
-
# write the inline STYLE attribute
|
253
|
-
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
254
|
-
end
|
255
|
-
|
256
|
-
doc = write_unmergable_css_rules(doc, unmergable_rules)
|
257
|
-
|
258
|
-
doc.search('*').remove_class if @options[:remove_classes]
|
259
|
-
|
260
|
-
@processed_doc = doc
|
261
|
-
|
262
|
-
@processed_doc.to_original_html
|
263
|
-
end
|
264
|
-
|
265
|
-
# Check for an XHTML doctype
|
266
|
-
def is_xhtml?
|
267
|
-
intro = @doc.to_s.strip.split("\n")[0..2].join(' ')
|
268
|
-
is_xhtml = (intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i)
|
269
|
-
is_xhtml = is_xhtml ? true : false
|
270
|
-
$stderr.puts "Is XHTML? #{is_xhtml.inspect}\nChecked:\n#{intro}" if @options[:debug]
|
271
|
-
is_xhtml
|
272
|
-
end
|
273
|
-
|
274
|
-
protected
|
275
|
-
# Load the HTML file and convert it into an Hpricot document.
|
276
|
-
#
|
277
|
-
# Returns an Hpricot document.
|
278
|
-
def load_html(input) # :nodoc:
|
279
|
-
thing = nil
|
280
|
-
|
281
|
-
# TODO: duplicate options
|
282
|
-
if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
|
283
|
-
thing = input
|
284
|
-
elsif @is_local_file
|
285
|
-
@base_dir = File.dirname(input)
|
286
|
-
thing = File.open(input, 'r')
|
287
|
-
else
|
288
|
-
thing = open(input)
|
289
|
-
end
|
290
|
-
|
291
|
-
# TODO: deal with Hpricot seg faults on empty input
|
292
|
-
thing ? Hpricot(thing) : nil
|
293
|
-
end
|
294
176
|
|
177
|
+
protected
|
295
178
|
def load_css_from_local_file!(path)
|
296
179
|
css_block = ''
|
297
180
|
begin
|
@@ -300,11 +183,18 @@ protected
|
|
300
183
|
css_block << line
|
301
184
|
end
|
302
185
|
end
|
303
|
-
|
186
|
+
|
187
|
+
load_css_from_string(css_block)
|
304
188
|
rescue; end
|
305
189
|
end
|
306
190
|
|
191
|
+
def load_css_from_string(css_string)
|
192
|
+
@css_parser.add_block!(css_string, {:base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld]})
|
193
|
+
end
|
194
|
+
|
307
195
|
def load_css_from_options! # :nodoc:
|
196
|
+
load_css_from_string(@options[:css_string]) if @options[:css_string]
|
197
|
+
|
308
198
|
@css_files.each do |css_file|
|
309
199
|
if Premailer.local_data?(css_file)
|
310
200
|
load_css_from_local_file!(css_file)
|
@@ -314,7 +204,7 @@ protected
|
|
314
204
|
end
|
315
205
|
end
|
316
206
|
|
317
|
-
|
207
|
+
# Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
|
318
208
|
def load_css_from_html! # :nodoc:
|
319
209
|
if tags = @doc.search("link[@rel='stylesheet'], style")
|
320
210
|
tags.each do |tag|
|
@@ -326,10 +216,10 @@ protected
|
|
326
216
|
load_css_from_local_file!(link_uri)
|
327
217
|
else
|
328
218
|
$stderr.puts "Loading css from uri: " + link_uri if @options[:verbose]
|
329
|
-
@css_parser.load_uri!(link_uri)
|
219
|
+
@css_parser.load_uri!(link_uri, {:only_media_types => [:screen, :handheld]})
|
330
220
|
end
|
331
221
|
|
332
|
-
elsif tag.to_s.strip =~ /^\<style/i
|
222
|
+
elsif tag.to_s.strip =~ /^\<style/i
|
333
223
|
@css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
|
334
224
|
end
|
335
225
|
end
|
@@ -337,6 +227,17 @@ protected
|
|
337
227
|
end
|
338
228
|
end
|
339
229
|
|
230
|
+
|
231
|
+
|
232
|
+
# here be deprecated methods
|
233
|
+
public
|
234
|
+
def local_uri?(uri) # :nodoc:
|
235
|
+
warn "[DEPRECATION] `local_uri?` is deprecated. Please use `Premailer.local_data?` instead."
|
236
|
+
Premailer.local_data?(uri)
|
237
|
+
end
|
238
|
+
|
239
|
+
# here be instance methods
|
240
|
+
|
340
241
|
def media_type_ok?(media_types) # :nodoc:
|
341
242
|
return true if media_types.nil? or media_types.empty?
|
342
243
|
return media_types.split(/[\s]+|,/).any? { |media_type| media_type.strip =~ /screen|handheld|all/i }
|
@@ -344,35 +245,12 @@ protected
|
|
344
245
|
return true
|
345
246
|
end
|
346
247
|
|
347
|
-
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
348
|
-
# and write it into the <tt>body</tt>.
|
349
|
-
#
|
350
|
-
# <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
351
|
-
#
|
352
|
-
# Returns an Hpricot document.
|
353
|
-
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
354
|
-
if head = doc.search('head')
|
355
|
-
styles = ''
|
356
|
-
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
357
|
-
styles += "#{selector} { #{declarations} }\n"
|
358
|
-
end
|
359
|
-
|
360
|
-
unless styles.empty?
|
361
|
-
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
|
362
|
-
head.html.empty? ? head.inner_html(style_tag) : head.append(style_tag)
|
363
|
-
end
|
364
|
-
else
|
365
|
-
$stderr.puts "Unable to write unmergable CSS rules: no <head> was found" if @options[:verbose]
|
366
|
-
end
|
367
|
-
doc
|
368
|
-
end
|
369
|
-
|
370
248
|
def append_query_string(doc, qs)
|
371
249
|
return doc if qs.nil?
|
372
250
|
|
373
251
|
qs.to_s.gsub!(/^[\?]*/, '').strip!
|
374
252
|
return doc if qs.empty?
|
375
|
-
|
253
|
+
|
376
254
|
begin
|
377
255
|
current_host = @base_url.host
|
378
256
|
rescue
|
@@ -380,10 +258,10 @@ protected
|
|
380
258
|
end
|
381
259
|
|
382
260
|
$stderr.puts "Attempting to append_query_string: #{qs}" if @options[:verbose]
|
383
|
-
|
261
|
+
|
384
262
|
doc.search('a').each do|el|
|
385
263
|
href = el.attributes['href'].to_s.strip
|
386
|
-
next if href.nil? or href.empty?
|
264
|
+
next if href.nil? or href.empty?
|
387
265
|
next if href[0,1] == '#' # don't bother with anchors
|
388
266
|
|
389
267
|
begin
|
@@ -391,7 +269,7 @@ protected
|
|
391
269
|
|
392
270
|
if current_host and href.host != nil and href.host != current_host
|
393
271
|
$stderr.puts "Skipping append_query_string for: #{href.to_s} because host is no good" if @options[:verbose]
|
394
|
-
next
|
272
|
+
next
|
395
273
|
end
|
396
274
|
|
397
275
|
if href.scheme and href.scheme != 'http' and href.scheme != 'https'
|
@@ -404,7 +282,7 @@ protected
|
|
404
282
|
else
|
405
283
|
href.query = qs
|
406
284
|
end
|
407
|
-
|
285
|
+
|
408
286
|
el['href'] = href.to_s
|
409
287
|
rescue URI::Error => e
|
410
288
|
$stderr.puts "Skipping append_query_string for: #{href.to_s} (#{e.message})" if @options[:verbose]
|
@@ -415,9 +293,18 @@ protected
|
|
415
293
|
doc
|
416
294
|
end
|
417
295
|
|
418
|
-
|
296
|
+
# Check for an XHTML doctype
|
297
|
+
def is_xhtml?
|
298
|
+
intro = @doc.to_s.strip.split("\n")[0..2].join(' ')
|
299
|
+
is_xhtml = (intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i)
|
300
|
+
is_xhtml = is_xhtml ? true : false
|
301
|
+
$stderr.puts "Is XHTML? #{is_xhtml.inspect}\nChecked:\n#{intro}" if @options[:debug]
|
302
|
+
is_xhtml
|
303
|
+
end
|
304
|
+
|
305
|
+
# Convert relative links to absolute links.
|
419
306
|
#
|
420
|
-
# Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
|
307
|
+
# Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
|
421
308
|
# as well as CSS <tt>url()</tt> declarations found in inline <tt>style</tt> attributes.
|
422
309
|
#
|
423
310
|
# <tt>doc</tt> is an Hpricot document and <tt>base_uri</tt> is either a string or a URI.
|
@@ -466,20 +353,11 @@ protected
|
|
466
353
|
doc
|
467
354
|
end
|
468
355
|
|
469
|
-
# here be deprecated methods
|
470
|
-
public
|
471
|
-
|
472
|
-
def local_uri?(uri) # :nodoc:
|
473
|
-
warn "[DEPRECATION] `local_uri?` is deprecated. Please use `Premailer.local_data?` instead."
|
474
|
-
Premailer.local_data?(uri)
|
475
|
-
end
|
476
|
-
|
477
|
-
# here be instance methods
|
478
356
|
|
479
357
|
def self.escape_string(str) # :nodoc:
|
480
|
-
str.gsub(/"
|
358
|
+
str.gsub(/"/ , "'")
|
481
359
|
end
|
482
|
-
|
360
|
+
|
483
361
|
def self.resolve_link(path, base_path) # :nodoc:
|
484
362
|
path.strip!
|
485
363
|
resolved = nil
|
@@ -488,7 +366,7 @@ public
|
|
488
366
|
return Premailer.canonicalize(resolved)
|
489
367
|
elsif base_path.kind_of?(URI)
|
490
368
|
resolved = base_path.merge(path)
|
491
|
-
return Premailer.canonicalize(resolved)
|
369
|
+
return Premailer.canonicalize(resolved)
|
492
370
|
elsif base_path.kind_of?(String) and base_path =~ /^(http[s]?|ftp):\/\//i
|
493
371
|
resolved = URI.parse(base_path)
|
494
372
|
resolved = resolved.merge(path)
|
@@ -510,7 +388,7 @@ public
|
|
510
388
|
else
|
511
389
|
return true
|
512
390
|
end
|
513
|
-
end
|
391
|
+
end
|
514
392
|
|
515
393
|
# from http://www.ruby-forum.com/topic/140101
|
516
394
|
def self.canonicalize(uri) # :nodoc:
|
@@ -525,54 +403,51 @@ public
|
|
525
403
|
u.to_s
|
526
404
|
end
|
527
405
|
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
warnings = []
|
533
|
-
properties = []
|
534
|
-
|
535
|
-
# Get a list off CSS properties
|
536
|
-
@processed_doc.search("*[@style]").each do |el|
|
537
|
-
style_url = el.attributes['style'].to_s.gsub(/([\w\-]+)[\s]*\:/i) do |s|
|
538
|
-
properties.push($1)
|
539
|
-
end
|
540
|
-
end
|
406
|
+
# Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
|
407
|
+
def check_client_support # :nodoc:
|
408
|
+
@client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
|
541
409
|
|
542
|
-
|
410
|
+
warnings = []
|
411
|
+
properties = []
|
543
412
|
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
property_support[prop]['support'] >= @options[:warn_level]
|
549
|
-
warnings.push({:message => "#{prop} CSS property",
|
550
|
-
:level => WARN_LABEL[property_support[prop]['support']],
|
551
|
-
:clients => property_support[prop]['unsupported_in'].join(', ')})
|
552
|
-
end
|
413
|
+
# Get a list off CSS properties
|
414
|
+
@processed_doc.search("*[@style]").each do |el|
|
415
|
+
style_url = el.attributes['style'].to_s.gsub(/([\w\-]+)[\s]*\:/i) do |s|
|
416
|
+
properties.push($1)
|
553
417
|
end
|
418
|
+
end
|
554
419
|
|
555
|
-
|
556
|
-
next unless data['support'] >= @options[:warn_level]
|
557
|
-
if @doc.search("*[@#{attribute}]").length > 0
|
558
|
-
warnings.push({:message => "#{attribute} HTML attribute",
|
559
|
-
:level => WARN_LABEL[property_support[prop]['support']],
|
560
|
-
:clients => property_support[prop]['unsupported_in'].join(', ')})
|
561
|
-
end
|
562
|
-
end
|
420
|
+
properties.uniq!
|
563
421
|
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
422
|
+
property_support = @client_support['css_properties']
|
423
|
+
properties.each do |prop|
|
424
|
+
if property_support.include?(prop) and
|
425
|
+
property_support[prop].include?('support') and
|
426
|
+
property_support[prop]['support'] >= @options[:warn_level]
|
427
|
+
warnings.push({:message => "#{prop} CSS property",
|
428
|
+
:level => WARN_LABEL[property_support[prop]['support']],
|
429
|
+
:clients => property_support[prop]['unsupported_in'].join(', ')})
|
571
430
|
end
|
572
|
-
|
573
|
-
return warnings
|
574
431
|
end
|
575
|
-
end
|
576
432
|
|
433
|
+
@client_support['attributes'].each do |attribute, data|
|
434
|
+
next unless data['support'] >= @options[:warn_level]
|
435
|
+
if @doc.search("*[@#{attribute}]").length > 0
|
436
|
+
warnings.push({:message => "#{attribute} HTML attribute",
|
437
|
+
:level => WARN_LABEL[data['support']],
|
438
|
+
:clients => data['unsupported_in'].join(', ')})
|
439
|
+
end
|
440
|
+
end
|
577
441
|
|
442
|
+
@client_support['elements'].each do |element, data|
|
443
|
+
next unless data['support'] >= @options[:warn_level]
|
444
|
+
if @doc.search(element).length > 0
|
445
|
+
warnings.push({:message => "#{element} HTML element",
|
446
|
+
:level => WARN_LABEL[data['support']],
|
447
|
+
:clients => data['unsupported_in'].join(', ')})
|
448
|
+
end
|
449
|
+
end
|
578
450
|
|
451
|
+
return warnings
|
452
|
+
end
|
453
|
+
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 1
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 1.
|
7
|
+
- 7
|
8
|
+
- 0
|
9
|
+
version: 1.7.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Alex Dunae
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date:
|
17
|
+
date: 2011-12-31 00:00:00 -08:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -43,8 +43,8 @@ dependencies:
|
|
43
43
|
segments:
|
44
44
|
- 1
|
45
45
|
- 1
|
46
|
-
-
|
47
|
-
version: 1.1.
|
46
|
+
- 6
|
47
|
+
version: 1.1.6
|
48
48
|
type: :runtime
|
49
49
|
version_requirements: *id002
|
50
50
|
- !ruby/object:Gem::Dependency
|
@@ -76,6 +76,9 @@ files:
|
|
76
76
|
- lib/premailer.rb
|
77
77
|
- lib/premailer/html_to_plain_text.rb
|
78
78
|
- lib/premailer/premailer.rb
|
79
|
+
- lib/premailer/adapter.rb
|
80
|
+
- lib/premailer/adapter/hpricot.rb
|
81
|
+
- lib/premailer/adapter/nokogiri.rb
|
79
82
|
- misc/client_support.yaml
|
80
83
|
- README.rdoc
|
81
84
|
has_rdoc: true
|