premailer 1.5.7 → 1.6.1

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/lib/premailer.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'yaml'
2
2
  require 'open-uri'
3
3
  require 'cgi'
4
- require 'nokogiri'
4
+ require 'hpricot'
5
5
  require 'css_parser'
6
6
  require 'premailer/html_to_plain_text'
7
7
  require 'premailer/premailer'
@@ -33,7 +33,7 @@ class Premailer
33
33
  include HtmlToPlainText
34
34
  include CssParser
35
35
 
36
- VERSION = '1.5.7'
36
+ VERSION = '1.6.1'
37
37
 
38
38
  CLIENT_SUPPORT_FILE = File.dirname(__FILE__) + '/../../misc/client_support.yaml'
39
39
 
@@ -71,10 +71,10 @@ class Premailer
71
71
  attr_reader :base_dir
72
72
 
73
73
 
74
- # processed HTML document (Nokogiri)
74
+ # processed HTML document (Hpricot)
75
75
  attr_reader :processed_doc
76
76
 
77
- # source HTML document (Nokogiri)
77
+ # source HTML document (Hpricot)
78
78
  attr_reader :doc
79
79
 
80
80
  module Warnings
@@ -100,6 +100,7 @@ class Premailer
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
+ # [+preserve_styles+] Whether to preserve any <tt>link rel=stylesheet</tt> and <tt>style</tt> elements. Default is +false+.
103
104
  # [+with_html_string+] Whether the +html+ param should be treated as a raw string.
104
105
  # [+verbose+] Whether to print errors and warnings to <tt>$stderr</tt>. Default is +false+.
105
106
  def initialize(html, options = {})
@@ -111,7 +112,9 @@ class Premailer
111
112
  :css => [],
112
113
  :css_to_attributes => true,
113
114
  :with_html_string => false,
115
+ :preserve_styles => false,
114
116
  :verbose => false,
117
+ :debug => false,
115
118
  :io_exceptions => false}.merge(options)
116
119
 
117
120
  @html_file = html
@@ -125,7 +128,7 @@ class Premailer
125
128
  @base_dir = nil
126
129
 
127
130
  if @options[:base_url]
128
- @base_url = URI.parse(@options.delete[:base_url])
131
+ @base_url = URI.parse(@options.delete(:base_url))
129
132
  elsif not @is_local_file
130
133
  @base_url = URI.parse(@html_file)
131
134
  end
@@ -137,8 +140,8 @@ class Premailer
137
140
  })
138
141
 
139
142
  @doc = load_html(@html_file)
140
-
141
- @html_charset = @doc.encoding
143
+ # TODO
144
+ @html_charset = nil # @doc.encoding || nil
142
145
  @processed_doc = @doc
143
146
  @processed_doc = convert_inline_links(@processed_doc, @base_url) if @base_url
144
147
  if options[:link_query_string]
@@ -157,7 +160,7 @@ class Premailer
157
160
 
158
161
  # Returns the original HTML as a string.
159
162
  def to_s
160
- is_xhtml? ? @doc.to_xhtml : @doc.to_html
163
+ @doc.to_original_html
161
164
  end
162
165
 
163
166
  # Converts the HTML document to a format suitable for plain-text e-mail.
@@ -195,18 +198,19 @@ class Premailer
195
198
  selector.gsub!(/([\s]|^)([\w]+)/) {|m| $1.to_s + $2.to_s.downcase }
196
199
 
197
200
  if selector =~ RE_UNMERGABLE_SELECTORS
198
- unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration))
201
+ unmergable_rules.add_rule_set!(RuleSet.new(selector, declaration)) unless @options[:preserve_styles]
199
202
  else
200
203
  begin
201
- doc.css(selector).each do |el|
202
- if el.elem?
204
+ doc.search(selector).each do |el|
205
+ if el.elem? and (el.name != 'head' and el.parent.name != 'head')
203
206
  # Add a style attribute or append to the existing one
204
207
  block = "[SPEC=#{specificity}[#{declaration}]]"
205
208
  el['style'] = (el.attributes['style'].to_s ||= '') + ' ' + block
206
209
  end
207
210
  end
208
- rescue Nokogiri::SyntaxError, RuntimeError
211
+ rescue Hpricot::Error, RuntimeError, ArgumentError
209
212
  $stderr.puts "CSS syntax error with selector: #{selector}" if @options[:verbose]
213
+ next
210
214
  end
211
215
  end
212
216
  end
@@ -255,19 +259,22 @@ class Premailer
255
259
 
256
260
  @processed_doc = doc
257
261
 
258
- doc.to_html
262
+ @processed_doc.to_original_html
259
263
  end
260
264
 
261
265
  # Check for an XHTML doctype
262
266
  def is_xhtml?
263
267
  intro = @doc.to_s.strip.split("\n")[0..2].join(' ')
264
- intro =~ /w3c\/\/[\s]*dtd[\s]+xhtml/i
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
265
272
  end
266
273
 
267
274
  protected
268
- # Load the HTML file and convert it into an Nokogiri document.
275
+ # Load the HTML file and convert it into an Hpricot document.
269
276
  #
270
- # Returns an Nokogiri document.
277
+ # Returns an Hpricot document.
271
278
  def load_html(input) # :nodoc:
272
279
  thing = nil
273
280
 
@@ -281,7 +288,7 @@ protected
281
288
  thing = open(input)
282
289
  end
283
290
 
284
- thing ? Nokogiri::HTML(thing) : nil
291
+ thing ? Hpricot(thing) : nil
285
292
  end
286
293
 
287
294
  def load_css_from_local_file!(path)
@@ -325,7 +332,7 @@ protected
325
332
  @css_parser.add_block!(tag.inner_html, :base_uri => @base_url, :base_dir => @base_dir, :only_media_types => [:screen, :handheld])
326
333
  end
327
334
  end
328
- tags.remove
335
+ tags.remove unless @options[:preserve_styles]
329
336
  end
330
337
  end
331
338
 
@@ -339,18 +346,22 @@ protected
339
346
  # Create a <tt>style</tt> element with un-mergable rules (e.g. <tt>:hover</tt>)
340
347
  # and write it into the <tt>body</tt>.
341
348
  #
342
- # <tt>doc</tt> is an Nokogiri document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
349
+ # <tt>doc</tt> is an Hpricot document and <tt>unmergable_css_rules</tt> is a Css::RuleSet.
343
350
  #
344
- # Returns an Nokogiri document.
351
+ # Returns an Hpricot document.
345
352
  def write_unmergable_css_rules(doc, unmergable_rules) # :nodoc:
346
- styles = ''
347
- unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
348
- styles += "#{selector} { #{declarations} }\n"
349
- end
350
-
351
- unless styles.empty?
352
- style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
353
- doc.css("head").children.last.after(style_tag)
353
+ if head = doc.search('head')
354
+ styles = ''
355
+ unmergable_rules.each_selector(:all, :force_important => true) do |selector, declarations, specificity|
356
+ styles += "#{selector} { #{declarations} }\n"
357
+ end
358
+
359
+ unless styles.empty?
360
+ style_tag = "\n<style type=\"text/css\">\n#{styles}</style>\n"
361
+ head.html.empty? ? head.inner_html(style_tag) : head.append(style_tag)
362
+ end
363
+ else
364
+ $stderr.puts "Unable to write unmergable CSS rules: no <head> was found" if @options[:verbose]
354
365
  end
355
366
  doc
356
367
  end
@@ -360,19 +371,37 @@ protected
360
371
 
361
372
  qs.to_s.strip!
362
373
  return doc if qs.empty?
374
+
375
+ begin
376
+ current_host = @base_url.host
377
+ rescue
378
+ current_host = nil
379
+ end
363
380
 
364
381
  $stderr.puts "Attempting to append_query_string: #{qs}" if @options[:verbose]
365
382
 
366
383
  doc.search('a').each do|el|
367
384
  href = el.attributes['href'].to_s.strip
368
385
  next if href.nil? or href.empty?
386
+ next if href[0] == '#' # don't bother with anchors
369
387
 
370
388
  begin
371
389
  href = URI.parse(href)
390
+
391
+ if current_host and href.host != nil and href.host != current_host
392
+ $stderr.puts "Skipping append_query_string for: #{href.to_s} because host is no good" if @options[:verbose]
393
+ next
394
+ end
395
+
396
+ if href.scheme and href.scheme != 'http' and href.scheme != 'https'
397
+ puts "Skipping append_query_string for: #{href.to_s} because scheme is no good" if @options[:verbose]
398
+ next
399
+ end
400
+
372
401
  if href.query
373
- href.query = href.query + '&amp' + qs
402
+ href.query = href.query + '&amp;' + qs
374
403
  else
375
- href.query = qs
404
+ href.query = '?' + qs
376
405
  end
377
406
 
378
407
  el['href'] = href.to_s
@@ -390,9 +419,9 @@ protected
390
419
  # Processes <tt>href</tt> <tt>src</tt> and <tt>background</tt> attributes
391
420
  # as well as CSS <tt>url()</tt> declarations found in inline <tt>style</tt> attributes.
392
421
  #
393
- # <tt>doc</tt> is an Nokogiri document and <tt>base_uri</tt> is either a string or a URI.
422
+ # <tt>doc</tt> is an Hpricot document and <tt>base_uri</tt> is either a string or a URI.
394
423
  #
395
- # Returns an Nokogiri document.
424
+ # Returns an Hpricot document.
396
425
  def convert_inline_links(doc, base_uri) # :nodoc:
397
426
  base_uri = URI.parse(base_uri) unless base_uri.kind_of?(URI)
398
427
 
@@ -406,11 +435,11 @@ protected
406
435
  tags.each do |tag|
407
436
  # skip links that look like they have merge tags
408
437
  # and mailto, ftp, etc...
409
- if tag.attributes[attribute] =~ /^(\{|\[|<|\#|mailto:|ftp:|gopher:)/i
438
+ if tag.attributes[attribute].to_s =~ /^(\{|\[|<|\#|data:|tel:|file:|sms:|callto:|facetime:|mailto:|ftp:|gopher:)/i
410
439
  next
411
440
  end
412
441
 
413
- if tag.attributes[attribute] =~ /^http/i
442
+ if tag.attributes[attribute].to_s =~ /^http/i
414
443
  begin
415
444
  merged = URI.parse(tag.attributes[attribute])
416
445
  rescue; next; end
metadata CHANGED
@@ -1,13 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: premailer
3
3
  version: !ruby/object:Gem::Version
4
- hash: 13
5
4
  prerelease: false
6
5
  segments:
7
6
  - 1
8
- - 5
9
- - 7
10
- version: 1.5.7
7
+ - 6
8
+ - 1
9
+ version: 1.6.1
11
10
  platform: ruby
12
11
  authors:
13
12
  - Alex Dunae
@@ -15,23 +14,22 @@ autorequire:
15
14
  bindir: bin
16
15
  cert_chain: []
17
16
 
18
- date: 2010-11-05 00:00:00 -07:00
17
+ date: 2010-11-16 00:00:00 -08:00
19
18
  default_executable:
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
22
- name: nokogiri
21
+ name: hpricot
23
22
  prerelease: false
24
23
  requirement: &id001 !ruby/object:Gem::Requirement
25
24
  none: false
26
25
  requirements:
27
26
  - - ">="
28
27
  - !ruby/object:Gem::Version
29
- hash: 7
30
28
  segments:
31
- - 1
32
- - 4
33
29
  - 0
34
- version: 1.4.0
30
+ - 8
31
+ - 3
32
+ version: 0.8.3
35
33
  type: :runtime
36
34
  version_requirements: *id001
37
35
  - !ruby/object:Gem::Dependency
@@ -42,7 +40,6 @@ dependencies:
42
40
  requirements:
43
41
  - - ">="
44
42
  - !ruby/object:Gem::Version
45
- hash: 21
46
43
  segments:
47
44
  - 1
48
45
  - 1
@@ -58,7 +55,6 @@ dependencies:
58
55
  requirements:
59
56
  - - ">="
60
57
  - !ruby/object:Gem::Version
61
- hash: 63
62
58
  segments:
63
59
  - 4
64
60
  - 0
@@ -77,7 +73,6 @@ extra_rdoc_files:
77
73
  files:
78
74
  - init.rb
79
75
  - bin/premailer
80
- - bin/trollop.rb
81
76
  - lib/premailer.rb
82
77
  - lib/premailer/html_to_plain_text.rb
83
78
  - lib/premailer/premailer.rb
@@ -101,7 +96,6 @@ required_ruby_version: !ruby/object:Gem::Requirement
101
96
  requirements:
102
97
  - - ">="
103
98
  - !ruby/object:Gem::Version
104
- hash: 3
105
99
  segments:
106
100
  - 0
107
101
  version: "0"
@@ -110,7 +104,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
104
  requirements:
111
105
  - - ">="
112
106
  - !ruby/object:Gem::Version
113
- hash: 3
114
107
  segments:
115
108
  - 0
116
109
  version: "0"
data/bin/trollop.rb DELETED
@@ -1,739 +0,0 @@
1
- ## lib/trollop.rb -- trollop command-line processing library
2
- ## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net)
3
- ## Copyright:: Copyright 2007 William Morgan
4
- ## License:: GNU GPL version 2
5
-
6
- require 'date'
7
-
8
- module Trollop
9
-
10
- VERSION = "1.15"
11
-
12
- ## Thrown by Parser in the event of a commandline error. Not needed if
13
- ## you're using the Trollop::options entry.
14
- class CommandlineError < StandardError; end
15
-
16
- ## Thrown by Parser if the user passes in '-h' or '--help'. Handled
17
- ## automatically by Trollop#options.
18
- class HelpNeeded < StandardError; end
19
-
20
- ## Thrown by Parser if the user passes in '-h' or '--version'. Handled
21
- ## automatically by Trollop#options.
22
- class VersionNeeded < StandardError; end
23
-
24
- ## Regex for floating point numbers
25
- FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))$/
26
-
27
- ## Regex for parameters
28
- PARAM_RE = /^-(-|\.$|[^\d\.])/
29
-
30
- ## The commandline parser. In typical usage, the methods in this class
31
- ## will be handled internally by Trollop::options. In this case, only the
32
- ## #opt, #banner and #version, #depends, and #conflicts methods will
33
- ## typically be called.
34
- ##
35
- ## If it's necessary to instantiate this class (for more complicated
36
- ## argument-parsing situations), be sure to call #parse to actually
37
- ## produce the output hash.
38
- class Parser
39
-
40
- ## The set of values that indicate a flag option when passed as the
41
- ## +:type+ parameter of #opt.
42
- FLAG_TYPES = [:flag, :bool, :boolean]
43
-
44
- ## The set of values that indicate a single-parameter (normal) option when
45
- ## passed as the +:type+ parameter of #opt.
46
- ##
47
- ## A value of +io+ corresponds to a readable IO resource, including
48
- ## a filename, URI, or the strings 'stdin' or '-'.
49
- SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date]
50
-
51
- ## The set of values that indicate a multiple-parameter option (i.e., that
52
- ## takes multiple space-separated values on the commandline) when passed as
53
- ## the +:type+ parameter of #opt.
54
- MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates]
55
-
56
- ## The complete set of legal values for the +:type+ parameter of #opt.
57
- TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
58
-
59
- INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
60
-
61
- ## The values from the commandline that were not interpreted by #parse.
62
- attr_reader :leftovers
63
-
64
- ## The complete configuration hashes for each option. (Mainly useful
65
- ## for testing.)
66
- attr_reader :specs
67
-
68
- ## Initializes the parser, and instance-evaluates any block given.
69
- def initialize *a, &b
70
- @version = nil
71
- @leftovers = []
72
- @specs = {}
73
- @long = {}
74
- @short = {}
75
- @order = []
76
- @constraints = []
77
- @stop_words = []
78
- @stop_on_unknown = false
79
-
80
- #instance_eval(&b) if b # can't take arguments
81
- cloaker(&b).bind(self).call(*a) if b
82
- end
83
-
84
- ## Define an option. +name+ is the option name, a unique identifier
85
- ## for the option that you will use internally, which should be a
86
- ## symbol or a string. +desc+ is a string description which will be
87
- ## displayed in help messages.
88
- ##
89
- ## Takes the following optional arguments:
90
- ##
91
- ## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s.
92
- ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+.
93
- ## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given.
94
- ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the the commandline the value will be +false+.
95
- ## [+:required+] If set to +true+, the argument must be provided on the commandline.
96
- ## [+:multi+] If set to +true+, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.)
97
- ##
98
- ## Note that there are two types of argument multiplicity: an argument
99
- ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
100
- ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
101
- ##
102
- ## Arguments that take multiple values should have a +:type+ parameter
103
- ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
104
- ## value of an array of the correct type (e.g. [String]). The
105
- ## value of this argument will be an array of the parameters on the
106
- ## commandline.
107
- ##
108
- ## Arguments that can occur multiple times should be marked with
109
- ## +:multi+ => +true+. The value of this argument will also be an array.
110
- ## In contrast with regular non-multi options, if not specified on
111
- ## the commandline, the default value will be [], not nil.
112
- ##
113
- ## These two attributes can be combined (e.g. +:type+ => +:strings+,
114
- ## +:multi+ => +true+), in which case the value of the argument will be
115
- ## an array of arrays.
116
- ##
117
- ## There's one ambiguous case to be aware of: when +:multi+: is true and a
118
- ## +:default+ is set to an array (of something), it's ambiguous whether this
119
- ## is a multi-value argument as well as a multi-occurrence argument.
120
- ## In thise case, Trollop assumes that it's not a multi-value argument.
121
- ## If you want a multi-value, multi-occurrence argument with a default
122
- ## value, you must specify +:type+ as well.
123
-
124
- def opt name, desc="", opts={}
125
- raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
126
-
127
- ## fill in :type
128
- opts[:type] = # normalize
129
- case opts[:type]
130
- when :boolean, :bool; :flag
131
- when :integer; :int
132
- when :integers; :ints
133
- when :double; :float
134
- when :doubles; :floats
135
- when Class
136
- case opts[:type].name
137
- when 'TrueClass', 'FalseClass'; :flag
138
- when 'String'; :string
139
- when 'Integer'; :int
140
- when 'Float'; :float
141
- when 'IO'; :io
142
- when 'Date'; :date
143
- else
144
- raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
145
- end
146
- when nil; nil
147
- else
148
- raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
149
- opts[:type]
150
- end
151
-
152
- ## for options with :multi => true, an array default doesn't imply
153
- ## a multi-valued argument. for that you have to specify a :type
154
- ## as well. (this is how we disambiguate an ambiguous situation;
155
- ## see the docs for Parser#opt for details.)
156
- disambiguated_default =
157
- if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type]
158
- opts[:default].first
159
- else
160
- opts[:default]
161
- end
162
-
163
- type_from_default =
164
- case disambiguated_default
165
- when Integer; :int
166
- when Numeric; :float
167
- when TrueClass, FalseClass; :flag
168
- when String; :string
169
- when IO; :io
170
- when Date; :date
171
- when Array
172
- if opts[:default].empty?
173
- raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
174
- end
175
- case opts[:default][0] # the first element determines the types
176
- when Integer; :ints
177
- when Numeric; :floats
178
- when String; :strings
179
- when IO; :ios
180
- when Date; :dates
181
- else
182
- raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
183
- end
184
- when nil; nil
185
- else
186
- raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
187
- end
188
-
189
- raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default
190
-
191
- opts[:type] = opts[:type] || type_from_default || :flag
192
-
193
- ## fill in :long
194
- opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
195
- opts[:long] =
196
- case opts[:long]
197
- when /^--([^-].*)$/
198
- $1
199
- when /^[^-]/
200
- opts[:long]
201
- else
202
- raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
203
- end
204
- raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
205
-
206
- ## fill in :short
207
- opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
208
- opts[:short] = case opts[:short]
209
- when /^-(.)$/; $1
210
- when nil, :none, /^.$/; opts[:short]
211
- else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
212
- end
213
-
214
- if opts[:short]
215
- raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
216
- raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
217
- end
218
-
219
- ## fill in :default for flags
220
- opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
221
-
222
- ## autobox :default for :multi (multi-occurrence) arguments
223
- opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)
224
-
225
- ## fill in :multi
226
- opts[:multi] ||= false
227
-
228
- opts[:desc] ||= desc
229
- @long[opts[:long]] = name
230
- @short[opts[:short]] = name if opts[:short] && opts[:short] != :none
231
- @specs[name] = opts
232
- @order << [:opt, name]
233
- end
234
-
235
- ## Sets the version string. If set, the user can request the version
236
- ## on the commandline. Should probably be of the form "<program name>
237
- ## <version number>".
238
- def version s=nil; @version = s if s; @version end
239
-
240
- ## Adds text to the help display. Can be interspersed with calls to
241
- ## #opt to build a multi-section help page.
242
- def banner s; @order << [:text, s] end
243
- alias :text :banner
244
-
245
- ## Marks two (or more!) options as requiring each other. Only handles
246
- ## undirected (i.e., mutual) dependencies. Directed dependencies are
247
- ## better modeled with Trollop::die.
248
- def depends *syms
249
- syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
250
- @constraints << [:depends, syms]
251
- end
252
-
253
- ## Marks two (or more!) options as conflicting.
254
- def conflicts *syms
255
- syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
256
- @constraints << [:conflicts, syms]
257
- end
258
-
259
- ## Defines a set of words which cause parsing to terminate when
260
- ## encountered, such that any options to the left of the word are
261
- ## parsed as usual, and options to the right of the word are left
262
- ## intact.
263
- ##
264
- ## A typical use case would be for subcommand support, where these
265
- ## would be set to the list of subcommands. A subsequent Trollop
266
- ## invocation would then be used to parse subcommand options, after
267
- ## shifting the subcommand off of ARGV.
268
- def stop_on *words
269
- @stop_words = [*words].flatten
270
- end
271
-
272
- ## Similar to #stop_on, but stops on any unknown word when encountered
273
- ## (unless it is a parameter for an argument). This is useful for
274
- ## cases where you don't know the set of subcommands ahead of time,
275
- ## i.e., without first parsing the global options.
276
- def stop_on_unknown
277
- @stop_on_unknown = true
278
- end
279
-
280
- ## Parses the commandline. Typically called by Trollop::options.
281
- def parse cmdline=ARGV
282
- vals = {}
283
- required = {}
284
-
285
- opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
286
- opt :help, "Show this message" unless @specs[:help] || @long["help"]
287
-
288
- @specs.each do |sym, opts|
289
- required[sym] = true if opts[:required]
290
- vals[sym] = opts[:default]
291
- vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil
292
- end
293
-
294
- resolve_default_short_options
295
-
296
- ## resolve symbols
297
- given_args = {}
298
- @leftovers = each_arg cmdline do |arg, params|
299
- sym = case arg
300
- when /^-([^-])$/
301
- @short[$1]
302
- when /^--([^-]\S*)$/
303
- @long[$1]
304
- else
305
- raise CommandlineError, "invalid argument syntax: '#{arg}'"
306
- end
307
- raise CommandlineError, "unknown argument '#{arg}'" unless sym
308
-
309
- if given_args.include?(sym) && !@specs[sym][:multi]
310
- raise CommandlineError, "option '#{arg}' specified multiple times"
311
- end
312
-
313
- given_args[sym] ||= {}
314
-
315
- given_args[sym][:arg] = arg
316
- given_args[sym][:params] ||= []
317
-
318
- # The block returns the number of parameters taken.
319
- num_params_taken = 0
320
-
321
- unless params.nil?
322
- if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
323
- given_args[sym][:params] << params[0, 1] # take the first parameter
324
- num_params_taken = 1
325
- elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
326
- given_args[sym][:params] << params # take all the parameters
327
- num_params_taken = params.size
328
- end
329
- end
330
-
331
- num_params_taken
332
- end
333
-
334
- ## check for version and help args
335
- raise VersionNeeded if given_args.include? :version
336
- raise HelpNeeded if given_args.include? :help
337
-
338
- ## check constraint satisfaction
339
- @constraints.each do |type, syms|
340
- constraint_sym = syms.find { |sym| given_args[sym] }
341
- next unless constraint_sym
342
-
343
- case type
344
- when :depends
345
- syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
346
- when :conflicts
347
- syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
348
- end
349
- end
350
-
351
- required.each do |sym, val|
352
- raise CommandlineError, "option '#{sym}' must be specified" unless given_args.include? sym
353
- end
354
-
355
- ## parse parameters
356
- given_args.each do |sym, given_data|
357
- arg = given_data[:arg]
358
- params = given_data[:params]
359
-
360
- opts = @specs[sym]
361
- raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag
362
-
363
- vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
364
-
365
- case opts[:type]
366
- when :flag
367
- vals[sym] = !opts[:default]
368
- when :int, :ints
369
- vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
370
- when :float, :floats
371
- vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
372
- when :string, :strings
373
- vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
374
- when :io, :ios
375
- vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
376
- when :date, :dates
377
- vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
378
- end
379
-
380
- if SINGLE_ARG_TYPES.include?(opts[:type])
381
- unless opts[:multi] # single parameter
382
- vals[sym] = vals[sym][0][0]
383
- else # multiple options, each with a single parameter
384
- vals[sym] = vals[sym].map { |p| p[0] }
385
- end
386
- elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
387
- vals[sym] = vals[sym][0] # single option, with multiple parameters
388
- end
389
- # else: multiple options, with multiple parameters
390
- end
391
-
392
- ## allow openstruct-style accessors
393
- class << vals
394
- def method_missing(m, *args)
395
- self[m] || self[m.to_s]
396
- end
397
- end
398
- vals
399
- end
400
-
401
- def parse_date_parameter param, arg #:nodoc:
402
- begin
403
- begin
404
- time = Chronic.parse(param)
405
- rescue NameError
406
- # chronic is not available
407
- end
408
- time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
409
- rescue ArgumentError => e
410
- raise CommandlineError, "option '#{arg}' needs a date"
411
- end
412
- end
413
-
414
- ## Print the help message to +stream+.
415
- def educate stream=$stdout
416
- width # just calculate it now; otherwise we have to be careful not to
417
- # call this unless the cursor's at the beginning of a line.
418
-
419
- left = {}
420
- @specs.each do |name, spec|
421
- left[name] = "--#{spec[:long]}" +
422
- (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
423
- case spec[:type]
424
- when :flag; ""
425
- when :int; " <i>"
426
- when :ints; " <i+>"
427
- when :string; " <s>"
428
- when :strings; " <s+>"
429
- when :float; " <f>"
430
- when :floats; " <f+>"
431
- when :io; " <filename/uri>"
432
- when :ios; " <filename/uri+>"
433
- when :date; " <date>"
434
- when :dates; " <date+>"
435
- end
436
- end
437
-
438
- leftcol_width = left.values.map { |s| s.length }.max || 0
439
- rightcol_start = leftcol_width + 6 # spaces
440
-
441
- unless @order.size > 0 && @order.first.first == :text
442
- stream.puts "#@version\n" if @version
443
- stream.puts "Options:"
444
- end
445
-
446
- @order.each do |what, opt|
447
- if what == :text
448
- stream.puts wrap(opt)
449
- next
450
- end
451
-
452
- spec = @specs[opt]
453
- stream.printf " %#{leftcol_width}s: ", left[opt]
454
- desc = spec[:desc] + begin
455
- default_s = case spec[:default]
456
- when $stdout; "<stdout>"
457
- when $stdin; "<stdin>"
458
- when $stderr; "<stderr>"
459
- when Array
460
- spec[:default].join(", ")
461
- else
462
- spec[:default].to_s
463
- end
464
-
465
- if spec[:default]
466
- if spec[:desc] =~ /\.$/
467
- " (Default: #{default_s})"
468
- else
469
- " (default: #{default_s})"
470
- end
471
- else
472
- ""
473
- end
474
- end
475
- stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
476
- end
477
- end
478
-
479
- def width #:nodoc:
480
- @width ||= if $stdout.tty?
481
- begin
482
- require 'curses'
483
- Curses::init_screen
484
- x = Curses::cols
485
- Curses::close_screen
486
- x
487
- rescue Exception
488
- 80
489
- end
490
- else
491
- 80
492
- end
493
- end
494
-
495
- def wrap str, opts={} # :nodoc:
496
- if str == ""
497
- [""]
498
- else
499
- str.split("\n").map { |s| wrap_line s, opts }.flatten
500
- end
501
- end
502
-
503
- private
504
-
505
- ## yield successive arg, parameter pairs
506
- def each_arg args
507
- remains = []
508
- i = 0
509
-
510
- until i >= args.length
511
- if @stop_words.member? args[i]
512
- remains += args[i .. -1]
513
- return remains
514
- end
515
- case args[i]
516
- when /^--$/ # arg terminator
517
- remains += args[(i + 1) .. -1]
518
- return remains
519
- when /^--(\S+?)=(.*)$/ # long argument with equals
520
- yield "--#{$1}", [$2]
521
- i += 1
522
- when /^--(\S+)$/ # long argument
523
- params = collect_argument_parameters(args, i + 1)
524
- unless params.empty?
525
- num_params_taken = yield args[i], params
526
- unless num_params_taken
527
- if @stop_on_unknown
528
- remains += args[i + 1 .. -1]
529
- return remains
530
- else
531
- remains += params
532
- end
533
- end
534
- i += 1 + num_params_taken
535
- else # long argument no parameter
536
- yield args[i], nil
537
- i += 1
538
- end
539
- when /^-(\S+)$/ # one or more short arguments
540
- shortargs = $1.split(//)
541
- shortargs.each_with_index do |a, j|
542
- if j == (shortargs.length - 1)
543
- params = collect_argument_parameters(args, i + 1)
544
- unless params.empty?
545
- num_params_taken = yield "-#{a}", params
546
- unless num_params_taken
547
- if @stop_on_unknown
548
- remains += args[i + 1 .. -1]
549
- return remains
550
- else
551
- remains += params
552
- end
553
- end
554
- i += 1 + num_params_taken
555
- else # argument no parameter
556
- yield "-#{a}", nil
557
- i += 1
558
- end
559
- else
560
- yield "-#{a}", nil
561
- end
562
- end
563
- else
564
- if @stop_on_unknown
565
- remains += args[i .. -1]
566
- return remains
567
- else
568
- remains << args[i]
569
- i += 1
570
- end
571
- end
572
- end
573
-
574
- remains
575
- end
576
-
577
- def parse_integer_parameter param, arg
578
- raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
579
- param.to_i
580
- end
581
-
582
- def parse_float_parameter param, arg
583
- raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
584
- param.to_f
585
- end
586
-
587
- def parse_io_parameter param, arg
588
- case param
589
- when /^(stdin|-)$/i; $stdin
590
- else
591
- require 'open-uri'
592
- begin
593
- open param
594
- rescue SystemCallError => e
595
- raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
596
- end
597
- end
598
- end
599
-
600
- def collect_argument_parameters args, start_at
601
- params = []
602
- pos = start_at
603
- while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
604
- params << args[pos]
605
- pos += 1
606
- end
607
- params
608
- end
609
-
610
- def resolve_default_short_options
611
- @order.each do |type, name|
612
- next unless type == :opt
613
- opts = @specs[name]
614
- next if opts[:short]
615
-
616
- c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
617
- if c # found a character to use
618
- opts[:short] = c
619
- @short[c] = name
620
- end
621
- end
622
- end
623
-
624
- def wrap_line str, opts={}
625
- prefix = opts[:prefix] || 0
626
- width = opts[:width] || (self.width - 1)
627
- start = 0
628
- ret = []
629
- until start > str.length
630
- nextt =
631
- if start + width >= str.length
632
- str.length
633
- else
634
- x = str.rindex(/\s/, start + width)
635
- x = str.index(/\s/, start) if x && x < start
636
- x || str.length
637
- end
638
- ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
639
- start = nextt + 1
640
- end
641
- ret
642
- end
643
-
644
- ## instance_eval but with ability to handle block arguments
645
- ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
646
- def cloaker &b
647
- (class << self; self; end).class_eval do
648
- define_method :cloaker_, &b
649
- meth = instance_method :cloaker_
650
- remove_method :cloaker_
651
- meth
652
- end
653
- end
654
- end
655
-
656
- ## The top-level entry method into Trollop. Creates a Parser object,
657
- ## passes the block to it, then parses +args+ with it, handling any
658
- ## errors or requests for help or version information appropriately (and
659
- ## then exiting). Modifies +args+ in place. Returns a hash of option
660
- ## values.
661
- ##
662
- ## The block passed in should contain zero or more calls to +opt+
663
- ## (Parser#opt), zero or more calls to +text+ (Parser#text), and
664
- ## probably a call to +version+ (Parser#version).
665
- ##
666
- ## The returned block contains a value for every option specified with
667
- ## +opt+. The value will be the value given on the commandline, or the
668
- ## default value if the option was not specified on the commandline. For
669
- ## every option specified on the commandline, a key "<option
670
- ## name>_given" will also be set in the hash.
671
- ##
672
- ## Example:
673
- ##
674
- ## require 'trollop'
675
- ## opts = Trollop::options do
676
- ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
677
- ## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
678
- ## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
679
- ## opt :num_thumbs, "Number of thumbs", :type => :int # an integer --num-thumbs <i>, defaulting to nil
680
- ## end
681
- ##
682
- ## ## if called with no arguments
683
- ## p opts # => { :monkey => false, :goat => true, :num_limbs => 4, :num_thumbs => nil }
684
- ##
685
- ## ## if called with --monkey
686
- ## p opts # => {:monkey_given=>true, :monkey=>true, :goat=>true, :num_limbs=>4, :help=>false, :num_thumbs=>nil}
687
- ##
688
- ## See more examples at http://trollop.rubyforge.org.
689
- def options args = ARGV, *a, &b
690
- @p = Parser.new(*a, &b)
691
- begin
692
- vals = @p.parse args
693
- args.clear
694
- @p.leftovers.each { |l| args << l }
695
- vals
696
- rescue CommandlineError => e
697
- $stderr.puts "Error: #{e.message}."
698
- $stderr.puts "Try --help for help."
699
- exit(-1)
700
- rescue HelpNeeded
701
- @p.educate
702
- exit
703
- rescue VersionNeeded
704
- puts @p.version
705
- exit
706
- end
707
- end
708
-
709
- ## Informs the user that their usage of 'arg' was wrong, as detailed by
710
- ## 'msg', and dies. Example:
711
- ##
712
- ## options do
713
- ## opt :volume, :default => 0.0
714
- ## end
715
- ##
716
- ## die :volume, "too loud" if opts[:volume] > 10.0
717
- ## die :volume, "too soft" if opts[:volume] < 0.1
718
- ##
719
- ## In the one-argument case, simply print that message, a notice
720
- ## about -h, and die. Example:
721
- ##
722
- ## options do
723
- ## opt :whatever # ...
724
- ## end
725
- ##
726
- ## Trollop::die "need at least one filename" if ARGV.empty?
727
- def die arg, msg=nil
728
- if msg
729
- $stderr.puts "Error: argument --#{@p.specs[arg][:long]} #{msg}."
730
- else
731
- $stderr.puts "Error: #{arg}."
732
- end
733
- $stderr.puts "Try --help for help."
734
- exit(-1)
735
- end
736
-
737
- module_function :options, :die
738
-
739
- end # module