citrus 2.3.2 → 2.3.3

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.
@@ -0,0 +1,55 @@
1
+ examples = File.expand_path('..', __FILE__)
2
+ $LOAD_PATH.unshift(examples) unless $LOAD_PATH.include?(examples)
3
+
4
+ # This file contains a suite of tests for the IPv6Address grammar found in
5
+ # ipv6address.citrus.
6
+
7
+ require 'citrus'
8
+ Citrus.require 'ipv6address'
9
+ require 'test/unit'
10
+
11
+ class IPv6AddressTest < Test::Unit::TestCase
12
+ def test_hexdig
13
+ match = IPv6Address.parse('0', :root => :HEXDIG)
14
+ assert(match)
15
+
16
+ match = IPv6Address.parse('A', :root => :HEXDIG)
17
+ assert(match)
18
+ end
19
+
20
+ def test_1
21
+ match = IPv6Address.parse('1:2:3:4:5:6:7:8')
22
+ assert(match)
23
+ assert_equal(6, match.version)
24
+ end
25
+
26
+ def test_2
27
+ match = IPv6Address.parse('12AD:34FC:A453:1922::')
28
+ assert(match)
29
+ assert_equal(6, match.version)
30
+ end
31
+
32
+ def test_3
33
+ match = IPv6Address.parse('12AD::34FC')
34
+ assert(match)
35
+ assert_equal(6, match.version)
36
+ end
37
+
38
+ def test_4
39
+ match = IPv6Address.parse('12AD::')
40
+ assert(match)
41
+ assert_equal(6, match.version)
42
+ end
43
+
44
+ def test_5
45
+ match = IPv6Address.parse('::')
46
+ assert(match)
47
+ assert_equal(6, match.version)
48
+ end
49
+
50
+ def test_invalid
51
+ assert_raise Citrus::ParseError do
52
+ IPv6Address.parse('1:2')
53
+ end
54
+ end
55
+ end
@@ -1,4 +1,8 @@
1
+ # encoding: UTF-8
2
+
1
3
  require 'strscan'
4
+ require 'pathname'
5
+ require 'citrus/version'
2
6
 
3
7
  # Citrus is a compact and powerful parsing library for Ruby that combines the
4
8
  # elegance and expressiveness of the language with the simplicity and power of
@@ -8,23 +12,29 @@ require 'strscan'
8
12
  module Citrus
9
13
  autoload :File, 'citrus/file'
10
14
 
11
- # The current version of Citrus as [major, minor, patch].
12
- VERSION = [2, 3, 2]
13
-
14
15
  # A pattern to match any character, including newline.
15
- DOT = /./m
16
+ DOT = /./mu
16
17
 
17
18
  Infinity = 1.0 / 0
18
19
 
19
20
  CLOSE = -1
20
21
 
21
- # Returns the current version of Citrus as a string.
22
- def self.version
23
- VERSION.join('.')
22
+ @cache = {}
23
+
24
+ # Returns a map of paths of files that have been loaded via #load to the
25
+ # result of #eval on the code in that file.
26
+ #
27
+ # Note: These paths are not absolute unless you pass an absolute path to
28
+ # #load. That means that if you change the working directory and try to
29
+ # #require the same file with a different relative path, it will be loaded
30
+ # twice.
31
+ def self.cache
32
+ @cache
24
33
  end
25
34
 
26
- # Evaluates the given Citrus parsing expression grammar +code+ in the global
27
- # scope. Returns an array of any grammar modules that are created.
35
+ # Evaluates the given Citrus parsing expression grammar +code+ and returns an
36
+ # array of any grammar modules that are created. Accepts the same +options+ as
37
+ # GrammarMethods#parse.
28
38
  #
29
39
  # Citrus.eval(<<CITRUS)
30
40
  # grammar MyGrammar
@@ -40,27 +50,73 @@ module Citrus
40
50
  end
41
51
 
42
52
  # Evaluates the given expression and creates a new Rule object from it.
53
+ # Accepts the same +options+ as #eval.
43
54
  #
44
55
  # Citrus.rule('"a" | "b"')
45
56
  # # => #<Citrus::Rule: ... >
46
57
  #
47
58
  def self.rule(expr, options={})
48
- File.parse(expr, options.merge(:root => :rule_body)).value
59
+ eval(expr, options.merge(:root => :expression))
49
60
  end
50
61
 
51
- # Loads the grammar from the given +file+ into the global scope using #eval.
62
+ # Loads the grammar(s) from the given +file+. Accepts the same +options+ as
63
+ # #eval, plus the following:
64
+ #
65
+ # force:: Normally this method will not reload a file that is already in
66
+ # the #cache. However, if this option is +true+ the file will be
67
+ # loaded, regardless of whether or not it is in the cache. Defaults
68
+ # to +false+.
52
69
  #
53
70
  # Citrus.load('mygrammar')
54
71
  # # => [MyGrammar]
55
72
  #
56
73
  def self.load(file, options={})
57
- file << '.citrus' unless ::File.file?(file)
58
- raise ArgumentError, "Cannot find file #{file}" unless ::File.file?(file)
59
- raise ArgumentError, "Cannot read file #{file}" unless ::File.readable?(file)
60
- eval(::File.read(file), options)
74
+ file += '.citrus' unless file =~ /\.citrus$/
75
+ force = options.delete(:force)
76
+
77
+ if force || !@cache[file]
78
+ raise LoadError, "Cannot find file #{file}" unless ::File.file?(file)
79
+ raise LoadError, "Cannot read file #{file}" unless ::File.readable?(file)
80
+
81
+ begin
82
+ @cache[file] = eval(::File.read(file), options)
83
+ rescue SyntaxError => e
84
+ e.message.replace("#{::File.expand_path(file)}: #{e.message}")
85
+ raise e
86
+ end
87
+ end
88
+
89
+ @cache[file]
90
+ end
91
+
92
+ # Searches the <tt>$LOAD_PATH</tt> for a +file+ with the .citrus suffix and
93
+ # attempts to load it via #load. Returns the path to the file that was loaded
94
+ # on success, +nil+ on failure. Accepts the same +options+ as #load.
95
+ #
96
+ # path = Citrus.require('mygrammar')
97
+ # # => "/path/to/mygrammar.citrus"
98
+ # Citrus.cache[path]
99
+ # # => [MyGrammar]
100
+ #
101
+ def self.require(file, options={})
102
+ file += '.citrus' unless file =~ /\.citrus$/
103
+ found = nil
104
+
105
+ (Pathname.new(file).absolute? ? [''] : $LOAD_PATH).each do |dir|
106
+ found = Dir[::File.join(dir, file)].first
107
+ break if found
108
+ end
109
+
110
+ if found
111
+ Citrus.load(found, options)
112
+ else
113
+ raise LoadError, "Cannot find file #{file}"
114
+ end
115
+
116
+ found
61
117
  end
62
118
 
63
- # A standard error class that all Citrus errors extend.
119
+ # A base class for all Citrus errors.
64
120
  class Error < RuntimeError; end
65
121
 
66
122
  # Raised when a parse fails.
@@ -71,7 +127,11 @@ module Citrus
71
127
  @line_offset = input.line_offset(offset)
72
128
  @line_number = input.line_number(offset)
73
129
  @line = input.line(offset)
74
- super("Failed to parse input on line #{line_number} at offset #{line_offset}\n#{detail}")
130
+
131
+ message = "Failed to parse input on line #{line_number}"
132
+ message << " at offset #{line_offset}\n#{detail}"
133
+
134
+ super(message)
75
135
  end
76
136
 
77
137
  # The 0-based offset at which the error occurred in the input, i.e. the
@@ -96,6 +156,20 @@ module Citrus
96
156
  end
97
157
  end
98
158
 
159
+ # Raised when Citrus.load fails to load a file.
160
+ class LoadError < Error; end
161
+
162
+ # Raised when Citrus::File.parse fails.
163
+ class SyntaxError < Error
164
+ # The +error+ given here is an instance of Citrus::ParseError.
165
+ def initialize(error)
166
+ message = "Malformed Citrus syntax on line #{error.line_number}"
167
+ message << " at offset #{error.line_offset}\n#{error.detail}"
168
+
169
+ super(message)
170
+ end
171
+ end
172
+
99
173
  # An Input is a scanner that is responsible for executing rules at different
100
174
  # positions in the input string and persisting event streams.
101
175
  class Input < StringScanner
@@ -172,12 +246,11 @@ module Citrus
172
246
  index = events.size
173
247
 
174
248
  if apply_rule(rule, position, events).size > index
175
- position += events[-1]
176
- @max_offset = position if position > @max_offset
249
+ @max_offset = pos if pos > @max_offset
250
+ else
251
+ self.pos = position
177
252
  end
178
253
 
179
- self.pos = position
180
-
181
254
  events
182
255
  end
183
256
 
@@ -260,7 +333,7 @@ module Citrus
260
333
  # created with this method may be assigned a name by being assigned to some
261
334
  # constant, e.g.:
262
335
  #
263
- # Calc = Citrus::Grammar.new {}
336
+ # MyGrammar = Citrus::Grammar.new {}
264
337
  #
265
338
  def self.new(&block)
266
339
  mod = Module.new { include Grammar }
@@ -284,9 +357,11 @@ module Citrus
284
357
  super
285
358
  end
286
359
 
287
- # Parses the given +string+ using this grammar's root rule. Optionally, the
288
- # name of a different rule may be given here as the value of the +:root+
289
- # option. Otherwise, all options are the same as in Rule#parse.
360
+ # Parses the given +string+ using this grammar's root rule. Accepts the same
361
+ # +options+ as Rule#parse, plus the following:
362
+ #
363
+ # root:: The name of the root rule to start parsing at. Defaults to this
364
+ # grammar's #root.
290
365
  def parse(string, options={})
291
366
  rule_name = options.delete(:root) || root
292
367
  raise Error, "No root rule specified" unless rule_name
@@ -307,8 +382,7 @@ module Citrus
307
382
  end
308
383
 
309
384
  # Returns an array of all names of rules in this grammar as symbols ordered
310
- # in the same way they were defined (i.e. rules that were defined later
311
- # appear later in the array).
385
+ # in the same way they were declared.
312
386
  def rule_names
313
387
  @rule_names ||= []
314
388
  end
@@ -370,7 +444,6 @@ module Citrus
370
444
 
371
445
  rules[sym] || super_rule(sym)
372
446
  rescue => e
373
- # This preserves the backtrace.
374
447
  e.message.replace("Cannot create rule \"#{name}\": #{e.message}")
375
448
  raise e
376
449
  end
@@ -447,7 +520,7 @@ module Citrus
447
520
  ext(Choice.new(args), block)
448
521
  end
449
522
 
450
- # Adds +label+ to the given +rule+.A block may be provided to specify
523
+ # Adds +label+ to the given +rule+. A block may be provided to specify
451
524
  # semantic behavior (via #ext).
452
525
  def label(rule, label, &block)
453
526
  rule = ext(rule, block)
@@ -491,7 +564,7 @@ module Citrus
491
564
  end
492
565
  end
493
566
 
494
- # The grammar this rule belongs to.
567
+ # The grammar this rule belongs to, if any.
495
568
  attr_accessor :grammar
496
569
 
497
570
  # Sets the name of this rule.
@@ -528,7 +601,7 @@ module Citrus
528
601
  # The module this rule uses to extend new matches.
529
602
  attr_reader :extension
530
603
 
531
- # The default set of options to use when calling #parse or #test.
604
+ # The default set of options to use when calling #parse.
532
605
  def default_options # :nodoc:
533
606
  { :consume => true,
534
607
  :memoize => false,
@@ -549,19 +622,14 @@ module Citrus
549
622
  def parse(string, options={})
550
623
  opts = default_options.merge(options)
551
624
 
552
- input = if opts[:memoize]
553
- MemoizedInput.new(string)
554
- else
555
- Input.new(string)
556
- end
557
-
625
+ input = (opts[:memoize] ? MemoizedInput : Input).new(string)
558
626
  input.pos = opts[:offset] if opts[:offset] > 0
559
627
 
560
628
  events = input.exec(self)
561
629
  length = events[-1]
562
630
 
563
631
  if !length || (opts[:consume] && length < (string.length - opts[:offset]))
564
- raise ParseError.new(input)
632
+ raise ParseError, input
565
633
  end
566
634
 
567
635
  Match.new(string.slice(opts[:offset], length), events)
@@ -623,8 +691,6 @@ module Citrus
623
691
  end
624
692
  end
625
693
 
626
- alias_method :eql?, :==
627
-
628
694
  def inspect # :nodoc:
629
695
  to_s
630
696
  end
@@ -636,8 +702,8 @@ module Citrus
636
702
 
637
703
  # A Proxy is a Rule that is a placeholder for another rule. It stores the
638
704
  # name of some other rule in the grammar internally and resolves it to the
639
- # actual Rule object at runtime. This lazy evaluation permits us to create
640
- # Proxy objects for rules that we may not know the definition of yet.
705
+ # actual Rule object at runtime. This lazy evaluation permits creation of
706
+ # Proxy objects for rules that may not yet be defined.
641
707
  module Proxy
642
708
  include Rule
643
709
 
@@ -707,8 +773,7 @@ module Citrus
707
773
  rule = grammar.rule(rule_name)
708
774
 
709
775
  unless rule
710
- raise RuntimeError,
711
- "No rule named \"#{rule_name}\" in grammar #{grammar.name}"
776
+ raise Error, "No rule named \"#{rule_name}\" in grammar #{grammar}"
712
777
  end
713
778
 
714
779
  rule
@@ -738,8 +803,8 @@ module Citrus
738
803
  rule = grammar.super_rule(rule_name)
739
804
 
740
805
  unless rule
741
- raise RuntimeError,
742
- "No rule named \"#{rule_name}\" in hierarchy of grammar #{grammar.name}"
806
+ raise Error,
807
+ "No rule named \"#{rule_name}\" in hierarchy of grammar #{grammar}"
743
808
  end
744
809
 
745
810
  rule
@@ -773,12 +838,12 @@ module Citrus
773
838
 
774
839
  # Returns an array of events for this rule on the given +input+.
775
840
  def exec(input, events=[])
776
- length = input.scan_full(@regexp, false, false)
841
+ match = input.scan(@regexp)
777
842
 
778
- if length
843
+ if match
779
844
  events << self
780
845
  events << CLOSE
781
- events << length
846
+ events << match.length
782
847
  end
783
848
 
784
849
  events
@@ -1011,7 +1076,8 @@ module Citrus
1011
1076
  def initialize(rule='', min=1, max=Infinity)
1012
1077
  raise ArgumentError, "Min cannot be greater than max" if min > max
1013
1078
  super([rule])
1014
- @range = Range.new(min, max)
1079
+ @min = min
1080
+ @max = max
1015
1081
  end
1016
1082
 
1017
1083
  # Returns the Rule object this rule uses to match.
@@ -1026,9 +1092,8 @@ module Citrus
1026
1092
  index = events.size
1027
1093
  start = index - 1
1028
1094
  length = n = 0
1029
- m = max
1030
1095
 
1031
- while n < m && input.exec(rule, events).size > index
1096
+ while n < max && input.exec(rule, events).size > index
1032
1097
  length += events[-1]
1033
1098
  index = events.size
1034
1099
  n += 1
@@ -1045,14 +1110,10 @@ module Citrus
1045
1110
  end
1046
1111
 
1047
1112
  # The minimum number of times this rule must match.
1048
- def min
1049
- @range.begin
1050
- end
1113
+ attr_reader :min
1051
1114
 
1052
1115
  # The maximum number of times this rule may match.
1053
- def max
1054
- @range.end
1055
- end
1116
+ attr_reader :max
1056
1117
 
1057
1118
  # Returns the operator this rule uses as a string. Will be one of
1058
1119
  # <tt>+</tt>, <tt>?</tt>, or <tt>N*M</tt>.
@@ -1170,7 +1231,7 @@ module Citrus
1170
1231
 
1171
1232
  while events[0].elide?
1172
1233
  elisions.unshift(events.shift)
1173
- events = events.slice(0, events.length - 2)
1234
+ events.slice!(-2, events.length)
1174
1235
  end
1175
1236
 
1176
1237
  events[0].extend_match(self)
@@ -1178,6 +1239,9 @@ module Citrus
1178
1239
  elisions.each do |rule|
1179
1240
  rule.extend_match(self)
1180
1241
  end
1242
+ else
1243
+ # Create a default stream of events for the given string.
1244
+ events = [Rule.for(string), CLOSE, string.length]
1181
1245
  end
1182
1246
 
1183
1247
  @events = events
@@ -1194,114 +1258,28 @@ module Citrus
1194
1258
  # Returns a hash of capture names to arrays of matches with that name,
1195
1259
  # in the order they appeared in the input.
1196
1260
  def captures
1197
- @captures ||= begin
1198
- captures = {}
1199
- stack = []
1200
- offset = 0
1201
- close = false
1202
- index = 0
1203
- last_length = nil
1204
- in_proxy = false
1205
- count = 0
1206
-
1207
- while index < @events.size
1208
- event = @events[index]
1209
-
1210
- if close
1211
- start = stack.pop
1212
-
1213
- if Rule === start
1214
- rule = start
1215
- os = stack.pop
1216
- start = stack.pop
1217
-
1218
- match = Match.new(@string.slice(os, event), @events[start..index])
1219
-
1220
- # We can lookup immediate submatches by their index.
1221
- if stack.size == 1
1222
- captures[count] = match
1223
- count += 1
1224
- end
1225
-
1226
- # We can lookup matches that were created by proxy by the name of
1227
- # the rule they are proxy for.
1228
- if Proxy === rule
1229
- if captures[rule.rule_name]
1230
- captures[rule.rule_name] << match
1231
- else
1232
- captures[rule.rule_name] = [match]
1233
- end
1234
- end
1235
-
1236
- # We can lookup matches that were created by rules with labels by
1237
- # that label.
1238
- if rule.label
1239
- if captures[rule.label]
1240
- captures[rule.label] << match
1241
- else
1242
- captures[rule.label] = [match]
1243
- end
1244
- end
1245
-
1246
- in_proxy = false
1247
- end
1248
-
1249
- unless last_length
1250
- last_length = event
1251
- end
1252
-
1253
- close = false
1254
- elsif event == CLOSE
1255
- close = true
1256
- else
1257
- stack << index
1258
-
1259
- # We can calculate the offset of this rule event by adding back the
1260
- # last match length.
1261
- if last_length
1262
- offset += last_length
1263
- last_length = nil
1264
- end
1265
-
1266
- # We should not create captures when traversing the portion of the
1267
- # event stream that is masked by a proxy in the original rule
1268
- # definition.
1269
- unless in_proxy || stack.size == 1
1270
- stack << offset
1271
- stack << event
1272
- in_proxy = true if Proxy === event
1273
- end
1274
- end
1275
-
1276
- index += 1
1277
- end
1278
-
1279
- captures
1280
- end
1261
+ process_events! unless @captures
1262
+ @captures
1281
1263
  end
1282
1264
 
1283
1265
  # Returns an array of all immediate submatches of this match.
1284
1266
  def matches
1285
- @matches ||= (0...captures.size).map {|n| captures[n] }.compact
1267
+ process_events! unless @matches
1268
+ @matches
1286
1269
  end
1287
1270
 
1288
1271
  # A shortcut for retrieving the first immediate submatch of this match.
1289
1272
  def first
1290
- captures[0]
1273
+ matches.first
1291
1274
  end
1292
1275
 
1293
- # The default value for a match is its string value. This method is
1294
- # overridden in most cases to be more meaningful according to the desired
1295
- # interpretation.
1296
- alias_method :value, :to_s
1297
-
1298
1276
  # Allows methods of this match's string to be called directly and provides
1299
1277
  # a convenient interface for retrieving the first match with a given name.
1300
1278
  def method_missing(sym, *args, &block)
1301
1279
  if @string.respond_to?(sym)
1302
1280
  @string.__send__(sym, *args, &block)
1303
1281
  else
1304
- captures[sym].first if captures[sym]
1282
+ captures[sym].first
1305
1283
  end
1306
1284
  end
1307
1285
 
@@ -1311,6 +1289,32 @@ module Citrus
1311
1289
 
1312
1290
  alias_method :to_str, :to_s
1313
1291
 
1292
+ # The default value for a match is its string value. This method is
1293
+ # overridden in most cases to be more meaningful according to the desired
1294
+ # interpretation.
1295
+ alias_method :value, :to_s
1296
+
1297
+ # Returns this match plus all sub #matches in an array.
1298
+ def to_a
1299
+ [captures[0]] + matches
1300
+ end
1301
+
1302
+ alias_method :to_ary, :to_a
1303
+
1304
+ # Returns the capture at the given +key+. If it is an Integer (and an
1305
+ # optional length) or a Range, the result of #to_a with the same arguments
1306
+ # is returned. Otherwise, the value at +key+ in #captures is returned.
1307
+ def [](key, *args)
1308
+ case key
1309
+ when Integer, Range
1310
+ to_a[key, *args]
1311
+ else
1312
+ captures[key]
1313
+ end
1314
+ end
1315
+
1316
+ alias_method :fetch, :[]
1317
+
1314
1318
  def ==(other)
1315
1319
  case other
1316
1320
  when String
@@ -1322,8 +1326,6 @@ module Citrus
1322
1326
  end
1323
1327
  end
1324
1328
 
1325
- alias_method :eql?, :==
1326
-
1327
1329
  def inspect
1328
1330
  @string.inspect
1329
1331
  end
@@ -1350,9 +1352,7 @@ module Citrus
1350
1352
  string = @string.slice(os, event)
1351
1353
  lines[start] = "#{space}#{string.inspect} rule=#{rule}, offset=#{os}, length=#{event}"
1352
1354
 
1353
- unless last_length
1354
- last_length = event
1355
- end
1355
+ last_length = event unless last_length
1356
1356
 
1357
1357
  close = false
1358
1358
  elsif event == CLOSE
@@ -1373,11 +1373,121 @@ module Citrus
1373
1373
 
1374
1374
  puts lines.compact.join("\n")
1375
1375
  end
1376
+
1377
+ private
1378
+
1379
+ # Initializes both the @captures and @matches instance variables.
1380
+ def process_events!
1381
+ @captures = captures_hash
1382
+ @matches = []
1383
+
1384
+ capture!(@events[0], self)
1385
+
1386
+ stack = []
1387
+ offset = 0
1388
+ close = false
1389
+ index = 0
1390
+ last_length = nil
1391
+ capture = true
1392
+
1393
+ while index < @events.size
1394
+ event = @events[index]
1395
+
1396
+ if close
1397
+ start = stack.pop
1398
+
1399
+ if Rule === start
1400
+ rule = start
1401
+ os = stack.pop
1402
+ start = stack.pop
1403
+
1404
+ match = Match.new(@string.slice(os, event), @events[start..index])
1405
+ capture!(rule, match)
1406
+
1407
+ @matches << match if stack.size == 1
1408
+
1409
+ capture = true
1410
+ end
1411
+
1412
+ last_length = event unless last_length
1413
+
1414
+ close = false
1415
+ elsif event == CLOSE
1416
+ close = true
1417
+ else
1418
+ stack << index
1419
+
1420
+ # We can calculate the offset of this rule event by adding back the
1421
+ # last match length.
1422
+ if last_length
1423
+ offset += last_length
1424
+ last_length = nil
1425
+ end
1426
+
1427
+ if capture && stack.size != 1
1428
+ stack << offset
1429
+ stack << event
1430
+
1431
+ # We should not create captures when traversing a portion of the
1432
+ # event stream that is masked by a proxy in the original rule
1433
+ # definition.
1434
+ capture = false if Proxy === event
1435
+ end
1436
+ end
1437
+
1438
+ index += 1
1439
+ end
1440
+
1441
+ # Add numeric indices to @captures.
1442
+ @captures[0] = self
1443
+
1444
+ @matches.each_with_index do |match, index|
1445
+ @captures[index + 1] = match
1446
+ end
1447
+ end
1448
+
1449
+ def capture!(rule, match)
1450
+ # We can lookup matches that were created by proxy by the name of
1451
+ # the rule they are proxy for.
1452
+ if Proxy === rule
1453
+ if @captures.key?(rule.rule_name)
1454
+ @captures[rule.rule_name] << match
1455
+ else
1456
+ @captures[rule.rule_name] = [match]
1457
+ end
1458
+ end
1459
+
1460
+ # We can lookup matches that were created by rules with labels by
1461
+ # that label.
1462
+ if rule.label
1463
+ if @captures.key?(rule.label)
1464
+ @captures[rule.label] << match
1465
+ else
1466
+ @captures[rule.label] = [match]
1467
+ end
1468
+ end
1469
+ end
1470
+
1471
+ # Returns a new Hash that is to be used for @captures. This hash normalizes
1472
+ # String keys to Symbols, returns +nil+ for unknown Numeric keys, and an
1473
+ # empty Array for all other unknown keys.
1474
+ def captures_hash
1475
+ Hash.new do |hash, key|
1476
+ case key
1477
+ when String
1478
+ hash[key.to_sym]
1479
+ when Numeric
1480
+ nil
1481
+ else
1482
+ []
1483
+ end
1484
+ end
1485
+ end
1376
1486
  end
1377
1487
  end
1378
1488
 
1379
1489
  class Object
1380
- # A sugar method for creating grammars.
1490
+ # A sugar method for creating Citrus grammars from any namespace.
1381
1491
  #
1382
1492
  # grammar :Calc do
1383
1493
  # end