premailer 1.5.4 → 1.5.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +5 -5
- data/bin/premailer +8 -2
- data/lib/premailer.rb +4 -6
- data/lib/premailer/html_to_plain_text.rb +21 -14
- data/lib/premailer/premailer.rb +236 -107
- metadata +54 -37
- data/CHANGELOG.rdoc +0 -57
- data/LICENSE.rdoc +0 -42
- data/rakefile.rb +0 -42
data/README.rdoc
CHANGED
@@ -20,7 +20,6 @@ script is my solution.
|
|
20
20
|
|
21
21
|
Download the Premailer gem from GemCutter.
|
22
22
|
|
23
|
-
gem sources -a http://gemcutter.org
|
24
23
|
sudo gem install premailer
|
25
24
|
|
26
25
|
=== Example
|
@@ -48,19 +47,20 @@ Contributions are most welcome. Premailer was rotting away in a private SVN rep
|
|
48
47
|
A few areas that are particularly in need of love:
|
49
48
|
* Testing suite
|
50
49
|
There were unit tests but they were so funky that it was better to just strip them out.
|
51
|
-
* Test running Premailer on local files
|
52
50
|
* Create a binary file for easing command line use, allowing the output to be piped in *nix systems
|
53
|
-
* Ruby 1.9 testing
|
54
51
|
* Test with Rails
|
55
52
|
* Move un-repeated background images defined in CSS to <tt><td background=""></tt> for Outlook
|
56
53
|
* Correctly parse http://www.webstandards.org/files/acid2/test.html
|
57
54
|
|
58
55
|
=== Credits and code
|
59
56
|
|
60
|
-
|
57
|
+
Thanks to {all the wonderful contributors}[http://github.com/alexdunae/premailer/contributors] for their updates.
|
58
|
+
|
59
|
+
Thanks to {Greenhood + Company}[http://www.greenhood.com/] for sponsoring some of the 1.5.6 updates,
|
60
|
+
and to {Campaign Monitor}[http://www.campaignmonitor.com] for supporting the web interface.
|
61
61
|
|
62
62
|
The web interface can be found at http://premailer.dialect.ca .
|
63
63
|
|
64
64
|
The source code can be found at http://github.com/alexdunae/premailer .
|
65
65
|
|
66
|
-
Written by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-
|
66
|
+
Written by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-2010.
|
data/bin/premailer
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
#
|
3
3
|
# = Premailer
|
4
|
-
require '
|
4
|
+
require 'rubygems'
|
5
|
+
require File.join(File.dirname(__FILE__), 'trollop')
|
5
6
|
require File.join(File.dirname(__FILE__), '../lib/premailer')
|
6
7
|
|
7
8
|
opts = Trollop::options do
|
@@ -23,7 +24,9 @@ EOS
|
|
23
24
|
opt :query_string, "Query string to append to links", :type => String, :short => 'q'
|
24
25
|
opt :line_length, "Length of lines when creating plaintext version", :type => :int, :default => 65, :short => 'l'
|
25
26
|
opt :remove_classes, "Remove classes from the HTML document?", :default => false
|
27
|
+
opt :css, "Manually specify css stylesheets", :type => String, :multi => true
|
26
28
|
opt :verbose, '', :default => false, :short => 'v'
|
29
|
+
opt :io_exceptions, "Abort on I/O errors loading style resources", :default => false, :short => 'e'
|
27
30
|
end
|
28
31
|
|
29
32
|
inputfile = ARGV.shift
|
@@ -38,7 +41,10 @@ premailer_opts = {
|
|
38
41
|
:query_string => opts[:query_string],
|
39
42
|
:show_warnings => opts[:show_warnings] ? Premailer::Warnings::SAFE : Premailer::Warnings::NONE,
|
40
43
|
:line_length => opts[:line_length],
|
41
|
-
:remove_classes => opts[:remove_classes]
|
44
|
+
:remove_classes => opts[:remove_classes],
|
45
|
+
:css => opts[:css],
|
46
|
+
:verbose => opts[:verbose],
|
47
|
+
:io_exceptions => opts[:io_exceptions],
|
42
48
|
}
|
43
49
|
|
44
50
|
premailer = Premailer.new(inputfile, premailer_opts)
|
data/lib/premailer.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
|
-
# Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-09
|
2
|
-
|
3
1
|
require 'yaml'
|
4
2
|
require 'open-uri'
|
5
|
-
require '
|
3
|
+
require 'cgi'
|
4
|
+
require 'nokogiri'
|
6
5
|
require 'css_parser'
|
7
|
-
|
8
|
-
require
|
9
|
-
require File.dirname(__FILE__) + "/premailer/premailer"
|
6
|
+
require 'premailer/html_to_plain_text'
|
7
|
+
require 'premailer/premailer'
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# coding: utf-8
|
2
2
|
require 'htmlentities'
|
3
3
|
|
4
4
|
# Support functions for Premailer
|
@@ -9,9 +9,9 @@ module HtmlToPlainText
|
|
9
9
|
# TODO:
|
10
10
|
# - add support for DL, OL
|
11
11
|
def convert_to_text(html, line_length = 65, from_charset = 'UTF-8')
|
12
|
-
r = Text::Reform.new(:trim => true,
|
13
|
-
|
14
|
-
|
12
|
+
#r = Text::Reform.new(:trim => true,
|
13
|
+
# :squeeze => false,
|
14
|
+
# :break => Text::Reform.break_wrap)
|
15
15
|
|
16
16
|
txt = html
|
17
17
|
|
@@ -20,22 +20,29 @@ module HtmlToPlainText
|
|
20
20
|
txt = he.decode(txt)
|
21
21
|
|
22
22
|
# handle headings (H1-H6)
|
23
|
-
txt.gsub!(/
|
23
|
+
txt.gsub!(/(<\/h[1-6]>)/i, "\n\\1") # move closing tags to new lines
|
24
|
+
txt.gsub!(/[\s]*<h([1-6]+)[^>]*>[\s]*(.*)[\s]*<\/h[1-6]+>/i) do |s|
|
24
25
|
hlevel = $1.to_i
|
25
|
-
|
26
|
-
htext = $2
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
|
27
|
+
htext = $2
|
28
|
+
htext.gsub!(/<br[\s]*\/?>/i, "\n") # handle <br>s
|
29
|
+
htext.gsub!(/<\/?[^>]*>/i, '') # strip tags
|
30
|
+
|
31
|
+
# determine maximum line length
|
32
|
+
hlength = 0
|
33
|
+
htext.each_line { |l| llength = l.strip.length; hlength = llength if llength > hlength }
|
34
|
+
hlength = line_length if hlength > line_length
|
30
35
|
|
31
36
|
case hlevel
|
32
37
|
when 1 # H1, asterisks above and below
|
33
|
-
('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength)
|
38
|
+
htext = ('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength)
|
34
39
|
when 2 # H1, dashes above and below
|
35
|
-
('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength)
|
40
|
+
htext = ('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength)
|
36
41
|
else # H3-H6, dashes below
|
37
|
-
htext + "\n" + ('-' *
|
42
|
+
htext = htext + "\n" + ('-' * hlength)
|
38
43
|
end
|
44
|
+
|
45
|
+
"\n\n" + htext + "\n\n"
|
39
46
|
end
|
40
47
|
|
41
48
|
# links
|
@@ -56,7 +63,7 @@ module HtmlToPlainText
|
|
56
63
|
txt.gsub!(/<\/?[^>]*>/, '')
|
57
64
|
|
58
65
|
# wrap text
|
59
|
-
txt = r.format(('[' * line_length), txt)
|
66
|
+
#txt = r.format(('[' * line_length), txt)
|
60
67
|
|
61
68
|
# remove linefeeds (\r\n and \r -> \n)
|
62
69
|
txt.gsub!(/\r\n?/, "\n")
|
data/lib/premailer/premailer.rb
CHANGED
@@ -33,21 +33,48 @@ class Premailer
|
|
33
33
|
include HtmlToPlainText
|
34
34
|
include CssParser
|
35
35
|
|
36
|
-
VERSION = '1.5.
|
36
|
+
VERSION = '1.5.6'
|
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
|
+
#
|
44
|
+
# TODO: too much repetition
|
45
|
+
# TODO: background=""
|
46
|
+
RELATED_ATTRIBUTES = {
|
47
|
+
'h1' => {'text-align' => 'align'},
|
48
|
+
'h2' => {'text-align' => 'align'},
|
49
|
+
'h3' => {'text-align' => 'align'},
|
50
|
+
'h4' => {'text-align' => 'align'},
|
51
|
+
'h5' => {'text-align' => 'align'},
|
52
|
+
'h6' => {'text-align' => 'align'},
|
53
|
+
'p' => {'text-align' => 'align'},
|
54
|
+
'div' => {'text-align' => 'align'},
|
55
|
+
'blockquote' => {'text-align' => 'align'},
|
56
|
+
'body' => {'background-color' => 'bgcolor'},
|
57
|
+
'table' => {'background-color' => 'bgcolor'},
|
58
|
+
'tr' => {'text-align' => 'align', 'background-color' => 'bgcolor'},
|
59
|
+
'th' => {'text-align' => 'align', 'background-color' => 'bgcolor', 'vertical-align' => 'valign'},
|
60
|
+
'td' => {'text-align' => 'align', 'background-color' => 'bgcolor', 'vertical-align' => 'valign'},
|
61
|
+
'img' => {'float' => 'align'}
|
62
|
+
}
|
43
63
|
|
44
64
|
# URI of the HTML file used
|
45
65
|
attr_reader :html_file
|
46
66
|
|
47
|
-
#
|
67
|
+
# base URL used to resolve links
|
68
|
+
attr_reader :base_url
|
69
|
+
|
70
|
+
# base directory used to resolve links for local files
|
71
|
+
attr_reader :base_dir
|
72
|
+
|
73
|
+
|
74
|
+
# processed HTML document (Nokogiri)
|
48
75
|
attr_reader :processed_doc
|
49
76
|
|
50
|
-
# source HTML document (
|
77
|
+
# source HTML document (Nokogiri)
|
51
78
|
attr_reader :doc
|
52
79
|
|
53
80
|
module Warnings
|
@@ -62,42 +89,61 @@ class Premailer
|
|
62
89
|
|
63
90
|
# Create a new Premailer object.
|
64
91
|
#
|
65
|
-
# +
|
66
|
-
# remote file or a
|
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
|
+
# must set the +:with_html_string+ option to +true+.
|
67
95
|
#
|
68
96
|
# ==== Options
|
69
97
|
# [+line_length+] Line length used by to_plain_text. Boolean, default is 65.
|
70
98
|
# [+warn_level+] What level of CSS compatibility warnings to show (see Warnings).
|
71
99
|
# [+link_query_string+] A string to append to every <a href=""> link. Do not include the initial +?+.
|
72
100
|
# [+base_url+] Used to calculate absolute URLs for local files.
|
73
|
-
|
101
|
+
# [+css+] Manually specify a CSS stylesheet.
|
102
|
+
# [+css_to_attributes+] Copy related CSS attributes into HTML attributes (e.g. +background-color+ to +bgcolor+)
|
103
|
+
# [+with_html_string+] Whether the +html+ param should be treated as a raw string.
|
104
|
+
def initialize(html, options = {})
|
74
105
|
@options = {:warn_level => Warnings::SAFE,
|
75
106
|
:line_length => 65,
|
76
107
|
:link_query_string => nil,
|
77
108
|
:base_url => nil,
|
78
|
-
:remove_classes => false
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
109
|
+
:remove_classes => false,
|
110
|
+
:css => [],
|
111
|
+
:css_to_attributes => true,
|
112
|
+
:with_html_string => false,
|
113
|
+
:verbose => false,
|
114
|
+
:io_exceptions => false}.merge(options)
|
115
|
+
|
116
|
+
@html_file = html
|
117
|
+
@is_local_file = @options[:with_html_string] || Premailer.local_data?(html)
|
118
|
+
|
119
|
+
@css_files = @options[:css]
|
85
120
|
|
86
121
|
@css_warnings = []
|
87
122
|
|
88
|
-
@
|
89
|
-
|
90
|
-
|
91
|
-
|
123
|
+
@base_url = nil
|
124
|
+
@base_dir = nil
|
125
|
+
|
126
|
+
if @options[:base_url]
|
127
|
+
@base_url = URI.parse(@options.delete[:base_url])
|
128
|
+
elsif not @is_local_file
|
129
|
+
@base_url = URI.parse(@html_file)
|
130
|
+
end
|
131
|
+
|
132
|
+
@css_parser = CssParser::Parser.new({
|
133
|
+
:absolute_paths => true,
|
134
|
+
:import => true,
|
135
|
+
:io_exceptions => @options[:io_exceptions]
|
136
|
+
})
|
92
137
|
|
93
|
-
@doc
|
138
|
+
@doc = load_html(@html_file)
|
139
|
+
|
140
|
+
@html_charset = @doc.encoding
|
94
141
|
@processed_doc = @doc
|
95
|
-
|
96
|
-
if
|
97
|
-
@processed_doc =
|
98
|
-
elsif not @is_local_file
|
99
|
-
@processed_doc = convert_inline_links(@processed_doc, @html_file)
|
142
|
+
@processed_doc = convert_inline_links(@processed_doc, @base_url) if @base_url
|
143
|
+
if options[:link_query_string]
|
144
|
+
@processed_doc = append_query_string(@processed_doc, options[:link_query_string])
|
100
145
|
end
|
146
|
+
load_css_from_options!
|
101
147
|
load_css_from_html!
|
102
148
|
end
|
103
149
|
|
@@ -119,7 +165,7 @@ class Premailer
|
|
119
165
|
def to_plain_text
|
120
166
|
html_src = ''
|
121
167
|
begin
|
122
|
-
html_src = @doc.search("body").
|
168
|
+
html_src = @doc.search("body").inner_html
|
123
169
|
rescue
|
124
170
|
html_src = @doc.to_html
|
125
171
|
end
|
@@ -151,17 +197,17 @@ class Premailer
|
|
151
197
|
unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
|
152
198
|
else
|
153
199
|
|
154
|
-
doc.
|
200
|
+
doc.css(selector).each do |el|
|
155
201
|
if el.elem?
|
156
202
|
# Add a style attribute or append to the existing one
|
157
203
|
block = "[SPEC=#{specificity}[#{declaration}]]"
|
158
|
-
el['style'] = (el.attributes['style'] ||= '') + ' ' + block
|
204
|
+
el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
|
159
205
|
end
|
160
206
|
end
|
161
207
|
end
|
162
208
|
end
|
163
209
|
|
164
|
-
# Read
|
210
|
+
# Read STYLE attributes and perform folding
|
165
211
|
doc.search("*[@style]").each do |el|
|
166
212
|
style = el.attributes['style'].to_s
|
167
213
|
|
@@ -172,9 +218,30 @@ class Premailer
|
|
172
218
|
declarations << rs
|
173
219
|
end
|
174
220
|
|
175
|
-
# Perform style folding
|
221
|
+
# Perform style folding
|
176
222
|
merged = CssParser.merge(declarations)
|
223
|
+
merged.expand_shorthand!
|
224
|
+
|
225
|
+
#if @options[:prefer_cellpadding] and (el.name == 'td' or el.name == 'th') and el['cellpadding'].nil?
|
226
|
+
# if cellpadding = equivalent_cellpadding(merged)
|
227
|
+
# el['cellpadding'] = cellpadding
|
228
|
+
# merged['padding-left'] = nil
|
229
|
+
# merged['padding-right'] = nil
|
230
|
+
# merged['padding-top'] = nil
|
231
|
+
# merged['padding-bottom'] = nil
|
232
|
+
# end
|
233
|
+
#end
|
234
|
+
|
235
|
+
# Duplicate CSS attributes as HTML attributes
|
236
|
+
if RELATED_ATTRIBUTES.has_key?(el.name)
|
237
|
+
RELATED_ATTRIBUTES[el.name].each do |css_att, html_att|
|
238
|
+
el[html_att] = merged[css_att].gsub(/;$/, '').strip if el[html_att].nil? and not merged[css_att].empty?
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
merged.create_dimensions_shorthand!
|
177
243
|
|
244
|
+
# write the inline STYLE attribute
|
178
245
|
el['style'] = Premailer.escape_string(merged.declarations_to_s)
|
179
246
|
end
|
180
247
|
|
@@ -189,41 +256,67 @@ class Premailer
|
|
189
256
|
|
190
257
|
|
191
258
|
protected
|
192
|
-
# Load the HTML file and convert it into an
|
259
|
+
# Load the HTML file and convert it into an Nokogiri document.
|
193
260
|
#
|
194
|
-
# Returns an
|
261
|
+
# Returns an Nokogiri document.
|
195
262
|
def load_html(path) # :nodoc:
|
196
|
-
if @
|
197
|
-
|
263
|
+
if @options[:with_html_string]
|
264
|
+
Nokogiri::HTML.parse(path)
|
265
|
+
elsif @options[:inline]
|
266
|
+
Nokogiri::HTML(path)
|
198
267
|
else
|
199
|
-
|
268
|
+
if @is_local_file
|
269
|
+
if path.is_a?(IO) || path.is_a?(StringIO)
|
270
|
+
Nokogiri::HTML(path.read)
|
271
|
+
else
|
272
|
+
@base_dir = File.dirname(path)
|
273
|
+
Nokogiri::HTML(File.open(path, "r") {|f| f.read })
|
274
|
+
end
|
275
|
+
else
|
276
|
+
Nokogiri::HTML(open(path))
|
277
|
+
end
|
200
278
|
end
|
201
279
|
end
|
202
|
-
|
280
|
+
|
281
|
+
def load_css_from_local_file!(path)
|
282
|
+
css_block = ''
|
283
|
+
begin
|
284
|
+
File.open(path, "r") do |file|
|
285
|
+
while line = file.gets
|
286
|
+
css_block << line
|
287
|
+
end
|
288
|
+
end
|
289
|
+
@css_parser.add_block!(css_block, {:base_uri => @base_url, :base_dir => @base_dir})
|
290
|
+
rescue; end
|
291
|
+
end
|
292
|
+
|
293
|
+
def load_css_from_options! # :nodoc:
|
294
|
+
@css_files.each do |css_file|
|
295
|
+
if Premailer.local_data?(css_file)
|
296
|
+
load_css_from_local_file!(css_file)
|
297
|
+
else
|
298
|
+
@css_parser.load_uri!(css_file)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
203
303
|
# Load CSS included in <tt>style</tt> and <tt>link</tt> tags from an HTML document.
|
204
304
|
def load_css_from_html! # :nodoc:
|
205
305
|
if tags = @doc.search("link[@rel='stylesheet'], style")
|
206
306
|
tags.each do |tag|
|
207
|
-
|
208
307
|
if tag.to_s.strip =~ /^\<link/i and tag.attributes['href'] and media_type_ok?(tag.attributes['media'])
|
209
308
|
|
210
309
|
link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
|
211
|
-
if
|
212
|
-
|
213
|
-
|
214
|
-
File.open(link_uri, "r") do |file|
|
215
|
-
while line = file.gets
|
216
|
-
css_block << line
|
217
|
-
end
|
218
|
-
end
|
219
|
-
@css_parser.add_block!(css_block, {:base_uri => @html_file})
|
220
|
-
rescue; end
|
310
|
+
if Premailer.local_data?(link_uri)
|
311
|
+
puts "Loading css from local file: " + link_uri if @options[:verbose]
|
312
|
+
load_css_from_local_file!(link_uri)
|
221
313
|
else
|
314
|
+
puts "Loading css from uri: " + link_uri if @options[:verbose]
|
222
315
|
@css_parser.load_uri!(link_uri)
|
223
316
|
end
|
224
317
|
|
225
|
-
elsif tag.to_s.strip =~ /^\<style/i
|
226
|
-
@css_parser.add_block!(tag.
|
318
|
+
elsif tag.to_s.strip =~ /^\<style/i
|
319
|
+
@css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
|
227
320
|
end
|
228
321
|
end
|
229
322
|
tags.remove
|
@@ -240,9 +333,9 @@ protected
|
|
240
333
|
# Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
|
241
334
|
# and write it into the <tt>body</tt>.
|
242
335
|
#
|
243
|
-
# <tt>doc</tt> is an
|
336
|
+
# <tt>doc</tt> is an Nokogiri document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
|
244
337
|
#
|
245
|
-
# Returns an
|
338
|
+
# Returns an Nokogiri document.
|
246
339
|
def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
|
247
340
|
styles = ''
|
248
341
|
unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
|
@@ -251,7 +344,26 @@ protected
|
|
251
344
|
|
252
345
|
unless styles.empty?
|
253
346
|
style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
|
254
|
-
doc.
|
347
|
+
doc.css("head").children.last.after(style_tag)
|
348
|
+
end
|
349
|
+
doc
|
350
|
+
end
|
351
|
+
|
352
|
+
def append_query_string(doc, qs)
|
353
|
+
doc.search('a').each do|el|
|
354
|
+
href = el.attributes['href'].to_s
|
355
|
+
next if href.nil? or href.empty?
|
356
|
+
|
357
|
+
href = URI.parse(href)
|
358
|
+
|
359
|
+
if href.query
|
360
|
+
href.query = href.query + '&' + qs
|
361
|
+
else
|
362
|
+
href.query = qs
|
363
|
+
end
|
364
|
+
|
365
|
+
el['href'] = href.to_s
|
366
|
+
|
255
367
|
end
|
256
368
|
doc
|
257
369
|
end
|
@@ -261,13 +373,13 @@ protected
|
|
261
373
|
# Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
|
262
374
|
# as well as CSS <tt>url()</tt> declarations found in inline <tt>style</tt> attributes.
|
263
375
|
#
|
264
|
-
# <tt>doc</tt> is an
|
376
|
+
# <tt>doc</tt> is an Nokogiri document and <tt>base_uri</tt> is either a string or a URI.
|
265
377
|
#
|
266
|
-
# Returns an
|
378
|
+
# Returns an Nokogiri document.
|
267
379
|
def convert_inline_links(doc, base_uri) # :nodoc:
|
268
380
|
base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
|
269
381
|
|
270
|
-
append_qs = @options[:link_query_string]
|
382
|
+
append_qs = @options[:link_query_string] || ''
|
271
383
|
|
272
384
|
['href', 'src', 'background'].each do |attribute|
|
273
385
|
tags = doc.search("*[@#{attribute}]")
|
@@ -297,17 +409,7 @@ protected
|
|
297
409
|
|
298
410
|
# make sure 'merged' is a URI
|
299
411
|
merged = URI.parse(merged.to_s) unless merged.kind_of?(URI)
|
300
|
-
|
301
|
-
# only append a querystring to <a> tags
|
302
|
-
if tag.name =~ /^a$/i and not append_qs.empty?
|
303
|
-
if merged.query
|
304
|
-
merged.query = merged.query + '&' + append_qs
|
305
|
-
else
|
306
|
-
merged.query = append_qs
|
307
|
-
end
|
308
|
-
end
|
309
412
|
tag[attribute] = merged.to_s
|
310
|
-
|
311
413
|
end # end of each tag
|
312
414
|
end # end of each attrs
|
313
415
|
|
@@ -317,13 +419,27 @@ protected
|
|
317
419
|
doc
|
318
420
|
end
|
319
421
|
|
422
|
+
# here be deprecated methods
|
423
|
+
public
|
424
|
+
|
425
|
+
def local_uri?(uri) # :nodoc:
|
426
|
+
warn "[DEPRECATION] `local_uri?` is deprecated. Please use `Premailer.local_data?` instead."
|
427
|
+
Premailer.local_data?(uri)
|
428
|
+
end
|
429
|
+
|
430
|
+
# here be instance methods
|
431
|
+
|
320
432
|
def self.escape_string(str) # :nodoc:
|
321
433
|
str.gsub(/"/, "'")
|
322
434
|
end
|
323
435
|
|
324
436
|
def self.resolve_link(path, base_path) # :nodoc:
|
437
|
+
path.strip!
|
325
438
|
resolved = nil
|
326
|
-
if
|
439
|
+
if path =~ /(http[s]?|ftp):\/\//i
|
440
|
+
resolved = path
|
441
|
+
return Premailer.canonicalize(resolved)
|
442
|
+
elsif base_path.kind_of?(URI)
|
327
443
|
resolved = base_path.merge(path)
|
328
444
|
return Premailer.canonicalize(resolved)
|
329
445
|
elsif base_path.kind_of?(String) and base_path =~ /^(http[s]?|ftp):\/\//i
|
@@ -336,67 +452,80 @@ protected
|
|
336
452
|
end
|
337
453
|
end
|
338
454
|
|
455
|
+
# Test the passed variable to see if we are in local or remote mode.
|
456
|
+
#
|
457
|
+
# IO objects return true, as do strings that look like URLs.
|
458
|
+
def self.local_data?(data)
|
459
|
+
if data.is_a?(IO) || data.is_a?(StringIO)
|
460
|
+
return true
|
461
|
+
elsif data =~ /^(http|https|ftp)\:\/\//i
|
462
|
+
return false
|
463
|
+
else
|
464
|
+
return true
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
339
468
|
# from http://www.ruby-forum.com/topic/140101
|
340
469
|
def self.canonicalize(uri) # :nodoc:
|
341
470
|
u = uri.kind_of?(URI) ? uri : URI.parse(uri.to_s)
|
342
471
|
u.normalize!
|
343
472
|
newpath = u.path
|
344
473
|
while newpath.gsub!(%r{([^/]+)/\.\./?}) { |match|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
474
|
+
$1 == '..' ? match : ''
|
475
|
+
} do end
|
476
|
+
newpath = newpath.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/')
|
477
|
+
u.path = newpath
|
478
|
+
u.to_s
|
479
|
+
end
|
351
480
|
|
352
|
-
|
353
|
-
|
354
|
-
|
481
|
+
# Check <tt>CLIENT_SUPPORT_FILE</tt> for any CSS warnings
|
482
|
+
def check_client_support # :nodoc:
|
483
|
+
@client_support = @client_support ||= YAML::load(File.open(CLIENT_SUPPORT_FILE))
|
355
484
|
|
356
|
-
|
357
|
-
|
485
|
+
warnings = []
|
486
|
+
properties = []
|
358
487
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
488
|
+
# Get a list off CSS properties
|
489
|
+
@processed_doc.search("*[@style]").each do |el|
|
490
|
+
style_url = el.attributes['style'].to_s.gsub(/([\w\-]+)[\s]*\:/i) do |s|
|
491
|
+
properties.push($1)
|
492
|
+
end
|
363
493
|
end
|
364
|
-
end
|
365
494
|
|
366
|
-
|
495
|
+
properties.uniq!
|
367
496
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
497
|
+
property_support = @client_support['css_properties']
|
498
|
+
properties.each do |prop|
|
499
|
+
if property_support.include?(prop) and
|
500
|
+
property_support[prop].include?('support') and
|
501
|
+
property_support[prop]['support'] >= @options[:warn_level]
|
502
|
+
warnings.push({:message => "#{prop} CSS property",
|
503
|
+
:level => WARN_LABEL[property_support[prop]['support']],
|
504
|
+
:clients => property_support[prop]['unsupported_in'].join(', ')})
|
505
|
+
end
|
376
506
|
end
|
377
|
-
end
|
378
507
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
508
|
+
@client_support['attributes'].each do |attribute, data|
|
509
|
+
next unless data['support'] >= @options[:warn_level]
|
510
|
+
if @doc.search("*[@#{attribute}]").length > 0
|
511
|
+
warnings.push({:message => "#{attribute} HTML attribute",
|
512
|
+
:level => WARN_LABEL[property_support[prop]['support']],
|
513
|
+
:clients => property_support[prop]['unsupported_in'].join(', ')})
|
514
|
+
end
|
385
515
|
end
|
386
|
-
end
|
387
516
|
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
517
|
+
@client_support['elements'].each do |element, data|
|
518
|
+
next unless data['support'] >= @options[:warn_level]
|
519
|
+
if @doc.search("element").length > 0
|
520
|
+
warnings.push({:message => "#{element} HTML element",
|
521
|
+
:level => WARN_LABEL[property_support[prop]['support']],
|
522
|
+
:clients => property_support[prop]['unsupported_in'].join(', ')})
|
523
|
+
end
|
394
524
|
end
|
395
|
-
end
|
396
525
|
|
397
|
-
|
526
|
+
return warnings
|
527
|
+
end
|
398
528
|
end
|
399
|
-
end
|
400
529
|
|
401
530
|
|
402
531
|
|
metadata
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: premailer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 15
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 5
|
9
|
+
- 6
|
10
|
+
version: 1.5.6
|
5
11
|
platform: ruby
|
6
12
|
authors:
|
7
13
|
- Alex Dunae
|
@@ -9,69 +15,74 @@ autorequire:
|
|
9
15
|
bindir: bin
|
10
16
|
cert_chain: []
|
11
17
|
|
12
|
-
date:
|
18
|
+
date: 2010-11-03 00:00:00 -07:00
|
13
19
|
default_executable:
|
14
20
|
dependencies:
|
15
21
|
- !ruby/object:Gem::Dependency
|
16
|
-
name:
|
17
|
-
|
18
|
-
|
19
|
-
|
22
|
+
name: nokogiri
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
20
26
|
requirements:
|
21
27
|
- - ">="
|
22
28
|
- !ruby/object:Gem::Version
|
23
|
-
|
24
|
-
|
29
|
+
hash: 7
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 4
|
33
|
+
- 0
|
34
|
+
version: 1.4.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
25
37
|
- !ruby/object:Gem::Dependency
|
26
38
|
name: css_parser
|
27
|
-
|
28
|
-
|
29
|
-
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
30
42
|
requirements:
|
31
43
|
- - ">="
|
32
44
|
- !ruby/object:Gem::Version
|
33
|
-
|
34
|
-
|
35
|
-
-
|
36
|
-
|
45
|
+
hash: 21
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 1
|
49
|
+
- 3
|
50
|
+
version: 1.1.3
|
37
51
|
type: :runtime
|
38
|
-
|
39
|
-
version_requirements: !ruby/object:Gem::Requirement
|
40
|
-
requirements:
|
41
|
-
- - ">="
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: 0.2.0
|
44
|
-
version:
|
52
|
+
version_requirements: *id002
|
45
53
|
- !ruby/object:Gem::Dependency
|
46
54
|
name: htmlentities
|
47
|
-
|
48
|
-
|
49
|
-
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
50
58
|
requirements:
|
51
59
|
- - ">="
|
52
60
|
- !ruby/object:Gem::Version
|
61
|
+
hash: 63
|
62
|
+
segments:
|
63
|
+
- 4
|
64
|
+
- 0
|
65
|
+
- 0
|
53
66
|
version: 4.0.0
|
54
|
-
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
55
69
|
description: Improve the rendering of HTML emails by making CSS inline, converting links and warning about unsupported code.
|
56
70
|
email: code@dunae.ca
|
57
71
|
executables:
|
58
72
|
- premailer
|
59
73
|
extensions: []
|
60
74
|
|
61
|
-
extra_rdoc_files:
|
62
|
-
|
75
|
+
extra_rdoc_files:
|
76
|
+
- README.rdoc
|
63
77
|
files:
|
64
78
|
- init.rb
|
65
|
-
-
|
79
|
+
- bin/premailer
|
80
|
+
- bin/trollop.rb
|
66
81
|
- lib/premailer.rb
|
67
82
|
- lib/premailer/html_to_plain_text.rb
|
68
83
|
- lib/premailer/premailer.rb
|
69
|
-
- CHANGELOG.rdoc
|
70
|
-
- LICENSE.rdoc
|
71
|
-
- README.rdoc
|
72
84
|
- misc/client_support.yaml
|
73
|
-
-
|
74
|
-
- bin/trollop.rb
|
85
|
+
- README.rdoc
|
75
86
|
has_rdoc: true
|
76
87
|
homepage: http://premailer.dialect.ca/
|
77
88
|
licenses: []
|
@@ -86,21 +97,27 @@ rdoc_options:
|
|
86
97
|
require_paths:
|
87
98
|
- lib
|
88
99
|
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
89
101
|
requirements:
|
90
102
|
- - ">="
|
91
103
|
- !ruby/object:Gem::Version
|
104
|
+
hash: 3
|
105
|
+
segments:
|
106
|
+
- 0
|
92
107
|
version: "0"
|
93
|
-
version:
|
94
108
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
95
110
|
requirements:
|
96
111
|
- - ">="
|
97
112
|
- !ruby/object:Gem::Version
|
113
|
+
hash: 3
|
114
|
+
segments:
|
115
|
+
- 0
|
98
116
|
version: "0"
|
99
|
-
version:
|
100
117
|
requirements: []
|
101
118
|
|
102
119
|
rubyforge_project:
|
103
|
-
rubygems_version: 1.3.
|
120
|
+
rubygems_version: 1.3.7
|
104
121
|
signing_key:
|
105
122
|
specification_version: 3
|
106
123
|
summary: Preflight for HTML e-mail.
|
data/CHANGELOG.rdoc
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
= Premailer CHANGELOG
|
2
|
-
|
3
|
-
== Version 1.5.4
|
4
|
-
* new bin/premailer script
|
5
|
-
* added missing htmlentities depenency to gemspec (thanks to http://github.com/usefulthink )
|
6
|
-
* fixed handling of unspecified <link> media types
|
7
|
-
|
8
|
-
== Version 1.5.3
|
9
|
-
* improved plaintext conversion
|
10
|
-
|
11
|
-
== Version 1.5.2
|
12
|
-
* released to GitHub
|
13
|
-
* fixed handling of mailto links
|
14
|
-
* various minor updates
|
15
|
-
|
16
|
-
== Version 1.5.1
|
17
|
-
* bugfix (http://code.google.com/p/premailer/issues/detail?id=1 and http://code.google.com/p/premailer/issues/detail?id=2) thanks to Russell Norris
|
18
|
-
* bugfix (http://code.google.com/p/premailer/issues/detail?id=4) thanks to Dave Holmes
|
19
|
-
|
20
|
-
== Version 1.5.0
|
21
|
-
* preview release of Ruby gem
|
22
|
-
|
23
|
-
== Version 1.4
|
24
|
-
* incremental parsing improvements
|
25
|
-
* respect <tt>@media</tt> rule (http://www.w3.org/TR/CSS21/media.html#at-media-rule)
|
26
|
-
* better quote escaping
|
27
|
-
|
28
|
-
== Version 1.3
|
29
|
-
* separate CSS parser into its own library
|
30
|
-
* handle <tt>background: red url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4%2F58BAAT%2FAf9jgNErAAAAAElFTkSuQmCC);</tt>
|
31
|
-
* preserve <tt>:hover</tt> etc... in head styles
|
32
|
-
|
33
|
-
== Version 1.2
|
34
|
-
* respect <tt>LINK</tt> media types
|
35
|
-
* better style folding
|
36
|
-
* incremental parsing improvements
|
37
|
-
|
38
|
-
== Version 1.1
|
39
|
-
* proper calculation of selector specificity per CSS 2.1 spec
|
40
|
-
* support for <tt>@import</tt>
|
41
|
-
* preliminary support for shorthand CSS properties (<tt>margin</tt>, <tt>padding</tt>)
|
42
|
-
* preliminary separation of CSS parser
|
43
|
-
|
44
|
-
== Version 1.0
|
45
|
-
* ported web interface to eRuby
|
46
|
-
* incremental parsing improvements
|
47
|
-
|
48
|
-
== Version 0.9
|
49
|
-
* initial proof-of-concept
|
50
|
-
* PHP web version
|
51
|
-
|
52
|
-
== TODO: Future
|
53
|
-
* complete shorthand properties support (<tt>border-width</tt>, <tt>font</tt>, <tt>background</tt>)
|
54
|
-
* UTF-8 and other charsets (test page: http://kianga.kcore.de/2004/09/21/utf8_test)
|
55
|
-
* make warnings for <tt>border</tt> match <tt>border-left</tt>, etc...
|
56
|
-
* Integrate CSS validator
|
57
|
-
* Remove unused classes and IDs
|
data/LICENSE.rdoc
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
= Premailer License
|
2
|
-
|
3
|
-
Copyright (c) 2007-09 Alex Dunae
|
4
|
-
|
5
|
-
Premailer is copyrighted free software by Alex Dunae (http://dunae.ca/).
|
6
|
-
You can redistribute it and/or modify it under the conditions below:
|
7
|
-
|
8
|
-
1. You may make and give away verbatim copies of the source form of the
|
9
|
-
software without restriction, provided that you duplicate all of the
|
10
|
-
original copyright notices and associated disclaimers.
|
11
|
-
|
12
|
-
2. You may modify your copy of the software in any way, provided that
|
13
|
-
you do at least ONE of the following:
|
14
|
-
|
15
|
-
a) place your modifications in the Public Domain or otherwise
|
16
|
-
make them Freely Available, such as by posting said
|
17
|
-
modifications to the internet or an equivalent medium, or by
|
18
|
-
allowing the author to include your modifications in the software.
|
19
|
-
|
20
|
-
b) use the modified software only within your corporation or
|
21
|
-
organization.
|
22
|
-
|
23
|
-
c) rename any non-standard executables so the names do not conflict
|
24
|
-
with standard executables, which must also be provided.
|
25
|
-
|
26
|
-
d) make other distribution arrangements with the author.
|
27
|
-
|
28
|
-
3. You may modify and include the part of the software into any other
|
29
|
-
software (possibly commercial) as long as clear acknowledgement and
|
30
|
-
a link back to the original software (http://code.dunae.ca/premailer.web/)
|
31
|
-
is provided.
|
32
|
-
|
33
|
-
5. The scripts and library files supplied as input to or produced as
|
34
|
-
output from the software do not automatically fall under the
|
35
|
-
copyright of the software, but belong to whomever generated them,
|
36
|
-
and may be sold commercially, and may be aggregated with this
|
37
|
-
software.
|
38
|
-
|
39
|
-
6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
|
40
|
-
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
41
|
-
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
42
|
-
PURPOSE.
|
data/rakefile.rb
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
require 'rake'
|
2
|
-
require 'fileutils'
|
3
|
-
require 'lib/premailer'
|
4
|
-
|
5
|
-
desc 'Default: parse a URL.'
|
6
|
-
task :default => [:inline]
|
7
|
-
|
8
|
-
desc 'Parse a URL and write out the output.'
|
9
|
-
task :inline do
|
10
|
-
url = ENV['url']
|
11
|
-
output = ENV['output']
|
12
|
-
|
13
|
-
if !url or url.empty? or !output or output.empty?
|
14
|
-
puts 'Usage: rake inline url=http://example.com/ output=output.html'
|
15
|
-
exit
|
16
|
-
end
|
17
|
-
|
18
|
-
premailer = Premailer.new(url, :warn_level => Premailer::Warnings::SAFE)
|
19
|
-
fout = File.open(output, "w")
|
20
|
-
fout.puts premailer.to_inline_css
|
21
|
-
fout.close
|
22
|
-
|
23
|
-
puts "Succesfully parsed '#{url}' into '#{output}'"
|
24
|
-
puts premailer.warnings.length.to_s + ' CSS warnings were found'
|
25
|
-
end
|
26
|
-
|
27
|
-
task :text do
|
28
|
-
url = ENV['url']
|
29
|
-
output = ENV['output']
|
30
|
-
|
31
|
-
if !url or url.empty? or !output or output.empty?
|
32
|
-
puts 'Usage: rake text url=http://example.com/ output=output.txt'
|
33
|
-
exit
|
34
|
-
end
|
35
|
-
|
36
|
-
premailer = Premailer.new(url, :warn_level => Premailer::Warnings::SAFE)
|
37
|
-
fout = File.open(output, "w")
|
38
|
-
fout.puts premailer.to_plain_text
|
39
|
-
fout.close
|
40
|
-
|
41
|
-
puts "Succesfully parsed '#{url}' into '#{output}'"
|
42
|
-
end
|