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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/Manifest.txt +47 -0
  4. data/README.md +18 -0
  5. data/Rakefile +30 -0
  6. data/lib/fbtxt/lexer/debuggable.rb +58 -0
  7. data/lib/fbtxt/lexer/lexer-logger.rb +20 -0
  8. data/lib/fbtxt/lexer/lexer-on_goal.rb +167 -0
  9. data/lib/fbtxt/lexer/lexer-on_group_def.rb +31 -0
  10. data/lib/fbtxt/lexer/lexer-on_prop_cards.rb +61 -0
  11. data/lib/fbtxt/lexer/lexer-on_prop_lineup.rb +82 -0
  12. data/lib/fbtxt/lexer/lexer-on_prop_misc.rb +108 -0
  13. data/lib/fbtxt/lexer/lexer-on_prop_penalties.rb +44 -0
  14. data/lib/fbtxt/lexer/lexer-on_round_def.rb +37 -0
  15. data/lib/fbtxt/lexer/lexer-on_top.rb +133 -0
  16. data/lib/fbtxt/lexer/lexer-prep_doc.rb +131 -0
  17. data/lib/fbtxt/lexer/lexer-prep_line.rb +63 -0
  18. data/lib/fbtxt/lexer/lexer-props.rb +66 -0
  19. data/lib/fbtxt/lexer/lexer-tokenize_line.rb +381 -0
  20. data/lib/fbtxt/lexer/lexer-tokenize_norm.rb +93 -0
  21. data/lib/fbtxt/lexer/lexer.rb +192 -0
  22. data/lib/fbtxt/lexer/lexer_buffer.rb +68 -0
  23. data/lib/fbtxt/lexer/lexer_context.rb +70 -0
  24. data/lib/fbtxt/lexer/lexer_token.rb +127 -0
  25. data/lib/fbtxt/lexer/token-date--helpers.rb +130 -0
  26. data/lib/fbtxt/lexer/token-date--names.rb +108 -0
  27. data/lib/fbtxt/lexer/token-date.rb +200 -0
  28. data/lib/fbtxt/lexer/token-date_duration.rb +171 -0
  29. data/lib/fbtxt/lexer/token-geo.rb +173 -0
  30. data/lib/fbtxt/lexer/token-goals--helpers.rb +114 -0
  31. data/lib/fbtxt/lexer/token-goals.rb +306 -0
  32. data/lib/fbtxt/lexer/token-group.rb +29 -0
  33. data/lib/fbtxt/lexer/token-note.rb +40 -0
  34. data/lib/fbtxt/lexer/token-prop.rb +334 -0
  35. data/lib/fbtxt/lexer/token-prop_name.rb +83 -0
  36. data/lib/fbtxt/lexer/token-round.rb +88 -0
  37. data/lib/fbtxt/lexer/token-score--helpers.rb +189 -0
  38. data/lib/fbtxt/lexer/token-score.rb +60 -0
  39. data/lib/fbtxt/lexer/token-score_full.rb +331 -0
  40. data/lib/fbtxt/lexer/token-score_fuller.rb +434 -0
  41. data/lib/fbtxt/lexer/token-score_legs.rb +59 -0
  42. data/lib/fbtxt/lexer/token-status.rb +192 -0
  43. data/lib/fbtxt/lexer/token-status_inline.rb +112 -0
  44. data/lib/fbtxt/lexer/token-text.rb +221 -0
  45. data/lib/fbtxt/lexer/token-time.rb +144 -0
  46. data/lib/fbtxt/lexer/token.rb +224 -0
  47. data/lib/fbtxt/lexer/version.rb +24 -0
  48. data/lib/fbtxt/lexer.rb +118 -0
  49. 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