parser_combinator_dsl 1.0.2 → 1.0.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.
- 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:
|