Bug 8375: (follow-up) fix font code and alignment
[koha.git] / C4 / Creators / Lib.pm
1 package C4::Creators::Lib;
2
3 # Copyright 2009 Foundations Bible College.
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 use strict;
21 use warnings;
22
23 use autouse 'Data::Dumper' => qw(Dumper);
24
25 use C4::Context;
26 use C4::Debug;
27
28 BEGIN {
29     use version; our $VERSION = qv('3.07.00.049');
30     use base qw(Exporter);
31     our @EXPORT = qw(get_all_templates
32                      get_all_layouts
33                      get_all_profiles
34                      get_all_image_names
35                      get_batch_summary
36                      get_label_summary
37                      get_card_summary
38                      get_barcode_types
39                      get_label_types
40                      get_font_types
41                      get_text_justification_types
42                      get_output_formats
43                      get_column_names
44                      get_table_names
45                      get_unit_values
46                      html_table
47     );
48 }
49
50 =head1 NAME
51
52 C4::Creators::Lib
53
54 =cut
55
56 =head1 FUNCTIONS
57
58 =cut
59
60 #=head2 C4::Creators::Lib::_SELECT()
61 #
62 #    This function returns a recordset upon success and 1 upon failure. Errors are logged to the Apache log.
63 #
64 #    examples:
65 #
66 #        my $field_value = _SELECT(field_name, table_name, condition);
67 #
68 #=cut
69
70 sub _SELECT {
71     my @params = @_;
72     my $query = "SELECT $params[0] FROM $params[1]";
73     $params[2] ? $query .= " WHERE $params[2];" : $query .= ';';
74     my $sth = C4::Context->dbh->prepare($query);
75 #    $sth->{'TraceLevel'} = 3;
76     $sth->execute();
77     if ($sth->err) {
78         warn sprintf('Database returned the following error: %s', $sth->errstr);
79         return 1;
80     }
81     my $record_set = [];
82     while (my $row = $sth->fetchrow_hashref()) {
83         push(@$record_set, $row);
84     }
85     return $record_set;
86 }
87
88 my $barcode_types = [
89     {type => 'CODE39',          name => 'Code 39',              desc => 'Translates the characters 0-9, A-Z, \'-\', \'*\', \'+\', \'$\', \'%\', \'/\', \'.\' and \' \' to a barcode pattern.',                                  selected => 0},
90     {type => 'CODE39MOD',       name => 'Code 39 + Modulo43',   desc => 'Translates the characters 0-9, A-Z, \'-\', \'*\', \'+\', \'$\', \'%\', \'/\', \'.\' and \' \' to a barcode pattern. Encodes Mod 43 checksum.',         selected => 0},
91     {type => 'CODE39MOD10',     name => 'Code 39 + Modulo10',   desc => 'Translates the characters 0-9, A-Z, \'-\', \'*\', \'+\', \'$\', \'%\', \'/\', \'.\' and \' \' to a barcode pattern. Encodes Mod 10 checksum.',         selected => 0},
92     {type => 'COOP2OF5',        name => 'COOP2of5',             desc => 'Creates COOP2of5 barcodes from a string consisting of the numeric characters 0-9',                                                                     selected => 0},
93     {type => 'EAN13',           name => 'EAN13',                desc => 'Creates EAN13 barcodes from a string of 12 or 13 digits. The check number (the 13:th digit) is calculated if not supplied.',                           selected => 0},
94 #    {type => 'EAN8',            name => 'EAN8',                 desc => 'Translates a string of 7 or 8 digits to EAN8 barcodes. The check number (the 8:th digit) is calculated if not supplied.',                              selected => 0},
95 #    {type => 'IATA2of5',        name => 'IATA2of5',             desc => 'Creates IATA2of5 barcodes from a string consisting of the numeric characters 0-9',                                                                     selected => 0},
96     {type => 'INDUSTRIAL2OF5',  name => 'Industrial2of5',       desc => 'Creates Industrial2of5 barcodes from a string consisting of the numeric characters 0-9',                                                               selected => 0},
97 #    {type => 'ITF',             name => 'Interleaved2of5',      desc => 'Translates the characters 0-9 to a barcodes. These barcodes could also be called 'Interleaved2of5'.',                                                  selected => 0},
98 #    {type => 'MATRIX2OF5',      name => 'Matrix2of5',           desc => 'Creates Matrix2of5 barcodes from a string consisting of the numeric characters 0-9',                                                                   selected => 0},
99 #    {type => 'NW7',             name => 'NW7',                  desc => 'Creates a NW7 barcodes from a string consisting of the numeric characters 0-9',                                                                        selected => 0},
100 #    {type => 'UPCA',            name => 'UPCA',                 desc => 'Translates a string of 11 or 12 digits to UPCA barcodes. The check number (the 12:th digit) is calculated if not supplied.',                           selected => 0},
101 #    {type => 'UPCE',            name => 'UPCE',                 desc => 'Translates a string of 6, 7 or 8 digits to UPCE barcodes. If the string is 6 digits long, '0' is added first in the string. The check number (the 8:th digit) is calculated if not supplied.',                                 selected => 0},
102 ];
103
104 my $label_types = [
105     {type => 'BIB',     name => 'Biblio',               desc => 'Only the bibliographic data is printed.',                              selected => 0},
106     {type => 'BARBIB',  name => 'Barcode/Biblio',       desc => 'Barcode proceeds bibliographic data.',                                 selected => 0},
107     {type => 'BIBBAR',  name => 'Biblio/Barcode',       desc => 'Bibliographic data proceeds barcode.',                                 selected => 0},
108     {type => 'ALT',     name => 'Alternating',          desc => 'Barcode and bibliographic data are printed on alternating labels.',    selected => 0},
109     {type => 'BAR',     name => 'Barcode',              desc => 'Only the barcode is printed.',                                         selected => 0},
110 ];
111
112 my $font_types = [
113     {type => 'TR',      name => 'Times-Roman',                  selected => 0},
114     {type => 'TB',      name => 'Times-Bold',                   selected => 0},
115     {type => 'TI',      name => 'Times-Italic',                 selected => 0},
116     {type => 'TBI',     name => 'Times-Bold-Italic',            selected => 0},
117     {type => 'C',       name => 'Courier',                      selected => 0},
118     {type => 'CB',      name => 'Courier-Bold',                 selected => 0},
119     {type => 'CO',      name => 'Courier-Oblique',              selected => 0},
120     {type => 'CBO',     name => 'Courier-Bold-Oblique',         selected => 0},
121     {type => 'H',       name => 'Helvetica',                    selected => 0},
122     {type => 'HO',      name => 'Helvetica-Oblique',            selected => 0},
123     {type => 'HB',      name => 'Helvetica-Bold',               selected => 0},
124     {type => 'HBO',     name => 'Helvetica-Bold-Oblique',       selected => 0},
125 ];
126
127 my $text_justification_types = [
128     {type => 'L',       name => 'Left',                         selected => 0},
129     {type => 'C',       name => 'Center',                       selected => 0},
130     {type => 'R',       name => 'Right',                        selected => 0},
131 #    {type => 'F',       name => 'Full',                         selected => 0},
132 ];
133
134 my $unit_values = [
135     {type       => 'POINT',      desc    => 'PostScript Points',  value   => 1,                 selected => 0},
136     {type       => 'AGATE',      desc    => 'Adobe Agates',       value   => 5.1428571,         selected => 0},
137     {type       => 'INCH',       desc    => 'US Inches',          value   => 72,                selected => 0},
138     {type       => 'MM',         desc    => 'SI Millimeters',     value   => 2.83464567,        selected => 0},
139     {type       => 'CM',         desc    => 'SI Centimeters',     value   => 28.3464567,        selected => 0},
140 ];
141
142 my $output_formats = [
143     {type       => 'pdf',       desc    => 'PDF File'},
144     {type       => 'csv',       desc    => 'CSV File'},
145 ];
146
147 =head2 C4::Creators::Lib::get_all_templates()
148
149   my $templates = get_all_templates();
150
151 This function returns a reference to a hash containing all templates upon success and 1 upon failure. Errors are logged to the Apache log.
152
153 =cut
154
155 sub get_all_templates {
156     my %params = @_;
157     my @templates = ();
158     my $query = "SELECT " . ($params{'field_list'} ? $params{'field_list'} : '*') . " FROM creator_templates";
159     $query .= ($params{'filter'} ? " WHERE $params{'filter'} " : '');
160     $query .= ($params{'orderby'} ? " ORDER BY $params{'orderby'} " : '');
161     my $sth = C4::Context->dbh->prepare($query);
162     $sth->execute();
163     if ($sth->err) {
164         warn sprintf('Database returned the following error: %s', $sth->errstr);
165         return -1;
166     }
167     ADD_TEMPLATES:
168     while (my $template = $sth->fetchrow_hashref) {
169         push(@templates, $template);
170     }
171     return \@templates;
172 }
173
174 =head2 C4::Creators::Lib::get_all_layouts()
175
176   my $layouts = get_all_layouts();
177
178 This function returns a reference to a hash containing all layouts upon success and 1 upon failure. Errors are logged to the Apache log.
179
180 =cut
181
182 sub get_all_layouts {
183     my %params = @_;
184     my @layouts = ();
185     my $query = "SELECT " . ($params{'field_list'} ? $params{'field_list'} : '*') . " FROM creator_layouts";
186     $query .= ($params{'filter'} ? " WHERE $params{'filter'} " : '');
187     $query .= ($params{'orderby'} ? " ORDER BY $params{'orderby'} " : '');
188     my $sth = C4::Context->dbh->prepare($query);
189     $sth->execute();
190     if ($sth->err) {
191         warn sprintf('Database returned the following error: %s', $sth->errstr);
192         return -1;
193     }
194     ADD_LAYOUTS:
195     while (my $layout = $sth->fetchrow_hashref) {
196         push(@layouts, $layout);
197     }
198     return \@layouts;
199 }
200
201 =head2 C4::Creators::Lib::get_all_profiles()
202
203   my $profiles = get_all_profiles();
204
205   my $profiles = get_all_profiles(field_list => field_list, filter => filter_string);
206
207 This function returns an arrayref whose elements are hashes containing all profiles upon success and 1 upon failure. Errors are logged
208 to the Apache log. Two parameters are accepted. The first limits the field(s) returned. This parameter should be string of comma separted
209 fields. ie. "field_1, field_2, ...field_n" The second limits the records returned based on a string containing a valud SQL 'WHERE' filter.
210
211 NOTE: Do not pass in the keyword 'WHERE.'
212
213 =cut
214
215 sub get_all_profiles {
216     my %params = @_;
217     my @profiles = ();
218     my $query = "SELECT " . ($params{'field_list'} ? $params{'field_list'} : '*') . " FROM printers_profile";
219     $query .= ($params{'filter'} ? " WHERE $params{'filter'};" : ';');
220     my $sth = C4::Context->dbh->prepare($query);
221 #    $sth->{'TraceLevel'} = 3 if $debug;
222     $sth->execute();
223     if ($sth->err) {
224         warn sprintf('Database returned the following error: %s', $sth->errstr);
225         return -1;
226     }
227     ADD_PROFILES:
228     while (my $profile = $sth->fetchrow_hashref) {
229         push(@profiles, $profile);
230     }
231     return \@profiles;
232 }
233
234 =head2 C4::Creators::Lib::get_all_image_names()
235
236 =cut
237
238 sub get_all_image_names {
239     my $image_names = [];
240     my $query = "SELECT image_name FROM creator_images";
241     my $sth = C4::Context->dbh->prepare($query);
242 #    $sth->{'TraceLevel'} = 3 if $debug;
243     $sth->execute();
244     if ($sth->err) {
245         warn sprintf('Database returned the following error: %s', $sth->errstr);
246         return -1;
247     }
248     grep {push @$image_names, {type => $$_[0], name => $$_[0], selected => 0}} @{$sth->fetchall_arrayref([0])};
249     return $image_names;
250 }
251
252 =head2 C4::Creators::Lib::get_batch_summary()
253
254   my $batches = get_batch_summary();
255
256   my $batches = get_batch_summary(filter => filter_string);
257
258 This function returns an arrayref whose elements are hashes containing the batch_ids of current batches along with the item count
259 for each batch upon success and 1 upon failure. Item counts are stored under the key '_item_count' Errors are logged to the Apache log.
260 One parameter is accepted which limits the records returned based on a string containing a valud SQL 'WHERE' filter.
261
262 NOTE: Do not pass in the keyword 'WHERE.'
263
264 =cut
265
266 sub get_batch_summary {
267     my %params = @_;
268     my @batches = ();
269     my $query = "SELECT batch_id,count(batch_id) as _item_count FROM creator_batches WHERE creator=?";
270     $query .= ($params{'filter'} ? " AND $params{'filter'}" : '');
271     $query .= " GROUP BY batch_id";
272     my $sth = C4::Context->dbh->prepare($query);
273 #    $sth->{'TraceLevel'} = 3;
274     $sth->execute($params{'creator'});
275     if ($sth->err) {
276         warn sprintf('Database returned the following error on attempted SELECT: %s', $sth->errstr);
277         return -1;
278     }
279     while (my $batch = $sth->fetchrow_hashref) {
280         push(@batches, $batch);
281     }
282     return \@batches;
283 }
284
285 =head2 C4::Creators::Lib::get_label_summary()
286
287   my $labels = get_label_summary();
288
289   my $labels = get_label_summary(items => @item_list);
290
291 This function returns an arrayref whose elements are hashes containing the label_ids of current labels along with the item count
292 for each label upon success and 1 upon failure. Item counts are stored under the key '_item_count' Errors are logged to the Apache log.
293 One parameter is accepted which limits the records returned based on a string containing a valud SQL 'WHERE' filter.
294
295 NOTE: Do not pass in the keyword 'WHERE.'
296
297 =cut
298
299 sub get_label_summary {
300     my %params = @_;
301     my $label_number = 0;
302     my @label_summaries = ();
303     my $query = "     SELECT b.title, b.author, bi.itemtype, i.barcode, i.biblionumber, i.itype
304                       FROM creator_batches AS c LEFT JOIN items AS i ON (c.item_number=i.itemnumber)
305                       LEFT JOIN biblioitems AS bi ON (i.biblioitemnumber=bi.biblioitemnumber)
306                       LEFT JOIN biblio AS b ON (bi.biblionumber=b.biblionumber)
307                       WHERE itemnumber=? AND batch_id=?;
308                   ";
309     my $sth = C4::Context->dbh->prepare($query);
310     foreach my $item (@{$params{'items'}}) {
311         $label_number++;
312         $sth->execute($item->{'item_number'}, $params{'batch_id'});
313         if ($sth->err) {
314             warn sprintf('Database returned the following error on attempted SELECT: %s', $sth->errstr);
315             return -1;
316         }
317         my $record = $sth->fetchrow_hashref;
318         my $label_summary;
319         $label_summary->{'_label_number'} = $label_number;
320         $record->{'author'} =~ s/[^\.|\w]$// if $record->{'author'};  # strip off ugly trailing chars... but not periods or word chars
321         $record->{'title'} =~ s/\W*$//;  # strip off ugly trailing chars
322         # FIXME contructing staff interface URLs should be done *much* higher up the stack - for the most part, C4 module code
323         # should not know that it's part of a web app
324         $record->{'title'} = '<a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=' . $record->{'biblionumber'} . '"> ' . $record->{'title'} . '</a>';
325         $label_summary->{'_summary'} = $record->{'title'} . " | " . ($record->{'author'} ? $record->{'author'} : 'N/A');
326         $label_summary->{'_item_type'} = C4::Context->preference("item-level_itypes") ? $record->{'itype'} : $record->{'itemtype'};
327         $label_summary->{'_barcode'} = $record->{'barcode'};
328         $label_summary->{'_item_number'} = $item->{'item_number'};
329         $label_summary->{'_label_id'} = $item->{'label_id'};
330         push (@label_summaries, $label_summary);
331     }
332     return \@label_summaries;
333 }
334
335 =head2 C4::Creators::Lib::get_card_summary()
336
337   my $cards = get_card_summary();
338
339   my $cards = get_card_summary(items => @item_list);
340
341 This function returns an arrayref whose elements are hashes containing the label_ids of current cards along with the item count
342 for each card upon success and 1 upon failure. Item counts are stored under the key '_item_count' Errors are logged to the Apache log.
343 One parameter is accepted which limits the records returned based on a string containing a valud SQL 'WHERE' filter.
344
345 NOTE: Do not pass in the keyword 'WHERE.'
346
347 =cut
348
349 sub get_card_summary {
350     my %params = @_;
351     my $card_number = 0;
352     my @card_summaries = ();
353     my $query = "SELECT CONCAT_WS(', ', surname, firstname) AS name, cardnumber FROM borrowers WHERE borrowernumber=?;";
354     my $sth = C4::Context->dbh->prepare($query);
355     foreach my $item (@{$params{'items'}}) {
356         $card_number++;
357         $sth->execute($item->{'borrower_number'});
358         if ($sth->err) {
359             warn sprintf('Database returned the following error on attempted SELECT: %s', $sth->errstr);
360             return -1;
361         }
362         my $record = $sth->fetchrow_hashref;
363         my $card_summary->{'_card_number'} = $card_number;
364         $card_summary->{'_summary'} = $record->{'name'};
365         $card_summary->{'borrowernumber'} = $item->{'borrower_number'};
366         $card_summary->{'_label_id'} = $item->{'label_id'};
367         push (@card_summaries, $card_summary);
368     }
369     return \@card_summaries;
370 }
371
372 =head2 C4::Creators::Lib::get_barcode_types()
373
374   my $barcode_types = get_barcode_types();
375
376 This function returns a reference to an array of hashes containing all barcode types along with their name and description.
377
378 =cut
379
380 sub get_barcode_types {
381     return $barcode_types;
382 }
383
384 =head2 C4::Creators::Lib::get_label_types()
385
386   my $label_types = get_label_types();
387
388 This function returns a reference to an array of hashes containing all label types along with their name and description.
389
390 =cut
391
392 sub get_label_types {
393     return $label_types;
394 }
395
396 =head2 C4::Creators::Lib::get_font_types()
397
398   my $font_types = get_font_types();
399
400 This function returns a reference to an array of hashes containing all font types along with their name and description.
401
402 =cut
403
404 sub get_font_types {
405     return $font_types;
406 }
407
408 =head2 C4::Creators::Lib::get_text_justification_types()
409
410   my $text_justification_types = get_text_justification_types();
411
412 This function returns a reference to an array of hashes containing all text justification types along with their name and description.
413
414 =cut
415
416 sub get_text_justification_types {
417     return $text_justification_types;
418 }
419
420 =head2 C4::Creators::Lib::get_unit_values()
421
422   my $unit_values = get_unit_values();
423
424 This function returns a reference to an array of  hashes containing all unit types along with their description and multiplier.
425 NOTE: All units are relative to a PostScript Point.
426 There are 72 PS points to the inch.
427
428 =cut
429
430 sub get_unit_values {
431     return $unit_values;
432 }
433
434 =head2 C4::Creators::Lib::get_output_formats()
435
436   my $label_output_formats = get_output_formats();
437
438 This function returns a reference to an array of hashes containing all label output formats along with their description.
439
440 =cut
441
442 sub get_output_formats {
443     return $output_formats;
444 }
445
446 =head2 C4::Creators::Lib::get_column_names($table_name)
447
448 Return an arrayref of an array containing the column names of the supplied table.
449
450 =cut
451
452 sub get_column_names {
453     my $table = shift;
454     my $dbh = C4::Context->dbh();
455     my $column_names = [];
456     my $sth = $dbh->column_info(undef,undef,$table,'%');
457     while (my $info = $sth->fetchrow_hashref()){
458         $$column_names[$info->{'ORDINAL_POSITION'}] = $info->{'COLUMN_NAME'};
459     }
460     return $column_names;
461 }
462
463 =head2 C4::Creators::Lib::get_table_names($search_term)
464
465 Return an arrayref of an array containing the table names which contain the supplied search term.
466
467 =cut
468
469 sub get_table_names {
470     my $search_term = shift;
471     my $dbh = C4::Context->dbh();
472     my $table_names = [];
473     my $sth = $dbh->table_info(undef,undef,"%$search_term%");
474     while (my $info = $sth->fetchrow_hashref()){
475         push (@$table_names, $info->{'TABLE_NAME'});
476     }
477     return $table_names;
478 }
479
480 =head2 C4::Creators::Lib::html_table()
481
482 This function returns an arrayref of an array of hashes contianing the supplied data formatted suitably to
483 be passed off as a template parameter and used to build an html table.
484
485    my $table = html_table(header_fields, array_of_row_data);
486    $template->param(
487        table_loop => $table,
488    );
489
490     html example:
491
492         <table>
493             [% FOREACH table_loo IN table_loop %]
494                 [% IF ( table_loo.header_fields ) %]
495                     <tr>
496                         [% FOREACH header_field IN table_loo.header_fields %]
497                             <th>[% header_field.field_label %]</th>
498                         [% END %]
499                     </tr>
500                 [% ELSE %]
501                     <tr>
502                         [% FOREACH text_field IN table_loo.text_fields %]
503                             [% IF ( text_field.select_field ) %]
504                                 <td><input type="checkbox" name="action" value="[% text_field.field_value %]"></td>
505                             [% ELSE %]
506                                 <td>[% text_field.field_value %]</td>
507                             [% END %]
508                         [% END %]
509                     </tr>
510                 [% END %]
511             [% END %]
512         </table>
513
514 =cut
515
516 sub html_table {
517     my $headers = shift;
518     my $data = shift;
519     return undef if scalar(@$data) == 0;      # no need to generate a table if there is not data to display
520     my $table = [];
521     my $fields = [];
522     my @table_columns = ();
523     my ($row_index, $col_index) = (0,0);
524     my $cols = 0;       # number of columns to wrap on
525     my $field_count = 0;
526     my $select_value = undef;
527     my $link_field = undef;
528     POPULATE_HEADER:
529     foreach my $header (@$headers) {
530         my @key = keys %$header;
531         if ($key[0] eq 'select' ) {
532             push (@table_columns, $key[0]);
533             $$fields[$col_index] = {hidden => 0, select_field => 0, field_name => ($key[0]), field_label => $header->{$key[0]}{'label'}};
534             # do special formatting stuff....
535             $select_value = $header->{$key[0]}{'value'};
536         }
537         else {
538             # do special formatting stuff....
539             $link_field->{$key[0]} = ($header->{$key[0]}{'link_field'} == 1 ? 1 : 0);
540             push (@table_columns, $key[0]);
541             $$fields[$col_index] = {hidden => 0, select_field => 0, field_name => ($key[0]), field_label => $header->{$key[0]}{'label'}};
542         }
543         $field_count++;
544         $col_index++;
545     }
546     $$table[$row_index] = {header_fields => $fields};
547     $cols = $col_index;
548     $field_count *= scalar(@$data);     # total fields to be displayed in the table
549     $col_index = 0;
550     $row_index++;
551     $fields = [];
552     POPULATE_TABLE:
553     foreach my $db_row (@$data) {
554         POPULATE_ROW:
555         foreach my $table_column (@table_columns) {
556             if (grep {$table_column eq $_} keys %$db_row) {
557                 $$fields[$col_index] = {hidden => 0, link_field => $link_field->{$table_column}, select_field => 0, field_name => ($table_column . "_tbl"), field_value => $db_row->{$table_column}};
558                 $col_index++;
559                 next POPULATE_ROW;
560             }
561             elsif ($table_column =~ m/^_((.*)_(.*$))/) {   # this a special case
562                 my $table_name = get_table_names($2);
563                 my $record_set = _SELECT($1, @$table_name[0], $2 . "_id = " . $db_row->{$2 . "_id"});
564                 $$fields[$col_index] = {hidden => 0, link_field => $link_field->{$table_column}, select_field => 0, field_name => ($table_column . "_tbl"), field_value => $$record_set[0]{$1}};
565                 $col_index++;
566                 next POPULATE_ROW;
567             }
568             elsif ($table_column eq 'select' ) {
569                 $$fields[$col_index] = {hidden => 0, select_field => 1, field_name => 'select', field_value => $db_row->{$select_value}};
570             }
571         }
572         $$table[$row_index] = {text_fields => $fields};
573         $col_index = 0;
574         $row_index++;
575         $fields = [];
576     }
577     return $table;
578 }
579
580 1;
581 __END__
582
583 =head1 AUTHOR
584
585 Chris Nighswonger <cnighswonger AT foundations DOT edu>
586
587 =cut