premailer 1.6.2 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|