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.
- data/README +30 -10
- data/Rakefile +1 -1
- data/citrus.gemspec +1 -1
- data/doc/syntax.markdown +9 -9
- data/examples/calc.citrus +2 -3
- data/examples/calc.rb +79 -72
- data/examples/ipaddress.citrus +16 -0
- data/examples/ipaddress.rb +23 -0
- data/examples/ipv4address.citrus +26 -0
- data/examples/ipv4address.rb +49 -0
- data/examples/{ip.citrus → ipv6address.citrus} +1 -40
- data/examples/ipv6address.rb +55 -0
- data/lib/citrus.rb +267 -157
- data/lib/citrus/file.rb +58 -64
- data/lib/citrus/version.rb +9 -0
- data/test/alias_test.rb +1 -1
- data/test/file_test.rb +101 -139
- data/test/helper.rb +0 -116
- data/test/match_test.rb +0 -1
- data/test/memoized_input_test.rb +1 -1
- data/test/multibyte_test.rb +57 -6
- data/test/parse_error_test.rb +4 -2
- metadata +112 -108
- data/examples/ip.rb +0 -77
- data/test/calc_file_test.rb +0 -16
- data/test/calc_test.rb +0 -11
@@ -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
|
data/lib/citrus.rb
CHANGED
@@ -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 = /./
|
16
|
+
DOT = /./mu
|
16
17
|
|
17
18
|
Infinity = 1.0 / 0
|
18
19
|
|
19
20
|
CLOSE = -1
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
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+
|
27
|
-
#
|
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
|
-
|
59
|
+
eval(expr, options.merge(:root => :expression))
|
49
60
|
end
|
50
61
|
|
51
|
-
# Loads the grammar from the given +file
|
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
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
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
|
-
|
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
|
-
|
176
|
-
|
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
|
-
#
|
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.
|
288
|
-
#
|
289
|
-
#
|
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
|
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
|
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 =
|
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
|
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
|
640
|
-
# Proxy objects for rules that
|
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
|
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
|
742
|
-
"No rule named \"#{rule_name}\" in hierarchy of grammar #{grammar
|
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
|
-
|
841
|
+
match = input.scan(@regexp)
|
777
842
|
|
778
|
-
if
|
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
|
-
@
|
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 <
|
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
|
-
|
1049
|
-
@range.begin
|
1050
|
-
end
|
1113
|
+
attr_reader :min
|
1051
1114
|
|
1052
1115
|
# The maximum number of times this rule may match.
|
1053
|
-
|
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
|
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
|
1198
|
-
|
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
|
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
|
-
|
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
|
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
|