citrus 2.2.2 → 2.3.0
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 +160 -98
- data/doc/background.markdown +16 -17
- data/doc/example.markdown +86 -46
- data/doc/syntax.markdown +59 -36
- data/examples/calc.citrus +9 -3
- data/examples/calc.rb +9 -3
- data/examples/ip.rb +2 -0
- data/lib/citrus.rb +576 -473
- data/lib/citrus/file.rb +80 -71
- data/test/alias_test.rb +8 -2
- data/test/and_predicate_test.rb +13 -2
- data/test/but_predicate_test.rb +9 -3
- data/test/calc_file_test.rb +9 -4
- data/test/calc_test.rb +4 -4
- data/test/choice_test.rb +11 -5
- data/test/extension_test.rb +2 -12
- data/test/file_test.rb +215 -175
- data/test/grammar_test.rb +1 -1
- data/test/helper.rb +2 -2
- data/test/input_test.rb +44 -48
- data/test/label_test.rb +14 -17
- data/test/match_test.rb +21 -63
- data/test/not_predicate_test.rb +13 -2
- data/test/repeat_test.rb +20 -20
- data/test/sequence_test.rb +22 -8
- data/test/string_terminal_test.rb +10 -5
- data/test/super_test.rb +19 -16
- data/test/terminal_test.rb +7 -2
- metadata +21 -12
- data/benchmark/after.dat +0 -192
- data/benchmark/before.dat +0 -192
- data/test/_files/grammar3.citrus +0 -112
data/lib/citrus/file.rb
CHANGED
@@ -28,34 +28,36 @@ module Citrus
|
|
28
28
|
|
29
29
|
rule :file do
|
30
30
|
all(:space, zero_or_more(any(:require, :grammar))) {
|
31
|
-
|
32
|
-
|
31
|
+
if captures[:require]
|
32
|
+
captures[:require].each {|r| require r.value }
|
33
|
+
end
|
34
|
+
|
35
|
+
(captures[:grammar] || []).map {|g| g.value }
|
33
36
|
}
|
34
37
|
end
|
35
38
|
|
36
39
|
rule :grammar do
|
37
|
-
all(:grammar_keyword, :module_name, :grammar_body, :end_keyword)
|
40
|
+
mod all(:grammar_keyword, :module_name, :grammar_body, :end_keyword) do
|
38
41
|
include ModuleNameHelpers
|
39
42
|
|
40
43
|
def value
|
41
44
|
module_namespace.const_set(module_basename, grammar_body.value)
|
42
45
|
end
|
43
|
-
|
46
|
+
end
|
44
47
|
end
|
45
48
|
|
46
49
|
rule :grammar_body do
|
47
50
|
zero_or_more(any(:include, :root, :rule)) {
|
48
51
|
grammar = Grammar.new
|
49
52
|
|
50
|
-
|
51
|
-
grammar.include(inc.value)
|
53
|
+
if captures[:include]
|
54
|
+
captures[:include].each {|inc| grammar.include(inc.value) }
|
52
55
|
end
|
53
56
|
|
54
|
-
root = find(:root).last
|
55
57
|
grammar.root(root.value) if root
|
56
58
|
|
57
|
-
|
58
|
-
grammar.rule(r.rule_name.value, r.value)
|
59
|
+
if captures[:rule]
|
60
|
+
captures[:rule].each {|r| grammar.rule(r.rule_name.value, r.value) }
|
59
61
|
end
|
60
62
|
|
61
63
|
grammar
|
@@ -71,42 +73,45 @@ module Citrus
|
|
71
73
|
rule :rule_body do
|
72
74
|
zero_or_one(:choice) {
|
73
75
|
# An empty rule definition matches the empty string.
|
74
|
-
|
76
|
+
choice ? choice.value : Rule.for('')
|
75
77
|
}
|
76
78
|
end
|
77
79
|
|
78
80
|
rule :choice do
|
79
|
-
all(:sequence, zero_or_more([ :bar, :sequence ]))
|
81
|
+
mod all(:sequence, zero_or_more([ :bar, :sequence ])) do
|
80
82
|
def rules
|
81
|
-
@rules ||=
|
82
|
-
[ sequence.value ] + matches[1].matches.map do |m|
|
83
|
-
m.matches[1].value
|
84
|
-
end
|
85
|
-
end
|
83
|
+
@rules ||= captures[:sequence].map {|s| s.value }
|
86
84
|
end
|
87
85
|
|
88
86
|
def value
|
89
87
|
rules.length > 1 ? Choice.new(rules) : rules.first
|
90
88
|
end
|
91
|
-
|
89
|
+
end
|
92
90
|
end
|
93
91
|
|
94
92
|
rule :sequence do
|
95
|
-
one_or_more(:
|
93
|
+
mod one_or_more(:label_expression) do
|
96
94
|
def rules
|
97
|
-
@rules ||=
|
95
|
+
@rules ||= captures[:label_expression].map {|e| e.value }
|
98
96
|
end
|
99
97
|
|
100
98
|
def value
|
101
99
|
rules.length > 1 ? Sequence.new(rules) : rules.first
|
102
100
|
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
rule :label_expression do
|
105
|
+
all(zero_or_one(:label), :expression) {
|
106
|
+
rule = expression.value
|
107
|
+
rule.label = label.value if label
|
108
|
+
rule
|
103
109
|
}
|
104
110
|
end
|
105
111
|
|
106
112
|
rule :expression do
|
107
113
|
all(:prefix, zero_or_one(:extension)) {
|
108
114
|
rule = prefix.value
|
109
|
-
extension = matches[1].first
|
110
115
|
rule.extension = extension.value if extension
|
111
116
|
rule
|
112
117
|
}
|
@@ -115,7 +120,6 @@ module Citrus
|
|
115
120
|
rule :prefix do
|
116
121
|
all(zero_or_one(:predicate), :suffix) {
|
117
122
|
rule = suffix.value
|
118
|
-
predicate = matches[0].first
|
119
123
|
rule = predicate.value(rule) if predicate
|
120
124
|
rule
|
121
125
|
}
|
@@ -124,44 +128,51 @@ module Citrus
|
|
124
128
|
rule :suffix do
|
125
129
|
all(:primary, zero_or_one(:repeat)) {
|
126
130
|
rule = primary.value
|
127
|
-
repeat = matches[1].first
|
128
131
|
rule = repeat.value(rule) if repeat
|
129
132
|
rule
|
130
133
|
}
|
131
134
|
end
|
132
135
|
|
133
136
|
rule :primary do
|
134
|
-
any(:grouping, :proxy, :
|
137
|
+
any(:grouping, :proxy, :terminal)
|
135
138
|
end
|
136
139
|
|
137
140
|
rule :grouping do
|
138
|
-
all(:lparen, :rule_body, :rparen) {
|
141
|
+
all(:lparen, :rule_body, :rparen) {
|
142
|
+
rule_body.value
|
143
|
+
}
|
139
144
|
end
|
140
145
|
|
141
146
|
## Lexical syntax
|
142
147
|
|
143
148
|
rule :require do
|
144
|
-
all(:require_keyword, :quoted_string) {
|
149
|
+
all(:require_keyword, :quoted_string) {
|
150
|
+
quoted_string.value
|
151
|
+
}
|
145
152
|
end
|
146
153
|
|
147
154
|
rule :include do
|
148
|
-
all(:include_keyword, :module_name)
|
155
|
+
mod all(:include_keyword, :module_name) do
|
149
156
|
include ModuleNameHelpers
|
150
157
|
|
151
158
|
def value
|
152
159
|
module_namespace.const_get(module_basename)
|
153
160
|
end
|
154
|
-
|
161
|
+
end
|
155
162
|
end
|
156
163
|
|
157
164
|
rule :root do
|
158
|
-
all(:root_keyword, :rule_name) {
|
165
|
+
all(:root_keyword, :rule_name) {
|
166
|
+
rule_name.value
|
167
|
+
}
|
159
168
|
end
|
160
169
|
|
161
170
|
# Rule names may contain letters, numbers, underscores, and dashes. They
|
162
171
|
# MUST start with a letter.
|
163
172
|
rule :rule_name do
|
164
|
-
all(/[a-zA-Z][a-zA-Z0-9_-]*/, :space) {
|
173
|
+
all(/[a-zA-Z][a-zA-Z0-9_-]*/, :space) {
|
174
|
+
first.to_s
|
175
|
+
}
|
165
176
|
end
|
166
177
|
|
167
178
|
rule :proxy do
|
@@ -180,6 +191,10 @@ module Citrus
|
|
180
191
|
}
|
181
192
|
end
|
182
193
|
|
194
|
+
rule :terminal do
|
195
|
+
any(:string_terminal, :regular_expression, :character_class, :dot)
|
196
|
+
end
|
197
|
+
|
183
198
|
rule :string_terminal do
|
184
199
|
any(:quoted_string, :case_insensitive_string) {
|
185
200
|
StringTerminal.new(super(), flags)
|
@@ -187,50 +202,50 @@ module Citrus
|
|
187
202
|
end
|
188
203
|
|
189
204
|
rule :quoted_string do
|
190
|
-
all(/(["'])(?:\\?.)*?\1/, :space)
|
205
|
+
mod all(/(["'])(?:\\?.)*?\1/, :space) do
|
191
206
|
def value
|
192
|
-
eval(first)
|
207
|
+
eval(first.to_s)
|
193
208
|
end
|
194
209
|
|
195
210
|
def flags
|
196
211
|
0
|
197
212
|
end
|
198
|
-
|
213
|
+
end
|
199
214
|
end
|
200
215
|
|
201
216
|
rule :case_insensitive_string do
|
202
|
-
all(/`(?:\\?.)*?`/, :space)
|
217
|
+
mod all(/`(?:\\?.)*?`/, :space) do
|
203
218
|
def value
|
204
|
-
eval(first.gsub(/^`|`$/, '"'))
|
219
|
+
eval(first.to_s.gsub(/^`|`$/, '"'))
|
205
220
|
end
|
206
221
|
|
207
222
|
def flags
|
208
223
|
Regexp::IGNORECASE
|
209
224
|
end
|
210
|
-
|
211
|
-
end
|
212
|
-
|
213
|
-
rule :terminal do
|
214
|
-
any(:regular_expression, :character_class, :dot) {
|
215
|
-
Terminal.new(super())
|
216
|
-
}
|
225
|
+
end
|
217
226
|
end
|
218
227
|
|
219
228
|
rule :regular_expression do
|
220
229
|
all(/\/(?:\\?.)*?\/[imxouesn]*/, :space) {
|
221
|
-
eval(first)
|
230
|
+
Terminal.new(eval(first.to_s))
|
222
231
|
}
|
223
232
|
end
|
224
233
|
|
225
234
|
rule :character_class do
|
226
235
|
all(/\[(?:\\?.)*?\]/, :space) {
|
227
|
-
Regexp.new(
|
236
|
+
Terminal.new(Regexp.new(first.to_s, nil, 'n'))
|
228
237
|
}
|
229
238
|
end
|
230
239
|
|
231
240
|
rule :dot do
|
232
241
|
all('.', :space) {
|
233
|
-
DOT
|
242
|
+
Terminal.new(DOT)
|
243
|
+
}
|
244
|
+
end
|
245
|
+
|
246
|
+
rule :label do
|
247
|
+
all(/[a-zA-Z0-9_]+/, :space, ':', :space) {
|
248
|
+
first.to_sym
|
234
249
|
}
|
235
250
|
end
|
236
251
|
|
@@ -239,23 +254,31 @@ module Citrus
|
|
239
254
|
end
|
240
255
|
|
241
256
|
rule :tag do
|
242
|
-
all(:lt, :module_name, :gt)
|
257
|
+
mod all(:lt, :module_name, :gt) do
|
243
258
|
include ModuleNameHelpers
|
244
259
|
|
245
260
|
def value
|
246
261
|
module_namespace.const_get(module_basename)
|
247
262
|
end
|
248
|
-
|
263
|
+
end
|
249
264
|
end
|
250
265
|
|
251
266
|
rule :block do
|
252
267
|
all(:lcurly, zero_or_more(any(:block, /[^{}]+/)), :rcurly) {
|
253
|
-
eval(
|
268
|
+
proc = eval("Proc.new #{to_s}", TOPLEVEL_BINDING)
|
269
|
+
|
270
|
+
# Attempt to detect if this is a module block using some
|
271
|
+
# extremely simple heuristics.
|
272
|
+
if to_s =~ /\b(def|include) /
|
273
|
+
Module.new(&proc)
|
274
|
+
else
|
275
|
+
proc
|
276
|
+
end
|
254
277
|
}
|
255
278
|
end
|
256
279
|
|
257
280
|
rule :predicate do
|
258
|
-
any(:and, :not, :but
|
281
|
+
any(:and, :not, :but)
|
259
282
|
end
|
260
283
|
|
261
284
|
rule :and do
|
@@ -276,41 +299,27 @@ module Citrus
|
|
276
299
|
}
|
277
300
|
end
|
278
301
|
|
279
|
-
rule :label do
|
280
|
-
all(/[a-zA-Z0-9_]+/, :space, ':', :space) { |rule|
|
281
|
-
Label.new(rule, first.to_s)
|
282
|
-
}
|
283
|
-
end
|
284
|
-
|
285
302
|
rule :repeat do
|
286
|
-
any(:question, :plus, :star)
|
287
|
-
Repeat.new(rule, min, max)
|
288
|
-
}
|
303
|
+
any(:question, :plus, :star)
|
289
304
|
end
|
290
305
|
|
291
306
|
rule :question do
|
292
|
-
all('?', :space) {
|
293
|
-
|
294
|
-
def max; 1 end
|
307
|
+
all('?', :space) { |rule|
|
308
|
+
Repeat.new(rule, 0, 1)
|
295
309
|
}
|
296
310
|
end
|
297
311
|
|
298
312
|
rule :plus do
|
299
|
-
all('+', :space) {
|
300
|
-
|
301
|
-
def max; Infinity end
|
313
|
+
all('+', :space) { |rule|
|
314
|
+
Repeat.new(rule, 1, Infinity)
|
302
315
|
}
|
303
316
|
end
|
304
317
|
|
305
318
|
rule :star do
|
306
|
-
all(/[0-9]*/, '*', /[0-9]*/, :space) {
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
def max
|
312
|
-
matches[2] == '' ? Infinity : matches[2].to_i
|
313
|
-
end
|
319
|
+
all(/[0-9]*/, '*', /[0-9]*/, :space) { |rule|
|
320
|
+
min = captures[0] == '' ? 0 : captures[0].to_i
|
321
|
+
max = captures[2] == '' ? Infinity : captures[2].to_i
|
322
|
+
Repeat.new(rule, min, max)
|
314
323
|
}
|
315
324
|
end
|
316
325
|
|
data/test/alias_test.rb
CHANGED
@@ -14,7 +14,7 @@ class AliasTest < Test::Unit::TestCase
|
|
14
14
|
rule_a = grammar.rule(:a)
|
15
15
|
rule_b = grammar.rule(:b)
|
16
16
|
events = rule_a.exec(Input.new('abc'))
|
17
|
-
assert_equal([rule_a
|
17
|
+
assert_equal([rule_a, CLOSE, 3], events)
|
18
18
|
end
|
19
19
|
|
20
20
|
def test_exec_miss
|
@@ -38,11 +38,17 @@ class AliasTest < Test::Unit::TestCase
|
|
38
38
|
rule_b2 = grammar2.rule(:b)
|
39
39
|
rule_a1 = grammar1.rule(:a)
|
40
40
|
events = rule_b2.exec(Input.new('abc'))
|
41
|
-
assert_equal([rule_b2
|
41
|
+
assert_equal([rule_b2, CLOSE, 3], events)
|
42
42
|
end
|
43
43
|
|
44
44
|
def test_to_s
|
45
45
|
rule = Alias.new(:alpha)
|
46
46
|
assert_equal('alpha', rule.to_s)
|
47
47
|
end
|
48
|
+
|
49
|
+
def test_to_s
|
50
|
+
rule = Alias.new(:alpha)
|
51
|
+
rule.label = 'a_label'
|
52
|
+
assert_equal('a_label:alpha', rule.to_s)
|
53
|
+
end
|
48
54
|
end
|
data/test/and_predicate_test.rb
CHANGED
@@ -9,7 +9,7 @@ class AndPredicateTest < Test::Unit::TestCase
|
|
9
9
|
def test_exec
|
10
10
|
rule = AndPredicate.new('abc')
|
11
11
|
events = rule.exec(Input.new('abc'))
|
12
|
-
assert_equal([rule
|
12
|
+
assert_equal([rule, CLOSE, 0], events)
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_exec_miss
|
@@ -19,14 +19,25 @@ class AndPredicateTest < Test::Unit::TestCase
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_consumption
|
22
|
-
rule = AndPredicate.new('
|
22
|
+
rule = AndPredicate.new(Sequence.new(['a', 'b', 'c']))
|
23
|
+
|
23
24
|
input = Input.new('abc')
|
24
25
|
events = rule.exec(input)
|
25
26
|
assert_equal(0, input.pos)
|
27
|
+
|
28
|
+
input = Input.new('def')
|
29
|
+
events = rule.exec(input)
|
30
|
+
assert_equal(0, input.pos)
|
26
31
|
end
|
27
32
|
|
28
33
|
def test_to_s
|
29
34
|
rule = AndPredicate.new('a')
|
30
35
|
assert_equal('&"a"', rule.to_s)
|
31
36
|
end
|
37
|
+
|
38
|
+
def test_to_s_with_label
|
39
|
+
rule = AndPredicate.new('a')
|
40
|
+
rule.label = 'a_label'
|
41
|
+
assert_equal('a_label:&"a"', rule.to_s)
|
42
|
+
end
|
32
43
|
end
|
data/test/but_predicate_test.rb
CHANGED
@@ -10,10 +10,10 @@ class ButPredicateTest < Test::Unit::TestCase
|
|
10
10
|
rule = ButPredicate.new('abc')
|
11
11
|
|
12
12
|
events = rule.exec(Input.new('def'))
|
13
|
-
assert_equal([rule
|
13
|
+
assert_equal([rule, CLOSE, 3], events)
|
14
14
|
|
15
15
|
events = rule.exec(Input.new('defabc'))
|
16
|
-
assert_equal([rule
|
16
|
+
assert_equal([rule, CLOSE, 3], events)
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_exec_miss
|
@@ -23,7 +23,7 @@ class ButPredicateTest < Test::Unit::TestCase
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def test_consumption
|
26
|
-
rule = ButPredicate.new('
|
26
|
+
rule = ButPredicate.new(Sequence.new(['a', 'b', 'c']))
|
27
27
|
|
28
28
|
input = Input.new('def')
|
29
29
|
events = rule.exec(input)
|
@@ -38,4 +38,10 @@ class ButPredicateTest < Test::Unit::TestCase
|
|
38
38
|
rule = ButPredicate.new('a')
|
39
39
|
assert_equal('~"a"', rule.to_s)
|
40
40
|
end
|
41
|
+
|
42
|
+
def test_to_s_with_label
|
43
|
+
rule = ButPredicate.new('a')
|
44
|
+
rule.label = 'a_label'
|
45
|
+
assert_equal('a_label:~"a"', rule.to_s)
|
46
|
+
end
|
41
47
|
end
|
data/test/calc_file_test.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
require File.expand_path('../helper', __FILE__)
|
2
2
|
|
3
|
-
if defined?(Calc)
|
4
|
-
Object.__send__(:remove_const, :Calc)
|
5
|
-
end
|
6
|
-
|
7
3
|
Citrus.load File.expand_path('../../examples/calc', __FILE__)
|
8
4
|
|
9
5
|
class CalcFileTest < Test::Unit::TestCase
|
10
6
|
include CalcTestMethods
|
7
|
+
|
8
|
+
# It's a bit hacky, but since this test runs before calc_test.rb we can
|
9
|
+
# avoid getting the "already defined constant" error by renaming the Calc
|
10
|
+
# constant here.
|
11
|
+
CalcFile = Object.__send__(:remove_const, :Calc)
|
12
|
+
|
13
|
+
def do_test(expr)
|
14
|
+
super(expr, CalcFile)
|
15
|
+
end
|
11
16
|
end
|