parser_combinator_dsl 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/base_parsers.rb +19 -6
- data/lib/combinators.rb +69 -5
- data/lib/grammar.rb +1 -0
- data/lib/parser_result.rb +11 -2
- data/test/spec_helpers.rb +3 -2
- data/test/test_recipe_parser.rb +57 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: edaf570ec26adf2f4bf73b9dc01e54631d30ec848bc54ee064dc514af2f776e8
|
4
|
+
data.tar.gz: 44264ddfb196dc449b64e1d3cc50103046655332c8224603649901062faa0a28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 136543bfc38ee26b7c979514526efafc527731424f22d8db7fe2e9ae7aa94b516e20ae83acaddbaee75a71dd4dbbdd97153aab1d29f8df9523d6b547857c6421
|
7
|
+
data.tar.gz: 97abd30a6674af378ba2a5f6717a391a86524b79b20f486284f36c9f0631b99d024b801f19c232c5bc31a8339ee3f57b0ea07323175e156dcc7a7c112ef2c04f
|
data/lib/base_parsers.rb
CHANGED
@@ -18,7 +18,7 @@ module BaseParsers
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def whitespace
|
21
|
-
many0 { anyChar([
|
21
|
+
many0 { anyChar([" ", "\b", "\f", "\n", "\r", "\t"]) }
|
22
22
|
end
|
23
23
|
|
24
24
|
def one(char)
|
@@ -52,6 +52,7 @@ module BaseParsers
|
|
52
52
|
def many1(&wrapper)
|
53
53
|
Parser.new do |input|
|
54
54
|
matched = ""
|
55
|
+
output = []
|
55
56
|
remaining = input
|
56
57
|
parser = wrapper.call
|
57
58
|
|
@@ -59,10 +60,14 @@ module BaseParsers
|
|
59
60
|
result = parser.run(remaining)
|
60
61
|
break if remaining.nil? || result.fail?
|
61
62
|
matched = matched + result.matched
|
63
|
+
output += result.output unless (result.output.length == 1 and result.output[0].length == 1)
|
62
64
|
remaining = result.remaining
|
63
65
|
end
|
64
66
|
|
65
|
-
|
67
|
+
#puts "output: " + output.to_s unless output.empty?
|
68
|
+
output = nil if output.empty?
|
69
|
+
|
70
|
+
ParserResult.new(!matched.empty?, remaining, matched, output)
|
66
71
|
end
|
67
72
|
end
|
68
73
|
|
@@ -85,17 +90,25 @@ module BaseParsers
|
|
85
90
|
Parser.new do |input|
|
86
91
|
remaining = input
|
87
92
|
matched = ""
|
93
|
+
fail = nil
|
88
94
|
|
89
95
|
new_args = parsers.map do |parser|
|
90
96
|
result = parser.run(remaining)
|
91
|
-
|
97
|
+
unless result.ok?
|
98
|
+
fail = ParserResult.fail(input)
|
99
|
+
break
|
100
|
+
end
|
92
101
|
remaining = result.remaining
|
93
102
|
matched += result.matched
|
94
|
-
result.
|
103
|
+
result.output
|
95
104
|
end
|
96
105
|
|
97
|
-
|
98
|
-
|
106
|
+
if fail.nil?
|
107
|
+
output = callback.call(*new_args)
|
108
|
+
ParserResult.ok(output, matched: matched, remaining: remaining)
|
109
|
+
else
|
110
|
+
fail
|
111
|
+
end
|
99
112
|
end
|
100
113
|
end
|
101
114
|
|
data/lib/combinators.rb
CHANGED
@@ -24,13 +24,16 @@ module Combinators
|
|
24
24
|
def >>(other)
|
25
25
|
Parser.new do |input|
|
26
26
|
first = run(input)
|
27
|
-
matched = ""
|
27
|
+
matched = ""
|
28
|
+
output = []
|
28
29
|
if first.ok?
|
29
30
|
matched = matched + first.matched
|
31
|
+
output += first.output
|
30
32
|
second = other.run(first.remaining)
|
31
33
|
if second.ok?
|
32
34
|
matched = matched + second.matched
|
33
|
-
|
35
|
+
output = [*output, second.output]
|
36
|
+
ParserResult.ok(output, matched: matched, remaining: second.remaining)
|
34
37
|
else
|
35
38
|
ParserResult.fail(input)
|
36
39
|
end
|
@@ -45,12 +48,15 @@ module Combinators
|
|
45
48
|
Parser.new do |input|
|
46
49
|
first = run(input)
|
47
50
|
matched = ""
|
51
|
+
output = []
|
48
52
|
if first.ok?
|
49
53
|
matched = first.matched
|
54
|
+
output += first.output
|
50
55
|
second = other.run(first.remaining)
|
51
56
|
if second.ok?
|
52
57
|
matched = matched + second.matched
|
53
|
-
|
58
|
+
output = [*output, second.output]
|
59
|
+
ParserResult.ok(output, matched: matched, remaining: second.remaining)
|
54
60
|
else
|
55
61
|
first
|
56
62
|
end
|
@@ -65,17 +71,20 @@ module Combinators
|
|
65
71
|
Parser.new do |input|
|
66
72
|
first = run(input)
|
67
73
|
matched = ""
|
74
|
+
output = []
|
68
75
|
remaining = input
|
69
76
|
|
70
77
|
if first.ok?
|
71
78
|
matched = first.matched
|
79
|
+
output += first.output
|
72
80
|
remaining = first.remaining
|
73
81
|
end
|
74
82
|
|
75
83
|
second = other.run(remaining)
|
76
84
|
if second.ok?
|
77
85
|
matched = matched + second.matched
|
78
|
-
|
86
|
+
output = [*output, second.output]
|
87
|
+
ParserResult.ok(output, matched: matched, remaining: second.remaining)
|
79
88
|
else
|
80
89
|
ParserResult.fail(input)
|
81
90
|
end
|
@@ -89,7 +98,7 @@ module Combinators
|
|
89
98
|
if first.ok?
|
90
99
|
second = other.run(first.remaining)
|
91
100
|
if second.ok?
|
92
|
-
ParserResult.ok(matched: first.matched, remaining: second.remaining)
|
101
|
+
ParserResult.ok(first.output, matched: first.matched, remaining: second.remaining)
|
93
102
|
else
|
94
103
|
first
|
95
104
|
end
|
@@ -109,4 +118,59 @@ module Combinators
|
|
109
118
|
second.ok? ? second : ParserResult.fail(input)
|
110
119
|
end
|
111
120
|
end
|
121
|
+
|
122
|
+
# Match this, other is QUIETLY consumed
|
123
|
+
def /(other)
|
124
|
+
Parser.new do |input|
|
125
|
+
first = run(input)
|
126
|
+
matched = ""
|
127
|
+
if first.ok?
|
128
|
+
matched = matched + first.matched
|
129
|
+
second = other.run(first.remaining)
|
130
|
+
if second.ok?
|
131
|
+
matched = matched + second.matched
|
132
|
+
ParserResult.ok(first.output, matched: matched, remaining: second.remaining)
|
133
|
+
else
|
134
|
+
ParserResult.fail(input)
|
135
|
+
end
|
136
|
+
else
|
137
|
+
first
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Match other, this is QUIETLY consumed
|
143
|
+
def *(other)
|
144
|
+
Parser.new do |input|
|
145
|
+
first = run(input)
|
146
|
+
matched = ""
|
147
|
+
|
148
|
+
if first.ok?
|
149
|
+
matched += first.matched
|
150
|
+
second = other.run(first.remaining)
|
151
|
+
if second.ok?
|
152
|
+
matched += second.matched
|
153
|
+
ParserResult.ok(second.output, matched: matched, remaining: second.remaining)
|
154
|
+
else
|
155
|
+
ParserResult.fail(input)
|
156
|
+
end
|
157
|
+
else
|
158
|
+
first
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# other needs to fail for this to succeed; other is only peeking, not consuming
|
164
|
+
def !=(other)
|
165
|
+
Parser.new do |input|
|
166
|
+
second = other.run(input)
|
167
|
+
|
168
|
+
if second.ok?
|
169
|
+
ParserResult.fail(input)
|
170
|
+
else
|
171
|
+
run(input)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
112
176
|
end
|
data/lib/grammar.rb
CHANGED
@@ -14,6 +14,7 @@ class Grammar
|
|
14
14
|
instance_eval &block
|
15
15
|
end
|
16
16
|
|
17
|
+
# Adds or references a new rule
|
17
18
|
def rule(name, &wrapper)
|
18
19
|
return @rules.fetch(name.to_sym) { raise "Could not find rule: #{name}"} if wrapper.nil?
|
19
20
|
@rules[name.to_sym] = Parser.new { |input| wrapper.call.run(input) }
|
data/lib/parser_result.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
class ParserResult
|
2
2
|
attr_reader :success, :remaining, :matched, :output
|
3
|
-
def initialize(success, remaining, matched, output=
|
3
|
+
def initialize(success, remaining, matched, output=nil)
|
4
4
|
@success = success
|
5
5
|
@remaining = remaining
|
6
6
|
@matched = matched
|
7
|
-
@output = output
|
7
|
+
@output = output.nil? ? [matched] : output
|
8
8
|
end
|
9
9
|
|
10
10
|
def self.ok(output=nil, matched:, remaining:)
|
11
11
|
# yield matched if block_given?
|
12
|
+
output = [matched] if output.nil?
|
12
13
|
ParserResult.new(true, remaining, matched, output)
|
13
14
|
end
|
14
15
|
|
@@ -27,4 +28,12 @@ class ParserResult
|
|
27
28
|
def ==(other)
|
28
29
|
return other.instance_of?(self.class) && other.success == success && other.remaining == remaining && other.matched == matched
|
29
30
|
end
|
31
|
+
|
32
|
+
def to_s()
|
33
|
+
"ParserResult: {\n" +
|
34
|
+
"\tSuccess: " + success.to_s +
|
35
|
+
"\n\tRemaining: '" + remaining.to_s +
|
36
|
+
"'\n\tMatched: '" + matched.to_s +
|
37
|
+
"'\n\tOutput: " + output.to_s + "\n}"
|
38
|
+
end
|
30
39
|
end
|
data/test/spec_helpers.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
def assert_parses(parser, with:, remaining:, matched: nil, should_fail: false)
|
1
|
+
def assert_parses(parser, with:, remaining:, matched: nil, output: nil, should_fail: false)
|
2
2
|
result = parser.run(with)
|
3
|
-
puts
|
3
|
+
puts result.to_s
|
4
4
|
assert_equal !should_fail, result.success
|
5
5
|
assert_equal remaining, result.remaining
|
6
6
|
assert_equal matched, result.matched unless matched.nil?
|
7
|
+
assert_equal output, result.output unless output.nil?
|
7
8
|
end
|
8
9
|
|
9
10
|
def test_parser(parser, with:, should_fail: false)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "minitest/autorun"
|
2
|
+
require "pry"
|
3
|
+
require_relative "spec_helpers"
|
4
|
+
|
5
|
+
describe Grammar do
|
6
|
+
describe "Real world examples" do
|
7
|
+
it "output matches" do
|
8
|
+
parser = Grammar.build do
|
9
|
+
rule(:eol) { many0 { anyChar([" ", "\t"]) } < anyChar(["\n"]) }
|
10
|
+
rule(:instructions) { whitespace <= ( (str("Zubereitung") | str("Anweisungen")) / rule(:eol)) >> many1 { whitespace <= many1 { anyCharBut(["\n"]) } >= rule(:eol) } }
|
11
|
+
rule(:ingredient) { whitespace <= many1 { anyCharBut(["\n"]) } / rule(:eol) }
|
12
|
+
rule(:ingredients) { whitespace <= (str("Zutaten") / rule(:eol)) < many1 { rule(:ingredient) != rule(:instructions) } >= rule(:eol) }
|
13
|
+
rule(:recipe_name) { (str("Rezept ") <= many1 { anyCharBut(["\n"]) }) / many1 { rule(:eol) } }
|
14
|
+
rule(:recipe) { seq rule(:recipe_name), rule(:ingredients), rule(:instructions), lambda { |name, ingredients, instructions| [name, ingredients, instructions] } }
|
15
|
+
|
16
|
+
start(:recipe)
|
17
|
+
end
|
18
|
+
|
19
|
+
assert_parses parser, with:
|
20
|
+
"Rezept Tomatensahne
|
21
|
+
|
22
|
+
Zutaten
|
23
|
+
1 kg, Tomaten
|
24
|
+
100g, Zwiebeln
|
25
|
+
30g, Butter
|
26
|
+
1, Knoblauchzehe
|
27
|
+
1 EL, Mehl
|
28
|
+
2-3 EL, Tomatenmark
|
29
|
+
2 EL, Tomatenketchup
|
30
|
+
1/4 L, Brühe
|
31
|
+
1/4 L, Schlagsahne
|
32
|
+
|
33
|
+
Salz
|
34
|
+
Cayennepfeffer
|
35
|
+
1 TL, Zucker
|
36
|
+
1 TL, Zitronensaft
|
37
|
+
1 Bd., Basilikum
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
Anweisungen
|
42
|
+
Zwiebeln fein würfeln.
|
43
|
+
Tomaten häuten und würfeln.
|
44
|
+
Zwiebeln glasig dünsten, Knoblauch dazupressen.
|
45
|
+
Mit Mehl überstäuben, kurz anschwitzen.
|
46
|
+
Tomatenmark und -ketchup zugeben. Mit Brühe und Sahne auffüllen.
|
47
|
+
Sauce unter Rühren 5 Min. kochen.
|
48
|
+
Inzwischen Nudeln kochen, am besten Linguine.
|
49
|
+
Tomaten in der Sauce 5 Min. garen.
|
50
|
+
Mit Salz, Cayennepfeffer, Zucker und Zitronensaft herzhaft würzen.
|
51
|
+
Basilikumblätter abzupfen, in Streifen schneiden und auf die Sauce streuen.",
|
52
|
+
remaining: "",
|
53
|
+
output:
|
54
|
+
[["Tomatensahne"], ["Zutaten", ["1 kg, Tomaten", "100g, Zwiebeln", "30g, Butter", "1, Knoblauchzehe", "1 EL, Mehl", "2-3 EL, Tomatenmark", "2 EL, Tomatenketchup", "1/4 L, Brühe", "1/4 L, Schlagsahne", "Salz", "Cayennepfeffer", "1 TL, Zucker", "1 TL, Zitronensaft", "1 Bd., Basilikum"]], ["Anweisungen", ["Zwiebeln fein würfeln.", "Tomaten häuten und würfeln.", "Zwiebeln glasig dünsten, Knoblauch dazupressen.", "Mit Mehl überstäuben, kurz anschwitzen.", "Tomatenmark und -ketchup zugeben. Mit Brühe und Sahne auffüllen.", "Sauce unter Rühren 5 Min. kochen.", "Inzwischen Nudeln kochen, am besten Linguine.", "Tomaten in der Sauce 5 Min. garen.", "Mit Salz, Cayennepfeffer, Zucker und Zitronensaft herzhaft würzen.", "Basilikumblätter abzupfen, in Streifen schneiden und auf die Sauce streuen."]]]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: parser_combinator_dsl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Federico Ramirez
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-05-
|
12
|
+
date: 2018-05-31 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: "\n\tThis library provides a DSL which you can use to easily generate
|
15
15
|
parsers in Ruby.\n\n\tAt it's core, it's a parser combinator library, but you don't
|
@@ -30,6 +30,7 @@ files:
|
|
30
30
|
- test/test_base_parsers.rb
|
31
31
|
- test/test_combinators.rb
|
32
32
|
- test/test_json_demo.rb
|
33
|
+
- test/test_recipe_parser.rb
|
33
34
|
- test/test_tutorial.rb
|
34
35
|
homepage: https://github.com/moddx/parser-combinator
|
35
36
|
licenses:
|