fbtxt-document 0.9.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e52e2c2ec111cd72919ddb7c902d39824f00ada678705eaf58ee72dd996ec85
4
- data.tar.gz: 03ba7e0bf6255fca7abef9ac0adb0add827dc95d61ce72e1d71be88f641cb037
3
+ metadata.gz: cede645c5634b77011d3c7989db7af19e68083f8af89436af1aabb6c873d115e
4
+ data.tar.gz: a4e98460ccc55c134d835add92b8dcdd7d35640c831cf504b11a36f88d25f34b
5
5
  SHA512:
6
- metadata.gz: f6b2fe58e43b658dfd8f5716f05f3f264f70a25845edf1cd3829106ce170b8c181cd067afa79003cbbd3d739135371a4403f2054f7cd6cee740e15e308695763
7
- data.tar.gz: 43252d347f1619a5e5050de924db180e4d69407b6b250630f82228fb785d89f40844a51267a5e0561805ba8df01e904229528843c475a65f8ca5a6cd3625e0db
6
+ metadata.gz: ec91acce5438a7260b6f1fc17475f91c05bce5b469bd284e613afbe89a9e4fc3b1bd0929c1845d59baa977c67b1ba8625a914e2a01d074273842d29d0cfa6871
7
+ data.tar.gz: d6f6db6fa65ad6126235d0b5205d2b294205979244d2c1e014a141ecd195e22d4ebd4c4bc6a9f828b8cb02072615fb13092dfc563b63598555a1bde311a37d85
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ### 0.9.0
1
+ ### 0.9.1
2
2
  ### 0.0.1 / 2024-08-27
3
3
 
4
4
  * Everything is new. First release.
data/Manifest.txt CHANGED
@@ -3,17 +3,30 @@ Manifest.txt
3
3
  README.md
4
4
  Rakefile
5
5
  lib/fbtxt/document.rb
6
+ lib/fbtxt/document/document.rb
7
+ lib/fbtxt/document/export_utils.rb
6
8
  lib/fbtxt/document/match_tree-helpers.rb
7
9
  lib/fbtxt/document/match_tree.rb
8
- lib/fbtxt/document/match_tree/goal.rb
9
- lib/fbtxt/document/match_tree/group.rb
10
- lib/fbtxt/document/match_tree/match.rb
11
- lib/fbtxt/document/match_tree/round.rb
10
+ lib/fbtxt/document/match_tree_on/on_cards_line.rb
12
11
  lib/fbtxt/document/match_tree_on/on_date_header.rb
13
12
  lib/fbtxt/document/match_tree_on/on_goal_line.rb
14
13
  lib/fbtxt/document/match_tree_on/on_group_def.rb
14
+ lib/fbtxt/document/match_tree_on/on_lineup_line.rb
15
+ lib/fbtxt/document/match_tree_on/on_match_bye.rb
15
16
  lib/fbtxt/document/match_tree_on/on_match_line.rb
17
+ lib/fbtxt/document/match_tree_on/on_penalties.rb
18
+ lib/fbtxt/document/match_tree_on/on_referee_line.rb
16
19
  lib/fbtxt/document/match_tree_on/on_round_def.rb
17
20
  lib/fbtxt/document/match_tree_on/on_round_outline.rb
18
- lib/fbtxt/document/quick_match_reader.rb
21
+ lib/fbtxt/document/models/event.rb
22
+ lib/fbtxt/document/models/goal.rb
23
+ lib/fbtxt/document/models/group.rb
24
+ lib/fbtxt/document/models/lineup.rb
25
+ lib/fbtxt/document/models/match.rb
26
+ lib/fbtxt/document/models/minute.rb
27
+ lib/fbtxt/document/models/penalty.rb
28
+ lib/fbtxt/document/models/player.rb
29
+ lib/fbtxt/document/models/referee.rb
30
+ lib/fbtxt/document/models/round.rb
31
+ lib/fbtxt/document/models/score.rb
19
32
  lib/fbtxt/document/version.rb
@@ -0,0 +1,100 @@
1
+
2
+ ##
3
+ ## add shared quick (export) helper / utility methods
4
+ ###
5
+
6
+ module Fbtxt
7
+ module Export
8
+
9
+ #####
10
+ ## todo
11
+ ## move to Fbtxt::Export.genjson or such
12
+ ## or use Fbtxt::Serialize/Batch or ??
13
+
14
+
15
+ ##
16
+ # usage for config
17
+ # e.g.
18
+ # [['1930/worldcup.json', ['worldcup/1930--uruguay/cup.txt']],
19
+ # ['1934/worldcup.json', ['worldcup/1934--italy/cup.txt']],
20
+ # ['2026/worldcup.json', ['worldcup/2026--usa/cup.txt', 'worldcup/2026--usa/cup_finals.txt']]]
21
+ #
22
+ # todo/check:
23
+ # change/rename indir to sourcedir/rootdir or such - why? why not?
24
+ def self.genjson( config, outdir: '.',
25
+ indir: '.' )
26
+
27
+
28
+ config.each do |outfile, infiles|
29
+
30
+ ## step 1
31
+ ## collect (parse) docs
32
+ docs = []
33
+ infiles.each_with_index do |infile,i|
34
+ inpath = File.join( indir, infile )
35
+
36
+ print "==> reading"
37
+ print " [#{i+1}/#{infiles.size}]" if infiles.size > 1
38
+ print " #{inpath}...\n"
39
+
40
+ doc = Document.read( inpath )
41
+
42
+ if doc.errors?
43
+ puts "!! #{doc.errors.size} parse error(s) in #{inpath}:"
44
+ pp doc.errors
45
+ exit 1
46
+ end
47
+
48
+ docs << doc
49
+ end
50
+
51
+ data = _merge_matches( *docs )
52
+
53
+ outpath = File.join( outdir, outfile )
54
+
55
+ ####################
56
+ ## hack - use pretty_inspect for json pretty print
57
+ txtjson = data.pretty_inspect
58
+ txtjson = txtjson.gsub( '=>', ': ' )
59
+ ## puts txtjson[0,100] + "..."
60
+ ## double check for syntax errors
61
+ json = JSON.parse( txtjson )
62
+
63
+ ##
64
+ ## try alternate pretty print
65
+ ## puts JSON.pretty_generate( data, object_nl: "\n", array_nl: "\n", indent: 2)
66
+ ##
67
+ ## write_json( outpath, data )
68
+
69
+ puts " writing to >#{outpath}<"
70
+ puts txtjson[0,100] + "..."
71
+
72
+ write_text( outpath, txtjson )
73
+ end
74
+ end
75
+
76
+
77
+ def self._merge_matches( *docs )
78
+ last_name = nil
79
+ matches = []
80
+
81
+ docs.each do |doc|
82
+ name = doc.title
83
+
84
+ if last_name && name != last_name
85
+ puts "!! ERROR - league names do NOT match; cannot merge/concat - sorry"
86
+ puts " #{last_name} != #{name}"
87
+ exit 1
88
+ end
89
+
90
+ last_name = name
91
+ matches += doc.matches
92
+ end
93
+
94
+ data = { 'name' => last_name,
95
+ 'matches' => matches.as_json }
96
+ data
97
+ end
98
+
99
+ end # module Export
100
+ end # module Fbtxt
@@ -7,6 +7,10 @@ class MatchTree
7
7
  include Debuggable
8
8
 
9
9
 
10
+ include Models ## e.g. Fbtxt::Model(s)
11
+ ## Match,Round,Group,Goal,etc.
12
+
13
+
10
14
  ##
11
15
  ## note: allow start(_date) nil
12
16
  ## if in use (start: nil) years expected on first date!!!
@@ -69,14 +73,19 @@ class MatchTree
69
73
  on_match_line_bye( node )
70
74
  elsif node.is_a? RaccMatchParser::GoalLine
71
75
  on_goal_line( node )
72
- elsif node.is_a?( RaccMatchParser::LineupLine ) ||
73
- node.is_a?( RaccMatchParser::RefereeLine )
74
- ## skip lineup, referee props for now
76
+ elsif node.is_a? RaccMatchParser::PenaltiesLine
77
+ on_penalties( node )
78
+ elsif node.is_a? RaccMatchParser::LineupLine
79
+ on_lineup_line( node )
80
+ elsif node.is_a? RaccMatchParser::CardsLine
81
+ on_cards_line( node )
82
+ elsif node.is_a? RaccMatchParser::RefereeLine
83
+ on_referee_line( node )
75
84
  elsif node.is_a?( RaccMatchParser::Heading1 ) ||
76
85
  node.is_a?( RaccMatchParser::Heading2 ) ||
77
86
  node.is_a?( RaccMatchParser::Heading3 )
78
87
  ### skip headings (1/2/3) for now
79
- elsif node.is_a?( RaccMatchParser::BlankLine )
88
+ elsif node.is_a? RaccMatchParser::BlankLine
80
89
  ### skip for now; do nothing
81
90
  else
82
91
  ## report error
@@ -95,32 +104,5 @@ class MatchTree
95
104
  end # method convert
96
105
 
97
106
 
98
-
99
-
100
- def on_match_line_bye( node )
101
- _trace( "on match (bye): >#{node}<" )
102
-
103
- ## note - bye records NO date/time or ground (or score etc.)
104
- ## for now only team1/team2 and match status!!
105
- ## plus inherited round/group
106
-
107
- status = 'bye'
108
-
109
- team = node.team
110
-
111
- @teams[ team ] += 1
112
-
113
- group = nil
114
- group = @last_group if @last_group
115
-
116
- round = nil
117
- round = @last_round if @last_round
118
-
119
- @matches << Match.new( team1: team, ## note: for now always use mapping value e.g. rec (NOT string e.g. team1.name)
120
- round: round ? round.name : nil, ## note: for now always use string (assume unique canonical name for event)
121
- group: group ? group.name : nil, ## note: for now always use string (assume unique canonical name for event)
122
- status: status )
123
- ### todo: cache team lookups in hash?
124
- end
125
107
  end # class MatchTree
126
108
  end # module Fbtxt
@@ -0,0 +1,66 @@
1
+ module Fbtxt
2
+ class MatchTree
3
+
4
+ def on_cards_line( node )
5
+ _trace( "on cards: >#{node}<" )
6
+
7
+ ## get last match
8
+ match = @matches[-1]
9
+
10
+
11
+ ################################
12
+ ## CardsLine = Struct.new( :type, :bookings )
13
+ ## Booking = Struct.new( :name, :minute )
14
+
15
+
16
+ ## check if match has lineup ??
17
+ if match.lineup
18
+ ## type = node.type ## e.g. 'Y', 'Y/R', 'R', 'SENTOFF', etc.
19
+
20
+ ## note - flatten for now
21
+ ## fix-fix-fix use lineup[0], lineup[1] latter - why? why not?
22
+ bookings = node.bookings.flatten
23
+
24
+
25
+ bookings.each do |b|
26
+ ## find player ref in match lineup (team1/team2)
27
+ player = nil
28
+ ## note - expapand lineup[] to one or two items
29
+ [*match.lineup].each_with_index do |lineup,i|
30
+ player = lineup.find_player( b.name )
31
+ next if player.nil?
32
+
33
+ ## fix-fix-fix- keep player obj/ref - why? why not?
34
+ rec = EventCard.new(
35
+ type: node.type,
36
+ name: b.name,
37
+ minute: b.minute )
38
+
39
+ match.bookings ||= [[],[]]
40
+ match.bookings[i] << rec
41
+
42
+ break
43
+ end
44
+
45
+ if player.nil?
46
+ ## raise ArgumentError,
47
+ ## "player >#{b.name}< NOT found in match (lineup): #{match.pretty_inspect}"
48
+
49
+ ### log warn for now
50
+ ## might be trainer, staff or missing player from bench w/o sub
51
+ ## fix-fix-fix
52
+ ## log error!!
53
+ msg = "!! WARN - player >#{b.name}< NOT found; skipping booking #{b.pretty_inspect}"
54
+ puts msg
55
+ log( msg )
56
+ end
57
+
58
+ end
59
+ else
60
+ ### warn - "standalone" bookings for now not yet supported!!!
61
+ end
62
+
63
+ end
64
+
65
+ end ## class MatchTree
66
+ end ## module Fbtxt
@@ -5,6 +5,14 @@ class MatchTree
5
5
  def on_goal_line( node )
6
6
  _trace( "on goal line: >#{node}<" )
7
7
 
8
+ ###
9
+ # note: goals formats
10
+ # (i) [[],[]] (nested/paired) or
11
+ # (ii) [] (flat/undefined)
12
+ ##
13
+ ## add Array#paired? and
14
+ ## Array#flat?
15
+ ## helper methods/checks or such - why? why not?
8
16
 
9
17
  if node.goals[0].is_a?( Array )
10
18
  goals1, goals2 = node.goals[0], node.goals[1]
@@ -17,25 +25,27 @@ class MatchTree
17
25
  pp [goals1,goals2] if debug?
18
26
 
19
27
 
28
+ ## quick & dirty - auto add goals to last match
29
+ ## note - for hacky (quick& dirty) multi-line support
30
+ ## always append for now
31
+ match = @matches[-1]
32
+
33
+
20
34
  ## special rule
21
35
  ## if goals 2 empty check if score for team 1 is zero
22
36
  ## and team 2 is NOT zero than
23
- ## make goals1 goald2!!
37
+ ## make goals1 goals2!!
24
38
  ## e.g. Norway 0-1 Austria
25
39
  ## (Hof 32)
26
-
27
- if goals2.empty? && !goals1.empty?
28
-
29
- match = @matches[-1]
40
+ if match.score &&
41
+ goals2.empty? && !goals1.empty?
30
42
 
31
43
  ##
32
- ## todo/fix
33
- ## move upstream
34
- ## use score1_zero? or such - why? why not?
35
- if (match.score.is_a?(Array) && match.score[0] == 0 ) ||
36
- (match.score.is_a?(Hash) && match.score[:et] && match.score[:et][0] == 0) ||
37
- (match.score.is_a?(Hash) && match.score[:et].nil? &&
38
- match.score[:ft] && match.score[:ft][0] == 0)
44
+ ## todo/fix: move upstream
45
+ ## use score1_zero? or such - why? why not?
46
+ if (match.score.reported? && match.score.reported[0] == 0 ) ||
47
+ (match.score.et? && match.score.et[0] == 0) ||
48
+ (match.score.ft? && !match.score.et? && match.score.ft[0] == 0)
39
49
  ## "parallel assignment (or multiple assignment") - swap values in single line
40
50
  goals2, goals1 = goals1, goals2
41
51
  end
@@ -43,18 +53,14 @@ class MatchTree
43
53
 
44
54
 
45
55
 
46
- ##
47
- ## todo/fix - sort goals by minute (+offset) !!!
48
-
49
56
  goals = []
50
57
 
51
58
  goals1.each do |rec|
52
59
  rec.minutes.each do |minute|
53
60
  goal = Goal.new(
54
- player: rec.player,
61
+ name: rec.player,
55
62
  team: 1,
56
- minute: minute.m,
57
- offset: minute.offset,
63
+ minute: minute.to_minute, ## from GoalMinute to Minute!!
58
64
  penalty: minute.pen || false, # note: pass along/use false NOT nil
59
65
  owngoal: minute.og || false
60
66
  )
@@ -65,10 +71,9 @@ class MatchTree
65
71
  goals2.each do |rec|
66
72
  rec.minutes.each do |minute|
67
73
  goal = Goal.new(
68
- player: rec.player,
74
+ name: rec.player,
69
75
  team: 2,
70
- minute: minute.m,
71
- offset: minute.offset,
76
+ minute: minute.to_minute, ## from GoalMinute to Minute!!
72
77
  penalty: minute.pen || false, # note: pass along/use false NOT nil
73
78
  owngoal: minute.og || false
74
79
  )
@@ -76,18 +81,31 @@ class MatchTree
76
81
  end
77
82
  end
78
83
 
84
+
79
85
  pp goals if debug?
80
86
 
87
+
88
+ ## todo/fix
89
+ ## PLUS auto-fill score1,score2 - why? why not?
90
+
81
91
  ## quick & dirty - auto add goals to last match
82
92
  ## note - for hacky (quick& dirty) multi-line support
83
93
  ## always append for now
84
- match = @matches[-1]
85
94
  match.goals ||= []
86
95
  match.goals += goals
87
96
 
88
- ## todo/fix
89
- ## sort by minute
90
- ## PLUS auto-fill score1,score2 - why? why not?
97
+
98
+ ## always keep goal list sorted by minute - why? why not?
99
+ ## - sort only on serialize (as_json) - why? why not?
100
+ match.goals = match.goals.sort do |l,r|
101
+ if l.minute && r.minute
102
+ ## note - minute is obj!! (Fbtxt::Minute!!)
103
+ l.minute <=> r.minute
104
+ else
105
+ ## keep as is (no minutes available)
106
+ 0
107
+ end
108
+ end
91
109
  end
92
110
 
93
111
 
@@ -0,0 +1,96 @@
1
+ module Fbtxt
2
+ class MatchTree
3
+
4
+
5
+ ##
6
+ ## fix - move "upstream to lineup" - why? why not?
7
+ ## - rename each_lineup?
8
+ ##
9
+ def _each_player( lineup, &blk )
10
+ lineup.each do |item|
11
+ blk.call(item)
12
+ ## plus (walk) recursive sub(stitution)s
13
+ current = item
14
+ while current && current.sub
15
+ blk.call( current.sub.sub )
16
+ current = current.sub.sub
17
+ end
18
+ end
19
+ end
20
+
21
+ ## change to _collect_inline_bookings/cards - why? why not?
22
+ def _collect_bookings( lineup )
23
+ recs = []
24
+
25
+ ## Lineup = Struct.new( :name, :captain, :cards, :sub )
26
+ ## Card = Struct.new( :name, :minute )
27
+ _each_player( lineup ) do |item|
28
+ if item.cards.is_a?(Array) && item.cards.size > 0
29
+ item.cards.each do |card|
30
+ recs << EventCard.new(
31
+ ## fix/fix/fix change to card.type upstream - why? why not?
32
+ type: card.name,
33
+ name: item.name,
34
+ minute: card.minute )
35
+
36
+ end
37
+ end
38
+ end
39
+
40
+
41
+ ## note - (auto-)sort by minute if present
42
+ recs = recs.sort do |l,r|
43
+ if l.minute && r.minute
44
+ ## note - minute is obj!! (Fbtxt::Minute!!)
45
+ l.minute <=> r.minute
46
+ else
47
+ 0 ## keep as is (no minutes available)
48
+ end
49
+ end
50
+
51
+ recs
52
+ end
53
+
54
+
55
+
56
+
57
+
58
+
59
+ def on_lineup_line( node )
60
+ _trace( "on lineup: >#{node}<" )
61
+
62
+ ## get last match
63
+ match = @matches[-1]
64
+
65
+ ## collect players
66
+ team_name = node.team
67
+ lineup = node.lineup
68
+
69
+ ## note -
70
+ ## lineup - uses nested array format
71
+ ## for formation
72
+ ##
73
+ ## quick & dirty hack - flatten for now
74
+ ## refine later
75
+ lineup = lineup.flatten
76
+
77
+ ## LineupLine = Struct.new( :team, :lineup, :formation, :coach ) do
78
+ ## Lineup = Struct.new( :name, :captain, :cards, :sub ) do
79
+ ## Sub = Struct.new( :minute, :sub ) do
80
+
81
+ ## todo/check if team name match
82
+ ## warn if no match
83
+
84
+ match.lineup ||= []
85
+
86
+ ## todo/fix-fix-fix add formation here too - why? why not?
87
+
88
+ match.lineup << Lineup.build( lineup )
89
+
90
+ match.bookings ||=[]
91
+ match.bookings << _collect_bookings( lineup )
92
+ end
93
+
94
+
95
+ end ## class MatchTree
96
+ end ## module Fbtxt
@@ -0,0 +1,35 @@
1
+ module Fbtxt
2
+ class MatchTree
3
+
4
+
5
+
6
+ def on_match_line_bye( node )
7
+ _trace( "on match (bye): >#{node}<" )
8
+
9
+ ## note - bye records NO date/time or ground (or score etc.)
10
+ ## for now only team1/team2 and match status!!
11
+ ## plus inherited round/group
12
+
13
+ status = 'bye'
14
+
15
+ team = node.team
16
+
17
+ @teams[ team ] += 1
18
+
19
+ group = nil
20
+ group = @last_group if @last_group
21
+
22
+ round = nil
23
+ round = @last_round if @last_round
24
+
25
+ @matches << Match.new( team1: team, ## note: for now always use mapping value e.g. rec (NOT string e.g. team1.name)
26
+ round: round ? round.name : nil, ## note: for now always use string (assume unique canonical name for event)
27
+ group: group ? group.name : nil, ## note: for now always use string (assume unique canonical name for event)
28
+ status: status )
29
+ ### todo: cache team lookups in hash?
30
+ end
31
+
32
+
33
+
34
+ end ## class MatchTree
35
+ end ## module Fbtxt
@@ -43,27 +43,9 @@ class MatchTree
43
43
  end
44
44
 
45
45
 
46
- ### todo/fix
47
- ## add keywords (e.g. ht, ft or such) to Score.new - why? why not?
48
- ## or use new Score.build( ht:, ft:, ) or such - why? why not?
49
- ## pp score
50
46
  score = nil
51
- score = node.score if node.score
52
-
53
- ## if node.score.is_a?(Array)
54
- ## ## assume "undefined" score
55
- ## score = node.score
56
- ## else ## (default) assume Hash
57
- ## # ht = node.score[:ht] || [nil,nil]
58
- ## # ft = node.score[:ft] || [nil,nil]
59
- ## # et = node.score[:et] || [nil,nil]
60
- ## # p = node.score[:p] || [nil,nil]
61
- ## # values = [*ht, *ft, *et, *p]
62
- ## # pp values
63
- ## ## pp node.score
64
- ## score = node.score
65
- ## end
66
- ## end
47
+ score = Score.build( node.score ) if node.score
48
+ ## pp score
67
49
 
68
50
 
69
51
  status = nil
@@ -0,0 +1,57 @@
1
+ module Fbtxt
2
+ class MatchTree
3
+
4
+
5
+ ## check - use on_penalties_line ???
6
+ def on_penalties( node )
7
+ _trace( "on penalties: >#{node}<" )
8
+
9
+ ## get last match
10
+ match = @matches[-1]
11
+
12
+
13
+ ## PenaltiesLine = Struct.new( :penalties )
14
+ ## Penalty = Struct.new( :name, :score, :note )
15
+
16
+ #####
17
+ ## note
18
+ ## (i) [] - single line (no separator)
19
+ ## (ii) [[],[]] - nested team1/team2 (separator required)
20
+
21
+
22
+ recs = nil
23
+
24
+ if node.penalties.is_a?(Array) && node.penalties.size == 2 &&
25
+ node.penalties[0].is_a?(Array) && node.penalties[1].is_a?(Array)
26
+
27
+ ## use zip to make (flattend) sequence - why? why not?
28
+ ## we do NOT know who really started (picked via coin toss!!)
29
+ ## keep nested arrays for now
30
+ recs = [[],[]]
31
+
32
+ node.penalties.each_with_index.each do |pens,i|
33
+ pens.each do |pen|
34
+ recs[i] << Penalty.new(
35
+ name: pen.name,
36
+ score: pen.score,
37
+ note: pen.note )
38
+ end
39
+ end
40
+ else
41
+ ## assume chronological sequence
42
+ recs = []
43
+ node.penalties.each do |pen|
44
+ recs << Penalty.new(
45
+ name: pen.name,
46
+ score: pen.score,
47
+ note: pen.note )
48
+ end
49
+ end
50
+
51
+
52
+ match.penalties = recs
53
+ end
54
+
55
+
56
+ end ## class MatchTree
57
+ end ## module Fbtxt
@@ -0,0 +1,25 @@
1
+ module Fbtxt
2
+ class MatchTree
3
+
4
+
5
+ def on_referee_line( node )
6
+ _trace( "on referee(s): >#{node}<" )
7
+
8
+ ## get last match
9
+ match = @matches[-1]
10
+
11
+ ## support multiple referees (incl. assistant refs etc.)
12
+ ## RefereeLine = Struct.new( :referees )
13
+ ## Referee = Struct.new( :name, :country )
14
+
15
+ recs = []
16
+ node.referees.each do |ref|
17
+ recs << Referee.new( name: ref.name, country: ref.country )
18
+ end
19
+
20
+ match.referees = recs
21
+ end
22
+
23
+
24
+ end ## class MatchTree
25
+ end ## module Fbtxt