fbtxt-parser 0.9.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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +4 -0
  3. data/Manifest.txt +50 -0
  4. data/README.md +22 -0
  5. data/Rakefile +53 -0
  6. data/lib/fbtxt/parser/debuggable.rb +53 -0
  7. data/lib/fbtxt/parser/lexer-logger.rb +20 -0
  8. data/lib/fbtxt/parser/lexer-on_goal.rb +167 -0
  9. data/lib/fbtxt/parser/lexer-on_group_def.rb +31 -0
  10. data/lib/fbtxt/parser/lexer-on_prop_lineup.rb +79 -0
  11. data/lib/fbtxt/parser/lexer-on_prop_misc.rb +123 -0
  12. data/lib/fbtxt/parser/lexer-on_prop_penalties.rb +40 -0
  13. data/lib/fbtxt/parser/lexer-on_round_def.rb +37 -0
  14. data/lib/fbtxt/parser/lexer-on_top.rb +133 -0
  15. data/lib/fbtxt/parser/lexer-prep_doc.rb +131 -0
  16. data/lib/fbtxt/parser/lexer-prep_line.rb +63 -0
  17. data/lib/fbtxt/parser/lexer-tokenize.rb +468 -0
  18. data/lib/fbtxt/parser/lexer.rb +231 -0
  19. data/lib/fbtxt/parser/lexer_buffer.rb +68 -0
  20. data/lib/fbtxt/parser/lexer_token.rb +126 -0
  21. data/lib/fbtxt/parser/parse_tree--core.rb +29 -0
  22. data/lib/fbtxt/parser/parse_tree-match.rb +309 -0
  23. data/lib/fbtxt/parser/parse_tree-props.rb +233 -0
  24. data/lib/fbtxt/parser/parse_tree.rb +202 -0
  25. data/lib/fbtxt/parser/parser-runtime.rb +379 -0
  26. data/lib/fbtxt/parser/parser-top.rb +102 -0
  27. data/lib/fbtxt/parser/parser.rb +2343 -0
  28. data/lib/fbtxt/parser/token-date--helpers.rb +130 -0
  29. data/lib/fbtxt/parser/token-date--names.rb +108 -0
  30. data/lib/fbtxt/parser/token-date.rb +200 -0
  31. data/lib/fbtxt/parser/token-date_duration.rb +171 -0
  32. data/lib/fbtxt/parser/token-geo.rb +134 -0
  33. data/lib/fbtxt/parser/token-goals--helpers.rb +114 -0
  34. data/lib/fbtxt/parser/token-goals.rb +306 -0
  35. data/lib/fbtxt/parser/token-group.rb +29 -0
  36. data/lib/fbtxt/parser/token-note.rb +40 -0
  37. data/lib/fbtxt/parser/token-prop.rb +309 -0
  38. data/lib/fbtxt/parser/token-prop_name.rb +83 -0
  39. data/lib/fbtxt/parser/token-round.rb +88 -0
  40. data/lib/fbtxt/parser/token-score--helpers.rb +189 -0
  41. data/lib/fbtxt/parser/token-score.rb +60 -0
  42. data/lib/fbtxt/parser/token-score_full.rb +331 -0
  43. data/lib/fbtxt/parser/token-score_fuller.rb +434 -0
  44. data/lib/fbtxt/parser/token-score_legs.rb +59 -0
  45. data/lib/fbtxt/parser/token-status.rb +192 -0
  46. data/lib/fbtxt/parser/token-status_inline.rb +112 -0
  47. data/lib/fbtxt/parser/token-text.rb +221 -0
  48. data/lib/fbtxt/parser/token-time.rb +144 -0
  49. data/lib/fbtxt/parser/token.rb +224 -0
  50. data/lib/fbtxt/parser/version.rb +24 -0
  51. data/lib/fbtxt/parser.rb +140 -0
  52. metadata +145 -0
@@ -0,0 +1,468 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+ ###
6
+ ## use nested class for context - why? why not?
7
+ ## note: first arg passed in MUST be ref to lexer (instance)
8
+ class Context
9
+ ## passed along to on_round_def etc. handlers in tokenize_line
10
+ ## note - for now only offset (in line begin/end) gets updated !!!
11
+ attr_writer :offset
12
+ attr_reader :lineno
13
+
14
+ def initialize( lexer,
15
+ line:,
16
+ lineno:,
17
+ errors: )
18
+ @lexer = lexer
19
+ @line = line
20
+ @lineno = lineno
21
+ @errors = errors
22
+
23
+ @offset = [0,0] ## or use [] aka [nil,nil] for not defined??? why? why not?
24
+ ## @offset = offset ## MatchData offset e.g. [m.begin(0),m.end(0)]
25
+ end
26
+
27
+
28
+
29
+ def warn_on_else( match, mode: 'TOP' )
30
+ if match[:any]
31
+ _add_warn( "unexpected char >#{match[:any]}< (#{mode})" )
32
+ else
33
+ ## internal error - shouldn't really happen
34
+ _add_warn( "internal error - unknown match (#{mode}): #{match.inspect}")
35
+ end
36
+ end
37
+
38
+
39
+ def _add_warn( msg )
40
+ ## note - warns gets logged as error for now too
41
+ ## maybe add @warns later - why? why not?
42
+ ##
43
+ ## note - add +1 to offset (start at one - not zero-based)
44
+ ## will match with (external) text editors
45
+ msg = "parse error (tokenize) - " +
46
+ msg +
47
+ " in line @#{@lineno}:#{@offset[0]+1},#{@offset[1]+1} >#{@line}< "
48
+
49
+ @errors << msg
50
+ @lexer.log( "!! WARN - #{msg}" )
51
+
52
+ @lexer._warn( msg )
53
+ end
54
+
55
+ =begin
56
+ ## use report/log/??_parses_error
57
+ def _add_error( msg )
58
+ msg = "parse error (tokenize) -" +
59
+ msg +
60
+ " in line #{@lineno}@#{@offset[0]},#{@offse[1]} >#{@line}< "
61
+
62
+ @errors << msg
63
+ end
64
+ =end
65
+
66
+ end # class Context
67
+
68
+
69
+
70
+
71
+
72
+ def _tokenize_line( line, lineno )
73
+ tokens = []
74
+ errors = [] ## keep a list of errors - why? why not?
75
+
76
+
77
+ pos = 0 ## note - usually same as offset[1] aka offset[end] after match
78
+ ## track last offset (begin/end) - to report error on no match
79
+ ## or no match in end of string
80
+ offset = [0,0]
81
+ m = nil
82
+
83
+ ## track number of geo text seen
84
+ ## (use for - do NOT break on two spaces if no geo text seen yet!!)
85
+ @geo_count = 0
86
+
87
+ ####
88
+ ## quick hack - keep re state/mode between tokenize calls!!!
89
+ @re ||= RE ## note - switch between RE & INSIDE_RE
90
+
91
+
92
+ if @re == RE ## top-level
93
+ ### check for modes once (per line) here to speed-up parsing
94
+ ### for now goals only possible for start of line!!
95
+ ### fix - remove optional [] - why? why not?
96
+
97
+ ####
98
+ ## note - ord e.g. (45) for match number can only start a (match) line
99
+ ## "inline" use NOT possible
100
+ ## note - ord (for ordinal number!!!) e.g match number (1), (42), etc.
101
+ if (m = START_WITH_ORD.match(line))
102
+ ## note - strip enclosing () and convert to integer
103
+ tokens << Token.new(:ORD, m[:ord],
104
+ lineno: lineno, offset: m.offset(:ord),
105
+ value: m[:value].to_i(10) )
106
+
107
+ offset = m.offset(0)
108
+ pos = offset[1] ## update pos
109
+ elsif (m = START_WITH_YEAR.match(line))
110
+ tokens << Token.new(:YEAR, m[:year],
111
+ lineno: lineno, offset: m.offset(:year),
112
+ value: m[:year].to_i(10) )
113
+
114
+ offset = m.offset(0)
115
+ pos = offset[1] ## update pos
116
+
117
+ elsif (m = START_WITH_GROUP_DEF_LINE_RE.match( line ))
118
+ _trace( "ENTER GROUP_DEF_RE MODE" )
119
+ @re = GROUP_DEF_RE
120
+
121
+ tokens << Token.new( :GROUP_DEF, m[:group_def],
122
+ lineno: lineno, offset: m.offset(:group_def) )
123
+
124
+
125
+ offset = m.offset(0)
126
+ pos = offset[1] ## update pos
127
+
128
+ elsif (m = START_WITH_PROP_KEY_RE.match( line ))
129
+ ## start with prop key (match will switch into prop mode!!!)
130
+ ## - fix - remove leading spaces in regex (upstream) - why? why not?
131
+ ##
132
+ ### switch into new mode
133
+ ## switch context to PROP_RE
134
+ _trace("ENTER PROP_RE MODE" )
135
+ key = m[:key]
136
+
137
+
138
+ ### todo/fix - add prop yellow/red cards too - why? why not?
139
+ ## todo/fix - separate sent off and red card
140
+ ## sent-off - incl. red card, yellow/red card and the era before red cards!!
141
+ if ['sent-off',
142
+ 'sent off'].include?( key.downcase)
143
+ @re = PROP_CARDS_RE ## use CARDS_RE ???
144
+ tokens << Token.new(:PROP_SENTOFF, m[:key],
145
+ lineno: lineno, offset: m.offset(:key))
146
+ elsif ['red cards'].include?( key.downcase )
147
+ @re = PROP_CARDS_RE ## use CARDS_RE ???
148
+ tokens << Token.new(:PROP_REDCARDS, m[:key],
149
+ lineno: lineno, offset: m.offset(:key))
150
+ elsif ['yellow cards'].include?( key.downcase )
151
+ @re = PROP_CARDS_RE
152
+ tokens << Token.new(:PROP_YELLOWCARDS, m[:key],
153
+ lineno: lineno, offset: m.offset(:key))
154
+
155
+ ## todo: check allow yellow/red cards: or such as key!!!
156
+ ## check for alternate spellings
157
+ ## 'yellow/red cards' - note - meaning closes to yellow or red !!
158
+ ## thus use yellow-red cards as default/standard - why? why not?
159
+ elsif ['yellow-red cards',
160
+ 'yellowred cards'].include?( key.downcase )
161
+ @re = PROP_CARDS_RE
162
+ tokens << Token.new(:PROP_YELLOWREDCARDS, m[:key],
163
+ lineno: lineno, offset: m.offset(:key))
164
+
165
+
166
+ elsif ['ref', 'referee',
167
+ 'refs', 'referees' ## note - allow/support assistant refs
168
+ ].include?( key.downcase )
169
+ @re = PROP_REFEREE_RE
170
+ tokens << Token.new(:PROP_REFEREE, m[:key],
171
+ lineno: lineno, offset: m.offset(:key))
172
+ elsif ['att', 'attn', 'attendance'].include?( key.downcase )
173
+ @re = PROP_ATTENDANCE_RE
174
+ tokens << Token.new(:PROP_ATTENDANCE, m[:key],
175
+ lineno: lineno, offset: m.offset(:key))
176
+
177
+ # elsif ['goals'].include?( key.downcase )
178
+ # @re = PROP_GOAL_RE
179
+ # tokens << [:PROP_GOALS, m[:key]]
180
+
181
+ elsif ['penalties',
182
+ 'penalty shootout',
183
+ 'penalty shoot-out',
184
+ 'penalty kicks'].include?( key.downcase )
185
+ @re = PROP_PENALTIES_RE
186
+ tokens << Token.new(:PROP_PENALTIES, m[:key],
187
+ lineno: lineno, offset: m.offset(:key))
188
+ else ## assume (team) line-up
189
+ @re = PROP_LINEUP_RE
190
+ ## fix-fix-fix - rename to PROP_LINEUP !!
191
+ tokens << Token.new(:PROP, m[:key],
192
+ lineno: lineno, offset: m.offset(:key))
193
+ end
194
+
195
+ offset = m.offset(0)
196
+ pos = offset[1] ## update pos
197
+ ###
198
+ ### todo/fix
199
+ ### rename to START_WITH_ROUND_DEF_OUTLINE_RE !!!!
200
+ elsif (m = ROUND_DEF_OUTLINE_RE.match( line ))
201
+ _trace( "ENTER ROUND_DEF_RE MODE" )
202
+ @re = ROUND_DEF_RE
203
+
204
+ ## note - return ROUND_DEF NOT ROUND_OUTLINE token
205
+ ## fix - add leading ▪ too!!
206
+ tokens << Token.new( :ROUND_DEF, m[:round_outline],
207
+ lineno: lineno, offset: m.offset(:round_outline))
208
+
209
+ offset = m.offset(0)
210
+ pos = offset[1] ## update pos
211
+ elsif (m = ROUND_OUTLINE_RE.match( line ))
212
+ _trace( "ROUND_OUTLINE" )
213
+ ## note - derive round level from no of (leading) markers
214
+ ## e.g. ▪/:: is 1, ▪▪/::: is 2, ▪▪▪/:::: is 3, etc.
215
+ ## note - ascii-style starts with double ::, thus, autodecrement by one!
216
+ round_level = m[:round_marker].size
217
+ round_level -= 1 if m[:round_marker].start_with?( '::' )
218
+
219
+ tokens << Token.new( :ROUND_OUTLINE, m[:round_outline],
220
+ lineno: lineno, offset: m.offset(:round_outline),
221
+ value: { outline: m[:round_outline],
222
+ level: round_level})
223
+
224
+ ## note - eats-up line for now (change later to only eat-up marker e.g. »|>>)
225
+ offset = m.offset(0)
226
+ pos = offset[1] ## update pos
227
+ elsif (m = START_GOAL_LINE_RE.match( line )) ## line starting with ( - assume
228
+ ## switch context to GOAL_RE (goalline(s))
229
+ ####
230
+ ## note - check for alternate goal line styles / formats
231
+ if START_GOAL_LINE_COMPAT_RE.match(line )
232
+ ## "legacy" style starting with minute e.g.
233
+ ## (6 Puskás 0-1, 9 Czibor 0-2, 11 Morlock 1-2, 18 Rahn 2-2,
234
+ ## 84 Rahn 3-2)
235
+ @re = GOAL_COMPAT_RE
236
+ _trace( "ENTER GOAL_COMPAT_RE MODE" )
237
+
238
+ tokens << Token.virtual( :GOALS_COMPAT, lineno: lineno )
239
+ elsif START_GOAL_LINE_ALT_RE.match( line )
240
+ ## goals with scores e.g.
241
+ ## (1-0 Franck Ribéry, 2-0 Ivica Olić, 2-1 Wayne Rooney)
242
+ ## -or-
243
+ ## (Dion Beljo 1-0
244
+ ## 1-1 Andreas Gruber
245
+ ## Matthias Seidl 2-1)
246
+ @re = GOAL_ALT_RE
247
+ _trace( "ENTER GOAL_ALT_RE MODE" )
248
+
249
+ tokens << Token.virtual( :GOALS_ALT, lineno: lineno )
250
+ else
251
+ ## "standard" / default style
252
+ @re = GOAL_RE
253
+ _trace( "ENTER GOAL_RE MODE" )
254
+
255
+ tokens << Token.virtual( :GOALS, lineno: lineno )
256
+ end
257
+
258
+ ## note - eat-up ( for now
259
+ ## pass along "virtual" GOALS or GOALS_ALT token
260
+ ## (see INLINE_GOALS for the starting goal line inline)
261
+ ##
262
+ ## fix-fix-fix
263
+ ## keep offset at [0,0] - why? why not?
264
+ ## do NOT eat-up
265
+ ## or better
266
+ ## add tokens << Token.literal( '(', lineno: lineno, offset: ...) !!!
267
+ offset = m.offset(0)
268
+ pos = offset[1] ## update pos
269
+ end
270
+ end
271
+
272
+
273
+
274
+ old_pos = -1 ## allows to backtrack to old pos (used in geo)
275
+
276
+
277
+
278
+
279
+ ctx = Context.new( self,
280
+ line: line,
281
+ lineno: lineno,
282
+ errors: errors )
283
+
284
+
285
+ while m = @re.match( line, pos )
286
+ # if debug?
287
+ # pp m
288
+ # puts "pos: #{pos}"
289
+ # end
290
+ offset = m.offset(0)
291
+ ctx.offset = offset
292
+
293
+
294
+
295
+ if offset[0] != pos
296
+ ## match NOT starting at start/begin position!!!
297
+ ## report parse error!!!
298
+ msg = "parse error (tokenize) - skipping >#{line[pos..(offset[0]-1)]}< in line #{lineno}@#{offset[0]},#{offset[1]} >#{line}<"
299
+ errors << msg
300
+
301
+ log( msg )
302
+ puts "!! WARN - #{msg}"
303
+ end
304
+
305
+
306
+ ##
307
+ ## todo/fix - also check if possible
308
+ ## if no match but not yet end off string!!!!
309
+ ## report skipped text run too!!!
310
+
311
+ old_pos = pos
312
+ pos = offset[1]
313
+
314
+ # pp offset if debug?
315
+
316
+ ##
317
+ ## note: racc requires pairs e.g. [:TOKEN, VAL]
318
+ ## for VAL use "text" or ["text", { opts }] array
319
+
320
+
321
+
322
+ t = if @re == ROUND_DEF_RE then _on_round_def( m, ctx: ctx )
323
+ elsif @re == GROUP_DEF_RE then _on_group_def( m, ctx: ctx )
324
+ elsif @re == GEO_RE
325
+ ### note - possibly end inline geo on [ (and others?? in the future
326
+ ## note: break on double spaces e.g.
327
+ ## e.g. Jul/16 @ Arena Auf Schalke, Gelsenkirchen Serbia 0-1 England
328
+ if m[:spaces]
329
+ ### note - do NOT break out
330
+ ## if not text seen yet!!!
331
+ if @geo_count > 0
332
+ ## get out-off geo mode and backtrack (w/ next)
333
+ ##
334
+ ## todo/fix
335
+ ## add virtual geo_end token!!!
336
+ _trace( "LEAVE GEO_RE MODE, BACK TO TOP_LEVEL/RE" )
337
+ @re = RE
338
+ pos = old_pos
339
+ next ## backtrack (resume new loop step)
340
+ else
341
+ nil ## skip spaces
342
+ end
343
+ elsif m[:space]
344
+ nil ## skip (single) space
345
+ elsif m[:text]
346
+ @geo_count += 1
347
+ ## keep pos - why? why not?
348
+ Token.new(:GEO, m[:text],
349
+ lineno: lineno, offset: m.offset(:text))
350
+ elsif m[:geo_end] ## "hacky" special comma; always ends geo mode!!!
351
+ ## get out-off geo mode and backtrack (w/ next)
352
+ ## todo/fix
353
+ ## add (semi-) virtual geo_end token!!!
354
+ _trace( "LEAVE GEO_RE MODE, BACK TO TOP_LEVEL/RE" )
355
+ @re = RE
356
+ pos = old_pos
357
+ next ## backtrack (resume new loop step)
358
+ elsif m[:sym]
359
+ case m[:sym]
360
+ ## note - reset geo_count to 0 (avoids break on two spaces)
361
+ ## if separator seen!!
362
+ when ',' then @geo_count = 0
363
+ Token.literal( m[:sym], lineno: lineno, offset: m.offset(:sym))
364
+ when '›' then @geo_count = 0;
365
+ Token.literal( ',', lineno: lineno, offset: m.offset(:sym))
366
+ ## note - treat geo sep › (unicode) like comma for now!!!
367
+ when '>' then @geo_count = 0;
368
+ Token.literal( ',', lineno: lineno, offset: m.offset(:sym))
369
+ ## note - treat geo sep > (ascii) like comma for now!!!
370
+
371
+
372
+ when '[','▪'
373
+ ##
374
+ ## todo/fix
375
+ ## add virtual geo_end token!!!
376
+ ## get out-off geo mode and backtrack (w/ next)
377
+ _trace( "LEAVE GEO_RE MODE, BACK TO TOP_LEVEL/RE" )
378
+ @re = RE
379
+ pos = old_pos
380
+ next ## backtrack (resume new loop step)
381
+ ## fix-fix-fix merge '▪' with '['
382
+ else
383
+ Token.literal( m[:sym], lineno: lineno, offset: m.offset(:sym))
384
+ end
385
+ else
386
+ ctx.warn_on_else( m, mode: 'GEO' )
387
+ nil
388
+ end
389
+ elsif @re == PROP_CARDS_RE then _on_prop_cards( m, ctx: ctx )
390
+ elsif @re == PROP_LINEUP_RE then _on_prop_lineup( m, ctx: ctx )
391
+ elsif @re == PROP_ATTENDANCE_RE then _on_prop_attendance( m, ctx: ctx )
392
+ elsif @re == PROP_REFEREE_RE then _on_prop_referee( m, ctx: ctx )
393
+ elsif @re == PROP_PENALTIES_RE then _on_prop_penalties( m, ctx: ctx )
394
+ elsif @re == GOAL_COMPAT_RE then _on_goal_compat( m, ctx: ctx )
395
+ elsif @re == GOAL_ALT_RE then _on_goal_alt( m, ctx: ctx )
396
+ elsif @re == GOAL_RE then _on_goal( m, ctx: ctx )
397
+ ###################################################
398
+ ## assume TOP_LEVEL (a.k.a. RE) machinery
399
+ else
400
+ _on_top( m, ctx: ctx )
401
+ end
402
+
403
+
404
+ tokens << t if t
405
+
406
+ # if debug?
407
+ # print ">"
408
+ # print "*" * pos
409
+ # puts "#{line[pos..-1]}<"
410
+ # end
411
+ end
412
+
413
+ ## check if no match in end of string
414
+ if offset[1] != line.size
415
+ msg = "parse error (tokenize) - skipping >#{line[offset[1]..-1]}< in line #{lineno}@#{offset[1]},#{line.size} >#{line}<"
416
+ errors << msg
417
+
418
+ log( msg )
419
+ puts "!! WARN - #{msg}"
420
+ end
421
+
422
+
423
+ # if @re == GOAL_RE ### ALWAYS switch back to top level mode
424
+ # puts " LEAVE GOAL_RE MODE, BACK TO TOP_LEVEL/RE" if debug?
425
+ # @re = RE
426
+ # end
427
+
428
+ if @re == GEO_RE ### ALWAYS switch back to top level mode
429
+ _trace( "LEAVE GEO_RE MODE, BACK TO TOP_LEVEL/RE" )
430
+ @re = RE
431
+ end
432
+
433
+ ### ALWAYS switch back to top level mode
434
+ @re = RE if @re == GROUP_DEF_RE ||
435
+ @re == ROUND_DEF_RE
436
+
437
+ ##
438
+ ## if in prop mode continue if last token is [,-]
439
+ ## otherwise change back to "standard" mode
440
+ if @re == PROP_LINEUP_RE ||
441
+ @re == PROP_CARDS_RE ||
442
+ @re == PROP_PENALTIES_RE ||
443
+ @re == PROP_ATTENDANCE_RE ||
444
+ @re == PROP_REFEREE_RE
445
+
446
+ ## note - add :CARDS_SEP_ALT (aka -)!!! too
447
+ ## maybe later CARDS_NONE_LEFT too - why? why not?
448
+ if [',', '-', ';', :CARDS_SEP_ALT].include?( tokens[-1].type)
449
+ ## continue/stay in PROP_RE mode
450
+ ## todo/check - auto-add PROP_CONT token or such
451
+ ## to help parser with possible NEWLINE
452
+ ## conflicts - why? why not?
453
+ else
454
+ ## switch back to top-level mode!!
455
+ _trace( "LEAVE PROP_RE MODE, BACK TO TOP_LEVEL/RE" )
456
+ @re = RE
457
+ ## note - auto-add PROP_END (<PROP_END>)
458
+ tokens << Token.virtual(:PROP_END, lineno: lineno)
459
+ end
460
+ end
461
+
462
+
463
+ [tokens,errors]
464
+ end
465
+
466
+
467
+ end ## class Lexer
468
+ end ## module Fbtxt
@@ -0,0 +1,231 @@
1
+
2
+ module Fbtxt
3
+
4
+ class LexerResult
5
+ attr_reader :tokens, :errors
6
+ def initialize( tokens, errors=[] )
7
+ @tokens, @errors = tokens, errors
8
+ end
9
+
10
+ def ok?() @errors.size == 0; end
11
+ def nok?() !ok?; end
12
+ end # class LexerResult
13
+
14
+
15
+
16
+ class Lexer
17
+ include Debuggable ## auto-adds debug?, _trace, _info, etc.
18
+
19
+
20
+ def initialize( txt )
21
+ raise ArgumentError, "text as string expected for lexer; got #{txt.class}" unless txt.is_a?(String)
22
+
23
+ @txt = txt
24
+ end
25
+
26
+
27
+ def tokenize_with_errors( flatten: true )
28
+
29
+ tokens_by_line = [] ## note: add tokens line-by-line (flatten later)
30
+ errors = [] ## keep a list of errors - why? why not?
31
+
32
+
33
+ txt = _prep_doc( @txt )
34
+
35
+
36
+
37
+ ####
38
+ ## quick hack - keep re state/mode between tokenize calls!!!
39
+ @re ||= RE ## note - switch between RE & INSIDE_RE
40
+
41
+ lineno = 0
42
+ txt.each_line do |line|
43
+ lineno += 1
44
+
45
+ ## todo - "inlined virtual/collapsed/folded newlines"
46
+ ## check for "↵" !!!
47
+ ## and add to lineno
48
+
49
+
50
+ ## note - KEEP leading spaces for indent
51
+ ## use rstrip (NOT left/leading & right/trainling strip) only!!
52
+ ## note - remove/strip trailing newline (and optional spaces)!!!
53
+ ## trailing whitespace may incl. \n or \r\n!!!
54
+ line = line.rstrip
55
+
56
+
57
+ ### skip comments
58
+ ## todo/check - change to blank line
59
+ ## to keep lineno (closer to orginal) - why? why not?
60
+ next if line.match?(/\A [ ]* ## optional leading space(s)
61
+ \#
62
+ /x )
63
+
64
+ ## strip (inline) end-of-line comments (from line)
65
+ ## check/discuss: make - inline comment require trailing space
66
+ ## e.g. #1 vs # 1 - why? why not?
67
+ line = line.sub( / [ ]* ## (eat-up) optional leading space(s) too - why? why not?
68
+ \#{1,}.*?
69
+ \z
70
+ /x, '' )
71
+
72
+
73
+ # support __END__ marker to cut-off input
74
+ break if line.match?( /\A [ ]* ## optional leading space(s)
75
+ __END__
76
+ \z
77
+ /x )
78
+
79
+
80
+
81
+ ## auto-fixes line-by-line (e.g. check for tabs, smart quotes, etc.)
82
+ line = _prep_line( line )
83
+
84
+
85
+ _trace( "line #{lineno}: >#{line}<" )
86
+
87
+
88
+ ######
89
+ ### special case for empty line (aka BLANK)
90
+ if line.empty?
91
+ ## note - blank always resets parser mode to std/top-level!!!
92
+ @re = RE
93
+ tokens_by_line << [Token.virtual(:BLANK, lineno: lineno)]
94
+ elsif (m = HEADING_RE.match(line))
95
+ ## note - heading always resets parser mode to std/top-level!!!
96
+ @re = RE
97
+ _trace( 'HEADING' )
98
+ ## note - derive heading level from no of (leading) markers
99
+ ## e.g. = is 1, == is 2, == is 3, etc.
100
+ heading_level = m[:heading_marker].size
101
+ tokens_by_line << [Token.new(:"H#{heading_level}", m[:heading], lineno: lineno)]
102
+ elsif (m = NOTA_BENE_RE.match(line))
103
+ ## note - nota bene always resets parser mode to std/top-level!!!
104
+ @re = RE
105
+ tokens_by_line << [Token.new(:NOTA_BENE, m[:nota_bene], lineno: lineno)]
106
+ else
107
+
108
+ more_tokens, more_errors = _tokenize_line( line, lineno )
109
+
110
+ tokens_by_line << more_tokens
111
+ errors += more_errors
112
+ end
113
+
114
+ ## output last line from tokens by line in debug mode
115
+ _trace( " #{tokens_by_line[-1].size} token(s): " + tokens_by_line[-1].pretty_inspect )
116
+
117
+ end # each line
118
+
119
+
120
+
121
+
122
+ tokens_by_line = tokens_by_line.map do |tokens|
123
+
124
+ #################
125
+ ## transform tokens (using simple patterns)
126
+ ## to help along the (racc look ahead 1 - LA1) parser
127
+ nodes = []
128
+
129
+ buf = Tokens.new( tokens )
130
+ ## pp buf
131
+
132
+
133
+ loop do
134
+ break if buf.eos?
135
+
136
+ if buf.match?( :DATE, :TIME ) ## merge DATE TIME into DATETIME
137
+ date = buf.next
138
+ time = buf.next
139
+ ## puts "DATETIME:"
140
+ ## pp date, time
141
+
142
+ ## note: time value is { time: {} } or
143
+ ## { time: {}, time_local {} }
144
+ text = date.text + ' ' + time.text, ## concat string of two tokens
145
+ value = { date: date.value }.merge( time.value )
146
+
147
+ nodes << Token.new(:DATETIME, text,
148
+ lineno: date.lineno,
149
+ offset: [date.offset[0],
150
+ time.offset[1]],
151
+ value: value )
152
+ ### support date time with comma too - why? why not?
153
+ elsif buf.match?( :DATE, ',', :TIME )
154
+ date = buf.next
155
+ _ = buf.next ## ignore comma
156
+ time = buf.next
157
+ ## puts "DATETIME:"
158
+ ## pp date, time
159
+ text = date.text + ', ' + time.text ## concat string of two tokens
160
+ value = { date: date.value }.merge( time.value )
161
+
162
+ nodes << Token.new(:DATETIME, text,
163
+ lineno: date.lineno,
164
+ offset: [date.offset[0],
165
+ time.offset[1]],
166
+ value: value )
167
+ elsif buf.match?( :GOAL_MINUTE, ',', :GOAL_MINUTE )
168
+ ## note - only advance by two tokens!
169
+ ## allows more :GOAL_MINUTE sequences!! e.g. 12,13,14 etc!!!
170
+ ##
171
+ ## help parser with comma shift/reduce conflict
172
+ ## change ',' to GOAL_MINUTE_SEP !!!
173
+ nodes << buf.next ## pass through goal_minute
174
+ comma = buf.next ## eat-up goal_minute_sep a.k.a. comma (,)
175
+ ## and replace with dedicated sep(arator)
176
+ nodes << Token.new( :GOAL_MINUTE_SEP,
177
+ comma.text,
178
+ lineno: comma.lineno,
179
+ offset: comma.offset,
180
+ value: comma.value)
181
+ elsif buf.match?( ',', :INLINE_ATTENDANCE )
182
+ ## note - allow optional comma before inline attendance
183
+ ## help parser with comma shift/reduce conflict
184
+ ## change ',' to INLINE_ATTENDANCE_SEP !!!
185
+ comma = buf.next ## eat-up inline_attendance_sep a.k.a. comma (,)
186
+ ## and replace with dedicated sep(arator)
187
+ nodes << Token.new(:INLINE_ATTENDANCE_SEP,
188
+ comma.text,
189
+ lineno: comma.lineno,
190
+ offset: comma.offset,
191
+ value: comma.value)
192
+ nodes << buf.next ## pass through inline_attendance
193
+ else
194
+ ## pass through
195
+ nodes << buf.next
196
+ end
197
+ end # loop
198
+ nodes
199
+ end # map tokens_by_line
200
+
201
+
202
+ ## puts "tokens_by_line:"
203
+ ## pp tokens_by_line
204
+
205
+ if flatten
206
+ ## flatten tokens
207
+ tokens = []
208
+ tokens_by_line.each do |tok_line|
209
+
210
+ ## if debug?
211
+ ## pp tok_line
212
+ ## end
213
+
214
+ tokens += tok_line
215
+
216
+ ## auto-add newlines (unless BLANK!!)
217
+ unless tok_line[0] && tok_line[0].type == :BLANK
218
+ ## note - reuse lineno from first token in line
219
+ ## use last - why? why not?
220
+ tokens << Token.newline( lineno: tok_line[0].lineno )
221
+ end
222
+ end
223
+ [tokens,errors]
224
+ else
225
+ [tokens_by_line, errors]
226
+ end
227
+ end # method tokenize
228
+
229
+
230
+ end # class Lexer
231
+ end # module Fbtxt