meta_parse 0.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 (4) hide show
  1. checksums.yaml +7 -0
  2. data/lib/meta_parse.rb +337 -0
  3. data/lib/util.rb +112 -0
  4. metadata +59 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 35a61cdea68ccbca473d11440d4f21a6c0b19569
4
+ data.tar.gz: b13deef76ec7cbfe70b7177616b9326d9a51ebd8
5
+ SHA512:
6
+ metadata.gz: 991234302ec3a0309825a005feb0328e968324363d5881082e3188c5288b68c056ff857f53d1c08b28fc98bb86919e1a3045c28a37d914d8f67ead924b369c44
7
+ data.tar.gz: 123ec7e2bfde4e868fda6d93c3692b45dd449e37da1aa2cadf46269a89b132e4b0ab28dce160890846737fe0cc456929cd18cb4ee318016907d73264ae402da9
data/lib/meta_parse.rb ADDED
@@ -0,0 +1,337 @@
1
+ require 'strscan'
2
+
3
+ module MetaParse
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ ### Begin Interface
10
+
11
+ def parse_with_method(method_name, string)
12
+ self.send(method_name, string.meta(self), self)
13
+ end
14
+
15
+ module ClassMethods
16
+ # Defined a method which takes a scanner.
17
+ # The block passed should return a Matcher or Matcher spec, which is compiled to a Matcher if necessary.
18
+ # The result of calling the defined method is the same as calling match? on the resulting Matcher.
19
+ def match_method(name, &block)
20
+ match_spec = yield
21
+ matcher = MetaParse::Matcher.compile(match_spec)
22
+
23
+ define_matcher_method(name, matcher)
24
+ end
25
+
26
+ def define_matcher_method(name, matcher)
27
+ self.send(:define_method, name) do |scanner, context=nil|
28
+ matcher.match scanner, context
29
+ end
30
+ end
31
+
32
+ def rep(*args, &block)
33
+ Matcher.compile([:*, *args])
34
+ end
35
+
36
+ # Return a sequential matcher. If block is supplied, it defines a function which will be passed an array
37
+ # of all matched values and which should return a non-nil result for the match as a whole.
38
+ def seq(*args, &block)
39
+ if block_given?
40
+ wrapped = lambda { |scanner, context|
41
+ result = block.call context.matches
42
+ context.matches = []
43
+ result
44
+ }
45
+ args << wrapped
46
+ end
47
+ Matcher.compile([:and, *args])
48
+ end
49
+
50
+ def alt(*args)
51
+ Matcher.compile([:or, *args])
52
+ end
53
+
54
+ def comp(spec)
55
+ Matcher.compile(spec)
56
+ end
57
+
58
+ end
59
+ ### End Interface
60
+
61
+ class MetaScanner < StringScanner
62
+ attr_accessor :parser
63
+
64
+ def initialize(string, parser=nil)
65
+ super string
66
+ @parser = parser
67
+ end
68
+
69
+ # This is a special case and could actually be handled by match_string if necessary.
70
+ def match_char(char)
71
+ c = peek(1)
72
+ if c == char
73
+ self.pos += 1
74
+ c
75
+ end
76
+ end
77
+
78
+ def match_string(str, position2=0)
79
+ if (string.equal_at(pos, str, position2))
80
+ self.pos += str.length
81
+ str
82
+ end
83
+ end
84
+
85
+ def scan(spec)
86
+ case spec
87
+ when Regexp
88
+ result = super spec
89
+ matched
90
+ when String
91
+ if spec.length > 0
92
+ match_string(spec)
93
+ else
94
+ match_char(spec)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ class Matcher
101
+ attr_accessor :spec
102
+
103
+ def self.compile(spec)
104
+ case spec
105
+ when Matcher
106
+ spec
107
+ when String, Regexp
108
+ Matcher.new spec
109
+ when Array
110
+ case spec[0]
111
+ when :or
112
+ compiled_body = spec[1..-1].map { |x| self.compile x }
113
+ AlternativeMatcher.new(compiled_body)
114
+ when :and
115
+ compiled_body = spec[1..-1].map { |x| self.compile x }
116
+ SequentialMatcher.new(compiled_body)
117
+ when :*, :+, :'?'
118
+ compiled_body = self.compile(spec[1])
119
+ case spec[0]
120
+ when :'?'
121
+ min = 0
122
+ max = 1
123
+ when :+
124
+ min = 1
125
+ end
126
+ RepetitionMatcher.new(compiled_body, min, max, *spec[4..-1])
127
+ end
128
+ when Proc, Symbol
129
+ FunctionMatcher.new spec
130
+ end
131
+ end
132
+
133
+ def initialize(data)
134
+ @spec = data
135
+ end
136
+
137
+ def show
138
+ spec.inspect
139
+ end
140
+
141
+ def inspect
142
+ "<match #{show}>"
143
+ end
144
+
145
+ def match?(scanner, context=nil)
146
+ case scanner
147
+ when MetaScanner
148
+ result = scanner.scan spec
149
+ result
150
+ else
151
+ raise "match? requires scanner"
152
+ # FIXME: Why doesn't the coercion below work?
153
+ # when String
154
+ # match?(MetaParse::MetaScanner.new(scanner), context)
155
+ # (MetaScanner.new(scanner)).scan spec
156
+ end
157
+ end
158
+
159
+ def match(scanner, context=nil)
160
+ (stateful ? clone : self).match? scanner, context
161
+ end
162
+
163
+ def stateful
164
+ false
165
+ end
166
+
167
+ def m?(string)
168
+ match? MetaScanner.new(string)
169
+ end
170
+
171
+ def m(string)
172
+ match MetaScanner.new(string)
173
+ end
174
+
175
+ end
176
+
177
+ class RepetitionMatcher < Matcher
178
+ attr_accessor :min, :max, :reducer
179
+
180
+ def initialize(sub_match, min=0, max=nil, reducer=nil, initial_value=[])
181
+ @spec, @min, @max, @reducer, @initial_value = sub_match, min, max, reducer, initial_value
182
+ end
183
+
184
+ def stateful
185
+ true
186
+ end
187
+
188
+ def match?(scanner, context=nil)
189
+ matches = []
190
+ while (!max || (matches.count < max)) && (match = spec.match(scanner))
191
+ matches << match
192
+ end
193
+
194
+ unless min && (matches.count < min)
195
+ case finalizer
196
+ when Proc
197
+ finalizer.call(matches, *finalizer_args)
198
+ when Symbol
199
+ send(finalizer, matches, *finalizer_args)
200
+ when nil
201
+ matches
202
+ end
203
+ end
204
+ end
205
+
206
+ def match?(scanner, context=nil)
207
+ # Need to copy the initial value since it is potentially destructively modified (if an array, for example).
208
+ acc = begin @initial_value.dup
209
+ rescue TypeError
210
+ @initial_value
211
+ end
212
+ match_count = 0
213
+
214
+ while (!max || (match_count < max)) && (one_match = spec.match(scanner))
215
+ match_count += 1
216
+ if reducer
217
+ acc = reducer.call(acc, one_match)
218
+ else
219
+ acc << one_match
220
+ end
221
+ end
222
+ unless min && (match_count < min)
223
+ acc
224
+ end
225
+ end
226
+
227
+ def show
228
+ "[#{min}, #{max}] #{ spec.show }"
229
+ end
230
+ end
231
+
232
+ class AlternativeMatcher < Matcher
233
+ def match?(scanner, context=nil)
234
+ spec.each do |alternative|
235
+ result = alternative.match(scanner)
236
+ return result if result
237
+ end
238
+ return nil
239
+ end
240
+
241
+ def show
242
+ "first of: (#{ (spec.map &:show).join ', ' })"
243
+ end
244
+ end
245
+
246
+ class SequentialMatcher < Matcher
247
+ attr_accessor :matches
248
+
249
+ def stateful
250
+ true
251
+ end
252
+
253
+ def match?(scanner, context = nil)
254
+ @matches = []
255
+ initial_position = scanner.pos
256
+ last_match = nil
257
+
258
+ spec.each do |element|
259
+ last_match = element.match(scanner, self)
260
+ unless last_match
261
+ scanner.pos = initial_position
262
+ return nil
263
+ end
264
+ @matches << last_match
265
+ end
266
+ return last_match
267
+ end
268
+
269
+ def pop
270
+ @matches.pop
271
+ end
272
+
273
+ def push(value)
274
+ @matches.push(value)
275
+ end
276
+
277
+ def clear
278
+ @matches = []
279
+ end
280
+
281
+ def show
282
+ "sequentially: (#{ (spec.map &:show).join ', ' })"
283
+ end
284
+ end
285
+
286
+ # Arbitrary predicate, particularly useful for generating final return value from SequentialMatchers.
287
+ # Context is the containing matcher, and in the case of SequentialMatcher includes access to accumulated matches.
288
+ class FunctionMatcher < Matcher
289
+ include MetaParse
290
+
291
+ def match?(scanner, context=nil)
292
+ case spec
293
+ when Proc
294
+ spec.call(scanner, context)
295
+ when Symbol
296
+ scanner.parser.send spec, scanner, context
297
+ end
298
+ end
299
+ end
300
+
301
+ def all_matches(scanner, context)
302
+ matches_so_far = context.matches
303
+ context.matches = []
304
+ matches_so_far
305
+ end
306
+
307
+ def all_matches_joined(scanner, context)
308
+ all_matches(scanner,context).join
309
+ end
310
+
311
+ def join_strings(array, *args)
312
+ array.join(*args)
313
+ end
314
+
315
+ def collapse(&block)
316
+ lambda { |scanner, context|
317
+ stack = context.matches
318
+ result = block.call(stack)
319
+ context.clear
320
+ result
321
+ }
322
+ end
323
+ end
324
+
325
+ class String
326
+ def equal_at(position, string, position2=0)
327
+ for i in position2..(position2 + string.length - 1) do
328
+ return nil unless self[position] == string[i]
329
+ position += 1
330
+ end
331
+ return true
332
+ end
333
+
334
+ def meta(parser=nil)
335
+ MetaParse::MetaScanner.new(self, parser)
336
+ end
337
+ end
data/lib/util.rb ADDED
@@ -0,0 +1,112 @@
1
+ # require 'util'
2
+
3
+ module Util
4
+ def mgsub(string, substitution_hash)
5
+ Util::mgsub(string, substitution_hash)
6
+ end
7
+
8
+ # We can't use a closure (for now) because JRuby (as of 1.7.14) doesn't handle return correctly in lambdas.
9
+ # It returns from the containing function. Here we depend on an early return from the actual function only.
10
+ def self.make_substitution_function(regexp_hash)
11
+ ->(matched_string) {
12
+ result = nil # Poor man's early return because of JRuby bug.
13
+
14
+ regexp_hash.each do |regexp, substitution|
15
+ if !result && matched_string.match(regexp)
16
+ match_data = Regexp.last_match
17
+ result = case substitution
18
+ when String
19
+ substitution
20
+ else
21
+ substitution.call(match_data)
22
+ end
23
+ end
24
+ end
25
+
26
+ result
27
+ }
28
+ end
29
+
30
+ # mgsub == multiple, global substitution.
31
+ def self.mgsub(string, substitution_hash)
32
+ all_regexps = []
33
+ regexp_hash = {}
34
+
35
+ substitution_hash.each do |key, substitution|
36
+ regexp = to_regexp(key)
37
+ all_regexps << regexp
38
+ regexp_hash[regexp] = substitution
39
+ end
40
+
41
+ # puts "all_regexps: #{all_regexps.inspect}"
42
+ combined_regexp = Regexp.union(*all_regexps)
43
+
44
+ substitution_function = make_substitution_function(regexp_hash)
45
+
46
+ string.gsub(combined_regexp, &substitution_function)
47
+ end
48
+
49
+ def self.to_regexp(spec)
50
+ case spec
51
+ when Regexp
52
+ spec
53
+ when String
54
+ Regexp.escape(spec)
55
+ end
56
+ end
57
+ end
58
+
59
+ module Util
60
+ class Maybe < BasicObject
61
+ def initialize(object, default = nil)
62
+ @object = object
63
+ @default = default
64
+ @block = block
65
+ end
66
+
67
+
68
+ def send(method, *args, &block)
69
+ if @object.respond_to?(method)
70
+ @object.send(method, *args, &block)
71
+ else
72
+ @default
73
+ end
74
+ end
75
+
76
+ def method_missing(method, *args, &block)
77
+ send(method, *args, &block)
78
+ end
79
+ end
80
+ end
81
+
82
+ class Object
83
+ def self
84
+ self
85
+ end
86
+
87
+ def maybe(default = nil)
88
+ Util::Maybe.new(self, default)
89
+ end
90
+
91
+ def perhaps(default = self)
92
+ maybe(default)
93
+ end
94
+
95
+ class String < Object
96
+ def mgsub(substitution_hash)
97
+ Util::mgsub(self, substitution_hash)
98
+ end
99
+ end
100
+
101
+ =begin
102
+
103
+ Perhaps is intended especially for type coercions, like:
104
+
105
+ def whatever(input)
106
+ input = input.perhaps.to_path
107
+ input = input.to_s
108
+ end
109
+
110
+ =end
111
+
112
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: meta_parse
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Chhi'mèd Künzang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-11-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Functional recursive descent parsing.
28
+ email: clkunzang@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/meta_parse.rb
34
+ - lib/util.rb
35
+ homepage:
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 2.1.2
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Parse by recursive descent.
59
+ test_files: []