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
@@ -0,0 +1,42 @@
1
+ class Parslet::Scope
2
+ # Raised when the accessed slot has never been assigned a value.
3
+ #
4
+ class NotFound < StandardError
5
+ end
6
+
7
+ class Binding
8
+ attr_reader :parent
9
+
10
+ def initialize(parent=nil)
11
+ @parent = parent
12
+ @hash = Hash.new
13
+ end
14
+
15
+ def [](k)
16
+ @hash.has_key?(k) && @hash[k] ||
17
+ parent && parent[k] or
18
+ raise NotFound
19
+ end
20
+ def []=(k,v)
21
+ @hash.store(k,v)
22
+ end
23
+ end
24
+
25
+ def [](k)
26
+ @current[k]
27
+ end
28
+ def []=(k,v)
29
+ @current[k] = v
30
+ end
31
+
32
+ def initialize
33
+ @current = Binding.new
34
+ end
35
+
36
+ def push
37
+ @current = Binding.new(@current)
38
+ end
39
+ def pop
40
+ @current = @current.parent
41
+ end
42
+ end
@@ -0,0 +1,105 @@
1
+ # A slice is a small part from the parse input. A slice mainly behaves like
2
+ # any other string, except that it remembers where it came from (offset in
3
+ # original input).
4
+ #
5
+ # == Extracting line and column
6
+ #
7
+ # Using the #line_and_column method, you can extract the line and column in
8
+ # the original input where this slice starts.
9
+ #
10
+ # Example:
11
+ # slice.line_and_column # => [1, 13]
12
+ # slice.offset # => 12
13
+ #
14
+ # == Likeness to strings
15
+ #
16
+ # Parslet::Slice behaves in many ways like a Ruby String. This likeness
17
+ # however is not complete - many of the myriad of operations String supports
18
+ # are not yet in Slice. You can always extract the internal string instance by
19
+ # calling #to_s.
20
+ #
21
+ # These omissions are somewhat intentional. Rather than maintaining a full
22
+ # delegation, we opt for a partial emulation that gets the job done.
23
+ #
24
+ class Parslet::Slice
25
+ attr_reader :str, :position, :line_cache
26
+
27
+ # Construct a slice using a string, an offset and an optional line cache.
28
+ # The line cache should be able to answer to the #line_and_column message.
29
+ #
30
+ def initialize(position, string, line_cache = nil)
31
+ @position = position
32
+ @str = string
33
+ @line_cache = line_cache
34
+ end
35
+
36
+ def offset
37
+ @position.charpos
38
+ end
39
+
40
+ # Compares slices to other slices or strings.
41
+ #
42
+ def ==(other)
43
+ str == other
44
+ end
45
+
46
+ # Match regular expressions.
47
+ #
48
+ def match(regexp)
49
+ str.match(regexp)
50
+ end
51
+
52
+ # Returns the slices size in characters.
53
+ #
54
+ def size
55
+ str.size
56
+ end
57
+
58
+ alias length size
59
+
60
+ # Concatenate two slices; it is assumed that the second slice begins
61
+ # where the first one ends. The offset of the resulting slice is the same
62
+ # as the one of this slice.
63
+ #
64
+ def +(other)
65
+ self.class.new(@position, str + other.to_s, line_cache)
66
+ end
67
+
68
+ # Returns a <line, column> tuple referring to the original input.
69
+ #
70
+ def line_and_column
71
+ raise ArgumentError, 'No line cache was given, cannot infer line and column.' \
72
+ unless line_cache
73
+
74
+ line_cache.line_and_column(@position.bytepos)
75
+ end
76
+
77
+ # Conversion operators -----------------------------------------------------
78
+ def to_str
79
+ str
80
+ end
81
+ alias to_s to_str
82
+
83
+ def to_slice
84
+ self
85
+ end
86
+
87
+ def to_sym
88
+ str.to_sym
89
+ end
90
+
91
+ def to_i
92
+ self.str.to_i
93
+ end
94
+
95
+ def to_f
96
+ str.to_f
97
+ end
98
+
99
+ # Inspection & Debugging ---------------------------------------------------
100
+
101
+ # Prints the slice as <code>"string"@offset</code>.
102
+ def inspect
103
+ str.inspect + "@#{offset}"
104
+ end
105
+ end
@@ -0,0 +1,99 @@
1
+
2
+
3
+ class Parslet::Source
4
+ # A cache for line start positions.
5
+ #
6
+ class LineCache
7
+ def initialize
8
+ # Stores line endings as a simple position number. The first line always
9
+ # starts at 0; numbers beyond the biggest entry are on any line > size,
10
+ # but probably make a scan to that position neccessary.
11
+ @line_ends = []
12
+ @line_ends.extend RangeSearch
13
+ @last_line_end = nil
14
+ end
15
+
16
+ # Returns a <line, column> tuple for the given input position. Input
17
+ # position must be given as byte offset into original string.
18
+ #
19
+ def line_and_column(pos)
20
+ pos = pos.bytepos if pos.respond_to? :bytepos
21
+ eol_idx = @line_ends.lbound(pos)
22
+
23
+ if eol_idx
24
+ # eol_idx points to the offset that ends the current line.
25
+ # Let's try to find the offset that starts it:
26
+ offset = eol_idx>0 && @line_ends[eol_idx-1] || 0
27
+ return [eol_idx+1, pos-offset+1]
28
+ else
29
+ # eol_idx is nil, that means that we're beyond the last line end that
30
+ # we know about. Pretend for now that we're just on the last line.
31
+ offset = @line_ends.last || 0
32
+ return [@line_ends.size+1, pos-offset+1]
33
+ end
34
+ end
35
+
36
+ def scan_for_line_endings(start_pos, buf)
37
+ return unless buf
38
+
39
+ buf = StringScanner.new(buf)
40
+ return unless buf.exist?(/\n/)
41
+
42
+ ## If we have already read part or all of buf, we already know about
43
+ ## line ends in that portion. remove it and correct cur (search index)
44
+ if @last_line_end && start_pos < @last_line_end
45
+ # Let's not search the range from start_pos to last_line_end again.
46
+ buf.pos = @last_line_end - start_pos
47
+ end
48
+
49
+ ## Scan the string for line endings; store the positions of all endings
50
+ ## in @line_ends.
51
+ while buf.skip_until(/\n/)
52
+ @last_line_end = start_pos + buf.pos
53
+ @line_ends << @last_line_end
54
+ end
55
+ end
56
+ end
57
+
58
+ # Mixin for arrays that implicitly give a number of ranges, where one range
59
+ # begins where the other one ends.
60
+ #
61
+ # Example:
62
+ #
63
+ # [10, 20, 30]
64
+ # # would describe [0, 10], (10, 20], (20, 30]
65
+ #
66
+ module RangeSearch
67
+ def find_mid(left, right)
68
+ # NOTE: Jonathan Hinkle reported that when mathn is required, just
69
+ # dividing and relying on the integer truncation is not enough.
70
+ left + ((right - left) / 2).floor
71
+ end
72
+
73
+ # Scans the array for the first number that is > than bound. Returns the
74
+ # index of that number.
75
+ #
76
+ def lbound(bound)
77
+ return nil if empty?
78
+ return nil unless last > bound
79
+
80
+ left = 0
81
+ right = size - 1
82
+
83
+ loop do
84
+ mid = find_mid(left, right)
85
+
86
+ if self[mid] > bound
87
+ right = mid
88
+ else
89
+ # assert: self[mid] <= bound
90
+ left = mid+1
91
+ end
92
+
93
+ if right <= left
94
+ return right
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,96 @@
1
+
2
+ require 'stringio'
3
+ require 'strscan'
4
+
5
+ require 'parslet/position'
6
+ require 'parslet/source/line_cache'
7
+
8
+ module Parslet
9
+ # Wraps the input string for parslet.
10
+ #
11
+ class Source
12
+ def initialize(str)
13
+ raise(
14
+ ArgumentError,
15
+ "Must construct Source with a string like object."
16
+ ) unless str.respond_to?(:to_str)
17
+
18
+ @str = StringScanner.new(str)
19
+
20
+ # maps 1 => /./m, 2 => /../m, etc...
21
+ @re_cache = Hash.new { |h,k|
22
+ h[k] = /(.|$){#{k}}/m }
23
+
24
+ @line_cache = LineCache.new
25
+ @line_cache.scan_for_line_endings(0, str)
26
+ end
27
+
28
+ # Checks if the given pattern matches at the current input position.
29
+ #
30
+ # @param pattern [Regexp] pattern to check for
31
+ # @return [Boolean] true if the pattern matches at #pos
32
+ #
33
+ def matches?(pattern)
34
+ @str.match?(pattern)
35
+ end
36
+ alias match matches?
37
+
38
+ # Consumes n characters from the input, returning them as a slice of the
39
+ # input.
40
+ #
41
+ def consume(n)
42
+ position = self.pos
43
+ slice_str = @str.scan(@re_cache[n])
44
+ slice = Parslet::Slice.new(
45
+ position,
46
+ slice_str,
47
+ @line_cache)
48
+
49
+ return slice
50
+ end
51
+
52
+ # Returns how many chars remain in the input.
53
+ #
54
+ def chars_left
55
+ @str.rest_size
56
+ end
57
+
58
+ # Returns how many chars there are between current position and the
59
+ # string given. If the string given doesn't occur in the source, then
60
+ # the remaining chars (#chars_left) are returned.
61
+ #
62
+ # @return [Fixnum] count of chars until str or #chars_left
63
+ #
64
+ def chars_until str
65
+ slice_str = @str.check_until(Regexp.new(Regexp.escape(str)))
66
+ return chars_left unless slice_str
67
+ return slice_str.size - str.size
68
+ end
69
+
70
+ # Position of the parse as a character offset into the original string.
71
+ #
72
+ # @note Please be aware of encodings at this point.
73
+ #
74
+ def pos
75
+ Position.new(@str.string, @str.pos)
76
+ end
77
+ def bytepos
78
+ @str.pos
79
+ end
80
+
81
+ # @note Please be aware of encodings at this point.
82
+ #
83
+ def bytepos=(n)
84
+ @str.pos = n
85
+ rescue RangeError
86
+ end
87
+
88
+ # Returns a <line, column> tuple for the given position. If no position is
89
+ # given, line/column information is returned for the current position
90
+ # given by #pos.
91
+ #
92
+ def line_and_column(position=nil)
93
+ @line_cache.line_and_column(position || self.bytepos)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,265 @@
1
+
2
+ require 'parslet/pattern'
3
+
4
+ # Transforms an expression tree into something else. The transformation
5
+ # performs a depth-first, post-order traversal of the expression tree. During
6
+ # that traversal, each time a rule matches a node, the node is replaced by the
7
+ # result of the block associated to the rule. Otherwise the node is accepted
8
+ # as is into the result tree.
9
+ #
10
+ # This is almost what you would generally do with a tree visitor, except that
11
+ # you can match several levels of the tree at once.
12
+ #
13
+ # As a consequence of this, the resulting tree will contain pieces of the
14
+ # original tree and new pieces. Most likely, you will want to transform the
15
+ # original tree wholly, so this isn't a problem.
16
+ #
17
+ # You will not be able to create a loop, given that each node will be replaced
18
+ # only once and then left alone. This means that the results of a replacement
19
+ # will not be acted upon.
20
+ #
21
+ # Example:
22
+ #
23
+ # class Example < Parslet::Transform
24
+ # rule(:string => simple(:x)) { # (1)
25
+ # StringLiteral.new(x)
26
+ # }
27
+ # end
28
+ #
29
+ # A tree transform (Parslet::Transform) is defined by a set of rules. Each
30
+ # rule can be defined by calling #rule with the pattern as argument. The block
31
+ # given will be called every time the rule matches somewhere in the tree given
32
+ # to #apply. It is passed a Hash containing all the variable bindings of this
33
+ # pattern match.
34
+ #
35
+ # In the above example, (1) illustrates a simple matching rule.
36
+ #
37
+ # Let's say you want to parse matching parentheses and distill a maximum nest
38
+ # depth. You would probably write a parser like the one in example/parens.rb;
39
+ # here's the relevant part:
40
+ #
41
+ # rule(:balanced) {
42
+ # str('(').as(:l) >> balanced.maybe.as(:m) >> str(')').as(:r)
43
+ # }
44
+ #
45
+ # If you now apply this to a string like '(())', you get a intermediate parse
46
+ # tree that looks like this:
47
+ #
48
+ # {
49
+ # l: '(',
50
+ # m: {
51
+ # l: '(',
52
+ # m: nil,
53
+ # r: ')'
54
+ # },
55
+ # r: ')'
56
+ # }
57
+ #
58
+ # This parse tree is good for debugging, but what we would really like to have
59
+ # is just the nesting depth. This transformation rule will produce that:
60
+ #
61
+ # rule(:l => '(', :m => simple(:x), :r => ')') {
62
+ # # innermost :m will contain nil
63
+ # x.nil? ? 1 : x+1
64
+ # }
65
+ #
66
+ # = Usage patterns
67
+ #
68
+ # There are four ways of using this class. The first one is very much
69
+ # recommended, followed by the second one for generality. The other ones are
70
+ # omitted here.
71
+ #
72
+ # Recommended usage is as follows:
73
+ #
74
+ # class MyTransformator < Parslet::Transform
75
+ # rule(...) { ... }
76
+ # rule(...) { ... }
77
+ # # ...
78
+ # end
79
+ # MyTransformator.new.apply(tree)
80
+ #
81
+ # Alternatively, you can use the Transform class as follows:
82
+ #
83
+ # transform = Parslet::Transform.new do
84
+ # rule(...) { ... }
85
+ # end
86
+ # transform.apply(tree)
87
+ #
88
+ # = Execution context
89
+ #
90
+ # The execution context of action blocks differs depending on the arity of
91
+ # said blocks. This can be confusing. It is however somewhat intentional. You
92
+ # should not create fat Transform descendants containing a lot of helper methods,
93
+ # instead keep your AST class construction in global scope or make it available
94
+ # through a factory. The following piece of code illustrates usage of global
95
+ # scope:
96
+ #
97
+ # transform = Parslet::Transform.new do
98
+ # rule(...) { AstNode.new(a_variable) }
99
+ # rule(...) { Ast.node(a_variable) } # modules are nice
100
+ # end
101
+ # transform.apply(tree)
102
+ #
103
+ # And here's how you would use a class builder (a factory):
104
+ #
105
+ # transform = Parslet::Transform.new do
106
+ # rule(...) { builder.add_node(a_variable) }
107
+ # rule(...) { |d| d[:builder].add_node(d[:a_variable]) }
108
+ # end
109
+ # transform.apply(tree, :builder => Builder.new)
110
+ #
111
+ # As you can see, Transform allows you to inject local context for your rule
112
+ # action blocks to use.
113
+ #
114
+ class Parslet::Transform
115
+ # FIXME: Maybe only part of it? Or maybe only include into constructor
116
+ # context?
117
+ include Parslet
118
+
119
+ class << self
120
+ # FIXME: Only do this for subclasses?
121
+ include Parslet
122
+
123
+ # Define a rule for the transform subclass.
124
+ #
125
+ def rule(expression, &block)
126
+ @__transform_rules ||= []
127
+ # Prepend new rules so they have higher precedence than older rules
128
+ @__transform_rules.unshift([Parslet::Pattern.new(expression), block])
129
+ end
130
+
131
+ # Allows accessing the class' rules
132
+ #
133
+ def rules
134
+ @__transform_rules ||= []
135
+ end
136
+
137
+ def inherited(subclass)
138
+ super
139
+ subclass.instance_variable_set(:@__transform_rules, rules.dup)
140
+ end
141
+ end
142
+
143
+ def initialize(raise_on_unmatch=false, &block)
144
+ @raise_on_unmatch = raise_on_unmatch
145
+ @rules = []
146
+
147
+ if block
148
+ instance_eval(&block)
149
+ end
150
+ end
151
+
152
+ # Defines a rule to be applied whenever apply is called on a tree. A rule
153
+ # is composed of two parts:
154
+ #
155
+ # * an *expression pattern*
156
+ # * a *transformation block*
157
+ #
158
+ def rule(expression, &block)
159
+ # Prepend new rules so they have higher precedence than older rules
160
+ @rules.unshift([Parslet::Pattern.new(expression), block])
161
+ end
162
+
163
+ # Applies the transformation to a tree that is generated by Parslet::Parser
164
+ # or a simple parslet. Transformation will proceed down the tree, replacing
165
+ # parts/all of it with new objects. The resulting object will be returned.
166
+ #
167
+ # Using the context parameter, you can inject bindings for the transformation.
168
+ # This can be used to allow access to the outside world from transform blocks,
169
+ # like so:
170
+ #
171
+ # document = # some class that you act on
172
+ # transform.apply(tree, document: document)
173
+ #
174
+ # The above will make document available to all your action blocks:
175
+ #
176
+ # # Variant A
177
+ # rule(...) { document.foo(bar) }
178
+ # # Variant B
179
+ # rule(...) { |d| d[:document].foo(d[:bar]) }
180
+ #
181
+ # @param obj PORO ast to transform
182
+ # @param context start context to inject into the bindings.
183
+ #
184
+ def apply(obj, context=nil)
185
+ transform_elt(
186
+ case obj
187
+ when Hash
188
+ recurse_hash(obj, context)
189
+ when Array
190
+ recurse_array(obj, context)
191
+ else
192
+ obj
193
+ end,
194
+ context
195
+ )
196
+ end
197
+
198
+ # Executes the block on the bindings obtained by Pattern#match, if such a match
199
+ # can be made. Depending on the arity of the given block, it is called in
200
+ # one of two environments: the current one or a clean toplevel environment.
201
+ #
202
+ # If you would like the current environment preserved, please use the
203
+ # arity 1 variant of the block. Alternatively, you can inject a context object
204
+ # and call methods on it (think :ctx => self).
205
+ #
206
+ # # the local variable a is simulated
207
+ # t.call_on_match(:a => :b) { a }
208
+ # # no change of environment here
209
+ # t.call_on_match(:a => :b) { |d| d[:a] }
210
+ #
211
+ def call_on_match(bindings, block)
212
+ if block
213
+ if block.arity == 1
214
+ return block.call(bindings)
215
+ else
216
+ context = Context.new(bindings)
217
+ return context.instance_eval(&block)
218
+ end
219
+ end
220
+ end
221
+
222
+ # Allow easy access to all rules, the ones defined in the instance and the
223
+ # ones predefined in a subclass definition.
224
+ #
225
+ def rules
226
+ self.class.rules + @rules
227
+ end
228
+
229
+ # @api private
230
+ #
231
+ def transform_elt(elt, context)
232
+ rules.each do |pattern, block|
233
+ if bindings=pattern.match(elt, context)
234
+ # Produces transformed value
235
+ return call_on_match(bindings, block)
236
+ end
237
+ end
238
+
239
+ # No rule matched - element is not transformed
240
+ if @raise_on_unmatch && elt.is_a?(Hash)
241
+ elt_types = elt.map do |key, value|
242
+ [ key, value.class ]
243
+ end.to_h
244
+ raise NotImplementedError, "Failed to match `#{elt_types.inspect}`"
245
+ else
246
+ return elt
247
+ end
248
+ end
249
+
250
+ # @api private
251
+ #
252
+ def recurse_hash(hsh, ctx)
253
+ hsh.inject({}) do |new_hsh, (k,v)|
254
+ new_hsh[k] = apply(v, ctx)
255
+ new_hsh
256
+ end
257
+ end
258
+ # @api private
259
+ #
260
+ def recurse_array(ary, ctx)
261
+ ary.map { |elt| apply(elt, ctx) }
262
+ end
263
+ end
264
+
265
+ require 'parslet/context'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parslet
4
+ VERSION = '3.0.0'
5
+ end