fbtxt-lexer 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/Manifest.txt +47 -0
- data/README.md +18 -0
- data/Rakefile +30 -0
- data/lib/fbtxt/lexer/debuggable.rb +58 -0
- data/lib/fbtxt/lexer/lexer-logger.rb +20 -0
- data/lib/fbtxt/lexer/lexer-on_goal.rb +167 -0
- data/lib/fbtxt/lexer/lexer-on_group_def.rb +31 -0
- data/lib/fbtxt/lexer/lexer-on_prop_cards.rb +61 -0
- data/lib/fbtxt/lexer/lexer-on_prop_lineup.rb +82 -0
- data/lib/fbtxt/lexer/lexer-on_prop_misc.rb +108 -0
- data/lib/fbtxt/lexer/lexer-on_prop_penalties.rb +44 -0
- data/lib/fbtxt/lexer/lexer-on_round_def.rb +37 -0
- data/lib/fbtxt/lexer/lexer-on_top.rb +133 -0
- data/lib/fbtxt/lexer/lexer-prep_doc.rb +131 -0
- data/lib/fbtxt/lexer/lexer-prep_line.rb +63 -0
- data/lib/fbtxt/lexer/lexer-props.rb +66 -0
- data/lib/fbtxt/lexer/lexer-tokenize_line.rb +381 -0
- data/lib/fbtxt/lexer/lexer-tokenize_norm.rb +93 -0
- data/lib/fbtxt/lexer/lexer.rb +192 -0
- data/lib/fbtxt/lexer/lexer_buffer.rb +68 -0
- data/lib/fbtxt/lexer/lexer_context.rb +70 -0
- data/lib/fbtxt/lexer/lexer_token.rb +127 -0
- data/lib/fbtxt/lexer/token-date--helpers.rb +130 -0
- data/lib/fbtxt/lexer/token-date--names.rb +108 -0
- data/lib/fbtxt/lexer/token-date.rb +200 -0
- data/lib/fbtxt/lexer/token-date_duration.rb +171 -0
- data/lib/fbtxt/lexer/token-geo.rb +173 -0
- data/lib/fbtxt/lexer/token-goals--helpers.rb +114 -0
- data/lib/fbtxt/lexer/token-goals.rb +306 -0
- data/lib/fbtxt/lexer/token-group.rb +29 -0
- data/lib/fbtxt/lexer/token-note.rb +40 -0
- data/lib/fbtxt/lexer/token-prop.rb +334 -0
- data/lib/fbtxt/lexer/token-prop_name.rb +83 -0
- data/lib/fbtxt/lexer/token-round.rb +88 -0
- data/lib/fbtxt/lexer/token-score--helpers.rb +189 -0
- data/lib/fbtxt/lexer/token-score.rb +60 -0
- data/lib/fbtxt/lexer/token-score_full.rb +331 -0
- data/lib/fbtxt/lexer/token-score_fuller.rb +434 -0
- data/lib/fbtxt/lexer/token-score_legs.rb +59 -0
- data/lib/fbtxt/lexer/token-status.rb +192 -0
- data/lib/fbtxt/lexer/token-status_inline.rb +112 -0
- data/lib/fbtxt/lexer/token-text.rb +221 -0
- data/lib/fbtxt/lexer/token-time.rb +144 -0
- data/lib/fbtxt/lexer/token.rb +224 -0
- data/lib/fbtxt/lexer/version.rb +24 -0
- data/lib/fbtxt/lexer.rb +118 -0
- metadata +142 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
module Fbtxt
|
|
2
|
+
class Lexer
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _tokenize_line( line, lineno )
|
|
7
|
+
tokens = []
|
|
8
|
+
errors = [] ## keep a list of errors - why? why not?
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
pos = 0 ## note - usually same as offset[1] aka offset[end] after match
|
|
12
|
+
## track last offset (begin/end) - to report error on no match
|
|
13
|
+
## or no match in end of string
|
|
14
|
+
offset = [0,0]
|
|
15
|
+
m = nil
|
|
16
|
+
|
|
17
|
+
## track number of geo text seen
|
|
18
|
+
## (use for - do NOT break on two spaces if no geo text seen yet!!)
|
|
19
|
+
@geo_count = 0
|
|
20
|
+
|
|
21
|
+
####
|
|
22
|
+
## quick hack - keep re state/mode between tokenize calls!!!
|
|
23
|
+
@re ||= RE ## note - switch between RE & INSIDE_RE
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if @re == RE ## top-level
|
|
27
|
+
### check for modes once (per line) here to speed-up parsing
|
|
28
|
+
### for now goals only possible for start of line!!
|
|
29
|
+
### fix - remove optional [] - why? why not?
|
|
30
|
+
|
|
31
|
+
####
|
|
32
|
+
## note - ord e.g. (45) for match number can only start a (match) line
|
|
33
|
+
## "inline" use NOT possible
|
|
34
|
+
## note - ord (for ordinal number!!!) e.g match number (1), (42), etc.
|
|
35
|
+
if (m = START_WITH_ORD.match(line))
|
|
36
|
+
## note - strip enclosing () and convert to integer
|
|
37
|
+
tokens << Token.new(:ORD, m[:ord],
|
|
38
|
+
lineno: lineno, offset: m.offset(:ord),
|
|
39
|
+
value: m[:value].to_i(10) )
|
|
40
|
+
|
|
41
|
+
offset = m.offset(0)
|
|
42
|
+
pos = offset[1] ## update pos
|
|
43
|
+
|
|
44
|
+
elsif (m = START_WITH_GROUP_DEF_LINE_RE.match( line ))
|
|
45
|
+
_trace( "ENTER GROUP_DEF_RE MODE" )
|
|
46
|
+
@re = GROUP_DEF_RE
|
|
47
|
+
|
|
48
|
+
tokens << Token.new( :GROUP_DEF, m[:group_def],
|
|
49
|
+
lineno: lineno, offset: m.offset(:group_def) )
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
offset = m.offset(0)
|
|
53
|
+
pos = offset[1] ## update pos
|
|
54
|
+
|
|
55
|
+
elsif (m = START_WITH_PROP_KEY_RE.match( line ))
|
|
56
|
+
## start with prop key (match will switch into prop mode!!!)
|
|
57
|
+
## - fix - remove leading spaces in regex (upstream) - why? why not?
|
|
58
|
+
##
|
|
59
|
+
### switch into new mode
|
|
60
|
+
## switch context to PROP_RE
|
|
61
|
+
_trace("ENTER PROP_RE MODE" )
|
|
62
|
+
|
|
63
|
+
## check for (well-known) property (e.g. yellow,red,ref,attn, etc.)
|
|
64
|
+
## find prop spec (token_sym, mode , ..)
|
|
65
|
+
prop = KNOWN_PROP_KEYS[ m[:key].downcase ]
|
|
66
|
+
|
|
67
|
+
if prop
|
|
68
|
+
## e.g. :PROP_YELLOWCARD, PROP_CARDS_RE
|
|
69
|
+
prop_token, prop_re, _ = prop
|
|
70
|
+
|
|
71
|
+
@re = prop_re
|
|
72
|
+
tokens << Token.new( prop_token, m[:key],
|
|
73
|
+
lineno: lineno, offset: m.offset(:key))
|
|
74
|
+
else ## assume (team) line-up
|
|
75
|
+
@re = PROP_LINEUP_RE
|
|
76
|
+
## fix-fix-fix - rename to PROP_LINEUP !!
|
|
77
|
+
tokens << Token.new(:PROP, m[:key],
|
|
78
|
+
lineno: lineno, offset: m.offset(:key))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
offset = m.offset(0)
|
|
82
|
+
pos = offset[1] ## update pos
|
|
83
|
+
|
|
84
|
+
elsif (m = START_WITH_YEAR.match(line))
|
|
85
|
+
tokens << Token.new(:YEAR, m[:year],
|
|
86
|
+
lineno: lineno, offset: m.offset(:year),
|
|
87
|
+
value: m[:year].to_i(10) )
|
|
88
|
+
|
|
89
|
+
offset = m.offset(0)
|
|
90
|
+
pos = offset[1] ## update pos
|
|
91
|
+
|
|
92
|
+
###
|
|
93
|
+
### todo/fix
|
|
94
|
+
### rename to START_WITH_ROUND_DEF_OUTLINE_RE !!!!
|
|
95
|
+
elsif (m = ROUND_DEF_OUTLINE_RE.match( line ))
|
|
96
|
+
_trace( "ENTER ROUND_DEF_RE MODE" )
|
|
97
|
+
@re = ROUND_DEF_RE
|
|
98
|
+
|
|
99
|
+
## note - return ROUND_DEF NOT ROUND_OUTLINE token
|
|
100
|
+
## fix - add leading ▪ too!!
|
|
101
|
+
tokens << Token.new( :ROUND_DEF, m[:round_outline],
|
|
102
|
+
lineno: lineno, offset: m.offset(:round_outline))
|
|
103
|
+
|
|
104
|
+
offset = m.offset(0)
|
|
105
|
+
pos = offset[1] ## update pos
|
|
106
|
+
elsif (m = ROUND_OUTLINE_RE.match( line ))
|
|
107
|
+
_trace( "ROUND_OUTLINE" )
|
|
108
|
+
## note - derive round level from no of (leading) markers
|
|
109
|
+
## e.g. ▪/:: is 1, ▪▪/::: is 2, ▪▪▪/:::: is 3, etc.
|
|
110
|
+
## note - ascii-style starts with double ::, thus, autodecrement by one!
|
|
111
|
+
round_level = m[:round_marker].size
|
|
112
|
+
round_level -= 1 if m[:round_marker].start_with?( '::' )
|
|
113
|
+
|
|
114
|
+
tokens << Token.new( :ROUND_OUTLINE, m[:round_outline],
|
|
115
|
+
lineno: lineno, offset: m.offset(:round_outline),
|
|
116
|
+
value: { outline: m[:round_outline],
|
|
117
|
+
level: round_level})
|
|
118
|
+
|
|
119
|
+
## note - eats-up line for now (change later to only eat-up marker e.g. »|>>)
|
|
120
|
+
offset = m.offset(0)
|
|
121
|
+
pos = offset[1] ## update pos
|
|
122
|
+
elsif (m = START_GOAL_LINE_RE.match( line )) ## line starting with ( - assume
|
|
123
|
+
## switch context to GOAL_RE (goalline(s))
|
|
124
|
+
####
|
|
125
|
+
## note - check for alternate goal line styles / formats
|
|
126
|
+
if START_GOAL_LINE_COMPAT_RE.match(line )
|
|
127
|
+
## "legacy" style starting with minute e.g.
|
|
128
|
+
## (6 Puskás 0-1, 9 Czibor 0-2, 11 Morlock 1-2, 18 Rahn 2-2,
|
|
129
|
+
## 84 Rahn 3-2)
|
|
130
|
+
@re = GOAL_COMPAT_RE
|
|
131
|
+
_trace( "ENTER GOAL_COMPAT_RE MODE" )
|
|
132
|
+
|
|
133
|
+
tokens << Token.virtual( :GOALS_COMPAT, lineno: lineno )
|
|
134
|
+
elsif START_GOAL_LINE_ALT_RE.match( line )
|
|
135
|
+
## goals with scores e.g.
|
|
136
|
+
## (1-0 Franck Ribéry, 2-0 Ivica Olić, 2-1 Wayne Rooney)
|
|
137
|
+
## -or-
|
|
138
|
+
## (Dion Beljo 1-0
|
|
139
|
+
## 1-1 Andreas Gruber
|
|
140
|
+
## Matthias Seidl 2-1)
|
|
141
|
+
@re = GOAL_ALT_RE
|
|
142
|
+
_trace( "ENTER GOAL_ALT_RE MODE" )
|
|
143
|
+
|
|
144
|
+
tokens << Token.virtual( :GOALS_ALT, lineno: lineno )
|
|
145
|
+
else
|
|
146
|
+
## "standard" / default style
|
|
147
|
+
@re = GOAL_RE
|
|
148
|
+
_trace( "ENTER GOAL_RE MODE" )
|
|
149
|
+
|
|
150
|
+
tokens << Token.virtual( :GOALS, lineno: lineno )
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
## note - eat-up ( for now
|
|
154
|
+
## pass along "virtual" GOALS or GOALS_ALT token
|
|
155
|
+
## (see INLINE_GOALS for the starting goal line inline)
|
|
156
|
+
##
|
|
157
|
+
## fix-fix-fix
|
|
158
|
+
## keep offset at [0,0] - why? why not?
|
|
159
|
+
## do NOT eat-up
|
|
160
|
+
## or better
|
|
161
|
+
## add tokens << Token.literal( '(', lineno: lineno, offset: ...) !!!
|
|
162
|
+
offset = m.offset(0)
|
|
163
|
+
pos = offset[1] ## update pos
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
old_pos = -1 ## allows to backtrack to old pos (used in geo)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
ctx = Context.new( self,
|
|
175
|
+
line: line,
|
|
176
|
+
lineno: lineno,
|
|
177
|
+
errors: errors )
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
while m = @re.match( line, pos )
|
|
181
|
+
# if debug?
|
|
182
|
+
# pp m
|
|
183
|
+
# puts "pos: #{pos}"
|
|
184
|
+
# end
|
|
185
|
+
offset = m.offset(0)
|
|
186
|
+
ctx.offset = offset
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if offset[0] != pos
|
|
191
|
+
## match NOT starting at start/begin position!!!
|
|
192
|
+
## report parse error!!!
|
|
193
|
+
msg = "parse error (tokenize) - skipping >#{line[pos..(offset[0]-1)]}< in line #{lineno}@#{offset[0]},#{offset[1]} >#{line}<"
|
|
194
|
+
errors << msg
|
|
195
|
+
|
|
196
|
+
log( msg )
|
|
197
|
+
puts "!! WARN - #{msg}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
##
|
|
202
|
+
## todo/fix - also check if possible
|
|
203
|
+
## if no match but not yet end off string!!!!
|
|
204
|
+
## report skipped text run too!!!
|
|
205
|
+
|
|
206
|
+
old_pos = pos
|
|
207
|
+
pos = offset[1]
|
|
208
|
+
|
|
209
|
+
# pp offset if debug?
|
|
210
|
+
|
|
211
|
+
##
|
|
212
|
+
## note: racc requires pairs e.g. [:TOKEN, VAL]
|
|
213
|
+
## for VAL use "text" or ["text", { opts }] array
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
t = if @re == ROUND_DEF_RE then _on_round_def( m, ctx: ctx )
|
|
218
|
+
elsif @re == GROUP_DEF_RE then _on_group_def( m, ctx: ctx )
|
|
219
|
+
elsif @re == GEO_RE
|
|
220
|
+
### note - possibly end inline geo on [ (and others?? in the future
|
|
221
|
+
## note: break on double spaces e.g.
|
|
222
|
+
## e.g. Jul/16 @ Arena Auf Schalke, Gelsenkirchen Serbia 0-1 England
|
|
223
|
+
if m[:spaces]
|
|
224
|
+
### note - do NOT break out
|
|
225
|
+
## if not text seen yet!!!
|
|
226
|
+
if @geo_count > 0
|
|
227
|
+
## get out-off geo mode and backtrack (w/ next)
|
|
228
|
+
##
|
|
229
|
+
## todo/fix
|
|
230
|
+
## add virtual geo_end token!!!
|
|
231
|
+
_trace( "LEAVE GEO_RE MODE, BACK TO TOP_LEVEL/RE" )
|
|
232
|
+
@re = RE
|
|
233
|
+
pos = old_pos
|
|
234
|
+
next ## backtrack (resume new loop step)
|
|
235
|
+
else
|
|
236
|
+
nil ## skip spaces
|
|
237
|
+
end
|
|
238
|
+
elsif m[:space]
|
|
239
|
+
nil ## skip (single) space
|
|
240
|
+
elsif m[:text]
|
|
241
|
+
@geo_count += 1
|
|
242
|
+
## keep pos - why? why not?
|
|
243
|
+
Token.new(:GEO, m[:text],
|
|
244
|
+
lineno: lineno, offset: m.offset(:text))
|
|
245
|
+
elsif m[:geo_end] ## "hacky" special comma; always ends geo mode!!!
|
|
246
|
+
## get out-off geo mode and backtrack (w/ next)
|
|
247
|
+
## todo/fix
|
|
248
|
+
## add (semi-) virtual geo_end token!!!
|
|
249
|
+
_trace( "LEAVE GEO_RE MODE, BACK TO TOP_LEVEL/RE" )
|
|
250
|
+
@re = RE
|
|
251
|
+
pos = old_pos
|
|
252
|
+
next ## backtrack (resume new loop step)
|
|
253
|
+
elsif m[:sym]
|
|
254
|
+
case m[:sym]
|
|
255
|
+
## note - reset geo_count to 0 (avoids break on two spaces)
|
|
256
|
+
## if separator seen!!
|
|
257
|
+
when ',' then @geo_count = 0
|
|
258
|
+
Token.literal( m[:sym], lineno: lineno, offset: m.offset(:sym))
|
|
259
|
+
when '›' then @geo_count = 0;
|
|
260
|
+
Token.literal( ',', lineno: lineno, offset: m.offset(:sym))
|
|
261
|
+
## note - treat geo sep › (unicode) like comma for now!!!
|
|
262
|
+
when '>' then @geo_count = 0;
|
|
263
|
+
Token.literal( ',', lineno: lineno, offset: m.offset(:sym))
|
|
264
|
+
## note - treat geo sep > (ascii) like comma for now!!!
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
when '[','▪'
|
|
268
|
+
##
|
|
269
|
+
## todo/fix
|
|
270
|
+
## add virtual geo_end token!!!
|
|
271
|
+
## get out-off geo mode and backtrack (w/ next)
|
|
272
|
+
_trace( "LEAVE GEO_RE MODE, BACK TO TOP_LEVEL/RE" )
|
|
273
|
+
@re = RE
|
|
274
|
+
pos = old_pos
|
|
275
|
+
next ## backtrack (resume new loop step)
|
|
276
|
+
## fix-fix-fix merge '▪' with '['
|
|
277
|
+
else
|
|
278
|
+
Token.literal( m[:sym], lineno: lineno, offset: m.offset(:sym))
|
|
279
|
+
end
|
|
280
|
+
else
|
|
281
|
+
ctx.warn_on_else( m, mode: 'GEO' )
|
|
282
|
+
nil
|
|
283
|
+
end
|
|
284
|
+
elsif @re == PROP_CARDS_RE then _on_prop_cards( m, ctx: ctx )
|
|
285
|
+
elsif @re == PROP_LINEUP_RE then _on_prop_lineup( m, ctx: ctx )
|
|
286
|
+
elsif @re == PROP_ATTENDANCE_RE then _on_prop_attendance( m, ctx: ctx )
|
|
287
|
+
elsif @re == PROP_REFEREE_RE then _on_prop_referee( m, ctx: ctx )
|
|
288
|
+
elsif @re == PROP_PENALTIES_RE then _on_prop_penalties( m, ctx: ctx )
|
|
289
|
+
elsif @re == PROP_COACH_RE then _on_prop_coach( m, ctx: ctx )
|
|
290
|
+
|
|
291
|
+
elsif @re == GOAL_COMPAT_RE then _on_goal_compat( m, ctx: ctx )
|
|
292
|
+
elsif @re == GOAL_ALT_RE then _on_goal_alt( m, ctx: ctx )
|
|
293
|
+
elsif @re == GOAL_RE then _on_goal( m, ctx: ctx )
|
|
294
|
+
###################################################
|
|
295
|
+
## assume TOP_LEVEL (a.k.a. RE) machinery
|
|
296
|
+
else
|
|
297
|
+
_on_top( m, ctx: ctx )
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
tokens << t if t
|
|
302
|
+
|
|
303
|
+
# if debug?
|
|
304
|
+
# print ">"
|
|
305
|
+
# print "*" * pos
|
|
306
|
+
# puts "#{line[pos..-1]}<"
|
|
307
|
+
# end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
## check if no match in end of string
|
|
313
|
+
if offset[1] != line.size
|
|
314
|
+
msg = "parse error (tokenize) - skipping >#{line[offset[1]..-1]}< in line #{lineno}@#{offset[1]},#{line.size} >#{line}<"
|
|
315
|
+
errors << msg
|
|
316
|
+
|
|
317
|
+
log( msg )
|
|
318
|
+
puts "!! WARN - #{msg}"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# if @re == GOAL_RE ### ALWAYS switch back to top level mode
|
|
323
|
+
# puts " LEAVE GOAL_RE MODE, BACK TO TOP_LEVEL/RE" if debug?
|
|
324
|
+
# @re = RE
|
|
325
|
+
# end
|
|
326
|
+
|
|
327
|
+
if @re == GEO_RE ### ALWAYS switch back to top level mode
|
|
328
|
+
_trace( "LEAVE GEO_RE MODE, BACK TO TOP_LEVEL/RE" )
|
|
329
|
+
@re = RE
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
### ALWAYS switch back to top level mode
|
|
333
|
+
@re = RE if @re == GROUP_DEF_RE ||
|
|
334
|
+
@re == ROUND_DEF_RE
|
|
335
|
+
|
|
336
|
+
=begin
|
|
337
|
+
##
|
|
338
|
+
## if in prop mode continue if last token is [,-]
|
|
339
|
+
## otherwise change back to "standard" mode
|
|
340
|
+
if @re == PROP_LINEUP_RE ||
|
|
341
|
+
@re == PROP_CARDS_RE ||
|
|
342
|
+
@re == PROP_PENALTIES_RE ||
|
|
343
|
+
@re == PROP_ATTENDANCE_RE ||
|
|
344
|
+
@re == PROP_REFEREE_RE
|
|
345
|
+
|
|
346
|
+
## note - add :CARDS_SEP_ALT (aka -)!!! too
|
|
347
|
+
## maybe later CARDS_NONE_LEFT too - why? why not?
|
|
348
|
+
if [',', '-', ';', :CARDS_SEP_ALT].include?( tokens[-1].type)
|
|
349
|
+
## continue/stay in PROP_RE mode
|
|
350
|
+
## todo/check - auto-add PROP_CONT token or such
|
|
351
|
+
## to help parser with possible NEWLINE
|
|
352
|
+
## conflicts - why? why not?
|
|
353
|
+
else
|
|
354
|
+
## switch back to top-level mode!!
|
|
355
|
+
_trace( "LEAVE PROP_RE MODE, BACK TO TOP_LEVEL/RE" )
|
|
356
|
+
@re = RE
|
|
357
|
+
## note - auto-add PROP_END (<PROP_END>)
|
|
358
|
+
tokens << Token.virtual(:PROP_END, lineno: lineno)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
=end
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
##########
|
|
366
|
+
## note - auto-add end token
|
|
367
|
+
## inside multi-line token format that is, prop_cont or goal_cont
|
|
368
|
+
if is_prop_cont? || is_goal_cont?
|
|
369
|
+
## do NOT add end token; continue
|
|
370
|
+
_trace( "auto-continue - is_prop_cont? #{is_prop_cont?}, is_goal_cont? #{is_goal_cont?}" )
|
|
371
|
+
else
|
|
372
|
+
tokens << Token.virtual(:END, lineno: lineno)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
[tokens,errors]
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
end ## class Lexer
|
|
381
|
+
end ## module Fbtxt
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
###
|
|
2
|
+
## tokenize pipeline (step 2) - normalize tokens
|
|
3
|
+
##
|
|
4
|
+
## transform (normalize) tokens (using simple patterns)
|
|
5
|
+
## to help along the (racc look ahead 1 - LA1) parser
|
|
6
|
+
|
|
7
|
+
module Fbtxt
|
|
8
|
+
class Lexer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def normalize_tokens( tokens_by_line )
|
|
12
|
+
|
|
13
|
+
tokens_by_line = tokens_by_line.map do |tokens|
|
|
14
|
+
|
|
15
|
+
nodes = []
|
|
16
|
+
|
|
17
|
+
buf = Tokens.new( tokens )
|
|
18
|
+
## pp buf
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
loop do
|
|
22
|
+
break if buf.eos?
|
|
23
|
+
|
|
24
|
+
if buf.match?( :DATE, :TIME ) ## merge DATE TIME into DATETIME
|
|
25
|
+
date = buf.next
|
|
26
|
+
time = buf.next
|
|
27
|
+
## puts "DATETIME:"
|
|
28
|
+
## pp date, time
|
|
29
|
+
|
|
30
|
+
## note: time value is { time: {} } or
|
|
31
|
+
## { time: {}, time_local {} }
|
|
32
|
+
text = date.text + ' ' + time.text, ## concat string of two tokens
|
|
33
|
+
value = { date: date.value }.merge( time.value )
|
|
34
|
+
|
|
35
|
+
nodes << Token.new(:DATETIME, text,
|
|
36
|
+
lineno: date.lineno,
|
|
37
|
+
offset: [date.offset[0],
|
|
38
|
+
time.offset[1]],
|
|
39
|
+
value: value )
|
|
40
|
+
### support date time with comma too - why? why not?
|
|
41
|
+
elsif buf.match?( :DATE, ',', :TIME )
|
|
42
|
+
date = buf.next
|
|
43
|
+
_ = buf.next ## ignore comma
|
|
44
|
+
time = buf.next
|
|
45
|
+
## puts "DATETIME:"
|
|
46
|
+
## pp date, time
|
|
47
|
+
text = date.text + ', ' + time.text ## concat string of two tokens
|
|
48
|
+
value = { date: date.value }.merge( time.value )
|
|
49
|
+
|
|
50
|
+
nodes << Token.new(:DATETIME, text,
|
|
51
|
+
lineno: date.lineno,
|
|
52
|
+
offset: [date.offset[0],
|
|
53
|
+
time.offset[1]],
|
|
54
|
+
value: value )
|
|
55
|
+
elsif buf.match?( :GOAL_MINUTE, ',', :GOAL_MINUTE )
|
|
56
|
+
## note - only advance by two tokens!
|
|
57
|
+
## allows more :GOAL_MINUTE sequences!! e.g. 12,13,14 etc!!!
|
|
58
|
+
##
|
|
59
|
+
## help parser with comma shift/reduce conflict
|
|
60
|
+
## change ',' to GOAL_MINUTE_SEP !!!
|
|
61
|
+
nodes << buf.next ## pass through goal_minute
|
|
62
|
+
comma = buf.next ## eat-up goal_minute_sep a.k.a. comma (,)
|
|
63
|
+
## and replace with dedicated sep(arator)
|
|
64
|
+
nodes << Token.new( :GOAL_MINUTE_SEP,
|
|
65
|
+
comma.text,
|
|
66
|
+
lineno: comma.lineno,
|
|
67
|
+
offset: comma.offset,
|
|
68
|
+
value: comma.value)
|
|
69
|
+
elsif buf.match?( ',', :INLINE_ATTENDANCE )
|
|
70
|
+
## note - allow optional comma before inline attendance
|
|
71
|
+
## help parser with comma shift/reduce conflict
|
|
72
|
+
## change ',' to INLINE_ATTENDANCE_SEP !!!
|
|
73
|
+
comma = buf.next ## eat-up inline_attendance_sep a.k.a. comma (,)
|
|
74
|
+
## and replace with dedicated sep(arator)
|
|
75
|
+
nodes << Token.new(:INLINE_ATTENDANCE_SEP,
|
|
76
|
+
comma.text,
|
|
77
|
+
lineno: comma.lineno,
|
|
78
|
+
offset: comma.offset,
|
|
79
|
+
value: comma.value)
|
|
80
|
+
nodes << buf.next ## pass through inline_attendance
|
|
81
|
+
else
|
|
82
|
+
## pass through
|
|
83
|
+
nodes << buf.next
|
|
84
|
+
end
|
|
85
|
+
end # loop
|
|
86
|
+
nodes
|
|
87
|
+
end # map tokens_by_line
|
|
88
|
+
|
|
89
|
+
tokens_by_line
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
end ## class Lexer
|
|
93
|
+
end ## module Fbtxt
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
|
|
2
|
+
module Fbtxt
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Lexer
|
|
7
|
+
include Debuggable ## auto-adds debug?, _trace, _info, etc.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def initialize( txt )
|
|
11
|
+
raise ArgumentError, "text as string expected for lexer; got #{txt.class}" unless txt.is_a?(String)
|
|
12
|
+
|
|
13
|
+
@txt = txt
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
## check if lexer is in prop(erty) mode
|
|
20
|
+
## note - property lines auto-continue
|
|
21
|
+
## and break on blank or another property line
|
|
22
|
+
## note - follow-up line/match MUST
|
|
23
|
+
## add PROP_END token to last line!!!
|
|
24
|
+
def is_prop_cont? ## use prop_mode? or such - why? why not?
|
|
25
|
+
@re == PROP_LINEUP_RE ||
|
|
26
|
+
@re == PROP_CARDS_RE ||
|
|
27
|
+
@re == PROP_PENALTIES_RE ||
|
|
28
|
+
@re == PROP_ATTENDANCE_RE ||
|
|
29
|
+
@re == PROP_REFEREE_RE ||
|
|
30
|
+
@re == PROP_COACH_RE
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
## auto-continue
|
|
35
|
+
## ends on closing-parenthesis `)`
|
|
36
|
+
def is_goal_cont?
|
|
37
|
+
@re == GOAL_RE ||
|
|
38
|
+
@re == GOAL_ALT_RE ||
|
|
39
|
+
@re == GOAL_COMPAT_RE
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def tokenize_with_errors( flatten: true )
|
|
46
|
+
|
|
47
|
+
tokens_by_line = [] ## note: add tokens line-by-line (flatten later)
|
|
48
|
+
errors = [] ## keep a list of errors - why? why not?
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
txt = _prep_doc( @txt )
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
####
|
|
56
|
+
## quick hack - keep re state/mode between tokenize calls!!!
|
|
57
|
+
@re ||= RE ## note - switch between RE & INSIDE_RE
|
|
58
|
+
|
|
59
|
+
lineno = 0
|
|
60
|
+
txt.each_line do |line|
|
|
61
|
+
lineno += 1
|
|
62
|
+
|
|
63
|
+
## todo - "inlined virtual/collapsed/folded newlines"
|
|
64
|
+
## check for "↵" !!!
|
|
65
|
+
## and add to lineno
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## note - KEEP leading spaces for indent
|
|
69
|
+
## use rstrip (NOT left/leading & right/trainling strip) only!!
|
|
70
|
+
## note - remove/strip trailing newline (and optional spaces)!!!
|
|
71
|
+
## trailing whitespace may incl. \n or \r\n!!!
|
|
72
|
+
line = line.rstrip
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### skip comments
|
|
76
|
+
## todo/check - change to blank line
|
|
77
|
+
## to keep lineno (closer to orginal) - why? why not?
|
|
78
|
+
next if line.match?(/\A [ ]* ## optional leading space(s)
|
|
79
|
+
\#
|
|
80
|
+
/x )
|
|
81
|
+
|
|
82
|
+
## strip (inline) end-of-line comments (from line)
|
|
83
|
+
## check/discuss: make - inline comment require trailing space
|
|
84
|
+
## e.g. #1 vs # 1 - why? why not?
|
|
85
|
+
line = line.sub( / [ ]* ## (eat-up) optional leading space(s) too - why? why not?
|
|
86
|
+
\#{1,}.*?
|
|
87
|
+
\z
|
|
88
|
+
/x, '' )
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# support __END__ marker to cut-off input
|
|
92
|
+
break if line.match?( /\A [ ]* ## optional leading space(s)
|
|
93
|
+
__END__
|
|
94
|
+
\z
|
|
95
|
+
/x )
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## auto-fixes line-by-line (e.g. check for tabs, smart quotes, etc.)
|
|
100
|
+
line = _prep_line( line )
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_trace( "line #{lineno}: >#{line}<" )
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
######
|
|
107
|
+
### special case for empty line (aka BLANK)
|
|
108
|
+
if line.empty?
|
|
109
|
+
## finish prop (if in prop mode)
|
|
110
|
+
tokens_by_line[-1] << Token.virtual(:PROP_END) if is_prop_cont?
|
|
111
|
+
|
|
112
|
+
## note - blank always resets parser mode to std/top-level!!!
|
|
113
|
+
@re = RE
|
|
114
|
+
tokens_by_line << [Token.virtual(:BLANK, lineno: lineno)]
|
|
115
|
+
elsif (m = HEADING_RE.match(line))
|
|
116
|
+
## finish prop (if in prop mode)
|
|
117
|
+
tokens_by_line[-1] << Token.virtual(:PROP_END) if is_prop_cont?
|
|
118
|
+
|
|
119
|
+
## note - heading always resets parser mode to std/top-level!!!
|
|
120
|
+
@re = RE
|
|
121
|
+
_trace( 'HEADING' )
|
|
122
|
+
## note - derive heading level from no of (leading) markers
|
|
123
|
+
## e.g. = is 1, == is 2, == is 3, etc.
|
|
124
|
+
heading_level = m[:heading_marker].size
|
|
125
|
+
tokens_by_line << [Token.new(:"H#{heading_level}", m[:heading], lineno: lineno)]
|
|
126
|
+
elsif (m = NOTA_BENE_RE.match(line))
|
|
127
|
+
## finish prop (if in prop mode)
|
|
128
|
+
tokens_by_line[-1] << Token.virtual(:PROP_END) if is_prop_cont?
|
|
129
|
+
|
|
130
|
+
## note - nota bene always resets parser mode to std/top-level!!!
|
|
131
|
+
@re = RE
|
|
132
|
+
tokens_by_line << [Token.new(:NOTA_BENE, m[:nota_bene], lineno: lineno)]
|
|
133
|
+
else
|
|
134
|
+
|
|
135
|
+
## finish prop (if in prop mode) and new prop upcoming!!
|
|
136
|
+
##
|
|
137
|
+
## todo/fix-fix-fix - add check for and track identation (left-side)
|
|
138
|
+
## (i) break if identation is same or less!!!
|
|
139
|
+
## (ii) handle "sub" properties too
|
|
140
|
+
if is_prop_cont? && (m = START_WITH_PROP_KEY_RE.match( line ))
|
|
141
|
+
_trace( "LEAVE PROP_RE MODE, BACK TO TOP_LEVEL/RE" )
|
|
142
|
+
@re = RE
|
|
143
|
+
tokens_by_line[-1] << Token.virtual(:PROP_END)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
more_tokens, more_errors = _tokenize_line( line, lineno )
|
|
147
|
+
|
|
148
|
+
tokens_by_line << more_tokens
|
|
149
|
+
errors += more_errors
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
## output last line from tokens by line in debug mode
|
|
154
|
+
_trace( " #{tokens_by_line[-1].size} token(s): " + tokens_by_line[-1].pretty_inspect )
|
|
155
|
+
|
|
156
|
+
end # each line
|
|
157
|
+
|
|
158
|
+
## finish prop (if in prop mode)
|
|
159
|
+
tokens_by_line[-1] << Token.virtual(:PROP_END) if is_prop_cont?
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
## note - always switch back to top-level at the end
|
|
163
|
+
@re = RE
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
#################
|
|
167
|
+
## transform (normalize) tokens (using simple patterns)
|
|
168
|
+
## to help along the (racc look ahead 1 - LA1) parser
|
|
169
|
+
tokens_by_line = normalize_tokens( tokens_by_line )
|
|
170
|
+
## puts "tokens_by_line:"
|
|
171
|
+
## pp tokens_by_line
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## flatten tokens
|
|
176
|
+
## check - simple use tokens_by_line.flatten - why? why not?
|
|
177
|
+
## tip: use flatten(1) !! - flattens exactly one level deep (only)
|
|
178
|
+
if flatten
|
|
179
|
+
tokens = []
|
|
180
|
+
tokens_by_line.each do |tok_line|
|
|
181
|
+
tokens += tok_line
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
[tokens,errors]
|
|
185
|
+
else
|
|
186
|
+
[tokens_by_line, errors]
|
|
187
|
+
end
|
|
188
|
+
end # method tokenize
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
end # class Lexer
|
|
192
|
+
end # module Fbtxt
|