citrus 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +86 -0
- data/Rakefile +67 -0
- data/citrus.gemspec +29 -0
- data/examples/calc.citrus +103 -0
- data/examples/calc.rb +95 -0
- data/examples/calc_sugar.rb +94 -0
- data/lib/citrus.rb +904 -0
- data/lib/citrus/debug.rb +37 -0
- data/lib/citrus/peg.rb +375 -0
- data/lib/citrus/sugar.rb +25 -0
- data/test/alias_test.rb +66 -0
- data/test/and_predicate_test.rb +27 -0
- data/test/calc_peg_test.rb +6 -0
- data/test/calc_sugar_test.rb +6 -0
- data/test/calc_test.rb +6 -0
- data/test/choice_test.rb +62 -0
- data/test/expression_test.rb +29 -0
- data/test/fixed_width_test.rb +37 -0
- data/test/grammar_test.rb +129 -0
- data/test/helper.rb +143 -0
- data/test/label_test.rb +26 -0
- data/test/match_test.rb +76 -0
- data/test/not_predicate_test.rb +27 -0
- data/test/peg_test.rb +663 -0
- data/test/repeat_test.rb +93 -0
- data/test/rule_test.rb +49 -0
- data/test/sequence_test.rb +53 -0
- data/test/super_test.rb +66 -0
- metadata +133 -0
data/README
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
~* Citrus *~
|
4
|
+
|
5
|
+
Parsing Expressions for Ruby
|
6
|
+
|
7
|
+
|
8
|
+
Citrus is a compact and powerful parsing library for Ruby that combines the
|
9
|
+
elegance and expressiveness of the language with the simplicity and power of
|
10
|
+
parsing expression grammars.
|
11
|
+
|
12
|
+
Citrus grammars look very much like Treetop grammars but take a completely
|
13
|
+
different approach. Instead of generating parsers from your grammars, Citrus
|
14
|
+
evaluates grammars and rules in memory as Ruby modules. In fact, you can even
|
15
|
+
define your grammars as Ruby modules in the first place, entirely skipping the
|
16
|
+
parsing/evaluation step.
|
17
|
+
|
18
|
+
Terminals are represented as either strings or regular expressions. Support for
|
19
|
+
sequences, choices, labels, repetition, and lookahead (both positive and
|
20
|
+
negative) are all included, as well as character classes and the dot-matches-
|
21
|
+
anything symbol.
|
22
|
+
|
23
|
+
To try it out, fire up an IRB session from the root of the project and run one
|
24
|
+
of the examples.
|
25
|
+
|
26
|
+
$ irb -Ilib
|
27
|
+
> require 'citrus'
|
28
|
+
=> true
|
29
|
+
> Citrus.load 'examples/calc'
|
30
|
+
=> [Calc]
|
31
|
+
> match = Calc.parse '1 + 5'
|
32
|
+
=> #<Citrus::Match ...
|
33
|
+
> match.value
|
34
|
+
=> 6
|
35
|
+
|
36
|
+
Be sure to try requiring `citrus/debug' (instead of just `citrus') if you'd like
|
37
|
+
some better visualization of the match results.
|
38
|
+
|
39
|
+
The code base is very small and it's well-documented and tested, so it should be
|
40
|
+
fairly easy to understand for anyone who is familiar with parsing expressions.
|
41
|
+
|
42
|
+
|
43
|
+
** Links **
|
44
|
+
|
45
|
+
|
46
|
+
http://pdos.csail.mit.edu/~baford/packrat/
|
47
|
+
http://en.wikipedia.org/wiki/Parsing_expression_grammar
|
48
|
+
http://treetop.rubyforge.org/index.html
|
49
|
+
|
50
|
+
|
51
|
+
** Installation **
|
52
|
+
|
53
|
+
|
54
|
+
Via RubyGems:
|
55
|
+
|
56
|
+
$ sudo gem install citrus
|
57
|
+
|
58
|
+
From a local copy:
|
59
|
+
|
60
|
+
$ git clone git://github.com/mjijackson/citrus.git
|
61
|
+
$ cd citrus
|
62
|
+
$ rake package && sudo rake install
|
63
|
+
|
64
|
+
|
65
|
+
** License **
|
66
|
+
|
67
|
+
|
68
|
+
Copyright 2010 Michael Jackson
|
69
|
+
|
70
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
71
|
+
of this software and associated documentation files (the "Software"), to deal
|
72
|
+
in the Software without restriction, including without limitation the rights
|
73
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
74
|
+
copies of the Software, and to permit persons to whom the Software is
|
75
|
+
furnished to do so, subject to the following conditions:
|
76
|
+
|
77
|
+
The above copyright notice and this permission notice shall be included in
|
78
|
+
all copies or substantial portions of the Software.
|
79
|
+
|
80
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
81
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
82
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
83
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
84
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
85
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
86
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rake/clean'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
# TESTS #######################################################################
|
7
|
+
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.test_files = FileList['test/*_test.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
# DOCS ########################################################################
|
13
|
+
|
14
|
+
desc "Generate API documentation"
|
15
|
+
task :api => 'lib/citrus.rb' do |t|
|
16
|
+
output_dir = ENV['OUTPUT_DIR'] || 'api'
|
17
|
+
rm_rf output_dir
|
18
|
+
sh((<<-SH).gsub(/[\s\n]+/, ' ').strip)
|
19
|
+
hanna
|
20
|
+
--op #{output_dir}
|
21
|
+
--promiscuous
|
22
|
+
--charset utf8
|
23
|
+
--fmt html
|
24
|
+
--inline-source
|
25
|
+
--line-numbers
|
26
|
+
--accessor option_accessor=RW
|
27
|
+
--main Citrus
|
28
|
+
--title 'Citrus API Documentation'
|
29
|
+
#{t.prerequisites.join(' ')}
|
30
|
+
SH
|
31
|
+
end
|
32
|
+
|
33
|
+
CLEAN.include 'api'
|
34
|
+
|
35
|
+
# PACKAGING & INSTALLATION ####################################################
|
36
|
+
|
37
|
+
if defined?(Gem)
|
38
|
+
$spec = eval("#{File.read('citrus.gemspec')}")
|
39
|
+
|
40
|
+
directory 'dist'
|
41
|
+
|
42
|
+
def package(ext='')
|
43
|
+
"dist/#{$spec.name}-#{$spec.version}" + ext
|
44
|
+
end
|
45
|
+
|
46
|
+
file package('.gem') => %w< dist > + $spec.files do |f|
|
47
|
+
sh "gem build citrus.gemspec"
|
48
|
+
mv File.basename(f.name), f.name
|
49
|
+
end
|
50
|
+
|
51
|
+
file package('.tar.gz') => %w< dist > + $spec.files do |f|
|
52
|
+
sh "git archive --format=tar HEAD | gzip > #{f.name}"
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "Build packages"
|
56
|
+
task :package => %w< .gem .tar.gz >.map {|e| package(e) }
|
57
|
+
|
58
|
+
desc "Build and install as local gem"
|
59
|
+
task :install => package('.gem') do |t|
|
60
|
+
sh "gem install #{package('.gem')}"
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Upload gem to rubygems.org"
|
64
|
+
task :release => package('.gem') do |t|
|
65
|
+
sh "gem push #{package('.gem')}"
|
66
|
+
end
|
67
|
+
end
|
data/citrus.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'citrus'
|
3
|
+
s.version = '1.0.0'
|
4
|
+
s.date = '2010-05-13'
|
5
|
+
|
6
|
+
s.summary = 'Parsing Expressions for Ruby'
|
7
|
+
s.description = 'Parsing Expressions for Ruby'
|
8
|
+
|
9
|
+
s.author = 'Michael Jackson'
|
10
|
+
s.email = 'mjijackson@gmail.com'
|
11
|
+
|
12
|
+
s.require_paths = %w< lib >
|
13
|
+
|
14
|
+
s.files = Dir['lib/**/*.rb'] +
|
15
|
+
Dir['examples/**/*'] +
|
16
|
+
Dir['test/*.rb'] +
|
17
|
+
%w< citrus.gemspec Rakefile README >
|
18
|
+
|
19
|
+
s.test_files = s.files.select {|path| path =~ /^test\/.*_test.rb/ }
|
20
|
+
|
21
|
+
s.add_dependency('builder')
|
22
|
+
s.add_development_dependency('rake')
|
23
|
+
|
24
|
+
s.has_rdoc = true
|
25
|
+
s.rdoc_options = %w< --line-numbers --inline-source --title Citrus --main Citrus >
|
26
|
+
s.extra_rdoc_files = %w< README >
|
27
|
+
|
28
|
+
s.homepage = 'http://github.com/mjijackson/citrus'
|
29
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# A grammar for mathematical formulas that apply the basic four operations to
|
2
|
+
# non-negative numbers (integers and floats), respecting operator precedence and
|
3
|
+
# ignoring whitespace.
|
4
|
+
grammar Calc
|
5
|
+
rule term
|
6
|
+
(additive | factor) {
|
7
|
+
def value
|
8
|
+
first.value
|
9
|
+
end
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
rule additive
|
14
|
+
(factor operator:additive_op term) {
|
15
|
+
def value
|
16
|
+
operator.apply(factor.value, term.value)
|
17
|
+
end
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
rule factor
|
22
|
+
(multiplicative | primary) {
|
23
|
+
def value
|
24
|
+
first.value
|
25
|
+
end
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
rule multiplicative
|
30
|
+
(primary operator:multiplicative_op factor) {
|
31
|
+
def value
|
32
|
+
operator.apply(primary.value, factor.value)
|
33
|
+
end
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
rule primary
|
38
|
+
(term_paren | number) {
|
39
|
+
def value
|
40
|
+
first.value
|
41
|
+
end
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
rule term_paren
|
46
|
+
(lparen term rparen) {
|
47
|
+
def value
|
48
|
+
term.value
|
49
|
+
end
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
rule additive_op
|
54
|
+
(plus | minus) {
|
55
|
+
def apply(factor, term)
|
56
|
+
text.strip == '+' ? factor + term : factor - term
|
57
|
+
end
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
rule multiplicative_op
|
62
|
+
(star | slash) {
|
63
|
+
def apply(primary, factor)
|
64
|
+
text.strip == '*' ? primary * factor : primary / factor
|
65
|
+
end
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
rule number
|
70
|
+
(float | integer) {
|
71
|
+
def value
|
72
|
+
first.value
|
73
|
+
end
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
rule float
|
78
|
+
([0-9]+ '.' [0-9]+ space) {
|
79
|
+
def value
|
80
|
+
text.strip.to_f
|
81
|
+
end
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
rule integer
|
86
|
+
([0-9]+ space) {
|
87
|
+
def value
|
88
|
+
text.strip.to_i
|
89
|
+
end
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
rule lparen '(' space end
|
94
|
+
rule rparen ')' space end
|
95
|
+
rule plus '+' space end
|
96
|
+
rule minus '-' space end
|
97
|
+
rule star '*' space end
|
98
|
+
rule slash '/' space end
|
99
|
+
|
100
|
+
rule space
|
101
|
+
[ \t\n\r]*
|
102
|
+
end
|
103
|
+
end
|
data/examples/calc.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'citrus'
|
2
|
+
|
3
|
+
# A grammar for mathematical formulas that apply the basic four operations to
|
4
|
+
# non-negative numbers (integers and floats), respecting operator precedence and
|
5
|
+
# ignoring whitespace.
|
6
|
+
module Calc
|
7
|
+
include Citrus::Grammar
|
8
|
+
|
9
|
+
module FirstValue
|
10
|
+
def value
|
11
|
+
first.value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
rule :term do
|
16
|
+
ext(any(:additive, :factor), FirstValue)
|
17
|
+
end
|
18
|
+
|
19
|
+
rule :additive do
|
20
|
+
all(:factor, label(:additive_op, :operator), :term) {
|
21
|
+
def value
|
22
|
+
operator.apply(factor.value, term.value)
|
23
|
+
end
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
rule :factor do
|
28
|
+
ext(any(:multiplicative, :primary), FirstValue)
|
29
|
+
end
|
30
|
+
|
31
|
+
rule :multiplicative do
|
32
|
+
all(:primary, label(:multiplicative_op, :operator), :factor) {
|
33
|
+
def value
|
34
|
+
operator.apply(primary.value, factor.value)
|
35
|
+
end
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
rule :primary do
|
40
|
+
ext(any(:term_paren, :number), FirstValue)
|
41
|
+
end
|
42
|
+
|
43
|
+
rule :term_paren do
|
44
|
+
all(:lparen, :term, :rparen) {
|
45
|
+
def value
|
46
|
+
term.value
|
47
|
+
end
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
rule :additive_op do
|
52
|
+
any(:plus, :minus) {
|
53
|
+
def apply(factor, term)
|
54
|
+
text.strip == '+' ? factor + term : factor - term
|
55
|
+
end
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
rule :multiplicative_op do
|
60
|
+
any(:star, :slash) {
|
61
|
+
def apply(primary, factor)
|
62
|
+
text.strip == '*' ? primary * factor : primary / factor
|
63
|
+
end
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
rule :number do
|
68
|
+
ext(any(:float, :integer), FirstValue)
|
69
|
+
end
|
70
|
+
|
71
|
+
rule :float do
|
72
|
+
all(/[0-9]+/, '.', /[0-9]+/, :space) {
|
73
|
+
def value
|
74
|
+
text.strip.to_f
|
75
|
+
end
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
rule :integer do
|
80
|
+
all(/[0-9]+/, :space) {
|
81
|
+
def value
|
82
|
+
text.strip.to_i
|
83
|
+
end
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
rule :lparen, ['(', :space]
|
88
|
+
rule :rparen, [')', :space]
|
89
|
+
rule :plus, ['+', :space]
|
90
|
+
rule :minus, ['-', :space]
|
91
|
+
rule :star, ['*', :space]
|
92
|
+
rule :slash, ['/', :space]
|
93
|
+
|
94
|
+
rule :space, /[ \t\n\r]*/
|
95
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'citrus/sugar'
|
2
|
+
|
3
|
+
# A grammar for mathematical formulas that apply the basic four operations to
|
4
|
+
# non-negative numbers (integers and floats), respecting operator precedence and
|
5
|
+
# ignoring whitespace.
|
6
|
+
Calc = Citrus::Grammar.new {
|
7
|
+
|
8
|
+
module FirstValue
|
9
|
+
def value
|
10
|
+
first.value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
rule term do
|
15
|
+
ext(any(additive, factor), FirstValue)
|
16
|
+
end
|
17
|
+
|
18
|
+
rule additive do
|
19
|
+
all(factor, label(additive_op, operator), term) {
|
20
|
+
def value
|
21
|
+
operator.apply(factor.value, term.value)
|
22
|
+
end
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
rule factor do
|
27
|
+
ext(any(multiplicative, primary), FirstValue)
|
28
|
+
end
|
29
|
+
|
30
|
+
rule multiplicative do
|
31
|
+
all(primary, label(multiplicative_op, operator), factor) {
|
32
|
+
def value
|
33
|
+
operator.apply(primary.value, factor.value)
|
34
|
+
end
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
rule primary do
|
39
|
+
ext(any(term_paren, number), FirstValue)
|
40
|
+
end
|
41
|
+
|
42
|
+
rule term_paren do
|
43
|
+
all(lparen, term, rparen) {
|
44
|
+
def value
|
45
|
+
term.value
|
46
|
+
end
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
rule additive_op do
|
51
|
+
any(plus, minus) {
|
52
|
+
def apply(factor, term)
|
53
|
+
text.strip == '+' ? factor + term : factor - term
|
54
|
+
end
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
rule multiplicative_op do
|
59
|
+
any(star, slash) {
|
60
|
+
def apply(primary, factor)
|
61
|
+
text.strip == '*' ? primary * factor : primary / factor
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
rule number do
|
67
|
+
ext(any(float, integer), FirstValue)
|
68
|
+
end
|
69
|
+
|
70
|
+
rule float do
|
71
|
+
all(/[0-9]+/, '.', /[0-9]+/, space) {
|
72
|
+
def value
|
73
|
+
text.strip.to_f
|
74
|
+
end
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
rule integer do
|
79
|
+
all(/[0-9]+/, space) {
|
80
|
+
def value
|
81
|
+
text.strip.to_i
|
82
|
+
end
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
rule lparen, ['(', space]
|
87
|
+
rule rparen, [')', space]
|
88
|
+
rule plus, ['+', space]
|
89
|
+
rule minus, ['-', space]
|
90
|
+
rule star, ['*', space]
|
91
|
+
rule slash, ['/', space]
|
92
|
+
|
93
|
+
rule space, /[ \t\n\r]*/
|
94
|
+
}
|