pegparse 0.1.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.
@@ -0,0 +1,337 @@
1
+ require_relative '../lib/pegparse'
2
+ require 'set'
3
+
4
+ module Pegparse::Sample
5
+ end
6
+
7
+ class Pegparse::Sample::BshParser < Pegparse::ParserBase
8
+ def initialize(context_or_scanner)
9
+ super(context_or_scanner)
10
+ self.start_rule_symbol = :start_rule
11
+ end
12
+
13
+ def line_comment
14
+ read(/#.*/)
15
+ end
16
+
17
+ def inline_sp
18
+ read(/( |\\\n)*/)
19
+ end
20
+
21
+ def lf
22
+ choice(
23
+ ->{ super() },
24
+ ->{
25
+ sp()
26
+ read(';')
27
+ },
28
+ )
29
+ end
30
+
31
+ def keyword(key)
32
+ read(key)
33
+ backtrack if peek(/[A-Za-z0-9_]/)
34
+ end
35
+
36
+ def start_rule
37
+ ret = pipelines()
38
+ sp()
39
+ ret
40
+ end
41
+
42
+ def pipelines
43
+ zero_or_more {
44
+ sp()
45
+ pipeline()
46
+ }
47
+ end
48
+
49
+ BshBiopParser = Pegparse::BiopRuleChain.based_on(self) do
50
+ def operator_sp
51
+ inline_sp()
52
+ end
53
+
54
+ def operand_sp
55
+ inline_sp()
56
+ end
57
+ end
58
+ rule def pipeline
59
+ biop_parser = BshBiopParser.new(nil)
60
+ biop_parser
61
+ .left_op(['&&', '||'])
62
+ .left_op('|')
63
+ .term(->{ sentence() })
64
+ ret = biop_parser.parse(@context)
65
+
66
+ pipeline_closer = optional{
67
+ inline_sp()
68
+ read(/[&;]/)
69
+ }
70
+ if pipeline_closer == '&'
71
+ [:bg, ret]
72
+ else
73
+ ret
74
+ end
75
+ end
76
+
77
+ rule def sentence
78
+ choice(
79
+ ->{ if_sentence() },
80
+ ->{ while_sentence() },
81
+ ->{ for_sentence() },
82
+ ->{ case_sentence() },
83
+ ->{ command_sentence() },
84
+ )
85
+ end
86
+
87
+ rule def if_sentence
88
+ keyword('if')
89
+ sp()
90
+ cond = pipeline()
91
+ sp()
92
+ keyword('then')
93
+ sp()
94
+ if_body = pipelines()
95
+ elif_cond_bodies = zero_or_more {
96
+ sp()
97
+ keyword('elif')
98
+ sp()
99
+ elif_cond = pipeline()
100
+ sp()
101
+ keyword('then')
102
+ sp()
103
+ elif_body = pipelines()
104
+ [elif_cond, elif_body]
105
+ }
106
+ else_body = optional{
107
+ sp()
108
+ keyword('else')
109
+ sp()
110
+ pipelines()
111
+ }
112
+ sp()
113
+ keyword('fi')
114
+ if else_body
115
+ [:if, [cond, if_body], *elif_cond_bodies, [nil, else_body]]
116
+ else
117
+ [:if, [cond, if_body], *elif_cond_bodies]
118
+ end
119
+ end
120
+
121
+ rule def while_sentence
122
+ keyword('while')
123
+ sp()
124
+ backtrack
125
+ end
126
+
127
+ rule def for_sentence
128
+ keyword('for')
129
+ sp()
130
+ var = assign_varname()
131
+ sp()
132
+ keyword('in')
133
+ list = zero_or_more {
134
+ inline_sp()
135
+ normal_operand()
136
+ }
137
+ lf()
138
+ sp()
139
+ keyword('do')
140
+ body = pipelines()
141
+ keyword('done')
142
+ [:for, var, list, body]
143
+ end
144
+
145
+ rule def case_sentence
146
+ keyword('case')
147
+ sp()
148
+ backtrack
149
+ end
150
+
151
+ rule def command_sentence
152
+ envs = zero_or_more {
153
+ inline_sp()
154
+ assign_exp()
155
+ }
156
+ main = optional{
157
+ inline_sp()
158
+ cmd = execute_target_exp()
159
+
160
+ operands = zero_or_more {
161
+ inline_sp()
162
+ choice(
163
+ ->{ redirect_operand() },
164
+ ->{ normal_operand() }
165
+ )
166
+ }
167
+
168
+ redirects, normals = operands.partition{|op| op[0] == :redirect}
169
+ [cmd, redirects, operands]
170
+ }
171
+ backtrack if envs.empty? && !main
172
+
173
+ main ? [:command, envs, main[1], main[0], *main[2]] : [:env, envs]
174
+ end
175
+
176
+ rule def assign_exp
177
+ varname = assign_varname()
178
+ read('=')
179
+ val = assign_value()
180
+ ['=', varname, val]
181
+ end
182
+
183
+ def assign_varname
184
+ read(/[A-Za-z][A-Za-z0-9_]*/)
185
+ end
186
+
187
+ def assign_value
188
+ normal_operand()
189
+ end
190
+
191
+ def execute_target_exp
192
+ normal_operand()
193
+ end
194
+
195
+ rule def redirect_operand
196
+ src_fd = optional{
197
+ read(/[0-9]+/)
198
+ }
199
+ redirect = read(/>|>>|<|&>/)
200
+ inline_sp()
201
+ dest = normal_operand()
202
+ [:redirect, redirect, src_fd, dest]
203
+ end
204
+
205
+ rule def normal_operand
206
+ concats = one_or_more {
207
+ choice(
208
+ ->{ string_double_quote() },
209
+ ->{ string_single_quote() },
210
+ ->{ backquote_operand() },
211
+ ->{ raw_operand() },
212
+ )
213
+ }
214
+ if concats.size > 1
215
+ [:concat, *concats]
216
+ else
217
+ concats.first
218
+ end
219
+ end
220
+
221
+ def string_double_quote_special_process
222
+ choice(
223
+ ->{
224
+ read('\\')
225
+ escaped = read(/./m)
226
+ case escaped
227
+ when 'n'
228
+ "\n"
229
+ when "\n"
230
+ ""
231
+ else
232
+ escaped
233
+ end
234
+ },
235
+ ->{
236
+ inline_command()
237
+ },
238
+ ->{
239
+ variable_reference()
240
+ }
241
+ )
242
+ end
243
+
244
+ rule def string_double_quote
245
+ read('"')
246
+ ret = string_like('"', /[^"\\$]*/) {
247
+ string_double_quote_special_process()
248
+ }
249
+ read('"')
250
+ [:dquote, *ret]
251
+ end
252
+
253
+ rule def inline_command
254
+ read('$(')
255
+ body = pipelines()
256
+ read(')')
257
+ [:inline, body]
258
+ end
259
+
260
+ rule def variable_reference
261
+ read('$')
262
+ ref = choice(
263
+ ->{
264
+ name = read(/[A-Za-z0-9_]+/)
265
+ [name, nil]
266
+ },
267
+ ->{
268
+ read('{')
269
+ name = read(/[A-Za-z0-9_]+/)
270
+ substr = optional {
271
+ variable_expansion()
272
+ }
273
+ read('}')
274
+ [name, substr]
275
+ },
276
+ )
277
+ [:var, *ref]
278
+ end
279
+
280
+ rule def variable_expansion
281
+ choice(
282
+ ->{
283
+ read(':-')
284
+ backtrack
285
+ },
286
+ ->{
287
+ read('-')
288
+ exp = variable_expansion_string()
289
+ ['-', exp]
290
+ },
291
+ )
292
+ end
293
+
294
+ rule def variable_expansion_string
295
+ ret = string_like('}', /[^\\$}]*/) {
296
+ string_double_quote_special_process()
297
+ }
298
+ if ret.size > 1
299
+ ret
300
+ elsif ret.size == 1
301
+ ret.first
302
+ else
303
+ ''
304
+ end
305
+ end
306
+
307
+ rule def string_single_quote
308
+ read("'")
309
+ val = read(/[^']*/)
310
+ read("'")
311
+ val
312
+ end
313
+
314
+ rule def backquote_operand
315
+ read('`')
316
+ read('`')
317
+ backtrack
318
+ end
319
+
320
+ RESERVED_WORDS = %w(
321
+ if then elif else fi while do for in done
322
+ ).to_set
323
+ rule def raw_operand
324
+ backtrack if RESERVED_WORDS.include?(peek(/[^\s]*/))
325
+
326
+ ret = string_like(/[\s&|><;]/, /[^\s&|><;\\$]*/) {
327
+ string_double_quote_special_process()
328
+ }
329
+ if ret.size > 1
330
+ ret
331
+ elsif ret.size == 0 || ret.first.size == 0
332
+ backtrack()
333
+ else
334
+ ret.first
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,55 @@
1
+ require_relative '../lib/pegparse'
2
+
3
+ module Pegparse::Sample
4
+ end
5
+
6
+ class Pegparse::Sample::CalcParser < Pegparse::ParserBase
7
+ def initialize(context_or_scanner)
8
+ super(context_or_scanner)
9
+ self.start_rule_symbol = :expression
10
+ end
11
+
12
+ CalcBiopParser = Pegparse::BiopRuleChain.based_on(self) do
13
+ def operator_sp
14
+ read(/ */)
15
+ end
16
+ def operand_sp
17
+ read(/ */)
18
+ end
19
+ def construct_result(lhs, op, rhs)
20
+ case op
21
+ when '+'
22
+ lhs + rhs
23
+ when '-'
24
+ lhs - rhs
25
+ when '*'
26
+ lhs * rhs
27
+ when '/'
28
+ lhs / rhs
29
+ end
30
+ end
31
+ end
32
+ rule def expression
33
+ biop = CalcBiopParser.new(nil)
34
+ biop
35
+ .left_op(['+', '-'])
36
+ .left_op(['*', '/'])
37
+ .term(->{ term() })
38
+ biop.parse(@context)
39
+ end
40
+
41
+ rule def term
42
+ choice(
43
+ ->{
44
+ read(/[0-9]+(\.[0-9]+)?/).to_f
45
+ },
46
+ ->{
47
+ read('(')
48
+ exp = expression()
49
+ read(')')
50
+ exp
51
+ }
52
+ )
53
+ end
54
+
55
+ end
@@ -0,0 +1,92 @@
1
+ require_relative '../lib/pegparse'
2
+
3
+ module Pegparse::Sample
4
+ end
5
+
6
+ class Pegparse::Sample::JsonParser < Pegparse::ParserBase
7
+ class JsonNullNode
8
+ end
9
+
10
+ def initialize(context_or_scanner)
11
+ super(context_or_scanner)
12
+ self.start_rule_symbol = :start_rule
13
+ end
14
+
15
+ def start_rule
16
+ sp()
17
+ json_node()
18
+ end
19
+
20
+ rule def json_node
21
+ choice(
22
+ ->{ json_number_node() },
23
+ ->{ json_string_node() },
24
+ ->{ json_null_node() },
25
+ ->{ json_bool_node() },
26
+ ->{ json_object_node() },
27
+ ->{ json_array_node() },
28
+ )
29
+ end
30
+
31
+ def json_number_node
32
+ str = read(/[0-9]+(\.[0-9]+)?/)
33
+ str.to_f
34
+ end
35
+
36
+ rule def json_string_node
37
+ read('"')
38
+ ret = string_like('"', /[^"\\]*/){
39
+ read("\\")
40
+ read(/./m)
41
+ }
42
+ read('"')
43
+ ret[0]
44
+ end
45
+
46
+ def json_null_node
47
+ read('null')
48
+ JsonNullNode.new
49
+ end
50
+
51
+ def json_bool_node
52
+ choice(
53
+ ->{
54
+ read('true')
55
+ true
56
+ },
57
+ ->{
58
+ read('false')
59
+ false
60
+ },
61
+ )
62
+ end
63
+
64
+ rule def json_object_node
65
+ read('{')
66
+ sp()
67
+ pairs = separative(',') {
68
+ key = json_object_key()
69
+ sp()
70
+ read(':')
71
+ sp()
72
+ val = json_node()
73
+ [key, val]
74
+ }
75
+ sp()
76
+ read('}')
77
+ pairs.to_h
78
+ end
79
+
80
+ def json_object_key
81
+ json_string_node()
82
+ end
83
+
84
+ rule def json_array_node
85
+ read('[')
86
+ sp()
87
+ elements = separative(',') { json_node() }
88
+ sp()
89
+ read(']')
90
+ elements
91
+ end
92
+ end
@@ -0,0 +1,182 @@
1
+ require_relative '../lib/pegparse'
2
+
3
+ module Pegparse::Sample
4
+ end
5
+
6
+ class Pegparse::Sample::XmlParser < Pegparse::ParserBase
7
+ XmlNode = Struct.new(
8
+ :tag, :attrs, :inner_nodes,
9
+ keyword_init: true,
10
+ )
11
+
12
+ XmlAttr = Struct.new(
13
+ :name, :value,
14
+ keyword_init: true,
15
+ )
16
+
17
+ def initialize(context_or_scanner)
18
+ super(context_or_scanner)
19
+ self.start_rule_symbol = :start_rule
20
+ end
21
+
22
+ rule def block_comment
23
+ read('<!--')
24
+ read(/.*?-->/m)
25
+ end
26
+
27
+ def start_rule
28
+ sp()
29
+ decl = optional{ doc_declaration() }
30
+ nodes = xml_nodes()
31
+ [decl, nodes]
32
+ end
33
+
34
+ def intag_sp!
35
+ read(/\s+/)
36
+ end
37
+
38
+ def intag_sp
39
+ read(/\s*/)
40
+ end
41
+
42
+ def doc_declaration
43
+ choice(
44
+ ->{ xml_declaration() },
45
+ ->{ html_declaration() },
46
+ )
47
+ end
48
+
49
+ rule def xml_declaration
50
+ read('<?xml')
51
+ attrs = xml_attributes()
52
+ intag_sp()
53
+ read('>')
54
+ attrs
55
+ end
56
+
57
+ rule def html_declaration
58
+ read(/<!doctype html>/i)
59
+ end
60
+
61
+ def xml_attributes
62
+ attrs = zero_or_more {
63
+ intag_sp!()
64
+ xml_attribute_pair()
65
+ }
66
+ attrs
67
+ end
68
+
69
+ def xml_attribute_pair
70
+ name = xml_attribute_name()
71
+ read('=')
72
+ val = xml_attribute_value()
73
+ XmlAttr.new(name: name, value: val)
74
+ end
75
+
76
+ rule def xml_attribute_name
77
+ read(/[A-Za-z][A-Za-z0-9\-]*/)
78
+ end
79
+
80
+ rule def xml_tag_name
81
+ read(/[A-Za-z][A-Za-z0-9\-]*/)
82
+ end
83
+
84
+ rule def xml_attribute_value
85
+ choice(
86
+ ->{ single_quote_string() },
87
+ ->{ double_quote_string() },
88
+ )
89
+ end
90
+
91
+ def single_quote_string
92
+ read("'")
93
+ ret = string_like("'", /[^'\\]*/){
94
+ read('\\')
95
+ read(/./m)
96
+ }
97
+ read("'")
98
+ ret[0]
99
+ end
100
+
101
+ def double_quote_string
102
+ read('"')
103
+ ret = string_like('"', /[^"\\]*/){
104
+ read('\\')
105
+ read(/./m)
106
+ }
107
+ read('"')
108
+ ret[0]
109
+ end
110
+
111
+ rule def xml_open_tag
112
+ read('<')
113
+ name = xml_tag_name()
114
+ attrs = xml_attributes()
115
+ intag_sp()
116
+ read('>')
117
+ [name, attrs]
118
+ end
119
+
120
+ rule def xml_close_tag
121
+ read('</')
122
+ intag_sp()
123
+ name = xml_tag_name()
124
+ intag_sp()
125
+ read('>')
126
+ name
127
+ end
128
+
129
+ rule def xml_empty_element_tag
130
+ read('<')
131
+ name = xml_tag_name()
132
+ attrs = xml_attributes()
133
+ intag_sp()
134
+ read('/>')
135
+ [name, attrs]
136
+ end
137
+
138
+ SPECIAL_NONEST_TAG = %w(meta br)
139
+ rule def html_special_empty_element_tag
140
+ read('<')
141
+ name = xml_tag_name()
142
+ backtrack() unless SPECIAL_NONEST_TAG.include?(name)
143
+ attrs = xml_attributes()
144
+ intag_sp()
145
+ read(/\/?>/)
146
+ [name, attrs]
147
+ end
148
+
149
+ def xml_node
150
+ choice(
151
+ ->{
152
+ block_comment()
153
+ },
154
+ ->{
155
+ text = read(/[^<]+/)
156
+ text
157
+ },
158
+ ->{
159
+ tag = xml_empty_element_tag()
160
+ XmlNode.new(tag: tag[0], attrs: tag[1], inner_nodes: nil)
161
+ },
162
+ ->{
163
+ tag = html_special_empty_element_tag()
164
+ XmlNode.new(tag: tag[0], attrs: tag[1], inner_nodes: nil)
165
+ },
166
+ ->{
167
+ opentag = xml_open_tag()
168
+ inner = xml_nodes()
169
+ closetag = xml_close_tag()
170
+ backtrack if opentag[0] != closetag
171
+ XmlNode.new(tag: opentag[0], attrs: opentag[1], inner_nodes: inner)
172
+ }
173
+ )
174
+ end
175
+
176
+ def xml_nodes
177
+ nodes = zero_or_more {
178
+ xml_node()
179
+ }
180
+ nodes
181
+ end
182
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pegparse
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Riki Ishikawa
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-12-20 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: provide base class for PEG like recursive descent parser.
14
+ email:
15
+ - riki.ishikawa@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".rubocop.yml"
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - bin/console
27
+ - bin/setup
28
+ - lib/pegparse.rb
29
+ - lib/pegparse/biop_rule_chain.rb
30
+ - lib/pegparse/borrowed_areas.rb
31
+ - lib/pegparse/line_counter.rb
32
+ - lib/pegparse/parser_base.rb
33
+ - lib/pegparse/parser_context.rb
34
+ - lib/pegparse/parser_core.rb
35
+ - lib/pegparse/parser_errors.rb
36
+ - lib/pegparse/version.rb
37
+ - pegparse.gemspec
38
+ - samples/bsh_parser.rb
39
+ - samples/calc_parser.rb
40
+ - samples/json_parser.rb
41
+ - samples/xml_parser.rb
42
+ homepage: https://github.com/jljse/pegparse
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://github.com/jljse/pegparse
47
+ source_code_uri: https://github.com/jljse/pegparse
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.0.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.2.22
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: library to create recursive descent parser.
67
+ test_files: []