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,130 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+ ## "internal" date helpers
6
+ def self._build_date( m )
7
+ date = {}
8
+ ## map month names
9
+ ## note - allow any/upcase JULY/JUL etc. thus ALWAYS downcase for lookup
10
+ date[:y] = m[:year].to_i(10) if m[:year]
11
+ ## check - use y too for two-digit year or keep separate - why? why not?
12
+ date[:yy] = m[:yy].to_i(10) if m[:yy] ## two digit year (e.g. 25 or 78 etc.)
13
+ date[:m] = m[:month].to_i(10) if m[:month]
14
+ date[:m] = MONTH_MAP[ m[:month_name].downcase ] if m[:month_name]
15
+ date[:d] = m[:day].to_i(10) if m[:day]
16
+ date[:wday] = DAY_MAP[ m[:day_name].downcase ] if m[:day_name]
17
+
18
+ date
19
+ end
20
+
21
+ def self._build_date_legs( m )
22
+ legs = {}
23
+ ## map month names
24
+ ## note - allow any/upcase JULY/JUL etc. thus ALWAYS downcase for lookup
25
+ date = {}
26
+ date[:m] = MONTH_MAP[ m[:month_name1].downcase ]
27
+ date[:d] = m[:day1].to_i(10)
28
+ legs[:date1] = date
29
+
30
+ date = {}
31
+ date[:m] = MONTH_MAP[ m[:month_name2].downcase ] if m[:month_name2]
32
+ date[:d] = m[:day2].to_i(10)
33
+ legs[:date2] = date
34
+
35
+ legs
36
+ end
37
+
38
+
39
+ def self._build_duration( m )
40
+ ## todo/check/fix - if end: works for kwargs!!!!!
41
+ duration = { start: {}, end: {}}
42
+
43
+ duration[:start][:y] = m[:year1].to_i(10) if m[:year1]
44
+ duration[:start][:m] = MONTH_MAP[ m[:month_name1].downcase ] if m[:month_name1]
45
+ duration[:start][:d] = m[:day1].to_i(10) if m[:day1]
46
+ duration[:start][:wday] = DAY_MAP[ m[:day_name1].downcase ] if m[:day_name1]
47
+
48
+ duration[:end][:y] = m[:year2].to_i(10) if m[:year2]
49
+ duration[:end][:m] = MONTH_MAP[ m[:month_name2].downcase ] if m[:month_name2]
50
+ duration[:end][:d] = m[:day2].to_i(10) if m[:day2]
51
+ duration[:end][:wday] = DAY_MAP[ m[:day_name2].downcase ] if m[:day_name2]
52
+
53
+ duration
54
+ end
55
+
56
+
57
+
58
+
59
+ def _build_date( m ) self.class._build_date( m ); end
60
+ def _build_date_legs( m ) self.class._build_date_legs( m ); end
61
+ def _build_duration( m ) self.class._build_duration( m ); end
62
+
63
+
64
+
65
+
66
+ #############
67
+ ## "top-level" add a date parser helper
68
+
69
+ ## note: parse_date - returns Date object
70
+ ## _parse_date (with underscore) - return hash of "parsed" regex match data!!
71
+
72
+ def self.parse_date( str, start: nil )
73
+ if m = _parse_date( str )
74
+ year = m[:y]
75
+ yy = m[:yy]
76
+
77
+ ####
78
+ ## support two digit shortcut for year
79
+ if yy && year.nil?
80
+ ###
81
+ ## for now assume 00,01 to 30 is 2000,2001 to 2030
82
+ ## and 31 to 99 is 1931 to 1999
83
+ year = yy <= 30 ? 2000+yy : 1900+yy
84
+ end
85
+
86
+ month = m[:m]
87
+ day = m[:d]
88
+ wday = m[:wday]
89
+
90
+
91
+ if year.nil? ## try to calculate year
92
+ raise ArgumentError, "year required in date >#{str}< or pass along start date" if start.nil?
93
+
94
+ year = if month > start.month ||
95
+ (month == start.month && day >= start.day)
96
+ # assume same year as start_at event (e.g. 2013 for 2013/14 season)
97
+ start.year
98
+ else
99
+ # assume year+1 as start_at event (e.g. 2014 for 2013/14 season)
100
+ start.year+1
101
+ end
102
+ end
103
+ Date.new( year,month,day )
104
+ else
105
+ raise ArgumentError, "unexpected date format; cannot parse >#{str}<"
106
+ end
107
+ end
108
+
109
+
110
+
111
+ def self._parse_date( str )
112
+ ## note - strip - leading/trailing spaces automatic - why? why not?
113
+ m = DATE_RE.match( str.strip )
114
+
115
+ if m && m.pre_match == '' && m.post_match == ''
116
+ ## return hash table with captured components
117
+ date = _build_date( m )
118
+ date
119
+ elsif m
120
+ ## note - match BUT not anchored to start and end-of-string!!!
121
+ ## report, error somehow??
122
+ nil
123
+ else
124
+ nil ## no match - return nil
125
+ end
126
+ end
127
+
128
+
129
+ end # class Lexer
130
+ end # module Fbtxt
@@ -0,0 +1,108 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+ def self.parse_names( txt )
6
+ lines = [] # array of lines (with words)
7
+
8
+ txt.each_line do |line|
9
+ line = line.strip
10
+
11
+ next if line.empty?
12
+ next if line.start_with?( '#' ) ## skip comments too
13
+
14
+ ## strip inline (until end-of-line) comments too
15
+ ## e.g. Janvier Janv Jan ## check janv in use??
16
+ ## => Janvier Janv Jan
17
+
18
+ line = line.sub( /#.*/, '' ).strip
19
+ ## pp line
20
+
21
+ values = line.split( /[ \t]+/ )
22
+ ## pp values
23
+
24
+ ## todo/fix -- add check for duplicates
25
+ lines << values
26
+ end
27
+ lines
28
+
29
+ end # method parse
30
+
31
+
32
+ def self.build_names( lines )
33
+ ## join all words together into a single string e.g.
34
+ ## January|Jan|February|Feb|March|Mar|April|Apr|May|June|Jun|...
35
+ lines.map { |line| line.join('|') }.join('|')
36
+ end
37
+
38
+
39
+ def self.build_map( lines, downcase: false )
40
+ ## note: downcase name!!!
41
+ ## build a lookup map that maps the word to the index (line no) plus 1 e.g.
42
+ ## {"january" => 1, "jan" => 1,
43
+ ## "february" => 2, "feb" => 2,
44
+ ## "march" => 3, "mar" => 3,
45
+ ## "april" => 4, "apr" => 4,
46
+ ## "may" => 5,
47
+ ## "june" => 6, "jun" => 6, ...
48
+ lines.each_with_index.reduce( {} ) do |h,(line,i)|
49
+ line.each do |name|
50
+ h[ downcase ? name.downcase : name ] = i+1
51
+ end ## note: start mapping with 1 (and NOT zero-based, that is, 0)
52
+ h
53
+ end
54
+ end
55
+
56
+
57
+
58
+
59
+ MONTH_LINES = parse_names( <<TXT )
60
+ January Jan
61
+ February Feb
62
+ March Mar
63
+ April Apr
64
+ May
65
+ June Jun
66
+ July Jul
67
+ August Aug
68
+ September Sept Sep
69
+ October Oct
70
+ November Nov
71
+ December Dec
72
+ TXT
73
+
74
+ MONTH_NAMES = build_names( MONTH_LINES )
75
+ # pp MONTH_NAMES
76
+ MONTH_MAP = build_map( MONTH_LINES, downcase: true )
77
+ # pp MONTH_MAP
78
+
79
+
80
+
81
+ DAY_LINES = parse_names( <<TXT )
82
+ Monday Mon Mo
83
+ Tuesday Tues Tue Tu
84
+ Wednesday Wed We
85
+ Thursday Thurs Thur Thu Th
86
+ Friday Fri Fr
87
+ Saturday Sat Sa
88
+ Sunday Sun Su
89
+ TXT
90
+
91
+ DAY_NAMES = build_names( DAY_LINES )
92
+ # pp DAY_NAMES
93
+ DAY_MAP = build_map( DAY_LINES, downcase: true )
94
+ # pp DAY_MAP
95
+
96
+
97
+ #=>
98
+ # "January|Jan|February|Feb|March|Mar|April|Apr|May|June|Jun|
99
+ # July|Jul|August|Aug|September|Sept|Sep|October|Oct|
100
+ # November|Nov|December|Dec"
101
+ #
102
+ # "Monday|Mon|Mo|Tuesday|Tues|Tue|Tu|Wednesday|Wed|We|
103
+ # Thursday|Thurs|Thur|Thu|Th|Friday|Fri|Fr|
104
+ # Saturday|Sat|Sa|Sunday|Sun|Su"
105
+
106
+
107
+ end # class Lexer
108
+ end # module Fbtxt
@@ -0,0 +1,200 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+
6
+ # e.g. Fri Aug 9
7
+ # Fri Aug 9
8
+ ## Fri, Aug 9
9
+ ## Fri, Aug 9 2024
10
+ ## Fri, Aug 9, 2024
11
+ ## Aug 9, 2024
12
+ ## Aug 9, 2024
13
+ ## note - eat-up optional comma after DAY_NAMES!!
14
+ ##
15
+ ## note - Fri Aug/9 no longer supported!!!
16
+ DATE_I_RE = %r{
17
+ (?<date>
18
+ \b
19
+ ## optional day name
20
+ ((?<day_name>#{DAY_NAMES})
21
+ (?: ,?[ ]+)
22
+ )?
23
+ (?<month_name>#{MONTH_NAMES})
24
+ [ ]
25
+ (?<day>\d{1,2})
26
+ \b
27
+ ## optional year
28
+ ( ,? [ ] ## note - comma optinal with single space required for now
29
+ (?<year>\d{4}) ## optional year 2025 (yyyy)
30
+ \b
31
+ )?
32
+ )}ix
33
+
34
+
35
+ ### todo/fix - add (opt) day_name later
36
+ ## add (opt) year later
37
+ # e.g. Aug 9 & Aug 10
38
+ ### note - allow shortcut e.g. Aug 9 & 10
39
+ DATE_LEGS_I_RE = %r{
40
+ (?<date_legs>
41
+ \b
42
+ (?<month_name1>#{MONTH_NAMES})
43
+ [ ]
44
+ (?<day1>\d{1,2})
45
+ [ ] & [ ]
46
+ (?:
47
+ (?<month_name2>#{MONTH_NAMES})
48
+ [ ]
49
+ )? ## note - make 2nd month_name optional
50
+ (?<day2>\d{1,2})
51
+ \b
52
+ )}ix
53
+
54
+
55
+ ###
56
+ # e.g. 3 June or 10 June
57
+ ## note - allow more spaces between DAY_NAMES and DAY e.g.
58
+ ## Sun 1 Mar
59
+ ## Wed 4 Mar
60
+ ## Sat 14 Mar
61
+ ## Sat 11 Apr
62
+ ## Sat 11 Apr 2021
63
+ ## Sat 11 Apr 21
64
+ ##
65
+ ## Sat, 11 Apr
66
+ ## note - eat-up optional comma after DAY_NAMES!!
67
+ ##
68
+ ## note - Sat 14 Mar 17:30
69
+ ## check two-digit year (with NEGATIVE lookahead for time!!!)
70
+
71
+ DATE_II_RE = %r{
72
+ (?<date>
73
+ \b
74
+ ## optional day name
75
+ ((?<day_name>#{DAY_NAMES})
76
+ (?: ,?[ ]+)
77
+ )?
78
+ (?<day>\d{1,2})
79
+ [ ]
80
+ (?<month_name>#{MONTH_NAMES})
81
+ \b
82
+ ## optional year
83
+ ( [ ]
84
+ (?:
85
+ (?<year>\d{4}) ## optional year 2025 (yyyy)
86
+ |
87
+ (?:
88
+ (?<yy>\d{2}) ## optional year 25 (yy)
89
+ ## check NEGATIVE lookahead
90
+ (?! :|[:h]\d{2})
91
+ )
92
+ )
93
+ \b
94
+ )?
95
+ )}ix
96
+
97
+
98
+ # e.g. iso-date - 2011-08-25
99
+ ## note - allow/support ("shortcuts") e.g 2011-8-25 or 2011-8-3 / 2011-08-03 etc.
100
+ DATE_III_A_RE = %r{
101
+ (?<date>
102
+ \b
103
+ (?<year>\d{4})
104
+ -
105
+ (?<month>\d{1,2})
106
+ -
107
+ (?<day>\d{1,2})
108
+ \b
109
+ )}ix
110
+
111
+ ## starting w/ day/month/year e.g. 25-08-2011
112
+ DATE_III_B_RE = %r{
113
+ (?<date>
114
+ \b
115
+ ## optional day name
116
+ ((?<day_name>#{DAY_NAMES})
117
+ (?: ,?[ ]+)
118
+ )?
119
+ (?<day>\d{1,2})
120
+ -
121
+ (?<month>\d{1,2})
122
+ -
123
+ (?<year>\d{4})
124
+ \b
125
+ )}ix
126
+
127
+
128
+
129
+ ## allow (short)"european" style 8.8.
130
+ ## note - assume day/month!!!
131
+ DATE_IIII_RE = %r{
132
+ (?<date>
133
+ \b
134
+ ## optional day name
135
+ ((?<day_name>#{DAY_NAMES})
136
+ (?: ,?[ ]+)
137
+ )?
138
+ (?<day>\d{1,2})
139
+ \.
140
+ (?<month>\d{1,2})
141
+ \.
142
+ (?: (?:
143
+ (?<year>\d{4}) ## optional year 2025 (yyyy)
144
+ |
145
+ (?<yy>\d{2}) ## optional year 25 (yy)
146
+ )
147
+ \b
148
+ )?
149
+ )
150
+ }ix
151
+
152
+
153
+ ####################
154
+ ### 04/03/2026 or 4/3/2026
155
+ ## 04/03/26 or 4/3/26
156
+ ## 04/03 or 4/3
157
+ DATE_IIIII_RE = %r{
158
+ (?<date>
159
+ \b
160
+ ## optional day name
161
+ ((?<day_name>#{DAY_NAMES})
162
+ (?: ,?[ ]+)
163
+ )?
164
+ (?<day>\d{1,2})
165
+ /
166
+ (?<month>\d{1,2})
167
+ \b
168
+ (?:
169
+ /
170
+ (?:
171
+ (?<year>\d{4}) ## optional year 2025 (yyyy)
172
+ |
173
+ (?<yy>\d{2}) ## optional year 25 (yy)
174
+ )
175
+ \b
176
+ )?
177
+ )
178
+ }ix
179
+
180
+
181
+
182
+ #############################################
183
+ # map tables
184
+ # note: order matters; first come-first matched/served
185
+ DATE_RE = Regexp.union(
186
+ DATE_I_RE,
187
+ DATE_II_RE,
188
+ DATE_III_A_RE, ## e.g. 1973-08-14
189
+ DATE_III_B_RE,
190
+ DATE_IIII_RE, ## e.g. 8.8. or 8.13.79 or 08.14.1973
191
+ DATE_IIIII_RE, ## e.g. 08/14/1973
192
+ )
193
+
194
+ ## todo - add more format style here; change to Regexp.union later!!!
195
+ DATE_LEGS_RE = DATE_LEGS_I_RE
196
+
197
+
198
+
199
+ end # class Lexer
200
+ end # module Fbtxt
@@ -0,0 +1,171 @@
1
+ module Fbtxt
2
+ class Lexer
3
+
4
+
5
+
6
+
7
+ ###
8
+ # date duration
9
+ # use - or + as separator
10
+ # in theory plus( +) only if dates
11
+ # are two days next to each other
12
+ #
13
+ # otherwise define new dates type in the future? why? why not?
14
+ #
15
+ # check for plus (+) if dates are next to each other (t+1) - why? why not?
16
+
17
+ #
18
+ # Sun Jun 23 - Wed Jun 26 -- YES
19
+ # Jun 23 - Jun 26 -- YES
20
+ # Jun 25 - 26 - why? why not??? - YES - see blow variant iii!!!
21
+
22
+ # Tue Jun 25 + Wed Jun 26 -- NO
23
+ # Jun 25 + Jun 26 -- NO
24
+ # Jun 25 .. 26 - why? why not???
25
+ # Jun 25 to 26 - why? why not???
26
+ # Jun 25 + 26 - add - why? why not???
27
+ # Sun-Wed Jun 23-26 - add - why? why not???
28
+ # Wed+Thu Jun 26+27 2024 - add - why? why not???
29
+ #
30
+ # maybe use comma and plus for list of dates
31
+ # Tue Jun 25, Wed Jun 26, Thu Jun 27 ??
32
+ # Tue Jun 25 + Wed Jun 26 + Thu Jun 27 ??
33
+ #
34
+ # add back optional comma (before) year - why? why not?
35
+ #
36
+
37
+
38
+ ##
39
+ # todo add plus later on - why? why not?
40
+ ### todo/fix add optional comma (,) before year
41
+
42
+ ### regex note/tip/remindr - \b () \b MUST always get enclosed in parantheses
43
+ ## because alternation (|) has lowest priority/binding
44
+
45
+
46
+ DURATION_I_RE = %r{
47
+ (?<duration>
48
+ \b
49
+ (?:
50
+ ## optional day name
51
+ ((?<day_name1>#{DAY_NAMES})
52
+ [ ]
53
+ )?
54
+ (?<month_name1>#{MONTH_NAMES})
55
+ [ ]
56
+ (?<day1>\d{1,2})
57
+ ## optional year
58
+ ( ,? # optional comma
59
+ [ ]
60
+ (?<year1>\d{4})
61
+ )?
62
+
63
+ ## support + and - (add .. or such - why??)
64
+ [ ]* - [ ]*
65
+
66
+ ## optional day name
67
+ ((?<day_name2>#{DAY_NAMES})
68
+ [ ]
69
+ )?
70
+ (?<month_name2>#{MONTH_NAMES})
71
+ [ ]
72
+ (?<day2>\d{1,2})
73
+ ## optional year
74
+ ( ,? # optional comma
75
+ [ ]
76
+ (?<year2>\d{4})
77
+ )?
78
+ )
79
+ \b
80
+ )}ix
81
+
82
+
83
+
84
+ # FIX - remove this variant
85
+ # "standardize on month day [year]" !!!!
86
+
87
+ =begin
88
+ ###
89
+ # variant ii
90
+ # e.g. 26 July - 27 July
91
+ # 26 July,
92
+ XXX_DURATION_II_RE = %r{
93
+ (?<duration>
94
+ \b
95
+ (?
96
+ ## optional day name
97
+ ((?<day_name1>#{DAY_NAMES})
98
+ [ ]
99
+ )?
100
+ (?<day1>\d{1,2})
101
+ [ ]
102
+ (?<month_name1>#{MONTH_NAMES})
103
+ ## optional year
104
+ (
105
+ [ ]
106
+ (?<year1>\d{4})
107
+ )?
108
+
109
+ ## support + and - (add .. or such - why??)
110
+ [ ]*[-][ ]*
111
+
112
+ ## optional day name
113
+ ((?<day_name2>#{DAY_NAMES})
114
+ [ ]
115
+ )?
116
+ (?<day2>\d{1,2})
117
+ [ ]
118
+ (?<month_name2>#{MONTH_NAMES})
119
+ ## optional year
120
+ ( [ ]
121
+ (?<year2>\d{4})
122
+ )?
123
+ )
124
+ \b
125
+ )}ix
126
+ =end
127
+
128
+
129
+ # variant ii
130
+ # add support for shorthand
131
+ # August 16-18, 2011
132
+ # September 13-15, 2011
133
+ # October 18-20, 2011
134
+ # March 6-8 2012
135
+ # March 6-8
136
+ #
137
+ # - add support for August 16+17 or such (and check 16+18)
138
+ # use <op> to check if day2 is a plus or range or such - why? why not?
139
+
140
+ DURATION_II_RE = %r{
141
+ (?<duration>
142
+ \b
143
+ (?:
144
+ (?<month_name1>#{MONTH_NAMES})
145
+ [ ]
146
+ (?<day1>\d{1,2})
147
+ -
148
+ (?<day2>\d{1,2})
149
+ (?:
150
+ ,? ## optional comma
151
+ [ ]
152
+ (?<year1>\d{4})
153
+ )? ## optional year
154
+ )
155
+ \b
156
+ )}ix
157
+
158
+
159
+
160
+ #############################################
161
+ # map tables
162
+ # note: order matters; first come-first matched/served
163
+ DURATION_RE = Regexp.union(
164
+ DURATION_I_RE,
165
+ DURATION_II_RE,
166
+ )
167
+
168
+
169
+
170
+ end # class Lexer
171
+ end # module Fbtxt