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.
- checksums.yaml +7 -0
- data/lib/meta_parse.rb +337 -0
- data/lib/util.rb +112 -0
- 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: []
|