premailer 1.5.4 → 1.5.6
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 +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
|