plurimath-parslet 3.0.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.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +284 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +454 -0
  5. data/Rakefile +71 -0
  6. data/lib/parslet/accelerator/application.rb +62 -0
  7. data/lib/parslet/accelerator/engine.rb +112 -0
  8. data/lib/parslet/accelerator.rb +162 -0
  9. data/lib/parslet/atoms/alternative.rb +53 -0
  10. data/lib/parslet/atoms/base.rb +157 -0
  11. data/lib/parslet/atoms/can_flatten.rb +137 -0
  12. data/lib/parslet/atoms/capture.rb +38 -0
  13. data/lib/parslet/atoms/context.rb +103 -0
  14. data/lib/parslet/atoms/dsl.rb +112 -0
  15. data/lib/parslet/atoms/dynamic.rb +32 -0
  16. data/lib/parslet/atoms/entity.rb +45 -0
  17. data/lib/parslet/atoms/ignored.rb +26 -0
  18. data/lib/parslet/atoms/infix.rb +115 -0
  19. data/lib/parslet/atoms/lookahead.rb +52 -0
  20. data/lib/parslet/atoms/named.rb +32 -0
  21. data/lib/parslet/atoms/re.rb +41 -0
  22. data/lib/parslet/atoms/repetition.rb +87 -0
  23. data/lib/parslet/atoms/scope.rb +26 -0
  24. data/lib/parslet/atoms/sequence.rb +48 -0
  25. data/lib/parslet/atoms/str.rb +42 -0
  26. data/lib/parslet/atoms/visitor.rb +89 -0
  27. data/lib/parslet/atoms.rb +34 -0
  28. data/lib/parslet/cause.rb +101 -0
  29. data/lib/parslet/context.rb +21 -0
  30. data/lib/parslet/convenience.rb +33 -0
  31. data/lib/parslet/error_reporter/contextual.rb +120 -0
  32. data/lib/parslet/error_reporter/deepest.rb +100 -0
  33. data/lib/parslet/error_reporter/tree.rb +63 -0
  34. data/lib/parslet/error_reporter.rb +8 -0
  35. data/lib/parslet/export.rb +163 -0
  36. data/lib/parslet/expression/treetop.rb +92 -0
  37. data/lib/parslet/expression.rb +51 -0
  38. data/lib/parslet/graphviz.rb +97 -0
  39. data/lib/parslet/parser.rb +68 -0
  40. data/lib/parslet/pattern/binding.rb +49 -0
  41. data/lib/parslet/pattern.rb +113 -0
  42. data/lib/parslet/position.rb +21 -0
  43. data/lib/parslet/rig/rspec.rb +52 -0
  44. data/lib/parslet/scope.rb +42 -0
  45. data/lib/parslet/slice.rb +105 -0
  46. data/lib/parslet/source/line_cache.rb +99 -0
  47. data/lib/parslet/source.rb +96 -0
  48. data/lib/parslet/transform.rb +265 -0
  49. data/lib/parslet/version.rb +5 -0
  50. data/lib/parslet.rb +314 -0
  51. data/plurimath-parslet.gemspec +42 -0
  52. data/spec/acceptance/infix_parser_spec.rb +145 -0
  53. data/spec/acceptance/mixing_parsers_spec.rb +74 -0
  54. data/spec/acceptance/regression_spec.rb +329 -0
  55. data/spec/acceptance/repetition_and_maybe_spec.rb +44 -0
  56. data/spec/acceptance/unconsumed_input_spec.rb +21 -0
  57. data/spec/examples/boolean_algebra_spec.rb +257 -0
  58. data/spec/examples/calc_spec.rb +278 -0
  59. data/spec/examples/capture_spec.rb +137 -0
  60. data/spec/examples/comments_spec.rb +186 -0
  61. data/spec/examples/deepest_errors_spec.rb +420 -0
  62. data/spec/examples/documentation_spec.rb +205 -0
  63. data/spec/examples/email_parser_spec.rb +275 -0
  64. data/spec/examples/empty_spec.rb +37 -0
  65. data/spec/examples/erb_spec.rb +482 -0
  66. data/spec/examples/ip_address_spec.rb +153 -0
  67. data/spec/examples/json_spec.rb +413 -0
  68. data/spec/examples/local_spec.rb +302 -0
  69. data/spec/examples/mathn_spec.rb +151 -0
  70. data/spec/examples/minilisp_spec.rb +492 -0
  71. data/spec/examples/modularity_spec.rb +340 -0
  72. data/spec/examples/nested_errors_spec.rb +322 -0
  73. data/spec/examples/optimized_erb_spec.rb +299 -0
  74. data/spec/examples/parens_spec.rb +239 -0
  75. data/spec/examples/prec_calc_spec.rb +525 -0
  76. data/spec/examples/readme_spec.rb +228 -0
  77. data/spec/examples/scopes_spec.rb +187 -0
  78. data/spec/examples/seasons_spec.rb +196 -0
  79. data/spec/examples/sentence_spec.rb +119 -0
  80. data/spec/examples/simple_xml_spec.rb +250 -0
  81. data/spec/examples/string_parser_spec.rb +407 -0
  82. data/spec/fixtures/examples/boolean_algebra.rb +62 -0
  83. data/spec/fixtures/examples/calc.rb +86 -0
  84. data/spec/fixtures/examples/capture.rb +36 -0
  85. data/spec/fixtures/examples/comments.rb +22 -0
  86. data/spec/fixtures/examples/deepest_errors.rb +99 -0
  87. data/spec/fixtures/examples/documentation.rb +32 -0
  88. data/spec/fixtures/examples/email_parser.rb +42 -0
  89. data/spec/fixtures/examples/empty.rb +10 -0
  90. data/spec/fixtures/examples/erb.rb +39 -0
  91. data/spec/fixtures/examples/ip_address.rb +103 -0
  92. data/spec/fixtures/examples/json.rb +107 -0
  93. data/spec/fixtures/examples/local.rb +60 -0
  94. data/spec/fixtures/examples/mathn.rb +47 -0
  95. data/spec/fixtures/examples/minilisp.rb +75 -0
  96. data/spec/fixtures/examples/modularity.rb +60 -0
  97. data/spec/fixtures/examples/nested_errors.rb +95 -0
  98. data/spec/fixtures/examples/optimized_erb.rb +105 -0
  99. data/spec/fixtures/examples/parens.rb +25 -0
  100. data/spec/fixtures/examples/prec_calc.rb +71 -0
  101. data/spec/fixtures/examples/readme.rb +59 -0
  102. data/spec/fixtures/examples/scopes.rb +43 -0
  103. data/spec/fixtures/examples/seasons.rb +40 -0
  104. data/spec/fixtures/examples/sentence.rb +18 -0
  105. data/spec/fixtures/examples/simple_xml.rb +51 -0
  106. data/spec/fixtures/examples/string_parser.rb +77 -0
  107. data/spec/parslet/atom_results_spec.rb +39 -0
  108. data/spec/parslet/atoms/alternative_spec.rb +26 -0
  109. data/spec/parslet/atoms/base_spec.rb +127 -0
  110. data/spec/parslet/atoms/capture_spec.rb +21 -0
  111. data/spec/parslet/atoms/combinations_spec.rb +5 -0
  112. data/spec/parslet/atoms/dsl_spec.rb +7 -0
  113. data/spec/parslet/atoms/entity_spec.rb +77 -0
  114. data/spec/parslet/atoms/ignored_spec.rb +15 -0
  115. data/spec/parslet/atoms/infix_spec.rb +5 -0
  116. data/spec/parslet/atoms/lookahead_spec.rb +22 -0
  117. data/spec/parslet/atoms/named_spec.rb +4 -0
  118. data/spec/parslet/atoms/re_spec.rb +14 -0
  119. data/spec/parslet/atoms/repetition_spec.rb +24 -0
  120. data/spec/parslet/atoms/scope_spec.rb +26 -0
  121. data/spec/parslet/atoms/sequence_spec.rb +28 -0
  122. data/spec/parslet/atoms/str_spec.rb +15 -0
  123. data/spec/parslet/atoms/visitor_spec.rb +101 -0
  124. data/spec/parslet/atoms_spec.rb +488 -0
  125. data/spec/parslet/convenience_spec.rb +54 -0
  126. data/spec/parslet/error_reporter/contextual_spec.rb +118 -0
  127. data/spec/parslet/error_reporter/deepest_spec.rb +82 -0
  128. data/spec/parslet/error_reporter/tree_spec.rb +7 -0
  129. data/spec/parslet/export_spec.rb +40 -0
  130. data/spec/parslet/expression/treetop_spec.rb +74 -0
  131. data/spec/parslet/minilisp.citrus +29 -0
  132. data/spec/parslet/minilisp.tt +29 -0
  133. data/spec/parslet/parser_spec.rb +36 -0
  134. data/spec/parslet/parslet_spec.rb +38 -0
  135. data/spec/parslet/pattern_spec.rb +272 -0
  136. data/spec/parslet/position_spec.rb +14 -0
  137. data/spec/parslet/rig/rspec_spec.rb +54 -0
  138. data/spec/parslet/scope_spec.rb +45 -0
  139. data/spec/parslet/slice_spec.rb +186 -0
  140. data/spec/parslet/source/line_cache_spec.rb +74 -0
  141. data/spec/parslet/source_spec.rb +210 -0
  142. data/spec/parslet/transform/context_spec.rb +56 -0
  143. data/spec/parslet/transform_spec.rb +183 -0
  144. data/spec/spec_helper.rb +74 -0
  145. data/spec/support/opal.rb +8 -0
  146. data/spec/support/opal.rb.erb +14 -0
  147. data/spec/support/parslet_matchers.rb +96 -0
  148. metadata +240 -0
data/lib/parslet.rb ADDED
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A simple parser generator library. Typical usage would look like this:
4
+ #
5
+ # require 'parslet'
6
+ #
7
+ # class MyParser < Parslet::Parser
8
+ # rule(:a) { str('a').repeat }
9
+ # root(:a)
10
+ # end
11
+ #
12
+ # pp MyParser.new.parse('aaaa') # => 'aaaa'@0
13
+ # pp MyParser.new.parse('bbbb') # => Parslet::Atoms::ParseFailed:
14
+ # # Don't know what to do with bbbb at line 1 char 1.
15
+ #
16
+ # The simple DSL allows you to define grammars in PEG-style. This kind of
17
+ # grammar construction does away with the ambiguities that usually comes with
18
+ # parsers; instead, it allows you to construct grammars that are easier to
19
+ # debug, since less magic is involved.
20
+ #
21
+ # Parslet is typically used in stages:
22
+ #
23
+ #
24
+ # * Parsing the input string; this yields an intermediary tree, see
25
+ # Parslet.any, Parslet.match, Parslet.str, Parslet::ClassMethods#rule and
26
+ # Parslet::ClassMethods#root.
27
+ # * Transformation of the tree into something useful to you, see
28
+ # Parslet::Transform, Parslet.simple, Parslet.sequence and Parslet.subtree.
29
+ #
30
+ # The first stage is traditionally intermingled with the second stage; output
31
+ # from the second stage is usually called the 'Abstract Syntax Tree' or AST.
32
+ #
33
+ # The stages are completely decoupled; You can change your grammar around and
34
+ # use the second stage to isolate the rest of your code from the changes
35
+ # you've effected.
36
+ #
37
+ # == Further reading
38
+ #
39
+ # All parslet atoms are subclasses of {Parslet::Atoms::Base}. You might want to
40
+ # look at all of those: {Parslet::Atoms::Re}, {Parslet::Atoms::Str},
41
+ # {Parslet::Atoms::Repetition}, {Parslet::Atoms::Sequence},
42
+ # {Parslet::Atoms::Alternative}.
43
+ #
44
+ # == When things go wrong
45
+ #
46
+ # A parse that fails will raise {Parslet::ParseFailed}. This exception contains
47
+ # all the details of what went wrong, including a detailed error trace that
48
+ # can be printed out as an ascii tree. ({Parslet::Cause})
49
+ #
50
+ module Parslet
51
+ # Extends classes that include Parslet with the module
52
+ # {Parslet::ClassMethods}.
53
+ #
54
+ def self.included(base)
55
+ base.extend(ClassMethods)
56
+ end
57
+
58
+ # Raised when the parse failed to match. It contains the message that should
59
+ # be presented to the user. More details can be extracted from the
60
+ # exceptions #parse_failure_cause member: It contains an instance of {Parslet::Cause} that
61
+ # stores all the details of your failed parse in a tree structure.
62
+ #
63
+ # begin
64
+ # parslet.parse(str)
65
+ # rescue Parslet::ParseFailed => failure
66
+ # puts failure.parse_failure_cause.ascii_tree
67
+ # end
68
+ #
69
+ # Alternatively, you can just require 'parslet/convenience' and call the
70
+ # method #parse_with_debug instead of #parse. This method will never raise
71
+ # and print error trees to stdout.
72
+ #
73
+ # require 'parslet/convenience'
74
+ # parslet.parse_with_debug(str)
75
+ #
76
+ class ParseFailed < StandardError
77
+ def initialize(message, parse_failure_cause=nil)
78
+ super(message)
79
+ @parse_failure_cause = parse_failure_cause
80
+ end
81
+
82
+ # Why the parse failed.
83
+ #
84
+ # @return [Parslet::Cause]
85
+ attr_reader :parse_failure_cause
86
+ end
87
+
88
+ module ClassMethods
89
+ # Define an entity for the parser. This generates a method of the same
90
+ # name that can be used as part of other patterns. Those methods can be
91
+ # freely mixed in your parser class with real ruby methods.
92
+ #
93
+ # class MyParser
94
+ # include Parslet
95
+ #
96
+ # rule(:bar) { str('bar') }
97
+ # rule(:twobar) do
98
+ # bar >> bar
99
+ # end
100
+ #
101
+ # root :twobar
102
+ # end
103
+ #
104
+ def rule(name, opts={}, &definition)
105
+ undef_method name if method_defined? name
106
+ define_method(name) do
107
+ @rules ||= {} # <name, rule> memoization
108
+ return @rules[name] if @rules.has_key?(name)
109
+
110
+ # Capture the self of the parser class along with the definition.
111
+ definition_closure = proc {
112
+ self.instance_eval(&definition)
113
+ }
114
+
115
+ @rules[name] = Atoms::Entity.new(name, opts[:label], &definition_closure)
116
+ end
117
+ end
118
+ end
119
+
120
+ # Allows for delayed construction of #match. See also Parslet.match.
121
+ #
122
+ # @api private
123
+ class DelayedMatchConstructor
124
+ def [](str)
125
+ Atoms::Re.new("[" + str + "]")
126
+ end
127
+ end
128
+
129
+ # Returns an atom matching a character class. All regular expressions can be
130
+ # used, as long as they match only a single character at a time.
131
+ #
132
+ # match('[ab]') # will match either 'a' or 'b'
133
+ # match('[\n\s]') # will match newlines and spaces
134
+ #
135
+ # There is also another (convenience) form of this method:
136
+ #
137
+ # match['a-z'] # synonymous to match('[a-z]')
138
+ # match['\n'] # synonymous to match('[\n]')
139
+ #
140
+ # @overload match(str)
141
+ # @param str [String] character class to match (regexp syntax)
142
+ # @return [Parslet::Atoms::Re] a parslet atom
143
+ #
144
+ def match(str=nil)
145
+ return DelayedMatchConstructor.new unless str
146
+
147
+ return Atoms::Re.new(str)
148
+ end
149
+ module_function :match
150
+
151
+ # Returns an atom matching the +str+ given:
152
+ #
153
+ # str('class') # will match 'class'
154
+ #
155
+ # @param str [String] string to match verbatim
156
+ # @return [Parslet::Atoms::Str] a parslet atom
157
+ #
158
+ def str(str)
159
+ Atoms::Str.new(str)
160
+ end
161
+ module_function :str
162
+
163
+ # Returns an atom matching any character. It acts like the '.' (dot)
164
+ # character in regular expressions.
165
+ #
166
+ # any.parse('a') # => 'a'
167
+ #
168
+ # @return [Parslet::Atoms::Re] a parslet atom
169
+ #
170
+ def any
171
+ Atoms::Re.new('.')
172
+ end
173
+ module_function :any
174
+
175
+ # Introduces a new capture scope. This means that all old captures stay
176
+ # accessible, but new values stored will only be available during the block
177
+ # given and the old values will be restored after the block.
178
+ #
179
+ # Example:
180
+ # # :a will be available until the end of the block. Afterwards,
181
+ # # :a from the outer scope will be available again, if such a thing
182
+ # # exists.
183
+ # scope { str('a').capture(:a) }
184
+ #
185
+ def scope(&block)
186
+ Parslet::Atoms::Scope.new(block)
187
+ end
188
+ module_function :scope
189
+
190
+ # Designates a piece of the parser as being dynamic. Dynamic parsers can
191
+ # either return a parser at runtime, which will be applied on the input, or
192
+ # return a result from a parse.
193
+ #
194
+ # Dynamic parse pieces are never cached and can introduce performance
195
+ # abnormalitites - use sparingly where other constructs fail.
196
+ #
197
+ # Example:
198
+ # # Parses either 'a' or 'b', depending on the weather
199
+ # dynamic { rand() < 0.5 ? str('a') : str('b') }
200
+ #
201
+ def dynamic(&block)
202
+ Parslet::Atoms::Dynamic.new(block)
203
+ end
204
+ module_function :dynamic
205
+
206
+ # Returns a parslet atom that parses infix expressions. Operations are
207
+ # specified as a list of <atom, precedence, associativity> tuples, where
208
+ # atom is simply the parslet atom that matches an operator, precedence is
209
+ # a number and associativity is either :left or :right.
210
+ #
211
+ # Higher precedence indicates that the operation should bind tighter than
212
+ # other operations with lower precedence. In common algebra, '+' has
213
+ # lower precedence than '*'. So you would have a precedence of 1 for '+' and
214
+ # a precedence of 2 for '*'. Only the order relation between these two
215
+ # counts, so any number would work.
216
+ #
217
+ # Associativity is what decides what interpretation to take for strings that
218
+ # are ambiguous like '1 + 2 + 3'. If '+' is specified as left associative,
219
+ # the expression would be interpreted as '(1 + 2) + 3'. If right
220
+ # associativity is chosen, it would be interpreted as '1 + (2 + 3)'. Note
221
+ # that the hash trees output reflect that choice as well.
222
+ #
223
+ # An optional block can be provided in order to manipulate the generated tree.
224
+ # The block will be called on each operator and passed 3 arguments: the left
225
+ # operand, the operator, and the right operand.
226
+ #
227
+ # Examples:
228
+ # infix_expression(integer, [add_op, 1, :left])
229
+ # # would parse things like '1 + 2'
230
+ #
231
+ # infix_expression(integer, [add_op, 1, :left]) { |l,o,r| { :plus => [l, r] } }
232
+ # # would parse '1 + 2 + 3' as:
233
+ # # { :plus => [1, { :plus => [2, 3] }] }
234
+ #
235
+ # @param element [Parslet::Atoms::Base] elements that take the NUMBER position
236
+ # in the expression
237
+ # @param operations [Array<(Parslet::Atoms::Base, Integer, {:left, :right})>]
238
+ #
239
+ # @see Parslet::Atoms::Infix
240
+ #
241
+ def infix_expression(element, *operations, &reducer)
242
+ Parslet::Atoms::Infix.new(element, operations, &reducer)
243
+ end
244
+ module_function :infix_expression
245
+
246
+ # A special kind of atom that allows embedding whole treetop expressions
247
+ # into parslet construction.
248
+ #
249
+ # # the same as str('a') >> str('b').maybe
250
+ # exp(%Q("a" "b"?))
251
+ #
252
+ # @param str [String] a treetop expression
253
+ # @return [Parslet::Atoms::Base] the corresponding parslet parser
254
+ #
255
+ def exp(str)
256
+ Parslet::Expression.new(str).to_parslet
257
+ end
258
+ module_function :exp
259
+
260
+ # Returns a placeholder for a tree transformation that will only match a
261
+ # sequence of elements. The +symbol+ you specify will be the key for the
262
+ # matched sequence in the returned dictionary.
263
+ #
264
+ # # This would match a body element that contains several declarations.
265
+ # { :body => sequence(:declarations) }
266
+ #
267
+ # The above example would match <code>:body => ['a', 'b']</code>, but not
268
+ # <code>:body => 'a'</code>.
269
+ #
270
+ # see {Parslet::Transform}
271
+ #
272
+ def sequence(symbol)
273
+ Pattern::SequenceBind.new(symbol)
274
+ end
275
+ module_function :sequence
276
+
277
+ # Returns a placeholder for a tree transformation that will only match
278
+ # simple elements. This matches everything that <code>#sequence</code>
279
+ # doesn't match.
280
+ #
281
+ # # Matches a single header.
282
+ # { :header => simple(:header) }
283
+ #
284
+ # see {Parslet::Transform}
285
+ #
286
+ def simple(symbol)
287
+ Pattern::SimpleBind.new(symbol)
288
+ end
289
+ module_function :simple
290
+
291
+ # Returns a placeholder for tree transformation patterns that will match
292
+ # any kind of subtree.
293
+ #
294
+ # { :expression => subtree(:exp) }
295
+ #
296
+ def subtree(symbol)
297
+ Pattern::SubtreeBind.new(symbol)
298
+ end
299
+ module_function :subtree
300
+
301
+ autoload :Expression, 'parslet/expression'
302
+ end
303
+
304
+ require 'parslet/version'
305
+ require 'parslet/slice'
306
+ require 'parslet/cause'
307
+ require 'parslet/source'
308
+ require 'parslet/atoms'
309
+ require 'parslet/pattern'
310
+ require 'parslet/pattern/binding'
311
+ require 'parslet/transform'
312
+ require 'parslet/parser'
313
+ require 'parslet/error_reporter'
314
+ require 'parslet/scope'
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/parslet/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'plurimath-parslet'
7
+ spec.version = Parslet::VERSION
8
+ spec.platform = Gem::Platform::RUBY
9
+
10
+ spec.authors = ['Kaspar Schiess', 'Ribose Inc.']
11
+ spec.email = ['open.source@ribose.com']
12
+
13
+ spec.summary = 'Parser construction library with great error reporting in Ruby.'
14
+ spec.description = 'A small Ruby library for constructing parsers in the PEG (Parsing Expression Grammar) fashion. ' \
15
+ 'This is a fork of the original parslet gem with Opal (JavaScript-based Ruby) compatibility.'
16
+ spec.homepage = 'https://github.com/plurimath/plurimath-parslet'
17
+ spec.license = 'MIT'
18
+
19
+ spec.metadata = {
20
+ 'bug_tracker_uri' => 'https://github.com/plurimath/plurimath-parslet/issues',
21
+ 'changelog_uri' => 'https://github.com/plurimath/plurimath-parslet/blob/main/HISTORY.txt',
22
+ 'documentation_uri' => 'https://kschiess.github.io/parslet/',
23
+ 'homepage_uri' => 'https://github.com/plurimath/plurimath-parslet',
24
+ 'source_code_uri' => 'https://github.com/plurimath/plurimath-parslet',
25
+ 'rubygems_mfa_required' => 'true',
26
+ }
27
+
28
+ spec.files = Dir.glob('{lib,spec,example}/**/*') + %w[
29
+ HISTORY.txt
30
+ LICENSE
31
+ Rakefile
32
+ README.adoc
33
+ plurimath-parslet.gemspec
34
+ ]
35
+ spec.require_paths = ['lib']
36
+
37
+ spec.required_ruby_version = '>= 2.7.0'
38
+
39
+ spec.add_development_dependency 'rake', '~> 13.0'
40
+ spec.add_development_dependency 'rdoc', '~> 6.0'
41
+ spec.add_development_dependency 'rspec', '~> 3.0'
42
+ end
@@ -0,0 +1,145 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Infix expression parsing' do
4
+ class InfixExpressionParser < Parslet::Parser
5
+ rule(:space) { Parslet.match['\s'] }
6
+
7
+ def cts(atom)
8
+ atom >> space.repeat
9
+ end
10
+
11
+ def infix(*args)
12
+ Infix.new(*args)
13
+ end
14
+
15
+ rule(:mul_op) { Parslet.match['*/'] >> str(' ').maybe }
16
+ rule(:add_op) { Parslet.match['+-'] >> str(' ').maybe }
17
+ rule(:digit) { Parslet.match['0-9'] }
18
+ rule(:integer) { cts digit.repeat(1).as(:int) }
19
+
20
+ rule(:expression) do
21
+ infix_expression(integer,
22
+ [mul_op, 2, :left],
23
+ [add_op, 1, :right])
24
+ end
25
+ end
26
+
27
+ let(:p) { InfixExpressionParser.new }
28
+
29
+ describe '#integer' do
30
+ let(:i) { p.integer }
31
+
32
+ it 'parses integers' do
33
+ i.should parse('1')
34
+ i.should parse('123')
35
+ end
36
+
37
+ it 'consumes trailing white space' do
38
+ i.should parse('1 ')
39
+ i.should parse('134 ')
40
+ end
41
+
42
+ it "doesn't parse floats" do
43
+ i.should_not parse('1.3')
44
+ end
45
+ end
46
+
47
+ describe '#multiplication' do
48
+ let(:m) { p.expression }
49
+
50
+ it 'parses simple multiplication' do
51
+ m.should parse('1*2').as(l: {int: '1'}, o: '*', r: {int: '2'})
52
+ end
53
+
54
+ it 'parses simple multiplication with spaces' do
55
+ m.should parse('1 * 2').as(l: {int: '1'}, o: '* ', r: {int: '2'})
56
+ end
57
+
58
+ it 'parses division' do
59
+ m.should parse('1/2')
60
+ end
61
+ end
62
+
63
+ describe '#addition' do
64
+ let(:a) { p.expression }
65
+
66
+ it 'parses simple addition' do
67
+ a.should parse('1+2')
68
+ end
69
+
70
+ it 'parses complex addition' do
71
+ a.should parse('1+2+3-4')
72
+ end
73
+
74
+ it 'parses a single element' do
75
+ a.should parse('1')
76
+ end
77
+ end
78
+
79
+ describe 'mixed operations' do
80
+ let(:mo) { p.expression }
81
+
82
+ describe 'inspection' do
83
+ it 'produces useful expressions' do
84
+ p.expression.parslet.inspect.should ==
85
+ 'infix_expression(INTEGER, [MUL_OP, ADD_OP])'
86
+ end
87
+ end
88
+
89
+ describe 'right associativity' do
90
+ it 'produces trees that lean right' do
91
+ mo.should parse('1+2+3').as(
92
+ l: {int: '1'}, o: '+', r: { l: {int: '2'}, o: '+', r: {int: '3'} },
93
+ )
94
+ end
95
+ end
96
+
97
+ describe 'left associativity' do
98
+ it 'produces trees that lean left' do
99
+ mo.should parse('1*2*3').as(
100
+ l: { l: {int: '1'}, o: '*', r: {int: '2'} }, o: '*', r: {int: '3'},
101
+ )
102
+ end
103
+ end
104
+
105
+ describe 'error handling' do
106
+ describe 'incomplete expression' do
107
+ it 'produces the right error' do
108
+ cause = catch_failed_parse do
109
+ mo.parse('1+')
110
+ end
111
+
112
+ cause.ascii_tree.to_s.should == <<~ERROR
113
+ INTEGER was expected at line 1 char 3.
114
+ `- Failed to match sequence (int:(DIGIT{1, }) SPACE{0, }) at line 1 char 3.
115
+ `- Expected at least 1 of DIGIT at line 1 char 3.
116
+ `- Premature end of input at line 1 char 3.
117
+ ERROR
118
+ end
119
+ end
120
+
121
+ describe 'invalid operator' do
122
+ it 'produces the right error' do
123
+ cause = catch_failed_parse do
124
+ mo.parse('1%')
125
+ end
126
+
127
+ cause.ascii_tree.to_s.should == <<~ERROR
128
+ Don't know what to do with "%" at line 1 char 2.
129
+ ERROR
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ describe 'providing a reducer block' do
136
+ class InfixExpressionReducerParser < Parslet::Parser
137
+ rule(:top) { infix_expression(str('a'), [str('-'), 1, :right]) { |l, _o, r| { and: [l, r] } } }
138
+ end
139
+
140
+ it 'applies the reducer' do
141
+ result = InfixExpressionReducerParser.new.top.parse('a-a-a')
142
+ strip_positions(result).should == { and: ['a', { and: %w[a a] }] }
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ require 'parslet'
4
+
5
+ describe 'Mixing parsers using alternate' do
6
+ class MixedParsersParser
7
+ include Parslet
8
+
9
+ rule(:parser1) do
10
+ str('a') >> match('\d').repeat(1).as(:number) >> (str(':') >> match('\d').repeat(4, 4).as(:year)).maybe
11
+ end
12
+
13
+ rule(:parser2) do
14
+ str('a') >> match('\d').repeat(1).as(:number) >> str(' Edition').as(:edition).maybe
15
+ end
16
+
17
+ rule(:failing_rule_example) do
18
+ str('(') >> ((parser1 | parser2) >> str(', ').maybe).repeat(1) >> str(')')
19
+ end
20
+
21
+ rule(:mixed_parsers) do
22
+ parser1 | parser2
23
+ end
24
+
25
+ rule(:identifiers) do
26
+ str('(') >> (match('[^),]').repeat(1).as(:identifier) >> str(', ').maybe).repeat(1) >> str(')')
27
+ end
28
+
29
+ def two_pass_parsing(code)
30
+ TransformIdentifiers.new.apply(MixedParsersParser.new.identifiers.parse(code))
31
+ end
32
+ end
33
+
34
+ class TransformIdentifiers < Parslet::Transform
35
+ rule(identifier: simple(:identifier)) do |x|
36
+ { identifier: MixedParsersParser.new.mixed_parsers.parse(x[:identifier].to_s) }
37
+ end
38
+ end
39
+
40
+ describe MixedParsersParser do
41
+ subject { MixedParsersParser.new }
42
+
43
+ let(:should_match_parser1) { 'a12345:1234' }
44
+ let(:should_match_parser2) { 'a12345 Edition' }
45
+ let(:should_match_any_parser) { 'a12345' }
46
+ let(:should_match_both_parsers) { "(#{should_match_parser1}, #{should_match_parser2})" }
47
+
48
+ let(:parser1_result) { subject.parser1.parse(should_match_parser1) }
49
+ let(:parser2_result) { subject.parser2.parse(should_match_parser2) }
50
+
51
+ it 'fails with alternating parsers' do
52
+ expect(subject.failing_rule_example).to parse("(#{should_match_any_parser})")
53
+ expect(subject.failing_rule_example).to parse("(#{should_match_parser1})")
54
+ expect(subject.failing_rule_example).not_to parse("(#{should_match_parser2})", trace: true)
55
+ expect(subject.failing_rule_example).not_to parse(should_match_both_parsers, trace: true)
56
+ end
57
+
58
+ it 'parses identifier' do
59
+ result = subject.identifiers.parse("(#{should_match_parser1})")
60
+ expect(strip_positions(result)).to eq([{ identifier: should_match_parser1 }])
61
+ end
62
+
63
+ it 'parses several identifiers' do
64
+ result = subject.identifiers.parse(should_match_both_parsers)
65
+ expect(strip_positions(result)).to eq([{ identifier: should_match_parser1 },
66
+ { identifier: should_match_parser2 }])
67
+ end
68
+
69
+ it 'parses using 2-level parsing' do
70
+ expect(subject.two_pass_parsing(should_match_both_parsers))
71
+ .to eq([{ identifier: parser1_result }, { identifier: parser2_result }])
72
+ end
73
+ end
74
+ end