premailer 1.5.6 → 1.5.7

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.
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