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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 588639c3751d09fb4485184ed90ff37680b490d7e5a4417142ca8587eb48242b
4
- data.tar.gz: 63c74b6fee69636440dc0c26a3c74c2145c39a7e879c3d3094913fe8c12e44ad
3
+ metadata.gz: dc0c51d446a26f7b640f5a4ab3c4054d131ab89f98ab7c0effca28387e06b9b9
4
+ data.tar.gz: 43860c32ec5abf88af046191eeb57941ebc67cfde701229b459dfb2c9439f525
5
5
  SHA512:
6
- metadata.gz: b62748fe59945691582eaec70ac2f1870133d4e4f661373dc10a8e3b40b2d8b879248b1a1f74571bb7e31acb314fb7c5b16171eebb303648984f6c55ce396008
7
- data.tar.gz: 0e4042446f70cf50db75874407aa111be819d1c0683bcd96ee68affc059fc8993d22ce6f14edb46aed16d39c253ab3b6b1a1d7e81045bd2e4b9e9248f9c46d70
6
+ metadata.gz: baf2791fe4d95b718a2251152d8edb917e92c3d0da35ed9578ae1b4ac8fb2c48964223819eb9e06bd6619a25ee1e4bf84c4b10c81cf955f4a7b877a3a0db727d
7
+ data.tar.gz: 0e8b566980d4870679e70426c5da2114abb3927c9c59f077f0e100b5c6685fab507bc141708744fb1d37f5294eb401900bf5019fa76a39e6d5e4e24a84ff5986
@@ -18,8 +18,8 @@ class ZenithalConverter
18
18
  @configs = {}
19
19
  @templates = {}
20
20
  @functions = {}
21
- @default_element_template = lambda{|s| (@type == :text) ? "" : Nodes[]}
22
- @default_text_template = lambda{|s| (@type == :text) ? "" : Nodes[]}
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 = (@type == :text) ? "" : 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 = (@type == :text) ? "" : 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 = (@type == :text) ? "" : 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|
@@ -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
- parser = Parser.build(self) do
51
- document = Document.new
52
- children = +parse_nodes({})
53
- +parse_eof
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
- return parser
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
- parser = Parser.build(self) do
64
- parsers = [parse_text(options)]
65
- unless options[:verbal]
66
- parsers.push(parse_element(options), parse_line_comment(options), parse_block_comment(options))
67
- @special_element_names.each do |kind, name|
68
- parsers.push(parse_special_element(kind, options))
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
- raw_nodes = +parsers.inject(:|).many
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 parser
98
+ return nodes
76
99
  end
77
100
 
78
101
  def parse_element(options)
79
- parser = Parser.build(self) do
80
- start_char = +parse_char_any([ELEMENT_START, MACRO_START])
81
- name = +parse_identifier(options)
82
- marks = +parse_marks(options)
83
- attributes = +parse_attributes(options).maybe || {}
84
- next_options = determine_options(name, marks, attributes, start_char == MACRO_START, options)
85
- children_list = +parse_children_list(next_options)
86
- if name == SYSTEM_INSTRUCTION_NAME
87
- +parse_space
88
- end
89
- if start_char == MACRO_START
90
- next process_macro(name, marks, attributes, children_list, options)
91
- else
92
- next create_element(name, marks, attributes, children_list, options)
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 parser
117
+ return element
96
118
  end
97
119
 
98
120
  def parse_special_element(kind, options)
99
- parser = Parser.build(self) do
100
- unless @special_element_names[kind]
101
- +parse_none
102
- end
103
- +parse_char(SPECIAL_ELEMENT_STARTS[kind])
104
- children = +parse_nodes(options)
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
- return parser
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
- return parse_mark(options).many
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).map{|_| mark}
141
+ next ->{parse_char(query).yield_self{|_| mark}}
118
142
  end
119
- return parsers.inject(:|)
143
+ mark = choose(*parsers)
144
+ return mark
120
145
  end
121
146
 
122
147
  def parse_attributes(options)
123
- parser = Parser.build(self) do
124
- +parse_char(ATTRIBUTE_START)
125
- first_attribute = +parse_attribute(true, options)
126
- rest_attribtues = +parse_attribute(false, options).many
127
- attributes = first_attribute.merge(*rest_attribtues)
128
- +parse_char(ATTRIBUTE_END)
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
- parser = Parser.build(self) do
136
- +parse_char(ATTRIBUTE_SEPARATOR) unless first
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
- return parser
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
- parser = Parser.build(self) do
149
- +parse_char(ATTRIBUTE_EQUAL)
150
- +parse_space
151
- value = +parse_string(options)
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
- parser = Parser.build(self) do
159
- +parse_char(STRING_START)
160
- strings = +(parse_string_plain(options) | parse_escape(:string, options)).many
161
- +parse_char(STRING_END)
162
- next strings.join
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
- parser = Parser.build(self) do
169
- chars = +parse_char_out([STRING_END, ESCAPE_START]).many(1)
170
- next chars.join
171
- end
172
- return parser
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
- parser = Parser.build(self) do
177
- first_children = +(parse_empty_children(options) | parse_children(options))
178
- rest_children_list = +parse_children(options).many
179
- children_list = [first_children] + rest_children_list
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
- parser = Parser.build(self) do
187
- +parse_char(CONTENT_START)
188
- children = +parse_nodes(options)
189
- +parse_char(CONTENT_END)
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
- parser = Parser.build(self) do
197
- +parse_char(CONTENT_END)
198
- next Nodes[]
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
- parser = Parser.build(self) do
205
- raw_texts = +(parse_text_plain(options) | parse_escape(:text, options)).many(1)
206
- next create_text(raw_texts.join, options)
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
- parser = Parser.build(self) do
213
- out_chars = [ESCAPE_START, CONTENT_END]
214
- unless options[:verbal]
215
- out_chars.push(ELEMENT_START, MACRO_START, CONTENT_START, COMMENT_DELIMITER)
216
- @special_element_names.each do |kind, name|
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
- return parser
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
- parser = Parser.build(self) do
228
- +parse_char(COMMENT_DELIMITER)
229
- +parse_char(COMMENT_DELIMITER)
230
- content = +parse_line_comment_content(options)
231
- +parse_char("\n")
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
- parser = Parser.build(self) do
239
- chars = +parse_char_out(["\n"]).many
240
- next chars.join
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
- parser = Parser.build(self) do
247
- +parse_char(COMMENT_DELIMITER)
248
- +parse_char(CONTENT_START)
249
- content = +parse_block_comment_content(options)
250
- +parse_char(CONTENT_END)
251
- +parse_char(COMMENT_DELIMITER)
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
- parser = Parser.build(self) do
259
- chars = +parse_char_out([CONTENT_END]).many
260
- next chars.join
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
- parser = Parser.build(self) do
267
- +parse_char(ESCAPE_START)
268
- char = +parse_char
269
- next create_escape(place, char, options)
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
- parser = Parser.build(self) do
276
- first_char = +parse_first_identifier_char(options)
277
- rest_chars = +parse_middle_identifier_char(options).many
278
- identifier = first_char + rest_chars.join
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
- return parse_char_any(VALID_FIRST_IDENTIFIER_CHARS)
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
- return parse_char_any(VALID_MIDDLE_IDENTIFIER_CHARS)
294
+ char = parse_char_any(VALID_MIDDLE_IDENTIFIER_CHARS)
295
+ return char
290
296
  end
291
297
 
292
298
  def parse_space
293
- return parse_char_any(SPACE_CHARS).many
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
- throw(:error, error_message("Processing instruction cannot have more than one argument"))
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
- throw(:error, error_message("Normal node cannot have more than one argument"))
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
- nodes = create_element(name, [], {}, [children], options)
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
- return Text.new(raw_text, true, nil, false)
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
- return Comment.new(" " + content.strip + " ")
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
- throw(:error, "Invalid escape")
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
- throw(:error, error_message("No such macro"))
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
- attr_reader :source
428
+ attr_accessor :exact
429
+ attr_accessor :whole
417
430
 
418
431
  def initialize(source)
419
- @source = StringReader.new(source)
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
- def parse
426
- result = parse_document.exec
427
- if result.success?
428
- return result.value
429
- else
430
- raise ZenithalParseError.new(result.message)
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 register_macro(name, &block)
435
- @macros.store(name, block)
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
- attr_reader :builder
7
-
8
- def initialize(builder, &method)
9
- @builder = builder
10
- @method = method
11
- end
12
-
13
- def self.build(source, &block)
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
- throw(:error, result.message)
15
+ @source = StringReader.new(source.to_s)
38
16
  end
17
+ @inside_run = false
39
18
  end
40
19
 
41
- alias -@ exec
42
- alias +@ exec_get
43
-
44
- def |(other)
45
- this = self
46
- if this.builder.equal?(other.builder)
47
- parser = Parser.new(this.builder) do
48
- mark = source.mark
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
- end
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 parser
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
- def map(&block)
96
- this = self
97
- parser = Parser.new(this.builder) do
98
- result = this.exec
99
- if result.success?
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 result is returned.
117
- # Otherwise, a success result with a string containing the matched chracter is returned.
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
- parser = Parser.new(self) do
120
- char = source.read
121
- unless char == nil
122
- predicate, message = false, nil
123
- case query
124
- when String
125
- predicate = query == char
126
- message = "Expected '#{query}'"
127
- when Regexp
128
- predicate = query =~ char
129
- message = "Expected /#{query}/"
130
- when Integer
131
- predicate = query == char.ord
132
- message = "Expected '#{query.chr}'"
133
- when Range
134
- predicate = query.cover?(char.ord)
135
- message = "Expected '#{query.begin}'..'#{query.end}'"
136
- when NilClass
137
- predicate = true
138
- message = ""
139
- end
140
- if predicate
141
- next Result.success(char)
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
- return parser
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
- return queries.map{|s| parse_char(s)}.inject(:|)
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 result is returned.
159
- # Otherwise, a success result with a string containing the next chracter is returned.
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
- parser = Parser.new(self) do
162
- char = source.read
163
- if char && chars.all?{|s| s != char}
164
- next Result.success(char)
165
- else
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
- return parser
103
+ char = @source.read
104
+ return char
171
105
  end
172
106
 
173
107
  def parse_eof
174
- parser = Parser.new(self) do
175
- char = source.read
176
- if char == nil
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
- return parser
112
+ char = @source.read
113
+ return true
183
114
  end
184
115
 
185
- # Returns a parser which always fails.
186
- # That is, it always returns an error result.
116
+ # Parses nothing; thus an error always occur.
187
117
  def parse_none
188
- parser = Parser.new(self) do
189
- next Result.error(error_message("This cannot happen"))
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
- return parser
131
+ unless value
132
+ @source.reset(mark)
133
+ throw_custom(message)
134
+ end
135
+ return value
192
136
  end
193
137
 
194
- end
195
-
196
-
197
- class Result
198
-
199
- attr_reader :value
200
- attr_reader :message
201
-
202
- def initialize(value, message)
203
- @value = value
204
- @message = message
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 self.success(value)
208
- return Result.new(value, nil)
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 self.error(message)
212
- return Result.new(nil, message)
190
+ def maybe(parser)
191
+ value = many(parser, 0..1).first
192
+ return value
213
193
  end
214
194
 
215
- def value=(value)
216
- if self.success?
217
- @value = value
218
- end
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
- def success?
222
- return !@message
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 error?
226
- return !!@message
207
+ def error_message(message)
208
+ return "[line #{@source.lineno}, column #{@source.columnno}] #{message}"
227
209
  end
228
210
 
229
211
  end
@@ -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
@@ -6,7 +6,7 @@ require 'zenml/utility'
6
6
 
7
7
  module Zenithal
8
8
 
9
- VERSION = "1.2.1"
9
+ VERSION = "1.3.0"
10
10
 
11
11
  require 'zenml/error'
12
12
  require 'zenml/reader'
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.2.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: 2019-10-28 00:00:00.000000000 Z
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.