premailer 1.5.6 → 1.5.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/bin/premailer +89 -65
  2. data/lib/premailer/premailer.rb +57 -40
  3. metadata +4 -4
@@ -1,76 +1,100 @@
1
1
  #!/usr/bin/env ruby
2
- #
3
- # = Premailer
2
+
3
+ require 'optparse'
4
4
  require 'rubygems'
5
- require File.join(File.dirname(__FILE__), 'trollop')
6
- require File.join(File.dirname(__FILE__), '../lib/premailer')
7
-
8
- opts = Trollop::options do
9
- version "Premailer #{Premailer::VERSION} (c) 2008-2009 Alex Dunae"
10
- banner <<-EOS
11
- Improve the rendering of HTML emails by making CSS inline, converting links and warning about unsupported code.
12
-
13
- Usage:
14
- premailer [options] inputfile [outputfile] [warningsfile]
15
-
16
- Example
17
- premailer http://example.com/
18
- premailer http://example.com/ out.html out.txt warnings.txt
19
- premailer --base-url=http://example.com/ src.html out.html
20
-
21
- Options:
22
- EOS
23
- opt :base_url, "Manually set the base URL, useful for local files", :type => String
24
- opt :query_string, "Query string to append to links", :type => String, :short => 'q'
25
- opt :line_length, "Length of lines when creating plaintext version", :type => :int, :default => 65, :short => 'l'
26
- opt :remove_classes, "Remove classes from the HTML document?", :default => false
27
- opt :css, "Manually specify css stylesheets", :type => String, :multi => true
28
- opt :verbose, '', :default => false, :short => 'v'
29
- opt :io_exceptions, "Abort on I/O errors loading style resources", :default => false, :short => 'e'
30
- end
5
+ require 'premailer'
6
+ require 'fcntl'
31
7
 
32
- inputfile = ARGV.shift
33
- outfile = ARGV.shift
34
- txtfile = ARGV.shift
35
- warningsfile = ARGV.shift
36
-
37
- Trollop::die "inputfile is missing" if inputfile.nil?
38
-
39
- premailer_opts = {
40
- :base_url => opts[:base_url],
41
- :query_string => opts[:query_string],
42
- :show_warnings => opts[:show_warnings] ? Premailer::Warnings::SAFE : Premailer::Warnings::NONE,
43
- :line_length => opts[:line_length],
44
- :remove_classes => opts[:remove_classes],
45
- :css => opts[:css],
46
- :verbose => opts[:verbose],
47
- :io_exceptions => opts[:io_exceptions],
8
+ # defaults
9
+ options = {
10
+ :base_url => nil,
11
+ :link_query_string => nil,
12
+ :remove_classes => false,
13
+ :verbose => false,
14
+ :line_length => 65
48
15
  }
49
16
 
50
- premailer = Premailer.new(inputfile, premailer_opts)
17
+ mode = :html
51
18
 
52
- # html output
53
- if outfile
54
- fout = File.open(outfile, 'w')
55
- fout.puts premailer.to_inline_css
56
- fout.close
57
- else
58
- p premailer.to_inline_css
59
- exit
60
- end
19
+ opts = OptionParser.new do |opts|
20
+ opts.banner = "Improve the rendering of HTML emails by making CSS inline, converting links and warning about unsupported code."
21
+ opts.define_head "Usage: premailer <uri|path> [options]"
22
+ opts.separator ""
23
+ opts.separator "Examples:"
24
+ opts.separator " premailer http://example.com/ > out.html"
25
+ opts.separator " premailer http://example.com/ --mode txt > out.txt"
26
+ opts.separator " cat input.html | premailer -q src=email > out.html"
27
+ opts.separator " premailer ./public/index.html"
28
+ opts.separator ""
29
+ opts.separator "Options:"
30
+
31
+ opts.on("--mode [MODE]", [:html, :txt], "Output type: either html or txt") do |v|
32
+ mode = v
33
+ end
34
+
35
+ opts.on("-b", "--base-url", String, "Manually set the base URL, useful for local files") do |v|
36
+ options[:base_url] = v
37
+ end
38
+
39
+ opts.on("-q", "--query-string STRING", String, "Query string to append to links (do not include the ?)") do |v|
40
+ options[:link_query_string] = v
41
+ end
42
+
43
+ opts.on("--css FILE,FILE", Array, "Additional CSS stylesheets") do |v|
44
+ options[:css] = v
45
+ end
46
+
47
+ opts.on("-r", "--remove-classes", "Remove classes from the HTML document?") do |v|
48
+ options[:remove_classes] = v
49
+ end
50
+
51
+ opts.on("-l", "--line-length N", Integer, "Length of lines when creating plaintext version (default: #{options[:line_length].to_s})") do |v|
52
+ options[:line_length] = v
53
+ end
61
54
 
62
- # plaintext output
63
- if txtfile
64
- fout = File.open(txtfile, 'w')
65
- fout.puts premailer.to_plain_text
66
- fout.close
55
+ opts.on("-d", "--io-exceptions", "Abort on I/O errors") do |v|
56
+ options[:io_exceptions] = v
57
+ end
58
+
59
+ opts.on("-v", "--verbose", "Print additional information at runtime") do |v|
60
+ options[:verbose] = v
61
+ end
62
+
63
+ opts.on_tail("-?", "--help", "Show this message") do
64
+ puts opts
65
+ exit
66
+ end
67
+
68
+ opts.on_tail("-V", "--version", "Show version") do
69
+ puts "Premailer #{Premailer::VERSION} (c) 2008-2010 Alex Dunae"
70
+ exit
71
+ end
67
72
  end
73
+ opts.parse!
74
+
75
+ $stderr.puts "Processing in #{mode} mode with options #{options.inspect}" if options[:verbose]
76
+
77
+ premailer = nil
68
78
 
69
- # warnings output
70
- if warningsfile
71
- fout = File.open(warningsfile, 'w')
72
- premailer.warnings.each do |w|
73
- fout.puts "- #{w[:message]} (#{w[:level]}) may not render properly in #{w[:clients]}"
79
+ # check for input from STDIN
80
+ if STDIN.fcntl(Fcntl::F_GETFL, 0) == 0
81
+ io = STDIN.to_io
82
+ premailer = Premailer.new(io, options)
83
+ else
84
+ uri = ARGV.shift
85
+
86
+ if uri.to_s.strip.empty?
87
+ puts opts
88
+ exit 1
74
89
  end
75
- fout.close
90
+
91
+ premailer = Premailer.new(uri, options)
92
+ end
93
+
94
+ if mode == :txt
95
+ p premailer.to_plain_text
96
+ else
97
+ p premailer.to_inline_css
76
98
  end
99
+
100
+ exit
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/ruby
2
2
  #
3
- # Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-09
3
+ # Premailer by Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2008-10
4
4
  #
5
5
  # Premailer processes HTML and CSS to improve e-mail deliverability.
6
6
  #
@@ -33,7 +33,7 @@ class Premailer
33
33
  include HtmlToPlainText
34
34
  include CssParser
35
35
 
36
- VERSION = '1.5.6'
36
+ VERSION = '1.5.7'
37
37
 
38
38
  CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
39
39
 
@@ -96,11 +96,12 @@ class Premailer
96
96
  # ==== Options
97
97
  # [+line_length+] Line length used by to_plain_text. Boolean, default is 65.
98
98
  # [+warn_level+] What level of CSS compatibility warnings to show (see Warnings).
99
- # [+link_query_string+] A string to append to every <a href=""> link. Do not include the initial +?+.
99
+ # [+link_query_string+] A string to append to every <tt>a href=""</tt> link. Do not include the initial <tt>?</tt>.
100
100
  # [+base_url+] Used to calculate absolute URLs for local files.
101
101
  # [+css+] Manually specify a CSS stylesheet.
102
102
  # [+css_to_attributes+] Copy related CSS attributes into HTML attributes (e.g. +background-color+ to +bgcolor+)
103
103
  # [+with_html_string+] Whether the +html+ param should be treated as a raw string.
104
+ # [+verbose+] Whether to print errors and warnings to <tt>$stderr</tt>. Default is +false+.
104
105
  def initialize(html, options = {})
105
106
  @options = {:warn_level => Warnings::SAFE,
106
107
  :line_length => 65,
@@ -153,10 +154,10 @@ class Premailer
153
154
  @css_warnings = check_client_support if @css_warnings.empty?
154
155
  @css_warnings
155
156
  end
156
-
157
+
157
158
  # Returns the original HTML as a string.
158
159
  def to_s
159
- @doc.to_html
160
+ is_xhtml? ? @doc.to_xhtml : @doc.to_html
160
161
  end
161
162
 
162
163
  # Converts the HTML document to a format suitable for plain-text e-mail.
@@ -188,21 +189,24 @@ class Premailer
188
189
  # Iterate through the rules and merge them into the HTML
189
190
  @css_parser.each_selector(:all) do |selector, declaration, specificity|
190
191
  # Save un-mergable rules separately
191
- selector.gsub!(/:link([\s]|$)+/i, '')
192
+ selector.gsub!(/:link([\s]*)+/i) {|m| $1 }
192
193
 
193
194
  # Convert element names to lower case
194
195
  selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
195
-
196
+
196
197
  if selector =~ RE_UNMERGABLE_SELECTORS
197
198
  unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
198
199
  else
199
-
200
- doc.css(selector).each do |el|
201
- if el.elem?
202
- # Add a style attribute or append to the existing one
203
- block = "[SPEC=#{specificity}[#{declaration}]]"
204
- el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
200
+ begin
201
+ doc.css(selector).each do |el|
202
+ if el.elem?
203
+ # Add a style attribute or append to the existing one
204
+ block = "[SPEC=#{specificity}[#{declaration}]]"
205
+ el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
206
+ end
205
207
  end
208
+ rescue Nokogiri::SyntaxError, RuntimeError
209
+ $stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
206
210
  end
207
211
  end
208
212
  end
@@ -254,28 +258,30 @@ class Premailer
254
258
  doc.to_html
255
259
  end
256
260
 
261
+ # Check for an XHTML doctype
262
+ def is_xhtml?
263
+ intro = @doc.to_s.strip.split("\n")[0..2].join(' ')
264
+ intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i
265
+ end
257
266
 
258
267
  protected
259
268
  # Load the HTML file and convert it into an Nokogiri document.
260
269
  #
261
270
  # Returns an Nokogiri document.
262
- def load_html(path) # :nodoc:
263
- if @options[:with_html_string]
264
- Nokogiri::HTML.parse(path)
265
- elsif @options[:inline]
266
- Nokogiri::HTML(path)
271
+ def load_html(input) # :nodoc:
272
+ thing = nil
273
+
274
+ # TODO: duplicate options
275
+ if @options[:with_html_string] or @options[:inline] or input.respond_to?(:read)
276
+ thing = input
277
+ elsif @is_local_file
278
+ @base_dir = File.dirname(input)
279
+ thing = File.open(input, 'r')
267
280
  else
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
281
+ thing = open(input)
278
282
  end
283
+
284
+ thing ? Nokogiri::HTML(thing) : nil
279
285
  end
280
286
 
281
287
  def load_css_from_local_file!(path)
@@ -308,10 +314,10 @@ protected
308
314
 
309
315
  link_uri = Premailer.resolve_link(tag.attributes['href'].to_s, @html_file)
310
316
  if Premailer.local_data?(link_uri)
311
- puts "Loading css from local file: " + link_uri if @options[:verbose]
317
+ $stderr.puts "Loading css from local file: " + link_uri if @options[:verbose]
312
318
  load_css_from_local_file!(link_uri)
313
319
  else
314
- puts "Loading css from uri: " + link_uri if @options[:verbose]
320
+ $stderr.puts "Loading css from uri: " + link_uri if @options[:verbose]
315
321
  @css_parser.load_uri!(link_uri)
316
322
  end
317
323
 
@@ -350,19 +356,30 @@ protected
350
356
  end
351
357
 
352
358
  def append_query_string(doc, qs)
359
+ return doc if qs.nil?
360
+
361
+ qs.to_s.strip!
362
+ return doc if qs.empty?
363
+
364
+ $stderr.puts "Attempting to append_query_string: #{qs}" if @options[:verbose]
365
+
353
366
  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)
367
+ href = el.attributes['href'].to_s.strip
368
+ next if href.nil? or href.empty?
369
+
370
+ begin
371
+ href = URI.parse(href)
372
+ if href.query
373
+ href.query = href.query + '&amp' + qs
374
+ else
375
+ href.query = qs
376
+ end
358
377
 
359
- if href.query
360
- href.query = href.query + '&amp' + qs
361
- else
362
- href.query = qs
378
+ el['href'] = href.to_s
379
+ rescue URI::Error => e
380
+ $stderr.puts "Skipping append_query_string for: #{href.to_s} (#{e.message})" if @options[:verbose]
381
+ next
363
382
  end
364
-
365
- el['href'] = href.to_s
366
383
 
367
384
  end
368
385
  doc
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: premailer
3
3
  version: !ruby/object:Gem::Version
4
- hash: 15
4
+ hash: 13
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
8
  - 5
9
- - 6
10
- version: 1.5.6
9
+ - 7
10
+ version: 1.5.7
11
11
  platform: ruby
12
12
  authors:
13
13
  - Alex Dunae
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-11-03 00:00:00 -07:00
18
+ date: 2010-11-05 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency