oo_peg 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,458 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../enumerable'
4
+ require_relative 'combinators/lazy'
5
+ module OOPeg
6
+ class Parser
7
+ class InfiniteLoop < Exception; end
8
+
9
+ ##
10
+ #
11
+ # === The raison d'être of a PEG Parser are in fact the combinators
12
+ #
13
+ # So, without further ado...
14
+ #
15
+ # ==== +many+
16
+ #
17
+ # If a parser +p+ parses a string, the parser +p.many+ parses, well
18
+ # many occurrences of such strings concatenated.
19
+ #
20
+ # *N.B.* By default +many+ parses the empty input (0 occurrences)
21
+ # and can therefore _never_ _fail_!
22
+ # but we can force a minimum of occurrences.
23
+ #
24
+ # As indicated below, +many+ returns a list of all parsed occurrences.
25
+ #
26
+ # # example: many, the combinator for lists
27
+ #
28
+ # vowel_parser = char_parser('aeiouy').many
29
+ #
30
+ # parse(vowel_parser, "").ast => []
31
+ # parse(vowel_parser, "ae").ast => %w[a e]
32
+ #
33
+ # And we can force a minimum of occurrences
34
+ #
35
+ # # example: manyer, more than many
36
+ #
37
+ # two_chars = char_parser.many(min: 2)
38
+ #
39
+ # parse(two_chars, 'ab').ast => %w[a b]
40
+ # parse(two_chars, 'a').error => '"many(char_parser(TrueSet))" did not succeed the required 2 times, but only 1'
41
+ #
42
+ # # We can see that the error message lacks some context, let's try again
43
+ #
44
+ # named = char_parser.many(min: 2, name: '2 chars')
45
+ # parse(named, '').error => '"2 chars" did not succeed the required 2 times, but only 0'
46
+ #
47
+ # It is very frequently not an array of parsed strings, that we want, but the whole string, enter the
48
+ #
49
+ # ==== +joined+ combinator
50
+ #
51
+ # # example: joining together
52
+ #
53
+ # all_parser = char_parser.many.joined
54
+ #
55
+ # parse(all_parser, "some text").ast => "some text"
56
+ #
57
+ # ==== +map+ modify the result's ast
58
+ #
59
+ # In the above example we have seen, that the +map+ combinator returns a list, oftentimes we want the
60
+ # parsed string to be returned...
61
+ #
62
+ # Enter +map+
63
+ #
64
+ # # example: mapping a list back to a string
65
+ #
66
+ # vowel_string_parser = char_parser('aeiouy').many.map(&:join)
67
+ # parse(vowel_string_parser, "yo").ast => "yo"
68
+ #
69
+ # ===== Tagging, a special form of map
70
+ #
71
+ # +tagged(tag)+ is a short, convenience method for <tt>map { [tag, it] }</tt>
72
+ #
73
+ # # example: tagging a result
74
+ #
75
+ # tagged_char_parser = char_parser.tagged(:char)
76
+ #
77
+ # parse(tagged_char_parser, 'q').ast => [:char, 'q']
78
+ #
79
+ #
80
+ # ==== +maybe+ (oftentimes called +option+ in other PEG Parsers)
81
+ #
82
+ # If a parser +p+ parses a string, the parser +p.maybe+ parses, well
83
+ # 0 or 1 occurrences of such a string.
84
+ #
85
+ # *N.B.* +maybe+ parses the empty input (0 occurrences)
86
+ # and can therefore _never_ _fail_!
87
+ #
88
+ # We could also implement maybe with map as follows, but there
89
+ # is a subtle difference between <tt>[], "", nil</tt> to be observed.
90
+ #
91
+ # # example: map and maybe
92
+ #
93
+ # optional_char = char_parser.many(max: 1).map(&:join)
94
+ # maybe_char = char_parser.maybe
95
+ #
96
+ # parse(optional_char, "abc").ast => "a"
97
+ # parse(optional_char, "").ast => ""
98
+ #
99
+ # parse(maybe_char, "abc").ast => "a"
100
+ # parse(maybe_char, "").ast => nil
101
+ #
102
+ # ==== +or+ (oftentimes called +select+ in other PEG Parsers)
103
+ #
104
+ # Can be an instance method
105
+ #
106
+ # # example: or as an instance method
107
+ #
108
+ # # yet another implementation of maybe :P
109
+ #
110
+ # maybe_parser = char_parser('a').or(char_parser('b'), true_parser)
111
+ #
112
+ # parse(maybe_parser, 'a').ast => 'a'
113
+ # parse(maybe_parser, 'b').ast => 'b'
114
+ # parse(maybe_parser, 'c').ast => nil
115
+ # parse(maybe_parser, 'c') is! ok
116
+ #
117
+ # Can be a class method
118
+ #
119
+ # # example: or as a class method
120
+ #
121
+ # int_or_number_parser = OOPeg::Parser.or(int_parser, set_parser('one', 'two'))
122
+ #
123
+ # parse(int_or_number_parser, '12').ast => 12
124
+ # parse(int_or_number_parser, 'one').ast => 'one'
125
+ # parse(int_or_number_parser, 'two').ast => 'two'
126
+ #
127
+ # parse(int_or_number_parser, 'three') not! ok
128
+ #
129
+ # ==== +and+ (oftentimes called +sequence+ in other PEG Parsers)
130
+ #
131
+ # Can be an instance method
132
+ #
133
+ # # example: and as an instance method
134
+ #
135
+ # # yet another implementation of set_parser("abc")
136
+ #
137
+ # abc_parser = char_parser('a').and(char_parser('b'), char_parser('c'))
138
+ #
139
+ # parse(abc_parser, 'abc').ast => %w[a b c]
140
+ # parse(abc_parser, 'bca') not! ok
141
+ #
142
+ # Can be a class method
143
+ #
144
+ # # example: and as a class method
145
+ #
146
+ # abc_parser = OOPeg::Parser.and(char_parser('a'), char_parser('b'), char_parser('c'))
147
+ #
148
+ # parse(abc_parser, 'abc').ast => %w[a b c]
149
+ # parse(abc_parser, 'bca') not! ok
150
+ #
151
+ # Sometimes we do not want some parsed text in our AST, therefore the +.and+ combinator
152
+ # _ignores_ +nil+ ASTs.
153
+ #
154
+ # # example: ignore a sign (at your own peril of course)
155
+ #
156
+ # parser = OOPeg::Parser.and( char_parser('+').map {nil}, char_class_parser(:digit).many.joined )
157
+ #
158
+ # parse(parser, "+42").ast => ['42']
159
+ #
160
+ # As it is a little bit cumbersome to write <tt>map { nil }</tt> we made a more idiomatic way to
161
+ # express this behavior:
162
+ #
163
+ # ==== +ignore+
164
+ #
165
+ # # example: a better way of ignorance
166
+ #
167
+ # parser = OOPeg::Parser.and( char_parser('+').ignore, char_class_parser(:digit).many.joined )
168
+ #
169
+ # parse(parser, "+42").ast => ['42']
170
+ #
171
+ #
172
+ # ==== +satifsfy+ can fail a successful result does not touch failing results
173
+ #
174
+ # This, e.g. is used in the +kwd_parser+ or +set_parser+
175
+ #
176
+ # # example: an odd parser
177
+ #
178
+ # odd_parser = int_parser.satisfy(name: "oddy", &:odd?)
179
+ #
180
+ # parse(odd_parser, '11').ast => 11
181
+ # parse(odd_parser, '10') not! ok
182
+ #
183
+ # parse(odd_parser, '10').error => 'oddy failed'
184
+ #
185
+ # **N.B.** it can also modify the result
186
+ #
187
+ # even_parser = int_parser.satisfy { |n|
188
+ # n.even? ? [:ok, n/2] : [:error, "#{n}'s odd"]
189
+ # }
190
+ #
191
+ # parse(even_parser, '84').ast => 42
192
+ #
193
+ # parse(even_parser, '73').error => "73's odd"
194
+ #
195
+ # ==== Change result for succeeding *and* failing parsers: +map_or_rename+
196
+ #
197
+ # This allows us to modify the name or error message in case of failure *and* still
198
+ # transforming the ast if a block is given.
199
+ #
200
+ # # example: A better odd parser?
201
+ #
202
+ # better = int_parser.satisfy(&:odd?).map_or_rename(error: "Oh no", &:succ)
203
+ #
204
+ # parse(better, '11').ast => 12
205
+ # parse(better, '12').error => "Oh no"
206
+ #
207
+ # # example: No need to provide a block
208
+ #
209
+ # parse(char_parser.map_or_rename(name: "My Parser"), '') not! ok
210
+ #
211
+ # ==== Lookahead, an important concept...
212
+ #
213
+ # We can just check on the future \\o/
214
+ #
215
+ # # example: Look ahead, do not advance
216
+ #
217
+ # result = parse(int_parser.lookahead, '42')
218
+ #
219
+ # result.ast is! nil
220
+ # result is! ok
221
+ # result.input.pos => 1
222
+ # result.input.content => %w[4 2]
223
+ #
224
+ # # example: Look ahead, be disappointed
225
+ #
226
+ # parse(int_parser.lookahead, 'alpha') not! ok
227
+ #
228
+ # === What About Recursive Parsers?
229
+ #
230
+ # Let us assume that we want to parse a language that is recursive, how can we create
231
+ # a recursive parser that does not loop up to a stack overflow?
232
+ #
233
+ # Here is a toy example to introduce the concept (but it will come in very handy later
234
+ # in our advanced OOPeg::Parsers::Advanced::SexpParser .
235
+ #
236
+ # ==== A parser for the language <tt>a^n • b^n</tt>, the naïve way.
237
+ #
238
+ #
239
+ # # example: Look Ma' a stack overflow
240
+ #
241
+ # $ab_count = 0
242
+ # def ab_parser
243
+ # $ab_count += 1
244
+ # return true_parser if $ab_count == 1_000
245
+ # char_parser('a').and(ab_parser, char_parser('b')).or(true_parser)
246
+ # end
247
+ #
248
+ # parse(ab_parser, 'aabb')
249
+ #
250
+ # $ab_count => 1_000
251
+ #
252
+ # This problem can be solved by delaying the recursively called parser, which is done,
253
+ # with the aptly named:
254
+ #
255
+ # ==== +delay+ combinator
256
+ #
257
+ # # example: Look Ma' it's working now
258
+ #
259
+ # $ab_count = 0
260
+ # def ab_parser
261
+ # $ab_count += 1
262
+ # return true_parser if $ab_count == 1_000
263
+ # char_parser('a').and(delay {ab_parser}, char_parser('b')).joined.or(true_parser)
264
+ # end
265
+ #
266
+ # parse(ab_parser, 'aabb').ast => "aabb"
267
+ #
268
+ # $ab_count => 3
269
+ #
270
+ module Combinators
271
+
272
+ private
273
+ def _debug(parser, name: nil)
274
+ Parser.new(parser.name) do |input|
275
+ puts "debugging #{name || parser.name}: #{input.inspect}"
276
+ result = Parser.parse(parser, input)
277
+ puts "debugging #{name || parser.name}: #{result}"
278
+ result
279
+ end
280
+ end
281
+
282
+ def _lookahead(parsers, name:)
283
+ parser = _select(parsers)
284
+ Parser.new(name || "lookahead(#{parser.name})") do |input, name|
285
+ case Parser.parse(parser, input)
286
+ in {ok: true}
287
+ {ok: true, ast: nil, input:}
288
+ in error
289
+ error
290
+ end
291
+ end
292
+ end
293
+
294
+ def _map(parser:, name:, &mapper)
295
+ raise ArgumentError, "missing mapper function" unless mapper
296
+
297
+ name ||= "map(#{parser.name})"
298
+ Parser.new(name) do |input|
299
+ result = Parser.parse(parser, input)
300
+ # require "debug"; binding.break
301
+ result.map(&mapper)
302
+ # require "debug"; binding.break
303
+ # case result
304
+ # in {ok: true, input: rest, ast:}
305
+ # Result.ok(ast: mapper.(ast), input: rest)
306
+ # in {error:}
307
+ # Result.nok(error:, input:, parser_name: name)
308
+ # end
309
+ end
310
+ end
311
+
312
+ def _many(parser, max:, min:, name:)
313
+ name ||= "many(#{parser.name})"
314
+ Parser.new(name) do |input|
315
+ total_ast = []
316
+ original_input = input
317
+ current_input = input
318
+ match_count = 0
319
+ loop do
320
+ if current_input.empty?
321
+ break Result.ok(ast: total_ast, input:) if match_count >= min
322
+ break Result.nok(
323
+ error: "#{name.inspect} did not succeed the required #{min} times, but only #{match_count}",
324
+ input: original_input,
325
+ parser_name: name)
326
+ end
327
+
328
+ case Parser.parse(parser, current_input)
329
+ in {ok: true, ast:, input:}
330
+ raise InfiniteLoop, "must not parse zero width inside many in parser: #{parser.name}" if input.pos == current_input.pos
331
+ current_input = input
332
+ total_ast = [*total_ast, ast]
333
+ match_count += 1
334
+ break Result.ok(ast: total_ast, input:) if max && match_count >= max
335
+ in _
336
+ break Result.ok(ast: total_ast, input:) if match_count >= min
337
+ break Result.nok(
338
+ error: "many #{name} did not succeed the required #{min} times, but only #{match_count}",
339
+ input: original_input,
340
+ parser_name: name)
341
+ end
342
+ end
343
+ end
344
+ end
345
+
346
+ def _map_or_rename(parser:, name: nil, error: nil, &mapper)
347
+ parser_name = name || parser.name
348
+ Parser.new(name) do |input|
349
+ result = Parser.parse(parser, input)
350
+ case result
351
+ in {ok: true, ast:}
352
+ mapper ? result.merge(ast: mapper.(ast)) : result
353
+ in _
354
+ result.merge(parser_name:, error:)
355
+ end
356
+ end
357
+ end
358
+
359
+ def _map_result(parser, name, mapper)
360
+ Parser.new(name) do |input, name|
361
+ result = Parser.parse(parser, input)
362
+ # require "debug"; binding.break
363
+ mapper.(result)
364
+ end
365
+ end
366
+
367
+ def _maybe(parser:, name: nil, replace_with: nil)
368
+ Parser.new(name || "maybe(#{parser.name})") do |input, name|
369
+ case Parser.parse(parser, input)
370
+ in {ok: false}
371
+ Result.ok(ast: replace_with, input:)
372
+ in success
373
+ success
374
+ end
375
+ end
376
+ end
377
+
378
+ def _satisfy(parser, name:, &satisfier)
379
+ name ||= "satisfy(#{parser.name})"
380
+
381
+ Parser.new(name) do |input|
382
+ original_input = input
383
+ case Parser.parse(parser, input)
384
+ in {ok: false} => error
385
+ error
386
+ in {ok: true, ast:, input:} => result
387
+ case satisfier.(ast)
388
+ in true
389
+ result
390
+ in [:ok, ast]
391
+ Result.ok(ast:, input:)
392
+ in [:error, error]
393
+ Result.nok(input: original_input, error:, parser_name: name)
394
+ in _
395
+ Result.nok(input: original_input, error: "#{name} failed", parser_name: name)
396
+ end
397
+ end
398
+ end
399
+ end
400
+
401
+ def _select(*parsers, name: nil)
402
+ parsers = parsers.flatten
403
+ name ||= "select(#{parsers.map(&:name).join(",")})"
404
+ raise ArgumentError, "all parsers must be instances of Parser" unless parsers.all? { Parser === it }
405
+ Parser.new(name || "select #{parsers.map(&:name).join(", ")}") do |input, name|
406
+ result = Result.nok(error: "No parser matched in select named #{name}", input:, parser_name: name)
407
+ parsers.each do |parser|
408
+ # case p(Parser.parse(parser, input))
409
+ this_result = Parser.parse(parser, input)
410
+ # require "debug"; binding.break
411
+ case this_result
412
+ in {ok: true} => result
413
+ break result
414
+ in _
415
+ nil
416
+ end
417
+ end
418
+ result
419
+ end
420
+ end
421
+
422
+ def _sequence(*parsers, name:)
423
+ parsers = parsers.flatten
424
+ raise ArgumentError, "all parsers must be instances of Parser" unless parsers.all? { Parser === it }
425
+ name ||= "seq(#{parsers.map(&:name).join(", ")})"
426
+ Parser.new(name) do |input|
427
+ original_input = input
428
+ result = parsers.reduce_while [input, []] do |(input, ast), parser|
429
+ # require "debug"; binding.break
430
+ parsed = Parser.parse(parser, input)
431
+ # p parsed
432
+ case parsed
433
+ in {ok: true, ast: nil, input:}
434
+ cont_reduce([input, ast])
435
+ in {ok: true, ast: ast_node, input:}
436
+ cont_reduce([input, [*ast, ast_node]])
437
+ in {ok: false, error:}
438
+ halt_reduce(Result.nok(input: original_input, error:, parser_name: name))
439
+ end
440
+ end
441
+
442
+ case result
443
+ in {ok: false} => error
444
+ result
445
+ in [input, ast]
446
+ Result.ok(ast:, input:)
447
+ end
448
+ end
449
+ end
450
+
451
+ def _tagged(tag, parser:, name:)
452
+ name ||= "tagged(#{parser.name} with #{tag.inspect})"
453
+ _map(parser:, name:) { [tag, it] }
454
+ end
455
+ end
456
+ end
457
+ end
458
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'parser/class_methods'
4
+ require_relative 'parser/combinators'
5
+ require_relative 'input'
6
+ require_relative 'parsers'
7
+ require_relative 'result'
8
+
9
+ module OOPeg
10
+ ##
11
+ #
12
+ # This class represents a, well, parser
13
+ #
14
+ # It exposes a class method +parse+ which takes an input or a string (making it to
15
+ # an instance of +OOPeg::Input+ if necessary) and returns a +OOPeg::Result+
16
+ #
17
+ # It also exposes an homonymous instance method which *must* be called with an
18
+ # instance of +OOPeg::Input+
19
+ #
20
+ # # example: the parse methods
21
+ #
22
+ # expect(OOPeg::Parser.parse(end_parser, "")).to be_a OOPeg::Result
23
+ #
24
+ # # but for efficeny there is no check or conversion in the instance
25
+ # # method
26
+ #
27
+ # expect { end_parser.parse("") }
28
+ # .to raise_error(NoMethodError, "undefined method 'content' for an instance of String")
29
+ #
30
+ class Parser
31
+ include Combinators
32
+ extend Combinators::Lazy
33
+ extend ClassMethods
34
+
35
+ attr_reader :name
36
+
37
+
38
+ def parse(input) = @parse_fn.(input)
39
+
40
+ # Combinators
41
+ #
42
+ def and(*parsers, name: nil) = _sequence([self, *parsers], name:)
43
+
44
+ def debug(name: nil) = _debug(self, name:)
45
+
46
+ def ignore = self.map { nil }
47
+
48
+ def joined(joiner: "") = self.map { |ast| ast.join(joiner) }
49
+
50
+ def lookahead(name: nil) = _lookahead(self, name:)
51
+
52
+ def many(name: nil, max: nil, min: 0) = _many(self, name:, max:, min:)
53
+ def map(name: nil, &blk) = _map(parser: self, name:, &blk)
54
+ def map_or_rename(name: nil, error: nil, &blk) = _map_or_rename(parser: self, name:, error:, &blk)
55
+ def maybe(name: nil, replace_with: nil) = _maybe(parser: self, name:, replace_with:)
56
+
57
+ def or(*parsers, name: nil) = _select(self, *parsers, name:)
58
+
59
+ def satisfy(name: nil, &satisfier) = _satisfy(self, name:, &satisfier)
60
+
61
+ def tagged(tag, name: nil) = _tagged(tag, parser: self, name:)
62
+
63
+ private
64
+ def initialize(name, &blk)
65
+ raise ArgumentError, "blk must be provided as a parse function" unless blk
66
+ @name = name
67
+ @parse_fn = blk
68
+ end
69
+
70
+ end
71
+ end
72
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OOPeg
4
+ module Parsers
5
+ module Advanced
6
+ module OperatorParser extend self
7
+ include OOPeg::Parsers
8
+
9
+ DEFAULT_OP_CHARS = "><+-*/=:.&|^~!"
10
+
11
+ def make(allowed: nil, min: 1, max: 3, name: nil)
12
+ allowed ||= DEFAULT_OP_CHARS
13
+ name ||= "OperatorParser"
14
+ char_parser(allowed)
15
+ .many(min:, max:, name:)
16
+ .joined(&:to_sym)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OOPeg
4
+ module Parsers
5
+ module Advanced
6
+ module SexpParser extend self
7
+ include OOPeg::Parsers
8
+ include Advanced
9
+
10
+ def make(
11
+ parens: [:sexp, %w[( )], :map, %w[{ }], :arr, %w[[ ]]],
12
+ head_parser: nil,
13
+ tail_parser: nil,
14
+ sep_parser: ws_parser,
15
+ name: nil)
16
+
17
+ name ||= "SexpParser"
18
+ tail_parser ||= _default_tail_parser
19
+ head_parser ||= tail_parser
20
+
21
+ ws_parser(min: 0)
22
+ .and(
23
+ Parser.or(
24
+ parens
25
+ .each_slice(2)
26
+ .map(&_sexp_parser(head_parser:, tail_parser:, sep_parser:))
27
+ )).map(&:first)
28
+ end
29
+
30
+ private
31
+ def _default_tail_parser
32
+ Parser.or(
33
+ id_parser.tagged(:id),
34
+ int_parser.tagged(:int),
35
+ string_parser.tagged(:str),
36
+ symbol_parser.tagged(:sym),
37
+ operator_parser.tagged(:op),
38
+ delay {sexp_parser}
39
+ )
40
+ end
41
+
42
+ def _head_and_tail
43
+ -> ast do
44
+ ast => [h, t]
45
+ [h, *t]
46
+ end
47
+ end
48
+
49
+ def _inner_parser(head_parser:, tail_parser:, sep_parser:)
50
+ Parser.and(
51
+ head_parser,
52
+ sep_parser.and(tail_parser).many.map { it.map(&:first) },
53
+ name: 'inner s-exp parser'
54
+ )
55
+ .map(&_head_and_tail)
56
+ .maybe(replace_with: [])
57
+ end
58
+
59
+ def _sexp_parser(head_parser:, tail_parser:, sep_parser:)
60
+ -> parens do
61
+ parens => [tag, parsers]
62
+ parsers.map { char_parser(it).ignore } => [open, close]
63
+
64
+ Parser.and(
65
+ open.and(ws_parser(min: 0)).ignore,
66
+ _inner_parser(head_parser:, tail_parser:, sep_parser:)
67
+ .tagged(tag),
68
+ ws_parser(min: 0).and(close).ignore
69
+ )
70
+ # .debug
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OOPeg
4
+ module Parsers
5
+ module Advanced
6
+ module StringParser extend self
7
+ include OOPeg::Parsers
8
+
9
+ def make(delim: %{'"}, doubled_escape: nil, extra_parser: nil, escape_with: "\\", name: "StringParser")
10
+ parser =
11
+ Parser.or(
12
+ delim.grapheme_clusters.map { |quote| make_delim_parser(quote, doubled_escape:, extra_parser:, escape_with:) }
13
+ )
14
+ parser.map_or_rename(error: "Missing closing delimiter", name:) { |ast| ast[1].join }
15
+ end
16
+
17
+ private
18
+
19
+ def make_delim_parser(delim, doubled_escape:, extra_parser:, escape_with:)
20
+ char_parser(delim)
21
+ .and(
22
+ inner_parser(delim, doubled_escape:, extra_parser:, escape_with:),
23
+ char_parser(delim)
24
+ )
25
+ end
26
+
27
+ def inner_parser(delim, doubled_escape:, extra_parser:, escape_with:)
28
+ parsers = [
29
+ (doubled_escape && word_parser(delim + delim).map { delim }),
30
+ word_parser(escape_with + delim).map { delim },
31
+ word_parser(escape_with + escape_with).map { escape_with },
32
+ char_parser(delim, negate: true)
33
+ ].compact
34
+ Parser.or(parsers).many
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OOPeg
4
+ module Parsers
5
+ module Advanced
6
+ module SymbolParser extend self
7
+ include OOPeg::Parsers
8
+
9
+ def make(prefix: %{:}, inner_class: [:alnum, '_'], lead_class: :alpha, name: nil)
10
+ name ||= "SymbolParser"
11
+ Parser
12
+ .and(
13
+ char_parser(prefix).ignore,
14
+ id_parser(inner_class:, lead_class:, name:)
15
+ )
16
+ .map { it.first.to_sym }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ # SPDX-License-Identifier: AGPL-3.0-or-later