zenml 1.2.1 → 1.3.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 +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.
|