zenml 1.2.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/source/zenml/converter.rb +10 -5
- data/source/zenml/parser.rb +213 -185
- data/source/zenml/parser_utility.rb +157 -175
- data/source/zenml/reader.rb +6 -11
- data/source/zenml.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc0c51d446a26f7b640f5a4ab3c4054d131ab89f98ab7c0effca28387e06b9b9
|
4
|
+
data.tar.gz: 43860c32ec5abf88af046191eeb57941ebc67cfde701229b459dfb2c9439f525
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: baf2791fe4d95b718a2251152d8edb917e92c3d0da35ed9578ae1b4ac8fb2c48964223819eb9e06bd6619a25ee1e4bf84c4b10c81cf955f4a7b877a3a0db727d
|
7
|
+
data.tar.gz: 0e8b566980d4870679e70426c5da2114abb3927c9c59f077f0e100b5c6685fab507bc141708744fb1d37f5294eb401900bf5019fa76a39e6d5e4e24a84ff5986
|
data/source/zenml/converter.rb
CHANGED
@@ -18,8 +18,8 @@ class ZenithalConverter
|
|
18
18
|
@configs = {}
|
19
19
|
@templates = {}
|
20
20
|
@functions = {}
|
21
|
-
@default_element_template = lambda{|
|
22
|
-
@default_text_template = lambda{|
|
21
|
+
@default_element_template = lambda{|_| empty_nodes}
|
22
|
+
@default_text_template = lambda{|_| empty_nodes}
|
23
23
|
end
|
24
24
|
|
25
25
|
def convert(initial_scope = "")
|
@@ -59,7 +59,7 @@ class ZenithalConverter
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def apply(element, scope, *args)
|
62
|
-
nodes =
|
62
|
+
nodes = empty_nodes
|
63
63
|
element.children.each do |child|
|
64
64
|
case child
|
65
65
|
when Element
|
@@ -78,7 +78,7 @@ class ZenithalConverter
|
|
78
78
|
end
|
79
79
|
|
80
80
|
def apply_select(element, xpath, scope, *args)
|
81
|
-
nodes =
|
81
|
+
nodes = empty_nodes
|
82
82
|
element.each_xpath(xpath) do |child|
|
83
83
|
case child
|
84
84
|
when Element
|
@@ -97,7 +97,7 @@ class ZenithalConverter
|
|
97
97
|
end
|
98
98
|
|
99
99
|
def call(element, name, *args)
|
100
|
-
nodes =
|
100
|
+
nodes = empty_nodes
|
101
101
|
@functions.each do |function_name, block|
|
102
102
|
if function_name == name
|
103
103
|
nodes = instance_exec(element, *args, &block)
|
@@ -123,6 +123,11 @@ class ZenithalConverter
|
|
123
123
|
end
|
124
124
|
end
|
125
125
|
|
126
|
+
def empty_nodes
|
127
|
+
return (@type == :text) ? "" : Nodes[]
|
128
|
+
end
|
129
|
+
|
130
|
+
## Returns a simple converter that converts an XML document to the equivalent HTML document.
|
126
131
|
def self.simple_html(document)
|
127
132
|
converter = ZenithalConverter.new(document, :text)
|
128
133
|
converter.add([//], [""]) do |element|
|
data/source/zenml/parser.rb
CHANGED
@@ -44,262 +44,271 @@ module ZenithalParserMethod
|
|
44
44
|
0x3001..0xD7FF, 0xF900..0xFDCF, 0xFDF0..0xFFFD, 0x10000..0xEFFFF
|
45
45
|
]
|
46
46
|
|
47
|
+
def parse
|
48
|
+
unless @inside_run
|
49
|
+
warn("This method is now only for internal use. Use 'run' instead.", uplevel: 1)
|
50
|
+
end
|
51
|
+
if @whole
|
52
|
+
parse_document
|
53
|
+
else
|
54
|
+
parse_nodes({})
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
47
58
|
private
|
48
59
|
|
49
60
|
def parse_document
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
children.each do |child|
|
55
|
-
document.add(child)
|
56
|
-
end
|
57
|
-
next document
|
61
|
+
document = Document.new
|
62
|
+
children = parse_nodes({})
|
63
|
+
if @exact
|
64
|
+
parse_eof
|
58
65
|
end
|
59
|
-
|
66
|
+
children.each do |child|
|
67
|
+
document.add(child)
|
68
|
+
end
|
69
|
+
return document
|
60
70
|
end
|
61
71
|
|
62
72
|
def parse_nodes(options)
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
73
|
+
nodes = nil
|
74
|
+
if options[:plugin]
|
75
|
+
nodes = options[:plugin].parse
|
76
|
+
elsif options[:verbal]
|
77
|
+
raw_nodes = many(->{parse_text(options)})
|
78
|
+
nodes = raw_nodes.inject(Nodes[], :<<)
|
79
|
+
else
|
80
|
+
parsers = []
|
81
|
+
parsers << ->{parse_element(options)}
|
82
|
+
if options[:in_slash]
|
83
|
+
next_options = options.clone
|
84
|
+
next_options.delete(:in_slash)
|
85
|
+
else
|
86
|
+
next_options = options
|
87
|
+
end
|
88
|
+
@special_element_names.each do |kind, name|
|
89
|
+
unless kind == :slash && options[:in_slash]
|
90
|
+
parsers << ->{parse_special_element(kind, next_options)}
|
69
91
|
end
|
70
92
|
end
|
71
|
-
|
93
|
+
parsers << ->{parse_comment(options)}
|
94
|
+
parsers << ->{parse_text(options)}
|
95
|
+
raw_nodes = many(->{choose(*parsers)})
|
72
96
|
nodes = raw_nodes.inject(Nodes[], :<<)
|
73
|
-
next nodes
|
74
97
|
end
|
75
|
-
return
|
98
|
+
return nodes
|
76
99
|
end
|
77
100
|
|
78
101
|
def parse_element(options)
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
end
|
102
|
+
start_char = parse_char_any([ELEMENT_START, MACRO_START])
|
103
|
+
name = parse_identifier(options)
|
104
|
+
marks = parse_marks(options)
|
105
|
+
attributes = maybe(->{parse_attributes(options)}) || {}
|
106
|
+
macro = start_char == MACRO_START
|
107
|
+
next_options = determine_options(name, marks, attributes, macro, options)
|
108
|
+
children_list = parse_children_list(next_options)
|
109
|
+
if name == SYSTEM_INSTRUCTION_NAME
|
110
|
+
parse_space
|
111
|
+
end
|
112
|
+
if start_char == MACRO_START
|
113
|
+
element = process_macro(name, marks, attributes, children_list, options)
|
114
|
+
else
|
115
|
+
element = create_element(name, marks, attributes, children_list, options)
|
94
116
|
end
|
95
|
-
return
|
117
|
+
return element
|
96
118
|
end
|
97
119
|
|
98
120
|
def parse_special_element(kind, options)
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
+parse_char(SPECIAL_ELEMENT_ENDS[kind])
|
106
|
-
next create_special_element(kind, children, options)
|
121
|
+
parse_char(SPECIAL_ELEMENT_STARTS[kind])
|
122
|
+
if kind == :slash
|
123
|
+
next_options = options.clone
|
124
|
+
next_options[:in_slash] = true
|
125
|
+
else
|
126
|
+
next_options = options
|
107
127
|
end
|
108
|
-
|
128
|
+
children = parse_nodes(next_options)
|
129
|
+
parse_char(SPECIAL_ELEMENT_ENDS[kind])
|
130
|
+
element = create_special_element(kind, children, options)
|
131
|
+
return element
|
109
132
|
end
|
110
133
|
|
111
134
|
def parse_marks(options)
|
112
|
-
|
135
|
+
marks = many(->{parse_mark(options)})
|
136
|
+
return marks
|
113
137
|
end
|
114
138
|
|
115
139
|
def parse_mark(options)
|
116
140
|
parsers = MARKS.map do |mark, query|
|
117
|
-
next parse_char(query).
|
141
|
+
next ->{parse_char(query).yield_self{|_| mark}}
|
118
142
|
end
|
119
|
-
|
143
|
+
mark = choose(*parsers)
|
144
|
+
return mark
|
120
145
|
end
|
121
146
|
|
122
147
|
def parse_attributes(options)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
next attributes
|
130
|
-
end
|
131
|
-
return parser
|
148
|
+
parse_char(ATTRIBUTE_START)
|
149
|
+
first_attribute = parse_attribute(true, options)
|
150
|
+
rest_attribtues = many(->{parse_attribute(false, options)})
|
151
|
+
attributes = first_attribute.merge(*rest_attribtues)
|
152
|
+
parse_char(ATTRIBUTE_END)
|
153
|
+
return attributes
|
132
154
|
end
|
133
155
|
|
134
156
|
def parse_attribute(first, options)
|
135
|
-
|
136
|
-
|
137
|
-
+parse_space
|
138
|
-
name = +parse_identifier(options)
|
139
|
-
+parse_space
|
140
|
-
value = +parse_attribute_value(options).maybe || name
|
141
|
-
+parse_space
|
142
|
-
next {name => value}
|
157
|
+
unless first
|
158
|
+
parse_char(ATTRIBUTE_SEPARATOR)
|
143
159
|
end
|
144
|
-
|
160
|
+
parse_space
|
161
|
+
name = parse_identifier(options)
|
162
|
+
parse_space
|
163
|
+
value = maybe(->{parse_attribute_value(options)}) || name
|
164
|
+
parse_space
|
165
|
+
attribute = {name => value}
|
166
|
+
return attribute
|
145
167
|
end
|
146
168
|
|
147
169
|
def parse_attribute_value(options)
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
next value
|
153
|
-
end
|
154
|
-
return parser
|
170
|
+
parse_char(ATTRIBUTE_EQUAL)
|
171
|
+
parse_space
|
172
|
+
value = parse_string(options)
|
173
|
+
return value
|
155
174
|
end
|
156
175
|
|
157
176
|
def parse_string(options)
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
end
|
164
|
-
return parser
|
177
|
+
parse_char(STRING_START)
|
178
|
+
strings = many(->{parse_string_plain_or_escape(options)})
|
179
|
+
parse_char(STRING_END)
|
180
|
+
string = strings.join
|
181
|
+
return string
|
165
182
|
end
|
166
183
|
|
167
184
|
def parse_string_plain(options)
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
185
|
+
chars = many(->{parse_char_out([STRING_END, ESCAPE_START])}, 1..)
|
186
|
+
string = chars.join
|
187
|
+
return string
|
188
|
+
end
|
189
|
+
|
190
|
+
def parse_string_plain_or_escape(options)
|
191
|
+
string = choose(->{parse_escape(:string, options)}, ->{parse_string_plain(options)})
|
192
|
+
return string
|
173
193
|
end
|
174
194
|
|
175
195
|
def parse_children_list(options)
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
next children_list
|
181
|
-
end
|
182
|
-
return parser
|
196
|
+
first_children = choose(->{parse_empty_children(options)}, ->{parse_children(options)})
|
197
|
+
rest_children_list = many(->{parse_children(options)})
|
198
|
+
children_list = [first_children] + rest_children_list
|
199
|
+
return children_list
|
183
200
|
end
|
184
201
|
|
185
202
|
def parse_children(options)
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
next children
|
191
|
-
end
|
192
|
-
return parser
|
203
|
+
parse_char(CONTENT_START)
|
204
|
+
children = parse_nodes(options)
|
205
|
+
parse_char(CONTENT_END)
|
206
|
+
return children
|
193
207
|
end
|
194
208
|
|
195
209
|
def parse_empty_children(options)
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
end
|
200
|
-
return parser
|
210
|
+
parse_char(CONTENT_END)
|
211
|
+
children = Nodes[]
|
212
|
+
return children
|
201
213
|
end
|
202
214
|
|
203
215
|
def parse_text(options)
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
end
|
208
|
-
return parser
|
216
|
+
raw_texts = many(->{parse_text_plain_or_escape(options)}, 1..)
|
217
|
+
text = create_text(raw_texts.join, options)
|
218
|
+
return text
|
209
219
|
end
|
210
220
|
|
211
221
|
def parse_text_plain(options)
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
out_chars.push(SPECIAL_ELEMENT_STARTS[kind], SPECIAL_ELEMENT_ENDS[kind]) if name
|
218
|
-
end
|
222
|
+
out_chars = [ESCAPE_START, CONTENT_END]
|
223
|
+
unless options[:verbal]
|
224
|
+
out_chars.push(ELEMENT_START, MACRO_START, CONTENT_START, COMMENT_DELIMITER)
|
225
|
+
@special_element_names.each do |kind, name|
|
226
|
+
out_chars.push(SPECIAL_ELEMENT_STARTS[kind], SPECIAL_ELEMENT_ENDS[kind])
|
219
227
|
end
|
220
|
-
chars = +parse_char_out(out_chars).many(1)
|
221
|
-
next chars.join
|
222
228
|
end
|
223
|
-
|
229
|
+
chars = many(->{parse_char_out(out_chars)}, 1..)
|
230
|
+
string = chars.join
|
231
|
+
return string
|
232
|
+
end
|
233
|
+
|
234
|
+
def parse_text_plain_or_escape(options)
|
235
|
+
string = choose(->{parse_escape(:text, options)}, ->{parse_text_plain(options)})
|
236
|
+
return string
|
237
|
+
end
|
238
|
+
|
239
|
+
def parse_comment(options)
|
240
|
+
parse_char(COMMENT_DELIMITER)
|
241
|
+
comment = choose(->{parse_line_comment(options)}, ->{parse_block_comment(options)})
|
242
|
+
return comment
|
224
243
|
end
|
225
244
|
|
226
245
|
def parse_line_comment(options)
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
next create_comment(:line, content, options)
|
233
|
-
end
|
234
|
-
return parser
|
246
|
+
parse_char(COMMENT_DELIMITER)
|
247
|
+
content = parse_line_comment_content(options)
|
248
|
+
parse_char("\n")
|
249
|
+
comment = create_comment(:line, content, options)
|
250
|
+
return comment
|
235
251
|
end
|
236
252
|
|
237
253
|
def parse_line_comment_content(options)
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
end
|
242
|
-
return parser
|
254
|
+
chars = many(->{parse_char_out(["\n"])})
|
255
|
+
content = chars.join
|
256
|
+
return content
|
243
257
|
end
|
244
258
|
|
245
259
|
def parse_block_comment(options)
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
next create_comment(:block, content, options)
|
253
|
-
end
|
254
|
-
return parser
|
260
|
+
parse_char(CONTENT_START)
|
261
|
+
content = parse_block_comment_content(options)
|
262
|
+
parse_char(CONTENT_END)
|
263
|
+
parse_char(COMMENT_DELIMITER)
|
264
|
+
comment = create_comment(:block, content, options)
|
265
|
+
return comment
|
255
266
|
end
|
256
267
|
|
257
268
|
def parse_block_comment_content(options)
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
end
|
262
|
-
return parser
|
269
|
+
chars = many(->{parse_char_out([CONTENT_END])})
|
270
|
+
content = chars.join
|
271
|
+
return content
|
263
272
|
end
|
264
273
|
|
265
274
|
def parse_escape(place, options)
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
end
|
271
|
-
return parser
|
275
|
+
parse_char(ESCAPE_START)
|
276
|
+
char = parse_char
|
277
|
+
escape = create_escape(place, char, options)
|
278
|
+
return escape
|
272
279
|
end
|
273
280
|
|
274
281
|
def parse_identifier(options)
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
next identifier
|
280
|
-
end
|
281
|
-
return parser
|
282
|
+
first_char = parse_first_identifier_char(options)
|
283
|
+
rest_chars = many(->{parse_middle_identifier_char(options)})
|
284
|
+
identifier = first_char + rest_chars.join
|
285
|
+
return identifier
|
282
286
|
end
|
283
287
|
|
284
288
|
def parse_first_identifier_char(options)
|
285
|
-
|
289
|
+
char = parse_char_any(VALID_FIRST_IDENTIFIER_CHARS)
|
290
|
+
return char
|
286
291
|
end
|
287
292
|
|
288
293
|
def parse_middle_identifier_char(options)
|
289
|
-
|
294
|
+
char = parse_char_any(VALID_MIDDLE_IDENTIFIER_CHARS)
|
295
|
+
return char
|
290
296
|
end
|
291
297
|
|
292
298
|
def parse_space
|
293
|
-
|
299
|
+
space = many(->{parse_char_any(SPACE_CHARS)})
|
300
|
+
return space
|
294
301
|
end
|
295
302
|
|
296
|
-
# Determines options which are used when parsing the children nodes.
|
297
|
-
# This method may be overrided in order to change the parsing behaviour for another format based on ZenML.
|
298
303
|
def determine_options(name, marks, attributes, macro, options)
|
299
304
|
if marks.include?(:verbal)
|
300
305
|
options = options.clone
|
301
306
|
options[:verbal] = true
|
302
307
|
end
|
308
|
+
if macro && @plugins.key?(name)
|
309
|
+
options = options.clone
|
310
|
+
options[:plugin] = @plugins[name].call(attributes)
|
311
|
+
end
|
303
312
|
return options
|
304
313
|
end
|
305
314
|
|
@@ -312,12 +321,12 @@ module ZenithalParserMethod
|
|
312
321
|
end
|
313
322
|
if marks.include?(:instruction)
|
314
323
|
unless children_list.size <= 1
|
315
|
-
|
324
|
+
throw_custom(error_message("Processing instruction cannot have more than one argument"))
|
316
325
|
end
|
317
326
|
nodes = create_instruction(name, attributes, children_list.first, options)
|
318
327
|
else
|
319
328
|
unless marks.include?(:multiple) || children_list.size <= 1
|
320
|
-
|
329
|
+
throw_custom(error_message("Normal element cannot have more than one argument"))
|
321
330
|
end
|
322
331
|
nodes = create_normal_element(name, attributes, children_list, options)
|
323
332
|
end
|
@@ -369,21 +378,27 @@ module ZenithalParserMethod
|
|
369
378
|
|
370
379
|
def create_special_element(kind, children, options)
|
371
380
|
name = @special_element_names[kind]
|
372
|
-
|
381
|
+
if name
|
382
|
+
nodes = create_element(name, [], {}, [children], options)
|
383
|
+
else
|
384
|
+
throw_custom(error_message("No name specified for #{kind} elements"))
|
385
|
+
end
|
373
386
|
return nodes
|
374
387
|
end
|
375
388
|
|
376
389
|
def create_text(raw_text, options)
|
377
|
-
|
390
|
+
text = Text.new(raw_text, true, nil, false)
|
391
|
+
return text
|
378
392
|
end
|
379
393
|
|
380
394
|
def create_comment(kind, content, options)
|
381
|
-
|
395
|
+
comment = Comment.new(" " + content.strip + " ")
|
396
|
+
return comment
|
382
397
|
end
|
383
398
|
|
384
399
|
def create_escape(place, char, options)
|
385
400
|
unless ESCAPE_CHARS.include?(char)
|
386
|
-
|
401
|
+
throw_custom(error_message("Invalid escape"))
|
387
402
|
end
|
388
403
|
return char
|
389
404
|
end
|
@@ -395,44 +410,57 @@ module ZenithalParserMethod
|
|
395
410
|
raw_elements.each do |raw_element|
|
396
411
|
elements << raw_element
|
397
412
|
end
|
413
|
+
elsif @plugins.key?(name)
|
414
|
+
elements = children_list.first
|
398
415
|
else
|
399
|
-
|
416
|
+
throw_custom(error_message("No such macro '#{name}'"))
|
400
417
|
end
|
401
418
|
return elements
|
402
419
|
end
|
403
420
|
|
404
|
-
def error_message(message)
|
405
|
-
return "[line #{@source.lineno}] #{message}"
|
406
|
-
end
|
407
|
-
|
408
421
|
end
|
409
422
|
|
410
423
|
|
411
|
-
class ZenithalParser
|
424
|
+
class ZenithalParser < Parser
|
412
425
|
|
413
|
-
include CommonParserMethod
|
414
426
|
include ZenithalParserMethod
|
415
427
|
|
416
|
-
|
428
|
+
attr_accessor :exact
|
429
|
+
attr_accessor :whole
|
417
430
|
|
418
431
|
def initialize(source)
|
419
|
-
|
432
|
+
super(source)
|
420
433
|
@version = nil
|
434
|
+
@exact = true
|
435
|
+
@whole = true
|
421
436
|
@special_element_names = {:brace => nil, :bracket => nil, :slash => nil}
|
422
437
|
@macros = {}
|
438
|
+
@plugins = {}
|
423
439
|
end
|
424
440
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
441
|
+
# Registers a macro.
|
442
|
+
# To the argument block will be passed two arguments: the first is a hash of the attributes, the second is a list of the children nodes.
|
443
|
+
def register_macro(name, &block)
|
444
|
+
@macros.store(name, block)
|
445
|
+
end
|
446
|
+
|
447
|
+
def unregister_macro(name)
|
448
|
+
@macros.delete(name)
|
449
|
+
end
|
450
|
+
|
451
|
+
# Registers a plugin, which enables us to apply another parser in certain macros.
|
452
|
+
# If a class instance is passed, simply an instance of that class will be created and used as a custom parser.
|
453
|
+
# If a block is passed, it will be called to create a custom parser.
|
454
|
+
# To this block will be passed one argument: the attributes which are specified to the macro.
|
455
|
+
def register_plugin(name, clazz = nil, &block)
|
456
|
+
if clazz
|
457
|
+
block = lambda{|_| clazz.new(@source)}
|
431
458
|
end
|
459
|
+
@plugins.store(name, block)
|
432
460
|
end
|
433
461
|
|
434
|
-
def
|
435
|
-
@
|
462
|
+
def unregister_plugin(name)
|
463
|
+
@plugins.delete(name)
|
436
464
|
end
|
437
465
|
|
438
466
|
def brace_name=(name)
|
@@ -3,227 +3,209 @@
|
|
3
3
|
|
4
4
|
class Parser
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
parser = Parser.new(source) do
|
15
|
-
value = nil
|
16
|
-
message = catch(:error) do
|
17
|
-
value = block.call
|
18
|
-
end
|
19
|
-
if value
|
20
|
-
next Result.success(value)
|
21
|
-
else
|
22
|
-
next Result.error(message)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
return parser
|
26
|
-
end
|
27
|
-
|
28
|
-
def exec
|
29
|
-
return @builder.instance_eval(&@method)
|
30
|
-
end
|
31
|
-
|
32
|
-
def exec_get
|
33
|
-
result = self.exec
|
34
|
-
if result.success?
|
35
|
-
return result.value
|
6
|
+
ERROR_TAG = Object.new
|
7
|
+
|
8
|
+
def initialize(source)
|
9
|
+
case source
|
10
|
+
when StringReader
|
11
|
+
@source = source
|
12
|
+
when File
|
13
|
+
@source = StringReader.new(source.read)
|
36
14
|
else
|
37
|
-
|
15
|
+
@source = StringReader.new(source.to_s)
|
38
16
|
end
|
17
|
+
@inside_run = false
|
39
18
|
end
|
40
19
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
result = this.exec
|
50
|
-
if result.success?
|
51
|
-
next result
|
52
|
-
else
|
53
|
-
source.reset(mark)
|
54
|
-
result = other.exec
|
55
|
-
next result
|
56
|
-
end
|
20
|
+
def run
|
21
|
+
value = nil
|
22
|
+
message = catch(ERROR_TAG) do
|
23
|
+
begin
|
24
|
+
@inside_run = true
|
25
|
+
value = parse
|
26
|
+
ensure
|
27
|
+
@inside_run = false
|
57
28
|
end
|
58
|
-
return parser
|
59
|
-
else
|
60
|
-
raise StandardError.new("Different source")
|
61
29
|
end
|
62
|
-
|
63
|
-
|
64
|
-
def many(lower_limit = 0, upper_limit = nil)
|
65
|
-
this = self
|
66
|
-
parser = Parser.new(this.builder) do
|
67
|
-
values, count = [], 0
|
68
|
-
loop do
|
69
|
-
mark = source.mark
|
70
|
-
each_result = this.exec
|
71
|
-
if each_result.success?
|
72
|
-
values << each_result.value
|
73
|
-
count += 1
|
74
|
-
if upper_limit && count >= upper_limit
|
75
|
-
break
|
76
|
-
end
|
77
|
-
else
|
78
|
-
source.reset(mark)
|
79
|
-
break
|
80
|
-
end
|
81
|
-
end
|
82
|
-
if count >= lower_limit
|
83
|
-
next Result.success(values)
|
84
|
-
else
|
85
|
-
next Result.error("")
|
86
|
-
end
|
30
|
+
unless value
|
31
|
+
raise ZenithalParseError.new(message)
|
87
32
|
end
|
88
|
-
return
|
89
|
-
end
|
90
|
-
|
91
|
-
def maybe
|
92
|
-
return self.many(0, 1).map{|s| s.first}
|
33
|
+
return value
|
93
34
|
end
|
94
35
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
next Result.success(block.call(result.value))
|
101
|
-
else
|
102
|
-
next result
|
103
|
-
end
|
104
|
-
end
|
105
|
-
return parser
|
36
|
+
# Parses a whole data.
|
37
|
+
# This method is intended to be overridden in subclasses.
|
38
|
+
def parse
|
39
|
+
throw_custom("Not implemented")
|
40
|
+
return nil
|
106
41
|
end
|
107
42
|
|
108
|
-
end
|
109
|
-
|
110
|
-
|
111
|
-
module CommonParserMethod
|
112
|
-
|
113
43
|
private
|
114
44
|
|
115
45
|
# Parses a single character which matches the specified query.
|
116
|
-
# If the next character does not match the query or the end of file is reached, an error
|
117
|
-
# Otherwise, a
|
46
|
+
# If the next character does not match the query or the end of file is reached, then an error occurs and no input is consumed.
|
47
|
+
# Otherwise, a string which consists of the matched single chracter is returned.
|
118
48
|
def parse_char(query = nil)
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
else
|
143
|
-
next Result.error(error_message(message))
|
144
|
-
end
|
145
|
-
else
|
146
|
-
next Result.error(error_message("Unexpected end of file"))
|
49
|
+
char = @source.peek
|
50
|
+
if char
|
51
|
+
predicate, message = false, nil
|
52
|
+
case query
|
53
|
+
when String
|
54
|
+
predicate = query == char
|
55
|
+
message = "Expected '#{query}' but got '#{char}'"
|
56
|
+
when Regexp
|
57
|
+
predicate = query =~ char
|
58
|
+
message = "Expected /#{query}/ but got '#{char}'"
|
59
|
+
when Integer
|
60
|
+
predicate = query == char.ord
|
61
|
+
message = "Expected '##{query}' but got '#{char}'"
|
62
|
+
when Range
|
63
|
+
predicate = query.cover?(char.ord)
|
64
|
+
symbol = (query.exclude_end?) ? "..." : ".."
|
65
|
+
message = "Expected '##{query.begin}'#{symbol}'##{query.end}' but got '#{char}'"
|
66
|
+
when NilClass
|
67
|
+
predicate = true
|
68
|
+
message = ""
|
69
|
+
end
|
70
|
+
unless predicate
|
71
|
+
throw_custom(error_message(message))
|
147
72
|
end
|
73
|
+
else
|
74
|
+
throw_custom(error_message("Unexpected end of file"))
|
148
75
|
end
|
149
|
-
|
76
|
+
char = @source.read
|
77
|
+
return char
|
150
78
|
end
|
151
79
|
|
152
80
|
# Parses a single character which matches any of the specified queries.
|
153
81
|
def parse_char_any(queries)
|
154
|
-
|
82
|
+
parsers = []
|
83
|
+
queries.each do |query|
|
84
|
+
parsers << ->{parse_char(query)}
|
85
|
+
end
|
86
|
+
char = choose(*parsers)
|
87
|
+
return char
|
155
88
|
end
|
156
89
|
|
157
90
|
# Parses a single character other than the specified characters.
|
158
|
-
# If the next character coincides with any of the elements of the arguments, an error
|
159
|
-
# Otherwise, a
|
91
|
+
# If the next character coincides with any of the elements of the arguments, then an error occurs and no input is consumed.
|
92
|
+
# Otherwise, a string which consists of the next single chracter is returned.
|
160
93
|
def parse_char_out(chars)
|
161
|
-
|
162
|
-
|
163
|
-
if
|
164
|
-
|
165
|
-
|
166
|
-
message = "Expected other than " + chars.map{|s| "'#{s}'"}.join(", ")
|
167
|
-
next Result.error(error_message(message))
|
94
|
+
char = @source.peek
|
95
|
+
if char
|
96
|
+
if chars.any?{|s| s == char}
|
97
|
+
chars_string = chars.map{|s| "'#{s}'"}.join(", ")
|
98
|
+
throw_custom(error_message("Expected other than #{chars_string} but got '#{char}'"))
|
168
99
|
end
|
100
|
+
else
|
101
|
+
throw_custom(error_message("Unexpected end of file"))
|
169
102
|
end
|
170
|
-
|
103
|
+
char = @source.read
|
104
|
+
return char
|
171
105
|
end
|
172
106
|
|
173
107
|
def parse_eof
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
next Result.success(true)
|
178
|
-
else
|
179
|
-
next Result.error(error_message("Document ends before reaching end of file"))
|
180
|
-
end
|
108
|
+
char = @source.peek
|
109
|
+
if char
|
110
|
+
throw_custom(error_message("Document ends before reaching end of file"))
|
181
111
|
end
|
182
|
-
|
112
|
+
char = @source.read
|
113
|
+
return true
|
183
114
|
end
|
184
115
|
|
185
|
-
#
|
186
|
-
# That is, it always returns an error result.
|
116
|
+
# Parses nothing; thus an error always occur.
|
187
117
|
def parse_none
|
188
|
-
|
189
|
-
|
118
|
+
throw_custom(error_message("This cannot happen"))
|
119
|
+
return nil
|
120
|
+
end
|
121
|
+
|
122
|
+
# Simply executes the specified parser, but additionally performs backtracking on error.
|
123
|
+
# If an error occurs in executing the parser, this method rewinds the state of the input to that before executing, and then raises an error.
|
124
|
+
# Otherwise, a result obtained by the parser is returned.
|
125
|
+
def try(parser)
|
126
|
+
mark = @source.mark
|
127
|
+
value = nil
|
128
|
+
message = catch_custom do
|
129
|
+
value = parser.call
|
190
130
|
end
|
191
|
-
|
131
|
+
unless value
|
132
|
+
@source.reset(mark)
|
133
|
+
throw_custom(message)
|
134
|
+
end
|
135
|
+
return value
|
192
136
|
end
|
193
137
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
138
|
+
# First this method executes the first specified parser.
|
139
|
+
# If it fails without consuming any input, then this method tries the next specified parser and repeats this procedure.
|
140
|
+
def choose(*parsers)
|
141
|
+
value, message = nil, ""
|
142
|
+
parsers.each do |parser|
|
143
|
+
mark = @source.mark
|
144
|
+
message = catch_custom do
|
145
|
+
value = parser.call
|
146
|
+
end
|
147
|
+
if value
|
148
|
+
break
|
149
|
+
elsif mark != @source.mark
|
150
|
+
break
|
151
|
+
end
|
152
|
+
end
|
153
|
+
unless value
|
154
|
+
throw_custom(message)
|
155
|
+
end
|
156
|
+
return value
|
205
157
|
end
|
206
|
-
|
207
|
-
def
|
208
|
-
|
158
|
+
|
159
|
+
def many(parser, range = 0..)
|
160
|
+
values, message, count = [], "", 0
|
161
|
+
lower_limit, upper_limit = range.begin, range.end
|
162
|
+
if upper_limit && range.exclude_end?
|
163
|
+
upper_limit -= 1
|
164
|
+
end
|
165
|
+
loop do
|
166
|
+
mark = @source.mark
|
167
|
+
value = nil
|
168
|
+
message = catch_custom do
|
169
|
+
value = parser.call
|
170
|
+
end
|
171
|
+
if value
|
172
|
+
values << value
|
173
|
+
count += 1
|
174
|
+
if upper_limit && count >= upper_limit
|
175
|
+
break
|
176
|
+
end
|
177
|
+
else
|
178
|
+
if mark != @source.mark
|
179
|
+
throw_custom(message)
|
180
|
+
end
|
181
|
+
break
|
182
|
+
end
|
183
|
+
end
|
184
|
+
unless count >= lower_limit
|
185
|
+
throw_custom(message)
|
186
|
+
end
|
187
|
+
return values
|
209
188
|
end
|
210
189
|
|
211
|
-
def
|
212
|
-
|
190
|
+
def maybe(parser)
|
191
|
+
value = many(parser, 0..1).first
|
192
|
+
return value
|
213
193
|
end
|
214
194
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
195
|
+
# Catch a parse error.
|
196
|
+
# Do not use the standard exception mechanism during parsing.
|
197
|
+
def catch_custom(&block)
|
198
|
+
catch(ERROR_TAG, &block)
|
219
199
|
end
|
220
200
|
|
221
|
-
|
222
|
-
|
201
|
+
# Raises a parse error.
|
202
|
+
# Do not use the standard exception mechanism during parsing, and always use this method to avoid creating an unnecessary stacktrace.
|
203
|
+
def throw_custom(message)
|
204
|
+
throw(ERROR_TAG, message)
|
223
205
|
end
|
224
206
|
|
225
|
-
def
|
226
|
-
return
|
207
|
+
def error_message(message)
|
208
|
+
return "[line #{@source.lineno}, column #{@source.columnno}] #{message}"
|
227
209
|
end
|
228
210
|
|
229
211
|
end
|
data/source/zenml/reader.rb
CHANGED
@@ -4,18 +4,22 @@
|
|
4
4
|
class StringReader
|
5
5
|
|
6
6
|
attr_reader :lineno
|
7
|
+
attr_reader :columnno
|
7
8
|
|
8
9
|
def initialize(string)
|
9
10
|
@string = string.chars
|
10
11
|
@pos = -1
|
11
12
|
@lineno = 1
|
13
|
+
@columnno = 1
|
12
14
|
end
|
13
15
|
|
14
16
|
def read
|
15
17
|
@pos += 1
|
18
|
+
@columnno += 1
|
16
19
|
char = @string[@pos]
|
17
20
|
if char == "\n"
|
18
21
|
@lineno += 1
|
22
|
+
@columnno = 1
|
19
23
|
end
|
20
24
|
return char
|
21
25
|
end
|
@@ -25,23 +29,14 @@ class StringReader
|
|
25
29
|
return char
|
26
30
|
end
|
27
31
|
|
28
|
-
def unread(size = 1)
|
29
|
-
size.times do
|
30
|
-
char = @string[@pos]
|
31
|
-
@pos -= 1
|
32
|
-
if char == "\n"
|
33
|
-
@lineno -= 1
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
32
|
def mark
|
39
|
-
return [@pos, @lineno]
|
33
|
+
return [@pos, @lineno, @columnno]
|
40
34
|
end
|
41
35
|
|
42
36
|
def reset(mark)
|
43
37
|
@pos = mark[0]
|
44
38
|
@lineno = mark[1]
|
39
|
+
@columnno = mark[2]
|
45
40
|
end
|
46
41
|
|
47
42
|
end
|
data/source/zenml.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zenml
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ziphil
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-02-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
14
|
This gem serves a tool for parsing a document written in ZenML, an alternative new syntax for XML, to an XML node tree.
|