citrus 2.0.1 → 2.1.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/README +112 -50
- data/doc/background.markdown +8 -8
- data/doc/example.markdown +11 -12
- data/doc/index.markdown +4 -4
- data/doc/links.markdown +5 -4
- data/doc/syntax.markdown +2 -2
- data/doc/testing.markdown +60 -0
- data/lib/citrus.rb +167 -133
- data/lib/citrus/debug.rb +1 -1
- data/lib/citrus/file.rb +74 -28
- data/test/alias_test.rb +1 -1
- data/test/debug_test.rb +23 -0
- data/test/file_test.rb +53 -23
- data/test/input_test.rb +22 -0
- data/test/match_test.rb +0 -16
- metadata +7 -2
data/README
CHANGED
@@ -5,8 +5,8 @@
|
|
5
5
|
Parsing Expressions for Ruby
|
6
6
|
|
7
7
|
|
8
|
-
Citrus is a compact and powerful parsing library for
|
9
|
-
[Ruby](http://ruby-lang.org/) that combines the elegance and expressiveness of
|
8
|
+
Citrus is a compact and powerful parsing library for
|
9
|
+
[Ruby](http://ruby-lang.org/) that combines the elegance and expressiveness of
|
10
10
|
the language with the simplicity and power of
|
11
11
|
[parsing expressions](http://en.wikipedia.org/wiki/Parsing_expression_grammar).
|
12
12
|
|
@@ -16,13 +16,13 @@ the language with the simplicity and power of
|
|
16
16
|
|
17
17
|
Via [RubyGems](http://rubygems.org/):
|
18
18
|
|
19
|
-
$
|
19
|
+
$ gem install citrus
|
20
20
|
|
21
21
|
From a local copy:
|
22
22
|
|
23
23
|
$ git clone git://github.com/mjijackson/citrus.git
|
24
24
|
$ cd citrus
|
25
|
-
$ rake package
|
25
|
+
$ rake package install
|
26
26
|
|
27
27
|
|
28
28
|
# Background
|
@@ -77,23 +77,23 @@ thereof.
|
|
77
77
|
A Citrus grammar is really just a souped-up Ruby
|
78
78
|
[module](http://ruby-doc.org/core/classes/Module.html). These modules may be
|
79
79
|
included in other grammar modules in the same way that Ruby modules are normally
|
80
|
-
used. This property allows you to divide a complex grammar into more manageable,
|
81
|
-
reusable pieces that may be combined at runtime. Any grammar rule with the same
|
82
|
-
name as a rule in an included grammar may access that rule with a mechanism
|
80
|
+
used. This property allows you to divide a complex grammar into more manageable,
|
81
|
+
reusable pieces that may be combined at runtime. Any grammar rule with the same
|
82
|
+
name as a rule in an included grammar may access that rule with a mechanism
|
83
83
|
similar to Ruby's super keyword.
|
84
84
|
|
85
85
|
## Matches
|
86
86
|
|
87
|
-
Matches are created by rule objects when they match on the input. A
|
88
|
-
[Match](api/classes/Citrus/Match.html) is actually a
|
89
|
-
[String](http://ruby-doc.org/core/classes/String.html) object with some extra
|
87
|
+
Matches are created by rule objects when they match on the input. A
|
88
|
+
[Match](api/classes/Citrus/Match.html) is actually a
|
89
|
+
[String](http://ruby-doc.org/core/classes/String.html) object with some extra
|
90
90
|
information attached such as the name(s) of the rule(s) from which it was
|
91
91
|
generated and any submatches it may contain.
|
92
92
|
|
93
93
|
During a parse, matches are arranged in a tree structure where any match may
|
94
94
|
contain any number of other matches. This structure is determined by the way in
|
95
|
-
which the rule that generated each match is used in the grammar. For example, a
|
96
|
-
match that is created from a non-terminal rule that contains several other
|
95
|
+
which the rule that generated each match is used in the grammar. For example, a
|
96
|
+
match that is created from a non-terminal rule that contains several other
|
97
97
|
terminals will likewise contain several matches, one for each terminal.
|
98
98
|
|
99
99
|
Match objects may be extended with semantic information in the form of methods.
|
@@ -207,28 +207,28 @@ See [Label](api/classes/Citrus/Label.html) for more information.
|
|
207
207
|
|
208
208
|
## Precedence
|
209
209
|
|
210
|
-
The following table contains a list of all Citrus
|
211
|
-
precedence. A higher precedence indicates tighter binding.
|
212
|
-
|
213
|
-
Operator
|
214
|
-
|
215
|
-
''
|
216
|
-
""
|
217
|
-
[]
|
218
|
-
.
|
219
|
-
//
|
220
|
-
()
|
221
|
-
*
|
222
|
-
+
|
223
|
-
?
|
224
|
-
&
|
225
|
-
!
|
226
|
-
~
|
227
|
-
:
|
228
|
-
<>
|
229
|
-
{}
|
230
|
-
e1 e2
|
231
|
-
e1 | e2
|
210
|
+
The following table contains a list of all Citrus symbols and operators and
|
211
|
+
their precedence. A higher precedence indicates tighter binding.
|
212
|
+
|
213
|
+
Operator | Name | Precedence
|
214
|
+
--------- | ------------------------- | ----------
|
215
|
+
'' | String (single quoted) | 6
|
216
|
+
"" | String (double quoted) | 6
|
217
|
+
[] | Character class | 6
|
218
|
+
. | Dot (any character) | 6
|
219
|
+
// | Regular expression | 6
|
220
|
+
() | Grouping | 6
|
221
|
+
* | Repetition (arbitrary) | 5
|
222
|
+
+ | Repetition (one or more) | 5
|
223
|
+
? | Repetition (zero or one) | 5
|
224
|
+
& | And predicate | 4
|
225
|
+
! | Not predicate | 4
|
226
|
+
~ | But predicate | 4
|
227
|
+
: | Label | 4
|
228
|
+
<> | Extension (module name) | 3
|
229
|
+
{} | Extension (literal) | 3
|
230
|
+
e1 e2 | Sequence | 2
|
231
|
+
e1 | e2 | Ordered choice | 1
|
232
232
|
|
233
233
|
|
234
234
|
# Example
|
@@ -272,13 +272,12 @@ and "1 + 2+3", but it does not have enough semantic information to be able to
|
|
272
272
|
actually interpret these expressions.
|
273
273
|
|
274
274
|
At this point, when the grammar parses a string it generates a tree of
|
275
|
-
[Match](api/classes/Citrus/Match.html) objects. Each match is created by a rule
|
276
|
-
|
277
|
-
submatches it contains.
|
275
|
+
[Match](api/classes/Citrus/Match.html) objects. Each match is created by a rule
|
276
|
+
and may itself be comprised of any number of submatches.
|
278
277
|
|
279
278
|
Submatches are created whenever a rule contains another rule. For example, in
|
280
|
-
the grammar above
|
281
|
-
|
279
|
+
the grammar above `number` matches a string of digits followed by white space.
|
280
|
+
Thus, a match generated by this rule will contain two submatches.
|
282
281
|
|
283
282
|
We can define methods inside a set of curly braces that will be used to extend
|
284
283
|
matches when they are created. This works in similar fashion to using Ruby's
|
@@ -352,14 +351,14 @@ Congratulations! You just ran your first piece of Citrus code.
|
|
352
351
|
|
353
352
|
One interesting thing to notice about the above sequence of commands is the
|
354
353
|
return value of [Citrus#load](api/classes/Citrus.html#M000003). When you use
|
355
|
-
`Citrus.load` to
|
356
|
-
|
357
|
-
|
358
|
-
|
354
|
+
`Citrus.load` to load a grammar file (and likewise
|
355
|
+
[Citrus#eval](api/classes/Citrus.html#M000004) to evaluate a raw string of
|
356
|
+
grammar code), the return value is an array of all the grammars present in that
|
357
|
+
file.
|
359
358
|
|
360
|
-
Take a look at
|
359
|
+
Take a look at
|
361
360
|
[examples/calc.citrus](http://github.com/mjijackson/citrus/blob/master/examples/calc.citrus)
|
362
|
-
for an example of a calculator that is able to parse and evaluate more complex
|
361
|
+
for an example of a calculator that is able to parse and evaluate more complex
|
363
362
|
mathematical expressions.
|
364
363
|
|
365
364
|
## Implicit Value
|
@@ -383,23 +382,86 @@ as:
|
|
383
382
|
}
|
384
383
|
end
|
385
384
|
|
386
|
-
Since no method name is explicitly specified in the semantic blocks, they may be
|
385
|
+
Since no method name is explicitly specified in the semantic blocks, they may be
|
387
386
|
called using the `value` method.
|
388
387
|
|
389
388
|
|
389
|
+
# Testing
|
390
|
+
|
391
|
+
|
392
|
+
Citrus was designed to facilitate simple and powerful testing of grammars. To
|
393
|
+
demonstrate how this is to be done, we'll use the `Addition` grammar from our
|
394
|
+
previous [example](example.html). The following code demonstrates a simple test
|
395
|
+
case that could be used to test that our grammar works properly.
|
396
|
+
|
397
|
+
class AdditionTest < Test::Unit::TestCase
|
398
|
+
def test_additive
|
399
|
+
match = Addition.parse('23 + 12', :root => :additive)
|
400
|
+
assert(match)
|
401
|
+
assert_equal('23 + 12', match)
|
402
|
+
assert_equal(35, match.value)
|
403
|
+
end
|
404
|
+
|
405
|
+
def test_number
|
406
|
+
match = Addition.parse('23', :root => :number)
|
407
|
+
assert(match)
|
408
|
+
assert_equal('23', match)
|
409
|
+
assert_equal(23, match.value)
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
The key here is using the `root`
|
414
|
+
[option](api/classes/Citrus/GrammarMethods.html#M000031) when performing the
|
415
|
+
parse to specify the name of the rule at which the parse should start. In
|
416
|
+
`test_number`, since `:number` was given the parse will start at that rule as if
|
417
|
+
it were the root rule of the entire grammar. The ability to change the root rule
|
418
|
+
on the fly like this enables easy unit testing of the entire grammar.
|
419
|
+
|
420
|
+
Also note that because match objects are themselves strings, assertions may be
|
421
|
+
made to test equality of match objects with string values.
|
422
|
+
|
423
|
+
## Debugging
|
424
|
+
|
425
|
+
When a parse fails, a [ParseError](api/classes/Citrus/ParseError.html) object is
|
426
|
+
generated which provides a wealth of information about exactly where the parse
|
427
|
+
failed. Using this object, you could possibly provide some useful feedback to
|
428
|
+
the user about why the input was bad. The following code demonstrates one way
|
429
|
+
to do this.
|
430
|
+
|
431
|
+
def parse_some_stuff(stuff)
|
432
|
+
match = StuffGrammar.parse(stuff)
|
433
|
+
rescue Citrus::ParseError => e
|
434
|
+
raise ArgumentError, "Invalid stuff on line %d, offset %d!" %
|
435
|
+
[e.line_number, e.line_offset]
|
436
|
+
end
|
437
|
+
|
438
|
+
In addition to useful error objects, Citrus also includes a special file that
|
439
|
+
should help grammar authors when debugging grammars. To get this extra
|
440
|
+
functionality, simply `require 'citrus/debug'` instead of `require 'citrus'`
|
441
|
+
when running your code.
|
442
|
+
|
443
|
+
When debugging is enabled, you can visualize parse trees in the console as XML
|
444
|
+
documents. This can help when determining which rules are generating which
|
445
|
+
matches and how they are organized in the output. Also when debugging, each
|
446
|
+
match object automatically records its offset in the original input, which can
|
447
|
+
also be very helpful in keeping track of which offsets in the input generated
|
448
|
+
which matches.
|
449
|
+
|
450
|
+
|
390
451
|
# Links
|
391
452
|
|
392
453
|
|
393
454
|
The primary resource for all things to do with parsing expressions can be found
|
394
|
-
on the original [Packrat and Parsing Expression Grammars page](http://pdos.csail.mit.edu/~baford/packrat)
|
455
|
+
on the original [Packrat and Parsing Expression Grammars page](http://pdos.csail.mit.edu/~baford/packrat)
|
456
|
+
at MIT.
|
395
457
|
|
396
|
-
Also, a useful summary of parsing expression grammars can be found on
|
458
|
+
Also, a useful summary of parsing expression grammars can be found on
|
397
459
|
[Wikipedia](http://en.wikipedia.org/wiki/Parsing_expression_grammar).
|
398
460
|
|
399
461
|
Citrus draws inspiration from another Ruby library for writing parsing
|
400
462
|
expression grammars, Treetop. While Citrus' syntax is similar to that of
|
401
|
-
[Treetop](http://treetop.rubyforge.org), it's not identical. The link is
|
402
|
-
included here for those who may wish
|
463
|
+
[Treetop](http://treetop.rubyforge.org), it's not identical. The link is
|
464
|
+
included here for those who may wish to explore an alternative implementation.
|
403
465
|
|
404
466
|
|
405
467
|
# License
|
data/doc/background.markdown
CHANGED
@@ -50,23 +50,23 @@ thereof.
|
|
50
50
|
A Citrus grammar is really just a souped-up Ruby
|
51
51
|
[module](http://ruby-doc.org/core/classes/Module.html). These modules may be
|
52
52
|
included in other grammar modules in the same way that Ruby modules are normally
|
53
|
-
used. This property allows you to divide a complex grammar into more manageable,
|
54
|
-
reusable pieces that may be combined at runtime. Any grammar rule with the same
|
55
|
-
name as a rule in an included grammar may access that rule with a mechanism
|
53
|
+
used. This property allows you to divide a complex grammar into more manageable,
|
54
|
+
reusable pieces that may be combined at runtime. Any grammar rule with the same
|
55
|
+
name as a rule in an included grammar may access that rule with a mechanism
|
56
56
|
similar to Ruby's super keyword.
|
57
57
|
|
58
58
|
## Matches
|
59
59
|
|
60
|
-
Matches are created by rule objects when they match on the input. A
|
61
|
-
[Match](api/classes/Citrus/Match.html) is actually a
|
62
|
-
[String](http://ruby-doc.org/core/classes/String.html) object with some extra
|
60
|
+
Matches are created by rule objects when they match on the input. A
|
61
|
+
[Match](api/classes/Citrus/Match.html) is actually a
|
62
|
+
[String](http://ruby-doc.org/core/classes/String.html) object with some extra
|
63
63
|
information attached such as the name(s) of the rule(s) from which it was
|
64
64
|
generated and any submatches it may contain.
|
65
65
|
|
66
66
|
During a parse, matches are arranged in a tree structure where any match may
|
67
67
|
contain any number of other matches. This structure is determined by the way in
|
68
|
-
which the rule that generated each match is used in the grammar. For example, a
|
69
|
-
match that is created from a non-terminal rule that contains several other
|
68
|
+
which the rule that generated each match is used in the grammar. For example, a
|
69
|
+
match that is created from a non-terminal rule that contains several other
|
70
70
|
terminals will likewise contain several matches, one for each terminal.
|
71
71
|
|
72
72
|
Match objects may be extended with semantic information in the form of methods.
|
data/doc/example.markdown
CHANGED
@@ -39,13 +39,12 @@ and "1 + 2+3", but it does not have enough semantic information to be able to
|
|
39
39
|
actually interpret these expressions.
|
40
40
|
|
41
41
|
At this point, when the grammar parses a string it generates a tree of
|
42
|
-
[Match](api/classes/Citrus/Match.html) objects. Each match is created by a rule
|
43
|
-
|
44
|
-
submatches it contains.
|
42
|
+
[Match](api/classes/Citrus/Match.html) objects. Each match is created by a rule
|
43
|
+
and may itself be comprised of any number of submatches.
|
45
44
|
|
46
45
|
Submatches are created whenever a rule contains another rule. For example, in
|
47
|
-
the grammar above
|
48
|
-
|
46
|
+
the grammar above `number` matches a string of digits followed by white space.
|
47
|
+
Thus, a match generated by this rule will contain two submatches.
|
49
48
|
|
50
49
|
We can define methods inside a set of curly braces that will be used to extend
|
51
50
|
matches when they are created. This works in similar fashion to using Ruby's
|
@@ -119,14 +118,14 @@ Congratulations! You just ran your first piece of Citrus code.
|
|
119
118
|
|
120
119
|
One interesting thing to notice about the above sequence of commands is the
|
121
120
|
return value of [Citrus#load](api/classes/Citrus.html#M000003). When you use
|
122
|
-
`Citrus.load` to
|
123
|
-
|
124
|
-
|
125
|
-
|
121
|
+
`Citrus.load` to load a grammar file (and likewise
|
122
|
+
[Citrus#eval](api/classes/Citrus.html#M000004) to evaluate a raw string of
|
123
|
+
grammar code), the return value is an array of all the grammars present in that
|
124
|
+
file.
|
126
125
|
|
127
|
-
Take a look at
|
126
|
+
Take a look at
|
128
127
|
[examples/calc.citrus](http://github.com/mjijackson/citrus/blob/master/examples/calc.citrus)
|
129
|
-
for an example of a calculator that is able to parse and evaluate more complex
|
128
|
+
for an example of a calculator that is able to parse and evaluate more complex
|
130
129
|
mathematical expressions.
|
131
130
|
|
132
131
|
## Implicit Value
|
@@ -150,5 +149,5 @@ as:
|
|
150
149
|
}
|
151
150
|
end
|
152
151
|
|
153
|
-
Since no method name is explicitly specified in the semantic blocks, they may be
|
152
|
+
Since no method name is explicitly specified in the semantic blocks, they may be
|
154
153
|
called using the `value` method.
|
data/doc/index.markdown
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
Citrus is a compact and powerful parsing library for
|
2
|
-
[Ruby](http://ruby-lang.org/) that combines the elegance and expressiveness of
|
1
|
+
Citrus is a compact and powerful parsing library for
|
2
|
+
[Ruby](http://ruby-lang.org/) that combines the elegance and expressiveness of
|
3
3
|
the language with the simplicity and power of
|
4
4
|
[parsing expressions](http://en.wikipedia.org/wiki/Parsing_expression_grammar).
|
5
5
|
|
@@ -9,10 +9,10 @@ the language with the simplicity and power of
|
|
9
9
|
|
10
10
|
Via [RubyGems](http://rubygems.org/):
|
11
11
|
|
12
|
-
$
|
12
|
+
$ gem install citrus
|
13
13
|
|
14
14
|
From a local copy:
|
15
15
|
|
16
16
|
$ git clone git://github.com/mjijackson/citrus.git
|
17
17
|
$ cd citrus
|
18
|
-
$ rake package
|
18
|
+
$ rake package install
|
data/doc/links.markdown
CHANGED
@@ -2,12 +2,13 @@
|
|
2
2
|
|
3
3
|
|
4
4
|
The primary resource for all things to do with parsing expressions can be found
|
5
|
-
on the original [Packrat and Parsing Expression Grammars page](http://pdos.csail.mit.edu/~baford/packrat)
|
5
|
+
on the original [Packrat and Parsing Expression Grammars page](http://pdos.csail.mit.edu/~baford/packrat)
|
6
|
+
at MIT.
|
6
7
|
|
7
|
-
Also, a useful summary of parsing expression grammars can be found on
|
8
|
+
Also, a useful summary of parsing expression grammars can be found on
|
8
9
|
[Wikipedia](http://en.wikipedia.org/wiki/Parsing_expression_grammar).
|
9
10
|
|
10
11
|
Citrus draws inspiration from another Ruby library for writing parsing
|
11
12
|
expression grammars, Treetop. While Citrus' syntax is similar to that of
|
12
|
-
[Treetop](http://treetop.rubyforge.org), it's not identical. The link is
|
13
|
-
included here for those who may wish
|
13
|
+
[Treetop](http://treetop.rubyforge.org), it's not identical. The link is
|
14
|
+
included here for those who may wish to explore an alternative implementation.
|
data/doc/syntax.markdown
CHANGED
@@ -104,8 +104,8 @@ See [Label](api/classes/Citrus/Label.html) for more information.
|
|
104
104
|
|
105
105
|
## Precedence
|
106
106
|
|
107
|
-
The following table contains a list of all Citrus
|
108
|
-
precedence. A higher precedence indicates tighter binding.
|
107
|
+
The following table contains a list of all Citrus symbols and operators and
|
108
|
+
their precedence. A higher precedence indicates tighter binding.
|
109
109
|
|
110
110
|
Operator | Name | Precedence
|
111
111
|
------------------------- | ------------------------- | ----------
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Testing
|
2
|
+
|
3
|
+
|
4
|
+
Citrus was designed to facilitate simple and powerful testing of grammars. To
|
5
|
+
demonstrate how this is to be done, we'll use the `Addition` grammar from our
|
6
|
+
previous [example](example.html). The following code demonstrates a simple test
|
7
|
+
case that could be used to test that our grammar works properly.
|
8
|
+
|
9
|
+
class AdditionTest < Test::Unit::TestCase
|
10
|
+
def test_additive
|
11
|
+
match = Addition.parse('23 + 12', :root => :additive)
|
12
|
+
assert(match)
|
13
|
+
assert_equal('23 + 12', match)
|
14
|
+
assert_equal(35, match.value)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_number
|
18
|
+
match = Addition.parse('23', :root => :number)
|
19
|
+
assert(match)
|
20
|
+
assert_equal('23', match)
|
21
|
+
assert_equal(23, match.value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
The key here is using the `root`
|
26
|
+
[option](api/classes/Citrus/GrammarMethods.html#M000031) when performing the
|
27
|
+
parse to specify the name of the rule at which the parse should start. In
|
28
|
+
`test_number`, since `:number` was given the parse will start at that rule as if
|
29
|
+
it were the root rule of the entire grammar. The ability to change the root rule
|
30
|
+
on the fly like this enables easy unit testing of the entire grammar.
|
31
|
+
|
32
|
+
Also note that because match objects are themselves strings, assertions may be
|
33
|
+
made to test equality of match objects with string values.
|
34
|
+
|
35
|
+
## Debugging
|
36
|
+
|
37
|
+
When a parse fails, a [ParseError](api/classes/Citrus/ParseError.html) object is
|
38
|
+
generated which provides a wealth of information about exactly where the parse
|
39
|
+
failed. Using this object, you could possibly provide some useful feedback to
|
40
|
+
the user about why the input was bad. The following code demonstrates one way
|
41
|
+
to do this.
|
42
|
+
|
43
|
+
def parse_some_stuff(stuff)
|
44
|
+
match = StuffGrammar.parse(stuff)
|
45
|
+
rescue Citrus::ParseError => e
|
46
|
+
raise ArgumentError, "Invalid stuff on line %d, offset %d!" %
|
47
|
+
[e.line_number, e.line_offset]
|
48
|
+
end
|
49
|
+
|
50
|
+
In addition to useful error objects, Citrus also includes a special file that
|
51
|
+
should help grammar authors when debugging grammars. To get this extra
|
52
|
+
functionality, simply `require 'citrus/debug'` instead of `require 'citrus'`
|
53
|
+
when running your code.
|
54
|
+
|
55
|
+
When debugging is enabled, you can visualize parse trees in the console as XML
|
56
|
+
documents. This can help when determining which rules are generating which
|
57
|
+
matches and how they are organized in the output. Also when debugging, each
|
58
|
+
match object automatically records its offset in the original input, which can
|
59
|
+
also be very helpful in keeping track of which offsets in the input generated
|
60
|
+
which matches.
|
data/lib/citrus.rb
CHANGED
@@ -8,7 +8,7 @@ require 'strscan'
|
|
8
8
|
module Citrus
|
9
9
|
autoload :File, 'citrus/file'
|
10
10
|
|
11
|
-
VERSION = [2,
|
11
|
+
VERSION = [2, 1, 1]
|
12
12
|
|
13
13
|
# Returns the current version of Citrus as a string.
|
14
14
|
def self.version
|
@@ -27,92 +27,204 @@ module Citrus
|
|
27
27
|
file << '.citrus' unless F.file?(file)
|
28
28
|
raise "Cannot find file #{file}" unless F.file?(file)
|
29
29
|
raise "Cannot read file #{file}" unless F.readable?(file)
|
30
|
-
|
30
|
+
eval(F.read(file))
|
31
31
|
end
|
32
32
|
|
33
33
|
# Evaluates the given Citrus parsing expression grammar +code+ in the global
|
34
|
-
# scope.
|
35
|
-
#
|
34
|
+
# scope. Returns an array of any grammar modules that are created. Implicitly
|
35
|
+
# raises +SyntaxError+ on a failed parse.
|
36
36
|
def self.eval(code)
|
37
|
-
|
37
|
+
parse(code, :consume => true).value
|
38
38
|
end
|
39
39
|
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
40
|
+
# Parses the given Citrus +code+ using the given +options+. Returns the
|
41
|
+
# generated match tree. Raises a +SyntaxError+ if the parse fails.
|
42
|
+
def self.parse(code, options={})
|
43
|
+
begin
|
44
|
+
File.parse(code, options)
|
45
|
+
rescue ParseError => e
|
46
|
+
raise SyntaxError.new(e)
|
47
47
|
end
|
48
|
+
end
|
48
49
|
|
49
|
-
|
50
|
-
|
50
|
+
# A standard error class that all Citrus errors extend.
|
51
|
+
class Error < RuntimeError; end
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
# error
|
55
|
-
def
|
56
|
-
|
53
|
+
# Raised when there is an error parsing Citrus code.
|
54
|
+
class SyntaxError < Error
|
55
|
+
# The +error+ given here should be a +ParseError+ object.
|
56
|
+
def initialize(error)
|
57
|
+
msg = "Syntax error on line %d at offset %d\n%s" %
|
58
|
+
[error.line_number, error.line_offset, error.detail]
|
59
|
+
super(msg)
|
57
60
|
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Raised when a match cannot be found.
|
64
|
+
class NoMatchError < Error; end
|
58
65
|
|
59
|
-
|
60
|
-
|
61
|
-
|
66
|
+
# Raised when a parse fails.
|
67
|
+
class ParseError < Error
|
68
|
+
# The +input+ given here is an instance of Citrus::Input.
|
69
|
+
def initialize(input)
|
70
|
+
@offset = input.max_offset
|
71
|
+
@line_offset = input.line_offset(offset)
|
72
|
+
@line_number = input.line_number(offset)
|
73
|
+
@line = input.line(offset)
|
74
|
+
msg = "Failed to parse input at offset %d\n" % offset
|
75
|
+
msg << detail
|
76
|
+
super(msg)
|
62
77
|
end
|
63
78
|
|
64
|
-
#
|
79
|
+
# The 0-based offset at which the error occurred in the input, i.e. the
|
80
|
+
# maximum offset in the input that was successfully parsed before the error
|
65
81
|
# occurred.
|
66
|
-
|
67
|
-
line_index + 1
|
68
|
-
end
|
82
|
+
attr_reader :offset
|
69
83
|
|
70
|
-
|
84
|
+
# The 0-based offset at which the error occurred on the line on which it
|
85
|
+
# occurred in the input.
|
86
|
+
attr_reader :line_offset
|
71
87
|
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
len = line.length
|
78
|
-
return (offset - pos) if pos + len >= offset
|
79
|
-
pos += len
|
80
|
-
end
|
81
|
-
0
|
82
|
-
end
|
88
|
+
# The 1-based number of the line in the input where the error occurred.
|
89
|
+
attr_reader :line_number
|
90
|
+
|
91
|
+
# The text of the line in the input where the error occurred.
|
92
|
+
attr_reader :line
|
83
93
|
|
84
94
|
# Returns a string that, when printed, gives a visual representation of
|
85
95
|
# exactly where the error occurred on its line in the input.
|
86
96
|
def detail
|
87
97
|
"%s\n%s^" % [line, ' ' * line_offset]
|
88
98
|
end
|
99
|
+
end
|
89
100
|
|
90
|
-
|
101
|
+
# This class represents the core of the parsing algorithm. It wraps the input
|
102
|
+
# string and serves matches to all nonterminals.
|
103
|
+
class Input < StringScanner
|
104
|
+
def initialize(string)
|
105
|
+
super(string)
|
106
|
+
@max_offset = 0
|
107
|
+
end
|
108
|
+
|
109
|
+
# The maximum offset that has been achieved during a parse.
|
110
|
+
attr_reader :max_offset
|
111
|
+
|
112
|
+
# A nested hash of rule id's to offsets and their respective matches. Only
|
113
|
+
# present if memoing is enabled.
|
114
|
+
attr_reader :cache
|
115
|
+
|
116
|
+
# The number of times the cache was hit. Only present if memoing is enabled.
|
117
|
+
attr_reader :cache_hits
|
91
118
|
|
92
|
-
|
93
|
-
|
119
|
+
# Returns the length of this input.
|
120
|
+
def length
|
121
|
+
string.length
|
94
122
|
end
|
95
123
|
|
124
|
+
# Returns an array containing the lines of text in the input.
|
96
125
|
def lines
|
97
126
|
string.send(string.respond_to?(:lines) ? :lines : :to_s).to_a
|
98
127
|
end
|
99
128
|
|
129
|
+
# Iterates over the lines of text in the input using the given +block+.
|
100
130
|
def each_line(&block)
|
101
131
|
string.each_line(&block)
|
102
132
|
end
|
103
133
|
|
104
|
-
# Returns the 0-based
|
105
|
-
#
|
106
|
-
def
|
107
|
-
|
108
|
-
|
134
|
+
# Returns the 0-based offset of the given +pos+ in the input on the line
|
135
|
+
# on which it is found. +pos+ defaults to the current pointer position.
|
136
|
+
def line_offset(pos=pos)
|
137
|
+
p = 0
|
138
|
+
each_line do |line|
|
139
|
+
len = line.length
|
140
|
+
return (pos - p) if p + len >= pos
|
141
|
+
p += len
|
142
|
+
end
|
143
|
+
0
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns the 0-based number of the line that contains the character at the
|
147
|
+
# given +pos+. +pos+ defaults to the current pointer position.
|
148
|
+
def line_index(pos=pos)
|
149
|
+
p, n = 0, 0
|
109
150
|
each_line do |line|
|
110
|
-
|
111
|
-
return
|
112
|
-
|
151
|
+
p += line.length
|
152
|
+
return n if p >= pos
|
153
|
+
n += 1
|
113
154
|
end
|
114
155
|
0
|
115
156
|
end
|
157
|
+
|
158
|
+
# Returns the 1-based number of the line that contains the character at the
|
159
|
+
# given +pos+. +pos+ defaults to the current pointer position.
|
160
|
+
def line_number(pos=pos)
|
161
|
+
line_index(pos) + 1
|
162
|
+
end
|
163
|
+
|
164
|
+
alias lineno line_number
|
165
|
+
|
166
|
+
# Returns the text of the line that contains the character at the given
|
167
|
+
# +pos+. +pos+ defaults to the current pointer position.
|
168
|
+
def line(pos=pos)
|
169
|
+
lines[line_index(pos)]
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns the match for the given +rule+ at the current pointer position,
|
173
|
+
# which is +nil+ if no match can be made.
|
174
|
+
def match(rule)
|
175
|
+
offset = pos
|
176
|
+
match = rule.match(self)
|
177
|
+
|
178
|
+
if match
|
179
|
+
@max_offset = pos if pos > @max_offset
|
180
|
+
else
|
181
|
+
# Reset the position for the next attempt at a match.
|
182
|
+
self.pos = offset unless match
|
183
|
+
end
|
184
|
+
|
185
|
+
match
|
186
|
+
end
|
187
|
+
|
188
|
+
# Returns +true+ when using memoization to cache match results.
|
189
|
+
def memoized?
|
190
|
+
!! @cache
|
191
|
+
end
|
192
|
+
|
193
|
+
# Modifies this object to cache match results during a parse. This technique
|
194
|
+
# (also known as "Packrat" parsing) guarantees parsers will operate in
|
195
|
+
# linear time but costs significantly more in terms of time and memory
|
196
|
+
# required to perform a parse. For more information, please read the paper
|
197
|
+
# on Packrat parsing at http://pdos.csail.mit.edu/~baford/packrat/icfp02/.
|
198
|
+
def memoize!
|
199
|
+
return if memoized?
|
200
|
+
|
201
|
+
# Using +instance_eval+ here preserves access to +super+ within the
|
202
|
+
# methods we define inside the block.
|
203
|
+
instance_eval do
|
204
|
+
def match(rule) # :nodoc:
|
205
|
+
c = @cache[rule.id] ||= {}
|
206
|
+
|
207
|
+
if c.key?(pos)
|
208
|
+
@cache_hits += 1
|
209
|
+
c[pos]
|
210
|
+
else
|
211
|
+
c[pos] = super
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Resets all internal variables so that this object may be used in
|
216
|
+
# another parse.
|
217
|
+
def reset
|
218
|
+
super
|
219
|
+
@max_offset = 0
|
220
|
+
@cache = {}
|
221
|
+
@cache_hits = 0
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
@cache = {}
|
226
|
+
@cache_hits = 0
|
227
|
+
end
|
116
228
|
end
|
117
229
|
|
118
230
|
# Inclusion of this module into another extends the receiver with the grammar
|
@@ -361,85 +473,6 @@ module Citrus
|
|
361
473
|
end
|
362
474
|
end
|
363
475
|
|
364
|
-
# This class represents the core of the parsing algorithm. It wraps the input
|
365
|
-
# string and serves matches to all nonterminals.
|
366
|
-
class Input < StringScanner
|
367
|
-
def initialize(string)
|
368
|
-
super(string)
|
369
|
-
@max_offset = 0
|
370
|
-
end
|
371
|
-
|
372
|
-
# The maximum offset that has been achieved during a parse.
|
373
|
-
attr_reader :max_offset
|
374
|
-
|
375
|
-
# A nested hash of rule id's to offsets and their respective matches. Only
|
376
|
-
# present if memoing is enabled.
|
377
|
-
attr_reader :cache
|
378
|
-
|
379
|
-
# The number of times the cache was hit. Only present if memoing is enabled.
|
380
|
-
attr_reader :cache_hits
|
381
|
-
|
382
|
-
# Returns the length of this input.
|
383
|
-
def length
|
384
|
-
string.length
|
385
|
-
end
|
386
|
-
|
387
|
-
# Returns the match for a given +rule+ at the current position in the input.
|
388
|
-
def match(rule)
|
389
|
-
offset = pos
|
390
|
-
match = rule.match(self)
|
391
|
-
|
392
|
-
if match
|
393
|
-
@max_offset = pos if pos > @max_offset
|
394
|
-
else
|
395
|
-
# Reset the position for the next attempt at a match.
|
396
|
-
self.pos = offset
|
397
|
-
end
|
398
|
-
|
399
|
-
match
|
400
|
-
end
|
401
|
-
|
402
|
-
# Returns true if this input uses memoization to cache match results. See
|
403
|
-
# #memoize!.
|
404
|
-
def memoized?
|
405
|
-
!! @cache
|
406
|
-
end
|
407
|
-
|
408
|
-
# Modifies this object to cache match results during a parse. This technique
|
409
|
-
# (also known as "Packrat" parsing) guarantees parsers will operate in
|
410
|
-
# linear time but costs significantly more in terms of time and memory
|
411
|
-
# required to perform a parse. For more information, please read the paper
|
412
|
-
# on Packrat parsing at http://pdos.csail.mit.edu/~baford/packrat/icfp02/.
|
413
|
-
def memoize!
|
414
|
-
return if memoized?
|
415
|
-
|
416
|
-
# Using +instance_eval+ here preserves access to +super+ within the
|
417
|
-
# methods we define inside the block.
|
418
|
-
instance_eval do
|
419
|
-
def match(rule)
|
420
|
-
c = @cache[rule.id] ||= {}
|
421
|
-
|
422
|
-
if c.key?(pos)
|
423
|
-
@cache_hits += 1
|
424
|
-
c[pos]
|
425
|
-
else
|
426
|
-
c[pos] = super
|
427
|
-
end
|
428
|
-
end
|
429
|
-
|
430
|
-
def reset
|
431
|
-
super
|
432
|
-
@max_offset = 0
|
433
|
-
@cache = {}
|
434
|
-
@cache_hits = 0
|
435
|
-
end
|
436
|
-
end
|
437
|
-
|
438
|
-
@cache = {}
|
439
|
-
@cache_hits = 0
|
440
|
-
end
|
441
|
-
end
|
442
|
-
|
443
476
|
# A Rule is an object that is used by a grammar to create matches on the
|
444
477
|
# Input during parsing.
|
445
478
|
module Rule
|
@@ -448,7 +481,7 @@ module Citrus
|
|
448
481
|
# Citrus::Rule.eval('"a" | "b"')
|
449
482
|
#
|
450
483
|
def self.eval(expr)
|
451
|
-
|
484
|
+
Citrus.parse(expr, :root => :rule_body, :consume => true).value
|
452
485
|
end
|
453
486
|
|
454
487
|
# Returns a new Rule object depending on the type of object given.
|
@@ -668,7 +701,7 @@ module Citrus
|
|
668
701
|
|
669
702
|
# Returns the Match for this rule on +input+, +nil+ if no match can be made.
|
670
703
|
def match(input)
|
671
|
-
m = input.scan(
|
704
|
+
m = input.scan(rule)
|
672
705
|
create_match(m) if m
|
673
706
|
end
|
674
707
|
|
@@ -1016,7 +1049,8 @@ module Citrus
|
|
1016
1049
|
def method_missing(sym, *args)
|
1017
1050
|
m = first(sym)
|
1018
1051
|
return m if m
|
1019
|
-
raise 'No match named "%s" in %s (%s)' %
|
1052
|
+
raise NoMatchError, 'No match named "%s" in %s (%s)' %
|
1053
|
+
[sym, self, name || '<anonymous>']
|
1020
1054
|
end
|
1021
1055
|
|
1022
1056
|
def to_ary
|
@@ -1037,8 +1071,8 @@ class Object
|
|
1037
1071
|
# end
|
1038
1072
|
#
|
1039
1073
|
def grammar(name, &block)
|
1040
|
-
|
1041
|
-
|
1074
|
+
namespace = respond_to?(:const_set) ? self : Object
|
1075
|
+
namespace.const_set(name, Citrus::Grammar.new(&block))
|
1042
1076
|
rescue NameError
|
1043
1077
|
raise ArgumentError, 'Invalid grammar name: %s' % name
|
1044
1078
|
end
|
data/lib/citrus/debug.rb
CHANGED
data/lib/citrus/file.rb
CHANGED
@@ -1,41 +1,67 @@
|
|
1
1
|
require 'citrus'
|
2
2
|
|
3
3
|
module Citrus
|
4
|
+
# Some helper methods for rules that alias +module_name+ and don't want to
|
5
|
+
# use +Kernel#eval+ to retrieve Module objects.
|
6
|
+
module ModuleHelpers #:nodoc:
|
7
|
+
def module_segments
|
8
|
+
@module_segments ||= module_name.value.split('::')
|
9
|
+
end
|
10
|
+
|
11
|
+
def module_namespace
|
12
|
+
module_segments[0..-2].inject(Object) do |namespace, constant|
|
13
|
+
constant.empty? ? namespace : namespace.const_get(constant)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def module_basename
|
18
|
+
module_segments.last
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
4
22
|
# A grammar for Citrus grammar files. This grammar is used in Citrus#eval to
|
5
23
|
# parse and evaluate Citrus grammars and serves as a prime example of how to
|
6
24
|
# create a complex grammar complete with semantic interpretation in pure Ruby.
|
7
|
-
File = Grammar.new do
|
25
|
+
File = Grammar.new do #:nodoc:
|
8
26
|
|
9
27
|
## Hierarchical syntax
|
10
28
|
|
11
29
|
rule :file do
|
12
30
|
all(:space, zero_or_more(any(:require, :grammar))) {
|
13
|
-
find(:require).each {
|
14
|
-
find(:grammar).map {
|
31
|
+
find(:require).each {|r| require r.value }
|
32
|
+
find(:grammar).map {|g| g.value }
|
15
33
|
}
|
16
34
|
end
|
17
35
|
|
18
36
|
rule :grammar do
|
19
37
|
all(:grammar_keyword, :module_name, :grammar_body, :end_keyword) {
|
20
|
-
|
21
|
-
|
38
|
+
include ModuleHelpers
|
39
|
+
|
40
|
+
def value
|
41
|
+
module_namespace.const_set(module_basename, grammar_body.value)
|
42
|
+
end
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
rule :grammar_body do
|
47
|
+
zero_or_more(any(:include, :root, :rule)) {
|
48
|
+
grammar = Grammar.new
|
22
49
|
|
23
|
-
|
24
|
-
|
50
|
+
find(:include).map do |inc|
|
51
|
+
grammar.include(inc.value)
|
52
|
+
end
|
25
53
|
|
26
54
|
root = find(:root).last
|
27
55
|
grammar.root(root.value) if root
|
28
56
|
|
29
|
-
find(:rule).each
|
57
|
+
find(:rule).each do |r|
|
58
|
+
grammar.rule(r.rule_name.value, r.value)
|
59
|
+
end
|
30
60
|
|
31
61
|
grammar
|
32
62
|
}
|
33
63
|
end
|
34
64
|
|
35
|
-
rule :grammar_body do
|
36
|
-
zero_or_more(any(:include, :root, :rule))
|
37
|
-
end
|
38
|
-
|
39
65
|
rule :rule do
|
40
66
|
all(:rule_keyword, :rule_name, :rule_body, :end_keyword) {
|
41
67
|
rule_body.value
|
@@ -43,27 +69,37 @@ module Citrus
|
|
43
69
|
end
|
44
70
|
|
45
71
|
rule :rule_body do
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
values.length > 1 ? Choice.new(values) : values[0]
|
72
|
+
zero_or_one(:choice) {
|
73
|
+
# An empty rule definition matches the empty string.
|
74
|
+
matches.length > 0 ? choice.value : Rule.new('')
|
50
75
|
}
|
51
76
|
end
|
52
77
|
|
53
78
|
rule :choice do
|
54
|
-
zero_or_more([ :bar, :sequence ]) {
|
55
|
-
|
79
|
+
all(:sequence, zero_or_more([ :bar, :sequence ])) {
|
80
|
+
def rules
|
81
|
+
@rules ||= [ sequence.value ] + matches[1].matches.map {|m| m.matches[1].value }
|
82
|
+
end
|
83
|
+
|
84
|
+
def value
|
85
|
+
rules.length > 1 ? Choice.new(rules) : rules.first
|
86
|
+
end
|
56
87
|
}
|
57
88
|
end
|
58
89
|
|
59
90
|
rule :sequence do
|
60
|
-
|
61
|
-
|
62
|
-
|
91
|
+
one_or_more(:expression) {
|
92
|
+
def rules
|
93
|
+
@rules ||= matches.map {|m| m.value }
|
94
|
+
end
|
95
|
+
|
96
|
+
def value
|
97
|
+
rules.length > 1 ? Sequence.new(rules) : rules.first
|
98
|
+
end
|
63
99
|
}
|
64
100
|
end
|
65
101
|
|
66
|
-
rule :
|
102
|
+
rule :expression do
|
67
103
|
all(:prefix, zero_or_one(:extension)) {
|
68
104
|
rule = prefix.value
|
69
105
|
extension = matches[1].first
|
@@ -105,7 +141,13 @@ module Citrus
|
|
105
141
|
end
|
106
142
|
|
107
143
|
rule :include do
|
108
|
-
all(:include_keyword, :module_name) {
|
144
|
+
all(:include_keyword, :module_name) {
|
145
|
+
include ModuleHelpers
|
146
|
+
|
147
|
+
def value
|
148
|
+
module_namespace.const_get(module_basename)
|
149
|
+
end
|
150
|
+
}
|
109
151
|
end
|
110
152
|
|
111
153
|
rule :root do
|
@@ -142,13 +184,13 @@ module Citrus
|
|
142
184
|
|
143
185
|
rule :quoted_string do
|
144
186
|
all(/(["'])(?:\\?.)*?\1/, :space) {
|
145
|
-
eval(first
|
187
|
+
eval(first)
|
146
188
|
}
|
147
189
|
end
|
148
190
|
|
149
191
|
rule :character_class do
|
150
192
|
all(/\[(?:\\?.)*?\]/, :space) {
|
151
|
-
Regexp.new('\A' + first
|
193
|
+
Regexp.new('\A' + first, nil, 'n')
|
152
194
|
}
|
153
195
|
end
|
154
196
|
|
@@ -160,7 +202,7 @@ module Citrus
|
|
160
202
|
|
161
203
|
rule :regular_expression do
|
162
204
|
all(/\/(?:\\?.)*?\/[imxouesn]*/, :space) {
|
163
|
-
eval(first
|
205
|
+
eval(first)
|
164
206
|
}
|
165
207
|
end
|
166
208
|
|
@@ -198,12 +240,16 @@ module Citrus
|
|
198
240
|
|
199
241
|
rule :tag do
|
200
242
|
all(:lt, :module_name, :gt) {
|
201
|
-
|
243
|
+
include ModuleHelpers
|
244
|
+
|
245
|
+
def value
|
246
|
+
module_namespace.const_get(module_basename)
|
247
|
+
end
|
202
248
|
}
|
203
249
|
end
|
204
250
|
|
205
251
|
rule :block do
|
206
|
-
all(:lcurly, zero_or_more(any(:block, /[^}]+/)), :rcurly) {
|
252
|
+
all(:lcurly, zero_or_more(any(:block, /[^{}]+/)), :rcurly) {
|
207
253
|
eval('Proc.new ' + to_s)
|
208
254
|
}
|
209
255
|
end
|
data/test/alias_test.rb
CHANGED
data/test/debug_test.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
# This file tests functionality that is only present when debugging is enabled.
|
4
|
+
|
5
|
+
class DebugTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def test_offset
|
8
|
+
match = Words.parse('one two')
|
9
|
+
assert(match)
|
10
|
+
assert_equal(0, match.offset)
|
11
|
+
|
12
|
+
words = match.find(:word)
|
13
|
+
assert(match)
|
14
|
+
assert_equal(2, words.length)
|
15
|
+
|
16
|
+
assert_equal('one', words[0])
|
17
|
+
assert_equal(0, words[0].offset)
|
18
|
+
|
19
|
+
assert_equal('two', words[1])
|
20
|
+
assert_equal(4, words[1].offset)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/test/file_test.rb
CHANGED
@@ -210,20 +210,60 @@ class CitrusFileTest < Test::Unit::TestCase
|
|
210
210
|
assert_instance_of(AndPredicate, match.value)
|
211
211
|
end
|
212
212
|
|
213
|
+
def test_empty
|
214
|
+
grammar = file(:rule_body)
|
215
|
+
|
216
|
+
match = grammar.parse('')
|
217
|
+
assert(match)
|
218
|
+
end
|
219
|
+
|
220
|
+
def test_choice
|
221
|
+
grammar = file(:choice)
|
222
|
+
|
223
|
+
match = grammar.parse('"a" | "b"')
|
224
|
+
assert(match)
|
225
|
+
assert_equal(2, match.rules.length)
|
226
|
+
assert_instance_of(Choice, match.value)
|
227
|
+
|
228
|
+
match = grammar.parse('"a" | ("b" "c")')
|
229
|
+
assert(match)
|
230
|
+
assert_equal(2, match.rules.length)
|
231
|
+
assert_instance_of(Choice, match.value)
|
232
|
+
end
|
233
|
+
|
213
234
|
def test_sequence
|
214
235
|
grammar = file(:sequence)
|
215
236
|
|
216
237
|
match = grammar.parse('"" ""')
|
217
238
|
assert(match)
|
218
|
-
|
239
|
+
assert_equal(2, match.rules.length)
|
219
240
|
assert_instance_of(Sequence, match.value)
|
220
241
|
|
221
242
|
match = grammar.parse('"a" "b" "c"')
|
222
243
|
assert(match)
|
223
|
-
|
244
|
+
assert_equal(3, match.rules.length)
|
224
245
|
assert_instance_of(Sequence, match.value)
|
225
246
|
end
|
226
247
|
|
248
|
+
def test_expression
|
249
|
+
grammar = file(:expression)
|
250
|
+
|
251
|
+
match = grammar.parse('"" <Module>')
|
252
|
+
assert(match)
|
253
|
+
assert_kind_of(Rule, match.value)
|
254
|
+
assert_kind_of(Module, match.value.extension)
|
255
|
+
|
256
|
+
match = grammar.parse('"" {}')
|
257
|
+
assert(match)
|
258
|
+
assert_kind_of(Rule, match.value)
|
259
|
+
assert_kind_of(Module, match.value.extension)
|
260
|
+
|
261
|
+
match = grammar.parse('"" {} ')
|
262
|
+
assert(match)
|
263
|
+
assert_kind_of(Rule, match.value)
|
264
|
+
assert_kind_of(Module, match.value.extension)
|
265
|
+
end
|
266
|
+
|
227
267
|
def test_prefix
|
228
268
|
grammar = file(:prefix)
|
229
269
|
|
@@ -248,25 +288,6 @@ class CitrusFileTest < Test::Unit::TestCase
|
|
248
288
|
assert_instance_of(Label, match.value)
|
249
289
|
end
|
250
290
|
|
251
|
-
def test_appendix
|
252
|
-
grammar = file(:appendix)
|
253
|
-
|
254
|
-
match = grammar.parse('"" <Module>')
|
255
|
-
assert(match)
|
256
|
-
assert_kind_of(Rule, match.value)
|
257
|
-
assert_kind_of(Module, match.value.extension)
|
258
|
-
|
259
|
-
match = grammar.parse('"" {}')
|
260
|
-
assert(match)
|
261
|
-
assert_kind_of(Rule, match.value)
|
262
|
-
assert_kind_of(Module, match.value.extension)
|
263
|
-
|
264
|
-
match = grammar.parse('"" {} ')
|
265
|
-
assert(match)
|
266
|
-
assert_kind_of(Rule, match.value)
|
267
|
-
assert_kind_of(Module, match.value.extension)
|
268
|
-
end
|
269
|
-
|
270
291
|
def test_suffix
|
271
292
|
grammar = file(:suffix)
|
272
293
|
|
@@ -325,11 +346,11 @@ class CitrusFileTest < Test::Unit::TestCase
|
|
325
346
|
|
326
347
|
match = grammar.parse('include Module')
|
327
348
|
assert(match)
|
328
|
-
assert_equal(
|
349
|
+
assert_equal(Module, match.value)
|
329
350
|
|
330
351
|
match = grammar.parse('include ::Module')
|
331
352
|
assert(match)
|
332
|
-
assert_equal(
|
353
|
+
assert_equal(Module, match.value)
|
333
354
|
end
|
334
355
|
|
335
356
|
def test_root
|
@@ -577,6 +598,15 @@ class CitrusFileTest < Test::Unit::TestCase
|
|
577
598
|
match = grammar.parse("{\n def value\n 'a'\n end\n} ")
|
578
599
|
assert(match)
|
579
600
|
assert(match.value)
|
601
|
+
|
602
|
+
end
|
603
|
+
|
604
|
+
def test_block_with_interpolation
|
605
|
+
grammar = file(:block)
|
606
|
+
|
607
|
+
match = grammar.parse('{ "#{number}" }')
|
608
|
+
assert(match)
|
609
|
+
assert(match.value)
|
580
610
|
end
|
581
611
|
|
582
612
|
def test_repeat
|
data/test/input_test.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class InputTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def test_new_input
|
6
|
+
input = Input.new("abc\ndef\nghi")
|
7
|
+
assert_equal(0, input.line_offset)
|
8
|
+
assert_equal(0, input.line_index)
|
9
|
+
assert_equal(1, input.line_number)
|
10
|
+
assert_equal("abc\n", input.line)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_advanced_input
|
14
|
+
input = Input.new("abc\ndef\nghi")
|
15
|
+
input.pos = 6
|
16
|
+
assert_equal(2, input.line_offset)
|
17
|
+
assert_equal(1, input.line_index)
|
18
|
+
assert_equal(2, input.line_number)
|
19
|
+
assert_equal("def\n", input.line)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/test/match_test.rb
CHANGED
@@ -53,20 +53,4 @@ class MatchTest < Test::Unit::TestCase
|
|
53
53
|
assert_equal(15, match.find(:alpha).length)
|
54
54
|
end
|
55
55
|
|
56
|
-
def test_offset
|
57
|
-
match = Words.parse('one two')
|
58
|
-
assert(match)
|
59
|
-
assert_equal(0, match.offset)
|
60
|
-
|
61
|
-
words = match.find(:word)
|
62
|
-
assert(match)
|
63
|
-
assert_equal(2, words.length)
|
64
|
-
|
65
|
-
assert_equal('one', words[0])
|
66
|
-
assert_equal(0, words[0].offset)
|
67
|
-
|
68
|
-
assert_equal('two', words[1])
|
69
|
-
assert_equal(4, words[1].offset)
|
70
|
-
end
|
71
|
-
|
72
56
|
end
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 2
|
7
|
-
- 0
|
8
7
|
- 1
|
9
|
-
|
8
|
+
- 1
|
9
|
+
version: 2.1.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Michael Jackson
|
@@ -61,6 +61,7 @@ files:
|
|
61
61
|
- doc/license.markdown
|
62
62
|
- doc/links.markdown
|
63
63
|
- doc/syntax.markdown
|
64
|
+
- doc/testing.markdown
|
64
65
|
- examples/calc.citrus
|
65
66
|
- examples/calc.rb
|
66
67
|
- examples/ip.citrus
|
@@ -83,9 +84,11 @@ files:
|
|
83
84
|
- test/calc_file_test.rb
|
84
85
|
- test/calc_test.rb
|
85
86
|
- test/choice_test.rb
|
87
|
+
- test/debug_test.rb
|
86
88
|
- test/file_test.rb
|
87
89
|
- test/grammar_test.rb
|
88
90
|
- test/helper.rb
|
91
|
+
- test/input_test.rb
|
89
92
|
- test/label_test.rb
|
90
93
|
- test/match_test.rb
|
91
94
|
- test/multibyte_test.rb
|
@@ -143,8 +146,10 @@ test_files:
|
|
143
146
|
- test/calc_file_test.rb
|
144
147
|
- test/calc_test.rb
|
145
148
|
- test/choice_test.rb
|
149
|
+
- test/debug_test.rb
|
146
150
|
- test/file_test.rb
|
147
151
|
- test/grammar_test.rb
|
152
|
+
- test/input_test.rb
|
148
153
|
- test/label_test.rb
|
149
154
|
- test/match_test.rb
|
150
155
|
- test/multibyte_test.rb
|