iparser 1.1.6 → 1.1.7
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/lib/iparser.rb +2 -558
- data/lib/iparser/machine.rb +490 -0
- data/lib/iparser/state.rb +76 -0
- data/lib/iparser/version.rb +1 -1
- metadata +4 -11
- data/.gitignore +0 -9
- data/CODE_OF_CONDUCT.md +0 -13
- data/Gemfile +0 -4
- data/LICENSE.txt +0 -21
- data/README.md +0 -313
- data/Rakefile +0 -1
- data/bin/console +0 -14
- data/bin/setup +0 -7
- data/iparser.gemspec +0 -23
@@ -0,0 +1,490 @@
|
|
1
|
+
module Iparser
|
2
|
+
# Used for create parser machine.
|
3
|
+
class Machine
|
4
|
+
INITIAL_STATE = 'wait'
|
5
|
+
|
6
|
+
attr_reader :parserstate
|
7
|
+
|
8
|
+
# call-seq:
|
9
|
+
# Machine.new( )
|
10
|
+
def initialize ( )
|
11
|
+
@buffer = [] # буфер для символов входного потока
|
12
|
+
@states = [] # массив с состояниями парсера, <ParserState> объекты
|
13
|
+
@chain = [] # цепочка работающих состояний
|
14
|
+
#
|
15
|
+
# Машина состояний для метода classify.
|
16
|
+
#
|
17
|
+
@matchstate = {
|
18
|
+
:state => 0,
|
19
|
+
:index => 0
|
20
|
+
}
|
21
|
+
@parserstate = ''
|
22
|
+
end
|
23
|
+
|
24
|
+
# Сбрасывает чувствительные переменные.
|
25
|
+
def reset ( ) # :nodoc:
|
26
|
+
@buffer = []
|
27
|
+
@states.each do |s|
|
28
|
+
s.ientry = 0
|
29
|
+
s.ileave = 0
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return current state name.
|
34
|
+
def current_state ( )
|
35
|
+
return @chain.last.statename if @chain.size > 0
|
36
|
+
return nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Initialize parser object,
|
40
|
+
# should be called before call other methods.
|
41
|
+
def prestart ( )
|
42
|
+
reset( )
|
43
|
+
@matchstate[:state] = 0
|
44
|
+
@chain = [ @states[0], ]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Display information about of each state of parser.
|
48
|
+
def display ( )
|
49
|
+
puts 'Parser states: ' + @states.size.to_s
|
50
|
+
|
51
|
+
@states.each do |st|
|
52
|
+
puts
|
53
|
+
puts '** state: ' + st.statename
|
54
|
+
puts 'branches: '
|
55
|
+
st.branches.each do |br|
|
56
|
+
puts ' ' + @states[br].statename
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Обработка ввода для интерактивных режимов работы.
|
62
|
+
def interactive_input ( ) # :nodoc:
|
63
|
+
state = 0
|
64
|
+
rv = ""
|
65
|
+
str = gets
|
66
|
+
|
67
|
+
# Сразу нажата <Enter> => exit.
|
68
|
+
return rv if str[0] == '\n'
|
69
|
+
|
70
|
+
# Выполняем разбор посимвольно.
|
71
|
+
str.each_char do |c|
|
72
|
+
break if c == ?\n
|
73
|
+
case state
|
74
|
+
#
|
75
|
+
# Сборка символов и проверка на наличие
|
76
|
+
# экранирующего символа, значит это ESC-символы.
|
77
|
+
when 0
|
78
|
+
if c == '\\' then
|
79
|
+
state = 1
|
80
|
+
else
|
81
|
+
rv += c
|
82
|
+
end
|
83
|
+
#
|
84
|
+
# Анализ ESC символа.
|
85
|
+
when 1
|
86
|
+
case c
|
87
|
+
when '0'
|
88
|
+
rv += "\0"
|
89
|
+
when 'n'
|
90
|
+
rv += "\n"
|
91
|
+
when 'r'
|
92
|
+
rv += "\r"
|
93
|
+
when '\\'
|
94
|
+
rv += "\\"
|
95
|
+
when 'a'
|
96
|
+
rv += "\a"
|
97
|
+
when 'b'
|
98
|
+
rv += "\b"
|
99
|
+
when 't'
|
100
|
+
rv += "\t"
|
101
|
+
when 'v'
|
102
|
+
rv += "\v"
|
103
|
+
when 'f'
|
104
|
+
rv += "\f"
|
105
|
+
else
|
106
|
+
puts "\nERROR: unrecognized esc-symbols.\n"
|
107
|
+
exit
|
108
|
+
end
|
109
|
+
state = 0
|
110
|
+
end
|
111
|
+
end
|
112
|
+
return rv
|
113
|
+
end
|
114
|
+
|
115
|
+
# Обработка вывода для интерактивных режимов работы.
|
116
|
+
def interactive_output( istr ) # :nodoc:
|
117
|
+
str = []
|
118
|
+
istr.bytes.each do |c|
|
119
|
+
case c
|
120
|
+
when 0
|
121
|
+
str << c.to_s + ":\\0"
|
122
|
+
when 10
|
123
|
+
str << c.to_s + ":\\n"
|
124
|
+
when 13
|
125
|
+
str << c.to_s + ":\\r"
|
126
|
+
when 7
|
127
|
+
str << c.to_s + ":\\a"
|
128
|
+
when 8
|
129
|
+
str << c.to_s + ":\\b"
|
130
|
+
when 9
|
131
|
+
str << c.to_s + ":\\t"
|
132
|
+
when 11
|
133
|
+
str << c.to_s + ":\\v"
|
134
|
+
when 12
|
135
|
+
str << c.to_s + ":\\f"
|
136
|
+
else
|
137
|
+
str << c.to_s + ":" + c.chr
|
138
|
+
end
|
139
|
+
end
|
140
|
+
return str
|
141
|
+
end
|
142
|
+
|
143
|
+
# Run parser machine for check in interactive mode.
|
144
|
+
def interactive_parser ( )
|
145
|
+
puts 'Press <Enter> to exit...'
|
146
|
+
|
147
|
+
# Цикл обработки ввода.
|
148
|
+
loop do
|
149
|
+
str = interactive_input( )
|
150
|
+
break if str == ""
|
151
|
+
|
152
|
+
# Цикл посимвольной классификаци.
|
153
|
+
str.bytes.each do |c|
|
154
|
+
parse( c.chr )
|
155
|
+
puts 'parser: ' + @parserstate
|
156
|
+
puts 'symbol: ' + interactive_output( c.chr ).to_s
|
157
|
+
puts 'state: ' + @chain.last.statename
|
158
|
+
puts
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# call-seq:
|
164
|
+
# s = Parser::State.new('idle')
|
165
|
+
# p = Parser::Machine.new
|
166
|
+
# p << s
|
167
|
+
#
|
168
|
+
# Add any parser-state to current parser.
|
169
|
+
def addstate ( ps )
|
170
|
+
raise TypeError, ps.class.to_s + ': Incorrectly types for \'<<\' method of <Parser>.' unless
|
171
|
+
ps.instance_of? State
|
172
|
+
@states << ps
|
173
|
+
end
|
174
|
+
|
175
|
+
# call-seq:
|
176
|
+
# some_state1.branches << parser.state_index(some_state2).
|
177
|
+
# Return index
|
178
|
+
def state_index ( state )
|
179
|
+
raise TypeError, ps.class.to_s + ': Incorrectly types for \'state_index\' method of <Parser>.' unless
|
180
|
+
state.instance_of? State
|
181
|
+
|
182
|
+
@states.each_with_index do |st,i|
|
183
|
+
return i if state == st
|
184
|
+
end
|
185
|
+
raise "State <#{state.statename}> is not exist in Parser."
|
186
|
+
end
|
187
|
+
|
188
|
+
# Сравнивает символы входного потока
|
189
|
+
# с символами из указанного шаблона.
|
190
|
+
# В качестве шаблона выступают поля <entry> или <leave>
|
191
|
+
# объектов типа <ParserState>.
|
192
|
+
def cmp ( tmp, idx ) # :nodoc:
|
193
|
+
|
194
|
+
# проверка на случай если шаблон не задан,
|
195
|
+
# т.е. проинициализирован в [].
|
196
|
+
if tmp.size > 0 then
|
197
|
+
if idx < tmp.size then
|
198
|
+
case tmp[idx].class.to_s
|
199
|
+
when 'Regexp'
|
200
|
+
return true if @buffer.last =~ tmp[ idx ]
|
201
|
+
when 'String'
|
202
|
+
return true if @buffer.last == tmp[ idx ]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
return false
|
207
|
+
end
|
208
|
+
|
209
|
+
# Поиск в массиве указанного диапазона,
|
210
|
+
# указанного в параметрах символа.
|
211
|
+
#
|
212
|
+
# >=0 : индекс совпавшего элемента.
|
213
|
+
# -1 : нет совпадений.
|
214
|
+
def checkback ( tmp, len ) # :nodoc:
|
215
|
+
if len > 0 then
|
216
|
+
i = len
|
217
|
+
len.times do
|
218
|
+
i = i - 1
|
219
|
+
return i if cmp( tmp, i )
|
220
|
+
end
|
221
|
+
end
|
222
|
+
return -1
|
223
|
+
end
|
224
|
+
|
225
|
+
# Находит соответствие между символами входного потока
|
226
|
+
# и возможными переходами.
|
227
|
+
#
|
228
|
+
# При совпадени возвращает индекс состояния
|
229
|
+
# в массиве состояний, иначе:
|
230
|
+
#
|
231
|
+
# >0 : прыжок в новое состояние
|
232
|
+
# -1 : возврат в предыдущее состояние
|
233
|
+
# -2 : еще идет проверка.
|
234
|
+
# -3 : нет cовпадений (промах).
|
235
|
+
def classify ( state ) # :nodoc:
|
236
|
+
case @matchstate[:state]
|
237
|
+
|
238
|
+
# Состояние еще не определено.
|
239
|
+
# :state = 0
|
240
|
+
when 0
|
241
|
+
mcount = 0
|
242
|
+
mindex = 0
|
243
|
+
backtag = 0
|
244
|
+
#
|
245
|
+
# Проверка условия выхода из состояния.
|
246
|
+
if cmp( state.leave, state.ileave ) then
|
247
|
+
state.ileave = state.ileave.next
|
248
|
+
#
|
249
|
+
# Возврат в предыдущее состояние.
|
250
|
+
if state.ileave >= state.leave.size then
|
251
|
+
return -1
|
252
|
+
end
|
253
|
+
backtag = 1
|
254
|
+
mindex = -1
|
255
|
+
else
|
256
|
+
#
|
257
|
+
# Нет совпадения, но если уже часть сравнений
|
258
|
+
# успешна, то возможно входной символ совпадает
|
259
|
+
# с предыдущими, уже совпавшими символами,
|
260
|
+
# т.е. как откат в режиме <wait>.
|
261
|
+
#
|
262
|
+
i = checkback( state.leave, state.ileave )
|
263
|
+
|
264
|
+
if i != -1 then
|
265
|
+
state.ileave = i.next
|
266
|
+
backtag = 1
|
267
|
+
else
|
268
|
+
state.ileave = 0
|
269
|
+
backtag = 0
|
270
|
+
end
|
271
|
+
end
|
272
|
+
#
|
273
|
+
# Проверка возможных переходов для
|
274
|
+
# указанного в параметрах состояния.
|
275
|
+
state.branches.each do |b|
|
276
|
+
if cmp( @states[b].entry, @states[b].ientry ) then
|
277
|
+
mcount = mcount + 1
|
278
|
+
mindex = b
|
279
|
+
@states[b].ientry = @states[b].ientry.next
|
280
|
+
#
|
281
|
+
# состояние полностью пройдено.
|
282
|
+
if @states[ b ].ientry >= @states[ b ].entry.size then
|
283
|
+
return b
|
284
|
+
end
|
285
|
+
else
|
286
|
+
#
|
287
|
+
# Нет совпадения, но если уже часть сравнений
|
288
|
+
# успешна, то возможно входной символ совпадает
|
289
|
+
# с предыдущими, уже совпавшими символами,
|
290
|
+
# т.е. как откат в режиме <wait>.
|
291
|
+
i = checkback( @states[b].entry, @states[b].ientry )
|
292
|
+
|
293
|
+
if i != -1 then
|
294
|
+
mcount = mcount + 1
|
295
|
+
mindex = b
|
296
|
+
@states[b].ientry = i.next
|
297
|
+
else
|
298
|
+
@states[b].ientry = 0
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
#
|
303
|
+
# Анализ количества совпадений.
|
304
|
+
case (mcount + backtag)
|
305
|
+
#
|
306
|
+
# нет совпадений.
|
307
|
+
when 0
|
308
|
+
return (-3 + backtag)
|
309
|
+
#
|
310
|
+
# однозначное совпадение, но весь массив шаблонов
|
311
|
+
# еще не пройден.
|
312
|
+
when 1
|
313
|
+
if mindex == -1 then
|
314
|
+
@matchstate[:state] = 2
|
315
|
+
else
|
316
|
+
@matchstate[:state] = 1
|
317
|
+
@matchstate[:index] = mindex
|
318
|
+
end
|
319
|
+
return -2
|
320
|
+
#
|
321
|
+
# нет однозначного соответствия.
|
322
|
+
else
|
323
|
+
return -2
|
324
|
+
end
|
325
|
+
|
326
|
+
# Состояние точно определено (переход в перед).
|
327
|
+
# :state = 1
|
328
|
+
when 1
|
329
|
+
i = @matchstate[:index]
|
330
|
+
if cmp( @states[ i ].entry, @states[ i ].ientry ) then
|
331
|
+
#
|
332
|
+
# Инкремент счетчика (индекса) вхождений.
|
333
|
+
@states[ i ].ientry = @states[ i ].ientry.next
|
334
|
+
#
|
335
|
+
# Массив шаблонов совпадает полностью.
|
336
|
+
# можно считать, что 100% совпадение.
|
337
|
+
if @states[ i ].ientry >= @states[ i ].entry.size then
|
338
|
+
@matchstate[:state] = 0
|
339
|
+
return i
|
340
|
+
end
|
341
|
+
return -2
|
342
|
+
end
|
343
|
+
#
|
344
|
+
# Нет совпадения, но если уже часть сравнений
|
345
|
+
# успешна, то возможно входной символ совпадает
|
346
|
+
# с предыдущими, уже совпавшими символами,
|
347
|
+
# т.е. как откат в режиме <wait>.
|
348
|
+
idx = checkback( @states[i].entry, @states[i].ientry )
|
349
|
+
|
350
|
+
if idx != -1 then
|
351
|
+
@states[i].ientry = idx.next
|
352
|
+
return -2
|
353
|
+
end
|
354
|
+
@states[i].ientry = 0
|
355
|
+
@matchstate[:state] = 0
|
356
|
+
return -3
|
357
|
+
|
358
|
+
# Состояние точно определено (возврат назад).
|
359
|
+
# :state = 2
|
360
|
+
when 2
|
361
|
+
if cmp( state.leave, state.ileave ) then
|
362
|
+
state.ileave = state.ileave.next
|
363
|
+
#
|
364
|
+
# Возврат в предыдущее состояние.
|
365
|
+
if state.ileave >= state.leave.size then
|
366
|
+
@matchstate[:state] = 0
|
367
|
+
return -1
|
368
|
+
end
|
369
|
+
return -2
|
370
|
+
end
|
371
|
+
#
|
372
|
+
# Нет совпадения, но если уже часть сравнений
|
373
|
+
# успешна, то возможно входной символ совпадает
|
374
|
+
# с предыдущими, уже совпавшими символами,
|
375
|
+
# т.е. как откат в режиме <wait>.
|
376
|
+
#
|
377
|
+
i = checkback( state.leave, state.ileave )
|
378
|
+
|
379
|
+
if i != -1 then
|
380
|
+
state.ileave = i.next
|
381
|
+
return -2
|
382
|
+
end
|
383
|
+
state.ileave = 0
|
384
|
+
@matchstate[:state] = 0
|
385
|
+
return -3
|
386
|
+
|
387
|
+
end # case @matchstate
|
388
|
+
end
|
389
|
+
|
390
|
+
# Main method, used for parse input stream.
|
391
|
+
# Parse will be starting in unit with nil index (0).
|
392
|
+
#
|
393
|
+
# Return true if parsing process is successful, else return false.
|
394
|
+
def parse ( c )
|
395
|
+
@parserstate = INITIAL_STATE
|
396
|
+
retval = true
|
397
|
+
#
|
398
|
+
# * Фиксированное состояние (определенное): -1.
|
399
|
+
# * Не фиксированное состояние (неопределенное): -2.
|
400
|
+
# * Переход (смена состояний): >0.
|
401
|
+
#
|
402
|
+
@buffer << c
|
403
|
+
|
404
|
+
# Задан шаблон для игнорирования символов.
|
405
|
+
if @chain.last.ignore[:all].size > 0 then
|
406
|
+
return retval if @chain.last.ignore[:all].include?(c)
|
407
|
+
end
|
408
|
+
|
409
|
+
# Проверка переходов в другие состояния.
|
410
|
+
r = classify( @chain.last )
|
411
|
+
|
412
|
+
# Переход (прыжок) в другое состояние.
|
413
|
+
# <branch>:
|
414
|
+
if r >= 0 then
|
415
|
+
@chain << @states[r]
|
416
|
+
if @chain.last.run_init( @buffer ) == nil then
|
417
|
+
reset( )
|
418
|
+
@parserstate = 'branch'
|
419
|
+
else
|
420
|
+
@parserstate = 'error'
|
421
|
+
retval = false
|
422
|
+
end
|
423
|
+
|
424
|
+
# Возврат из текущего состояния.
|
425
|
+
# <back>:
|
426
|
+
elsif r == -1 then
|
427
|
+
if @chain.last.run_fini( @buffer ) == nil then
|
428
|
+
#
|
429
|
+
# если это состояние не первое в цепочке
|
430
|
+
# тогда откатываемся назад.
|
431
|
+
if @chain.size > 1 then
|
432
|
+
@chain.delete_at( @chain.size - 1 )
|
433
|
+
end
|
434
|
+
reset( )
|
435
|
+
@parserstate = 'back'
|
436
|
+
else
|
437
|
+
@parserstate = 'error'
|
438
|
+
retval = false
|
439
|
+
end
|
440
|
+
|
441
|
+
# Нет совпадений.
|
442
|
+
# <miss>:
|
443
|
+
elsif r == -3 then
|
444
|
+
#
|
445
|
+
# если в процессе состояния <wait>
|
446
|
+
# мы попали в <miss>, то накопленный
|
447
|
+
# буфер надо обработать.
|
448
|
+
@buffer.each do |ch|
|
449
|
+
@parserstate = 'miss'
|
450
|
+
tag = true
|
451
|
+
if @chain.last.ignore[:handler].size > 0 then
|
452
|
+
tag = false if @chain.last.ignore[:handler].include?(ch)
|
453
|
+
end
|
454
|
+
if tag == true then
|
455
|
+
r = @chain.last.run_handler( ch )
|
456
|
+
#
|
457
|
+
# Анализ результата обработки состояния.
|
458
|
+
case r.class.to_s
|
459
|
+
#
|
460
|
+
# Fixnum - переход на любое состояние (индекс).
|
461
|
+
when 'Fixnum'
|
462
|
+
if( (r >= 0) && (r < @states.size) ) then
|
463
|
+
@chain << @states[r]
|
464
|
+
reset( )
|
465
|
+
@parserstate = 'hardset'
|
466
|
+
else
|
467
|
+
raise TypeError, "Method <#{@chain.last.statename}> return incorrectly index."
|
468
|
+
end
|
469
|
+
#
|
470
|
+
# nil - ничего не возвращает.
|
471
|
+
when 'NilClass'
|
472
|
+
#
|
473
|
+
# else - расценивается как ошибка обработки.
|
474
|
+
# обработка ложится на плечи разработчика.
|
475
|
+
else
|
476
|
+
@parserstate = 'error'
|
477
|
+
retval = false
|
478
|
+
break
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
@buffer = []
|
483
|
+
end
|
484
|
+
return retval
|
485
|
+
end
|
486
|
+
|
487
|
+
private :reset, :cmp, :checkback, :interactive_input, :interactive_output
|
488
|
+
end # class Machine
|
489
|
+
|
490
|
+
end
|