Annotation of loncom/xml/lontable.pm, revision 1.22
1.1 foxr 1: # The LearningOnline Network with CAPA
2: # Generating TeX tables.
3: #
1.22 ! raeburn 4: # $Id: lontable.pm,v 1.21 2011/11/30 18:01:33 raeburn Exp $
1.1 foxr 5: #
6: #
7: # Copyright Michigan State University Board of Trustees
8: #
9: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
1.20 raeburn 10: #
11: # LON-CAPA is free software; you can redistribute it and/or modify
12: # it under the terms of the GNU General Public License as published by
13: # the Free Software Foundation; either version 2 of the License, or
1.1 foxr 14: # (at your option) any later version.
15: #
16: # LON-CAPA is distributed in the hope that it will be useful,
17: # but WITHOUT ANY WARRANTY; without even the implied warranty of
18: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19: # GNU General Public License for more details.
20: #
21: # You should have received a copy of the GNU General Public License
22: # along with LON-CAPA; if not, write to the Free Software
23: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24: #
25: # /home/httpd/html/adm/gpl.txt
26: #
27: # http://www.lon-capa.org/
28: ## Copyright for TtHfunc and TtMfunc by Ian Hutchinson.
29: # TtHfunc and TtMfunc (the "Code") may be compiled and linked into
30: # binary executable programs or libraries distributed by the
31: # Michigan State University (the "Licensee"), but any binaries so
32: # distributed are hereby licensed only for use in the context
33: # of a program or computational system for which the Licensee is the
34: # primary author or distributor, and which performs substantial
35: # additional tasks beyond the translation of (La)TeX into HTML.
36: # The C source of the Code may not be distributed by the Licensee
37: # to any other parties under any circumstances.
38: #
39:
40: # This module is a support packkage that helps londefdef generate
1.12 foxr 41: # LaTeX tables using the Apache::lonlatextable package. A prerequisite is that
1.5 foxr 42: # the print generator must have added the following to the LaTeX
1.1 foxr 43: #
44: # \usepackage{xtab}
45: # \usepackage{booktabs}
46: # \usepackage{array}
47: # \usepackage{colortbl}
48: # \usepackage{xcolor}
49: #
50: # These packages are installed in the packaged LaTeX distributions we know of as of
51: # 11/24/2008
52: #
53:
54:
55:
56: package Apache::lontable;
57: use strict;
1.12 foxr 58: use Apache::lonlatextable;
1.8 foxr 59: use Apache::lonnet; # for trace logging.
1.18 foxr 60:
61: my $tracing = 0; # Set to 1 to enable log tracing. 2 for local sub tracing.
1.1 foxr 62:
63: =pod
64:
65: =head1 lontable Table generation assistant for the LaTeX target
66:
67: This module contains support software for generating tables in LaTeX output mode
1.12 foxr 68: In this implementation, we use the Apache::lonlatextable package to do the actual final formatting.
1.1 foxr 69: Each table creates a new object. Table objects can have global properties configured.
70: The main operations on a table object are:
71:
72: =over 3
73:
74: =item start_row
75:
76: Opens a new table row.
77:
78: =item end_row
79:
80: Closes a table row.
81:
82: =item configure_row
83:
84: Modifies a configuration item in the currently open row.
85:
86: =item generate
87:
88: Returns the generated table string.
89:
90: =item configure
91:
92: Configures a table's global configuration.
93:
1.3 foxr 94: =item add_cell
95:
96: Add and configure a cell to the current row.6
97:
1.1 foxr 98: =back
99:
100: =cut
101:
102: =pod
103:
104: =head2 new - create a new object.
105:
106: Create a new table object. Any of the raw table configuration items can be
107: modified by this. These configuration items include:
108:
109: my $table = lontable::new(\%config_hash)
110:
1.17 foxr 111: =over 3
1.1 foxr 112:
1.7 foxr 113:
1.1 foxr 114: =item alignment
115:
116: Table alignment. Some table styles support this but not all.
117:
118: =item tableborder
119:
120: If true, a border is drawn around the table.
121:
122: =item cellborder
123:
124: If true, borders are drawn around the cells inside a table.
125:
126: =item caption
127:
128: The table caption text.
129:
130: =item theme
131:
132: The theme of the table to use. Defaults to Zurich. Themes we know about are:
133: NYC, NYC2, Zurich, Berlin, Dresden, Houston, Miami, plain, Paris. Other themes can be added
1.12 foxr 134: to the Apache::lonlatextable package, and they will become supported automatically, as theme names are
135: not error checked. Any use of a non-existent theme is reported by the Apache::lonlatextable package
1.1 foxr 136: when the table text is generated.
137:
1.7 foxr 138: =item width
139:
1.8 foxr 140: The width of the table. in any
141: TeX unit measure e.g. 10.8cm This forces the table to the
142: tabularx environment. It also forces the declarations for
143: cells to be paragraph mode which supports more internal formatting.
1.7 foxr 144:
1.1 foxr 145: =back
146:
147: =head3 Member data
148:
149: The object hash has the following members:
150:
151: =over 3
152:
153: =item column_count
154:
155: Maintained internally, the number of colums in the widest row.
156:
157: =item alignment
158:
159: Table alignment (configurable) "left", "center", or "right".
160:
161: =item outer_border
162:
163: True if a border should be drawn around the entire table (configurable)
164:
165: =item inner_borders
166:
167: True if a border should be drawn around all cells (configurable).
168:
169: =item caption
170:
171: Table caption (configurable).
172:
173: =item theme
174:
175: Theme desired (configurable).
176:
1.6 foxr 177: =item width
178:
179: If defined, the width of the table (should be supplied
180: in fraction of column width e.g. .75 for 75%.
181:
1.1 foxr 182: =item row_open
183:
184: True if a row is open and not yet closed.
185:
186: =item rows
187:
188: Array of row data. This is an array of hashes described below.
189:
190: =back
191:
192: =head3 Row data.
193:
194: Each row of table data is an element of the rows hash array. Hash elements are
195:
196: =over 3
197:
198:
199: =item default_halign
1.3 foxr 200: 0
1.1 foxr 201: Default horizontal alignment for cells in this row.
202:
203: =item default_valign
204:
205: Default vertical alignment for cells in this row (may be ignored).
206:
1.6 foxr 207: =item cell_width
208:
209: The width of the row in cells. This is the sum of the column spans
210: of the cells in the row.
211:
1.1 foxr 212: =item cells
213:
1.17 foxr 214:
1.1 foxr 215: Array of hashes where each element represents the data for a cell.
216: The contents of each element of this hash are described below:
217:
1.3 foxr 218: =item header
219:
220: If present, the row is a 'header' that is it was made via the
221: <th> tag.
222:
1.1 foxr 223: =item halign
224:
225: If present, overrides the row default horizontal alignment.
226:
227: =item valign
228:
229: if present, override the row default vertical alignment.
230:
231: =item rowspan
232:
233: If present, indicates the number of rows this cell spans.
234:
235: =item colspan
236:
237: If present indicates the number of columns this cell spans.
238: Note that a cell can span both rows and columns.
239:
1.6 foxr 240: =item start_col
241:
242: The starting column of the cell in the table grid.
243:
1.1 foxr 244: =item contents
245:
246: The contents of the cell.
247:
248: =back
249:
250:
251: =cut
252:
253: sub new {
254: my ($class, $configuration) = @_;
255:
1.17 foxr 256: if ($tracing) {&Apache::lonnet::logthis("new table"); }
1.1 foxr 257: # Initialize the object member data with the default values
258: # then override with any stuff in $configuration.
259:
260: my $self = {
261: alignment => "left",
262: outer_border => 0,
1.2 foxr 263: inner_border => 0,
1.1 foxr 264: caption => "",
1.11 foxr 265: theme => "plain",
1.1 foxr 266: column_count => 0,
267: row_open => 0,
1.17 foxr 268: rows => {
269: 'body' => [],
270: 'head' => [],
271: 'foot' => []
272: },
273: col_widths => {},
1.19 foxr 274: part => 'body', # one of 'body', 'head', 'foot'.
275: colgroups => [] # Stores information about column groups.
1.17 foxr 276:
1.1 foxr 277: };
278:
1.22 ! raeburn 279: foreach my $key (keys(%$configuration)) {
1.1 foxr 280: $self->{$key} = $$configuration{$key};
281: }
282:
283: bless($self, $class);
284:
285: return $self;
286: }
287:
1.3 foxr 288:
1.1 foxr 289: #-------------------------------------------------------------------------
290: #
291: # Methods that get/set table global configuration.
1.2 foxr 292: #
293:
294: =pod
295:
296: =head2 Gets/set alignment.
297:
298: If the method is passed a new alignment value, that replaces the current one.
299: Regardless, the current alignment is used:
300:
301: =head3 Examples:
302:
303: my $align = $table->alignment(); # Return current alignment
304: $table->alignment("center"); # Attempt centered alignment.
305:
306: =cut
307:
308: sub alignment {
309: my ($self, $new_value) = @_;
310:
1.8 foxr 311: if ($tracing) {&Apache::lonnet::logthis("alignment = $new_value");}
312:
1.2 foxr 313: if (defined($new_value)) {
1.5 foxr 314: $self->{'alignment'} = $new_value;
1.2 foxr 315: }
1.5 foxr 316: return $self->{'alignment'};
1.2 foxr 317: }
318:
319: =pod
320:
321: =head2 table_border
322:
323: Set or get the presence of an outer border in the table.
324: If passed a parameter, that parameter replaces the current request
325: for or not for an outer border. Regardless, the function returns
326: the final value of the outer_border request.
327:
328: =head3 Examples:
329:
330: $table->table_border(1); # Request an outer border.
331: my $outer_requested = $table->table_border();
332:
333: =cut
334:
335: sub table_border {
336: my ($self, $new_value) = @_;
337:
1.8 foxr 338: if ($tracing) {&Apache::lonnet::logthis("table_border $new_value");}
339:
1.2 foxr 340: if (defined($new_value)) {
1.5 foxr 341: $self->{'outer_border'} = $new_value;
1.2 foxr 342: }
1.5 foxr 343: return $self->{'outer_border'};
1.2 foxr 344: }
345:
346:
347: =pod
348:
349: =head2 cell_border
350:
351: Set or get the presence of a request for cells to have borders
352: drawn around them. If a paramter is passed, it will be treated as
353: a new value for the cell border configuration. Regardless,the final
354: value of that configuration parameter is returned.
1.16 foxr 355: Valid values for the parameter are:
356:
357: =over 2
358:
359: =item 0 - no borders present.
360:
361: =item 1 - All borders (borders around all four sides of the cell.
362:
363: =item 2 - Border at top and bottom of the cell.
364:
365: =item 3 - Border at the left and right sides of the cell.
366:
1.17 foxr 367: =item 4 - Border around groups (colgroups as well as thead/tfoot/tbody).
1.16 foxr 368:
1.17 foxr 369:
370: =back
1.2 foxr 371:
372: =head3 Examples:
373:
1.4 foxr 374: my $cell_border = $table->cell_border(); # ask if cell borders are requested.
1.2 foxr 375: $table->cell_border(1); # Request cell borders.
376:
377: =cut
378:
1.4 foxr 379: sub cell_border {
1.2 foxr 380: my ($self, $new_value) = @_;
1.8 foxr 381: if($tracing) {&Apache::lonnet::logthis("cell_border: $new_value"); }
1.2 foxr 382: if (defined($new_value)) {
1.5 foxr 383: $self->{'inner_border'} = $new_value;
1.2 foxr 384: }
1.5 foxr 385: return $self->{'inner_border'};
1.2 foxr 386: }
387:
388: =pod
389:
390: =head2 caption
391:
392: Gets and/or sets the caption string for the table. The caption string appears to label
393: the table. If a parameter is supplied it will become the new caption string.k
394:
395: =head3 Examples:
396:
397:
398: $my caption = $table->caption();
399: $table->caption("This is the new table caption");
400:
401: =cut
402:
403: sub caption {
404: my ($self, $new_value) = @_;
405:
1.8 foxr 406: if($tracing) {&Apache::lonnet::logthis("caption: $new_value"); }
1.2 foxr 407: if (defined($new_value)) {
1.5 foxr 408: $self->{'caption'} = $new_value;
1.2 foxr 409: }
410:
1.5 foxr 411: return $self->{'caption'};
1.2 foxr 412: }
413:
414: =pod
415:
416: =head2 theme
417:
418: Gets and optionally sets the table theme. The table theme describes how the
419: table will be typset by the table package. If a parameter is supplied it
420: will be the new theme selection.
421:
422: =head3 Examples:
1.1 foxr 423:
1.2 foxr 424: my $theme = $table->theme();
425: $table->theme("Dresden");
426:
427: =cut
428:
429: sub theme {
430: my ($self, $new_value) = @_;
1.8 foxr 431: if($tracing) {&Apache::lonnet::logthis("theme $new_value"); }
1.2 foxr 432: if (defined($new_value)) {
1.5 foxr 433: $self->{'theme'} = $new_value;
1.2 foxr 434: }
1.5 foxr 435: return $self->{'theme'};
1.2 foxr 436: }
437:
438: =pod
439:
1.17 foxr 440: =head2 width
1.7 foxr 441:
442: Gets and optionally sets the width of the table.
443:
1.17 foxr 444: =head3 Examples:
1.7 foxr 445:
446: my $newwidth = $table->width("10cm"); # 10cm width returns "10cm".
447:
448: =cut
449: sub width {
450: my ($self, $new_value) = @_;
1.8 foxr 451: if($tracing) {&Apache::lonnet::logthis("width = $new_value"); }
452:
1.7 foxr 453: if (defined($new_value)) {
454: $self->{'width'} = $new_value;
455: }
456: return $self->{'width'}; # Could be undef.
457: }
458:
459: =pod
460:
1.2 foxr 461: =head2 start_row
462:
463: Begins a new row in the table. If a row is already open, that row is
464: closed off prior to starting the new row. Rows can have the following attributes
465: which are specified by an optional hash passed in to this function.
466:
467: =over 3
468:
469: =item default_halign
470:
471: The default horizontal alignment of the row. This can be "left", "center", or "right"
472:
473: =item default_valign
474:
475: The default vertical alignment of the row. This can be "top", "center", or "bottom"
476:
477: =back
478:
479: =head3 Examples:
480:
481: $table_start_row(); # no attributes.
482: $table_start({default_halign => "center",
483: default_valign => "bottom"}); # Create setting the attrbutes.
484:
485: =cut
486:
487: sub start_row {
1.5 foxr 488: my ($self, $config) = @_;
1.8 foxr 489: if($tracing) {&Apache::lonnet::logthis("start_row"); }
1.5 foxr 490: if ($self->{'row_open'}) {
1.4 foxr 491: $self->end_row();
1.2 foxr 492: }
493: my $row_hash = {
494: default_halign => "left",
495: default_valign => "top",
1.6 foxr 496: cell_width => 0,
1.2 foxr 497: cells => []
498: };
499:
500: # Override the defaults if the config hash is present:
501:
1.5 foxr 502: if (defined($config)) {
1.22 ! raeburn 503: foreach my $key (keys(%$config)) {
1.5 foxr 504: $row_hash->{$key} = $config->{$key};
1.2 foxr 505: }
506: }
1.5 foxr 507:
1.2 foxr 508:
1.17 foxr 509: my $rows = $self->{'rows'}->{$self->{'part'}};
1.2 foxr 510: push(@$rows, $row_hash);
511:
1.5 foxr 512: $self->{"row_open"} = 1; # Row is now open and ready for business.
1.2 foxr 513: }
514:
515: =pod
516:
517: =head2 end_row
518:
519: Closes off a row. Once closed, cells cannot be added to this row again.
520:
521: =head3 Examples:
522:
1.4 foxr 523: $table->end_row();
1.2 foxr 524:
525:
526: =cut
527:
1.4 foxr 528: sub end_row {
1.2 foxr 529: my ($self) = @_;
1.8 foxr 530: if($tracing) {&Apache::lonnet::logthis("end_row"); }
1.5 foxr 531: if ($self->{'row_open'}) {
1.2 foxr 532:
533: # Mostly we need to determine if this row has the maximum
534: # cell count of any row in existence in the table:
1.17 foxr 535:
1.18 foxr 536:
1.17 foxr 537: my $row = $self->{'rows'}->{$self->{'part'}}->[-1];
1.5 foxr 538: my $cells = $row->{'cells'};
1.3 foxr 539:
1.6 foxr 540: if ($row->{'cell_width'} > $self->{'column_count'}) {
541: $self->{'column_count'} = $row->{'cell_width'};
1.2 foxr 542: }
543:
1.5 foxr 544: $self->{'row_open'} = 0;;
1.2 foxr 545: }
546: }
547:
548: =pod
549:
1.3 foxr 550: =head2 configure_row
551:
552: Modify the configuration of a row. If a row is not open, a new one will be opened.
553:
554: =head3 Parameters:
1.2 foxr 555:
1.3 foxr 556: config_hash - A hash that contains new values for the set of row confiuguration
557: items to be modified. There is currently no check/penalty for items that are not in
558: the set of defined configuration properties which are:
1.2 foxr 559:
1.3 foxr 560: =over 2
561:
562: =item default_halign
563:
564: The default horizontal alignment for text in cells in the row. This can be any of:
565: "left", "right" or "center".
566:
567: =item default_valign
568:
569: The default vertical alignment for text in cells in the row. This can be any of:
570:
571: "top", "bottom" or "center"
572:
1.14 foxr 573:
1.3 foxr 574: =back
1.2 foxr 575:
576: =cut
577:
1.3 foxr 578: sub configure_row {
579: my ($self, $config) = @_;
1.8 foxr 580: if($tracing) {&Apache::lonnet::logthis("configure_row");}
1.5 foxr 581: if (!$self->{'row_open'}) {
1.3 foxr 582: $self->start_row();
583: }
584:
1.17 foxr 585: my $row = $self->{'rows'}->{$self->{'part'}}->[-1];
1.22 ! raeburn 586: foreach my $config_item (keys(%$config)) {
1.3 foxr 587: $row->{$config_item} = $config->{$config_item};
588: }
1.2 foxr 589: }
1.1 foxr 590:
591:
1.3 foxr 592: =pod
593:
594: =head2 add_cell
595:
596: Add a new cell to a row. If there is a row above us, we need to
597: watch out for row spans that may force additional blank cell entries
598: to fill in the span.
599:
600: =head3 Parameters:
601:
602: =over 2
603:
604: =item text
605:
606: Text to put in the cell.
607:
608: =item cell_config
609:
610: Hash of configuration options that override the defaults. The recognized options,
611: and their defaults are:
612:
613: =over 2
614:
615: =item halign
616:
617: If nonblank overrides the row's default for the cell's horizontal alignment.
618:
619: =item valign
620:
621: If nonblank, overrides the row's default for the cdell's vertical alignment.
622:
623: =item rowspan
624:
625: Number of rows the cell spans.
626:
627: =item colspan
628:
629: Number of columns the cell spans.
630:
1.14 foxr 631: =item width
632:
633: LaTeX specification of the width of the cell.
634: Note that if there is a colspan this width is going to be equally divided
635: over the widths of the columnsn in the span.
636: Note as well that if width specification conflict, the last one specified wins...silently.
637:
1.3 foxr 638: =back
639:
1.17 foxr 640: =back
641:
1.3 foxr 642: =cut
643:
644: sub add_cell {
645: my ($self, $text, $config) = @_;
646:
1.8 foxr 647: if($tracing) {&Apache::lonnet::logthis("add_cell : $text"); }
648:
1.3 foxr 649: # If a row is not open, we must open it:
650:
1.5 foxr 651: if (!$self->{'row_open'}) {
1.3 foxr 652: $self->start_row();
653: }
1.17 foxr 654: my $rows = $self->{'rows'}->{$self->{'part'}};
1.6 foxr 655: my $current_row = $rows->[-1];
1.5 foxr 656: my $current_cells = $current_row->{'cells'};
1.6 foxr 657: my $last_coord = $current_row->{'cell_width'};
1.3 foxr 658:
1.6 foxr 659: # We have to worry about row spans if there is a prior row:
1.3 foxr 660:
1.6 foxr 661: if (scalar(@$rows) > 1) {
1.3 foxr 662:
1.6 foxr 663: my $last_row = $rows->[-2];
664: if ($last_coord < $last_row->{'cell_width'}) {
665: my $prior_coord = 0;
666: my $prior_cell_index = 0;
667: while ($prior_coord <= $last_coord) {
668:
669: # Pull a cell down if it's coord matches our start coord
670: # And there's a row span > 1.
671: # Having done so, we adjust our $last_coord to match the
672: # end point of the pulled down cell.
673:
674: my $prior_cell = $last_row->{'cells'}->[$prior_cell_index];
1.8 foxr 675: if (!defined($prior_cell)) {
676: last;
677: }
1.6 foxr 678: if (($prior_cell->{'start_col'} == $last_coord) &&
679: ($prior_cell->{'rowspan'} > 1)) {
680:
681: # Need to drop the cell down
682:
683: my %dropped_down_cell = %$prior_cell;
684: $dropped_down_cell{'rowspan'}--;
685: $dropped_down_cell{'contents'} = '';
686:
687: push(@$current_cells, \%dropped_down_cell);
688: $last_coord += $dropped_down_cell{'colspan'};
689: $current_row->{'cell_width'} = $last_coord;
690:
691: }
692: $prior_coord += $prior_cell->{'colspan'};
693: $prior_cell_index++;
694: }
1.3 foxr 695: }
1.6 foxr 696:
1.3 foxr 697: }
1.6 foxr 698:
1.3 foxr 699: #
700: # Now we're ready to build up our cell:
701:
702: my $cell = {
703: rowspan => 1,
704: colspan => 1,
1.6 foxr 705: start_col => $last_coord,
1.3 foxr 706: contents => $text
707: };
708:
709: if (defined($config)) {
710: foreach my $key (keys(%$config)) {
1.15 raeburn 711: if ($key eq 'colspan') {
712: next if ($config->{$key} == 0);
713: }
1.3 foxr 714: $cell->{$key} = $config->{$key};
715: }
716: }
1.14 foxr 717:
1.6 foxr 718: $current_row->{'cell_width'} += $cell->{'colspan'};
719:
1.14 foxr 720:
721: #
722: # Process the width if it exists. If supplied it must be of the form:
723: # float units
724: # Where units can be in, cm or mm.
725: # Regardless of the supplied units we will normalize to cm.
726: # This allows computation on units at final table generation time.
727: #
728:
729: if (exists($cell->{'width'})) {
730: my $width;
731: my $widthcm;
732: $width = $config->{'width'};
733: $widthcm = $self->size_to_cm($width);
734:
735: # If there's a column span, the actual width is divided by the span
736: # and applied to each of the columns in the span.
737:
738: $widthcm = $widthcm / $cell->{'colspan'};
739: for (my $i = $last_coord; $i < $last_coord + $cell->{'colspan'}; $i++) {
740: $self->{'col_widths'}->{$i} = $widthcm;
741: }
742:
743: }
744:
1.3 foxr 745: push(@$current_cells, $cell);
1.8 foxr 746:
747: if ($tracing) { &Apache::lonnet::logthis("add_cell done"); }
1.3 foxr 748: }
749:
1.8 foxr 750:
751: =pod
752:
753: =head2 append_cell_text
754:
755: Sometimes it's necessary to create/configure the cell and then later add text to it.
756: This sub allows text to be appended to the most recently created cell.
757:
758: =head3 Parameters
759:
760: The text to add to the cell.
761:
762: =cut
763: sub append_cell_text {
764: my ($this, $text) = @_;
765:
766: if($tracing) {&Apache::lonnet::logthis("append_cell_text: $text"); }
1.17 foxr 767: my $rows = $this->{'rows'}->{$this->{'part'}};
1.8 foxr 768: my $current_row = $rows->[-1];
769: my $cells = $current_row->{'cells'};
770: my $current_cell = $cells->[-1];
771: $current_cell->{'contents'} .= $text;
772:
773: }
1.17 foxr 774: #-------------------------- Support for row/column groups. ----
775:
776: =pod
777:
778: =head2 start_head
779:
780: starts the table head. This corresponds to the <thead> tag in
781: html/xml. All rows defined in this group will be
782: collected and placed at the front of the table come rendering time.
783: Furthermore, if the table has group borders enabled, a rule will be
784: rendered following and preceding this group of rows.
785:
786: =cut
787:
788: sub start_head {
789: my ($this) = @_;
790: if ($tracing) { &Apache::lonnet::logthis("start_head"); }
791: $this->{'part'} = 'head';
792: }
793:
794: =pod
795:
796: =head2 end_head
797:
798: Ends a table head. This corresponds to the
799: </thead> closing tag in html/xml.
800:
801: =cut
802:
803: sub end_head {
804: my ($this) = @_;
805: if ($tracing) { &Apache::lonnet::logthis("end_head"); }
806: $this->{'part'} = 'body';
807: }
808:
809: =pod
810:
811: =head2 start_foot
812:
813: Starts the table footer. All of the rows generated in the footer will
814: be rendered at the bottom of the table. This sub corresponds to the
815: <tfoot> tag in html/xml. If the table has group borders enabled, a rule
816: will be rendered at the top and bottom of the set of columns in this
817: group
818:
819: =cut
820:
821: sub start_foot {
822: my ($this) = @_;
823: if ($tracing) { &Apache::lonnet::logthis("start_foot"); }
824: $this->{'part'} = 'foot';
825: }
826:
827: =pod
828:
829: =head2 end_foot
830:
831: Ends the set of rows in the table footer. This corresponds to the
832: </tfoot> end tag in xml/html.
833:
834: =cut
835:
836: sub end_foot {
837: my ($this) = @_;
838: if ($tracing) { &Apache::lonnet::logthis("end_foot") }
839: $this->{'part'} = 'body';
840: }
841:
842: =pod
843:
844: =head2 start_body
845:
846: Starts the set of rows that will be in the table body. Note that if
847: we are not in the header or footer, body rows are implied.
848: This correspondes to the presence of a <tbody> tag in html/xml.
849: If group borders are on, a rule will be rendered at the top and bottom
850: of the body rows.
851:
852: =cut
853:
854: sub start_body {
855: my ($this) = @_;
856: if ($tracing) { &Apache::lonnet::logthis("start_body"); }
857: $this->{'part'} = 'body';
858: }
859:
860: =pod
861:
862: =head2 end_body
863:
864: Ends the set of rows in a table body. Note that in the event we are not
865: in the header or footer groups this code assumes we are in the body
866: group. I believe this is a good match to how mot browsers render.
867:
868: =cut
869:
870: sub end_body {
871: my ($this) = @_;
872: if ($tracing) { &Apache::lonnet::logthis("end_body"); }
873:
874: }
875:
876: =pod
877:
878: =head2 define_colgroup
879:
880: Define a column group a column group corresponds to the
881: <cgroup> tag in Html/Xml. A column group as we implement it has
882: the following properties tht will be shared amongst all cells in the
883: columns in the group unless overidden in the specific oell definition:
884:
885: =over 2
886:
887: =item span
888:
889: The number of columns in the column group. This defaults to 1.
890:
891: =item halign
892:
893: Horizontal alignment of the cells. This defaults to left.
894: Other values are left, center, right (justify and char are
895: accepted but treated as left).
896:
897: =item valign
898:
899: Vertical alignment of the cells. This defaults to middle.
900: Other values are top middle, bottom, (baseline is accepted and
901: treated as top).
902:
903: =back
904:
905: If group borders are turned on, a rule will be rendered
906: at the left and right side of the column group.
907:
908: =head3 parameters
909:
910: =over 2
911:
912: =item definition
913:
914: This is a hash that contains any of the keys described above that
915: define the column group.
916:
917: =back
918:
919:
920: =head3 Example
921:
922: $table->define_colgroup({
923: 'span' => 2,
924: 'halign' => 'center'
925: })
926:
927:
928:
929: =cut
930:
931: sub define_colgroup {
932: my ($this, $attributes) = @_;
933: if ($tracing) { &Apache::lonnet::logthis("col_group"); }
1.19 foxr 934: my $colgroups = $this->{'colgroups'};
935: push(@$colgroups, $attributes); # Colgroups always add at end.
936:
1.17 foxr 937:
938: }
1.8 foxr 939:
1.17 foxr 940: #------------------------- Render the table ---------------------
1.8 foxr 941:
1.6 foxr 942: =pod
943:
944: =head2 generate
945:
946: Call this when the structures for the table have been built.
947: This will generate and return the table object that can be used
948: to generate the table. Returning the table object allows for
949: a certain amount of testing to be done on the generated table.
950: The caller can then ask the table object to generate LaTeX.
951:
952: =cut
1.17 foxr 953:
1.6 foxr 954: sub generate {
955: my ($this) = @_;
1.8 foxr 956: my $useP = 0;
1.14 foxr 957:
958: my $colunits = 'cm'; # All widths get normalized to cm.
959: my $tablewidth;
1.6 foxr 960:
1.8 foxr 961: if($tracing) {&Apache::lonnet::logthis("generate"); }
1.12 foxr 962: my $table = Apache::lonlatextable->new();
963:
1.17 foxr 964: my $inner_border = $this->{'inner_border'};
965: my $outer_border = $this->{'outer_border'};
966: my $column_count = $this->{'column_count'};
1.8 foxr 967:
1.17 foxr 968: my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
969: my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
970: my $part_border = ($inner_border == 4);
971:
1.19 foxr 972:
973: # Add the caption if supplied.
1.7 foxr 974:
975: if ($this->{'caption'} ne "") {
976: $table->set_caption($this->caption);
977: }
978:
979: # Set the width if defined:
980:
1.14 foxr 981: my $default_width;
982: my $colwidths = $this->{'col_widths'};
1.7 foxr 983: if (defined ($this->{'width'})) {
1.14 foxr 984: $tablewidth = $this->{'width'};
985: $tablewidth = $this->size_to_cm($tablewidth);
986:
1.8 foxr 987: $useP = 1;
988:
1.14 foxr 989: # Figure out the default width for a column with unspecified
990: # We take the initially specified widths and sum them up.
991: # This is subtracted from total width above.
992: # If the result is negative we're going to allow a minimum of 2.54cm for
993: # each column and make the table spill appropriately.
994: # This (like a riot) is an ugly thing but I'm open to suggestions about
995: # how to handle it better (e.g. scaling down requested widths?).
996:
997: my $specified_width = 0.0;
998: my $specified_cols = 0;
1.22 ! raeburn 999: foreach my $col (keys(%$colwidths)) {
1.14 foxr 1000: $specified_width = $specified_width + $colwidths->{$col};
1001: $specified_cols++;
1002: }
1003: my $unspecified_cols = $this->{'column_count'} - $specified_cols;
1004:
1005: # If zero unspecified cols, we are pretty much done... just have to
1006: # adjust the total width to be specified width. Otherwise we
1007: # must figure out the default width and total width:
1008: #
1009: my $total_width;
1010: if($unspecified_cols == 0) {
1011: $total_width = $specified_width;
1012: } else {
1013: $default_width = ($tablewidth - $specified_width)/$unspecified_cols; # Could be negative....
1014: $total_width = $default_width * $unspecified_cols + $specified_width;
1015: }
1016:
1017: # if the default_width is < 0.0 the user has oversubscribed the width of the table with the individual
1018: # column. In this case, we're going to maintain the desired proportions of the user's columns, but
1019: # ensure that the unspecified columns get a fair share of the width..where a fair share is defined as
1020: # the total width of the table / unspecified column count.
1021: # We figure out what this means in terms of reducing the specified widths by dividing by a constant proportionality.
1022: # Note that this cannot happen if the user hasn't specified anywidths as the computation above would then
1023: # just make all columns equal fractions of the total table width.
1024:
1025: if ($default_width < 0) {
1026: $default_width = ($tablewidth/$unspecified_cols); # 'fair' default width.
1027: my $width_remaining = $tablewidth - $default_width*$unspecified_cols; # What's left for the specified cols.
1028: my $reduction = $tablewidth/$width_remaining; # Reduction fraction for specified cols
1.22 ! raeburn 1029: foreach my $col (keys(%$colwidths)) {
1.14 foxr 1030: $colwidths->{$col} = $colwidths->{$col}/$reduction;
1031: }
1032:
1033: }
1.7 foxr 1034: }
1.19 foxr 1035: # If rule is groups. we need to have a
1036: # list of the column numbers at which a column ends...
1037: # and the coldef needs to start with a |
1038: #
1039: my @colgroup_ends;
1040: my $colgroup_col = 0;
1041: my $group = 0;
1042: my $coldef = "";
1043: if ($outer_border || $cell_lr_border) {
1044: $coldef .= '|';
1045: }
1046: if ($part_border) {
1047: $coldef .= '|';
1048: my $colgroup_col = 0;
1049: my $colgroups = $this->{'colgroups'};
1050: foreach my $group (@$colgroups) {
1051: if (defined $group->{'span'}) {
1052: $colgroup_col += $group->{'span'};
1053: } else {
1054: $colgroup_col++;
1055: }
1056: push(@colgroup_ends, $colgroup_col);
1057: }
1058:
1059: }
1060: $this->render_part('head', $table, $useP, $default_width,
1061: \@colgroup_ends);
1062: $this->render_part('body', $table, $useP, $default_width,
1063: \@colgroup_ends);
1064: $this->render_part('foot', $table, $useP, $default_width,
1065: \@colgroup_ends);
1.17 foxr 1066:
1067:
1068:
1069:
1070:
1071: for (my $i =0; $i < $column_count; $i++) {
1072: if ($useP) {
1073: $coldef .= "p{$default_width $colunits}";
1074: } else {
1075: $coldef .= 'l';
1076: }
1077: if ($cell_lr_border ||
1078: ($outer_border && ($i == $column_count-1))) {
1079: $coldef .= '|';
1080: }
1.19 foxr 1081: if ($part_border && ($i == ($colgroup_ends[$group]-1))) {
1082: $coldef .= '|';
1083: $group++;
1084: }
1.17 foxr 1085: }
1086: $table->{'coldef'} = $coldef;
1087:
1088: # Return the table:
1089:
1090: if ($tracing) { &Apache::lonnet::logthis("Leaving generate"); }
1091:
1092:
1093: return $table;
1094:
1095: }
1096:
1097:
1098: #---------------------------------------------------------------------------
1099: #
1100: # Private methods:
1101: #
1102:
1103: #
1104: # Convert size with units -> size in cm.
1105: # The resulting size is floating point with no units so that it can be used in
1106: # computation. Note that an illegal or missing unit is treated silently as
1107: # cm for now.
1108: #
1109: sub size_to_cm {
1110: my ($this, $size_spec) = @_;
1111: my ($size, $units) = split(/ /, $size_spec);
1112: if (lc($units) eq 'mm') {
1113: return $size / 10.0;
1114: }
1115: if (lc($units) eq 'in') {
1116: return $size * 2.54;
1117: }
1118:
1119: return $size; # Default is cm.
1120: }
1121:
1122: #
1123: # Render a part of the table. The valid table parts are
1124: # head, body and foot. These corresopnd to the set of rows
1125: # define within <thead></thead>, <tbody></tbody> and <tfoot></tfoot>
1126: # respectively.
1127: #
1128: sub render_part {
1.19 foxr 1129: my ($this, $part, $table, $useP,
1130: $default_width, $colgroup_ends) = @_;
1.17 foxr 1131:
1132: if ($tracing) { &Apache::lonnet::logthis("render_part: $part") };
1133:
1134: # Do nothing if that part of the table is empty:
1135:
1136: if ($this->{'rows'}->{$part} == undef) {
1137: if ($tracing) {&Apache::lonnet::logthis("$part is empty"); }
1138: return;
1139: }
1.14 foxr 1140:
1.19 foxr 1141: my @cgends = @$colgroup_ends;
1.6 foxr 1142: # Build up the data:
1143:
1144: my @data;
1.17 foxr 1145: my $colwidths = $this->{'col_widths'};
1.19 foxr 1146: my $rows = $this->{'rows'}->{$part};
1.6 foxr 1147: my $row_count = scalar(@$rows);
1148: my $inner_border = $this->{'inner_border'};
1149: my $outer_border = $this->{'outer_border'};
1150: my $column_count = $this->{'column_count'};
1151:
1.16 foxr 1152: my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
1153: my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
1.17 foxr 1154: my $part_border = ($inner_border == 4);
1155: my $colunits = 'cm'; # All units in cm.
1156:
1.11 foxr 1157: # Add a top line if the outer or inner border is enabled:
1.17 foxr 1158: # or if group rules are on.
1159: #
1.11 foxr 1160:
1.17 foxr 1161: if ($outer_border || $cell_ul_border || $part_border) {
1.11 foxr 1162: push(@data, ["\\cline{1-$column_count}"]);
1163:
1164: }
1165:
1.6 foxr 1166: for (my $row = 0; $row < $row_count; $row++) {
1167: my @row;
1168: my $cells = $rows->[$row]->{'cells'};
1.7 foxr 1169: my $def_halign = $rows->[$row]->{'default_halign'};
1.6 foxr 1170: my $cell_count = scalar(@$cells);
1171: my $startcol = 1;
1172: my @underlines; # Array of \cline cells if cellborder on.
1173:
1.19 foxr 1174: my $colgroup_count = @cgends; # Number of column groups.
1175: my $cgroup = 0; # Group we are on.
1176: my $cgstart = 0; # Where the next cgroup starts.
1.11 foxr 1177:
1.6 foxr 1178: for (my $cell = 0; $cell < $cell_count; $cell++) {
1179: my $contents = $cells->[$cell]->{'contents'};
1.19 foxr 1180:
1.7 foxr 1181: #
1182: # Cell alignment is the default alignment unless
1183: # explicitly specified in the cell.
1184: # NOTE: at this point I don't know how to do vert alignment.
1185: #
1186:
1187: my $halign = $def_halign;
1188: if (defined ($cells->[$cell]->{'halign'})) {
1189: $halign = $cells->[$cell]->{'halign'};
1190: }
1191:
1192: # Create the horizontal alignment character:
1193:
1194: my $col_align = 'l';
1.8 foxr 1195: my $embeddedAlignStart = "";
1196: my $embeddedAlignEnd = "";
1197:
1.7 foxr 1198: if ($halign eq 'right') {
1199: $col_align = 'r';
1.13 raeburn 1200: $embeddedAlignStart = '\raggedleft';
1.7 foxr 1201: }
1202: if ($halign eq 'center') {
1203: $col_align = 'c';
1.8 foxr 1204: $embeddedAlignStart = '\begin{center}';
1205: $embeddedAlignEnd = '\end{center}';
1.7 foxr 1206: }
1.8 foxr 1207:
1208: # If the width has been specified, turn these into
1209: # para mode; and wrap the contents in the start/stop stuff:
1210:
1211: if ($useP) {
1.14 foxr 1212: my $cw;
1213: if (defined($colwidths->{$cell})) {
1214: $cw = $colwidths->{$cell};
1215: } else {
1216: $cw = $default_width;
1217: }
1.15 raeburn 1218: $cw = $cw * $cells->[$cell]->{'colspan'};
1.8 foxr 1219: $col_align = "p{$cw $colunits}";
1220: $contents = $embeddedAlignStart . $contents . $embeddedAlignEnd;
1221: }
1222:
1.16 foxr 1223: if ($cell_lr_border || ($outer_border && ($cell == 0))) {
1.7 foxr 1224: $col_align = '|'.$col_align;
1225: }
1.16 foxr 1226: if ($cell_lr_border || ($outer_border && ($cell == ($cell_count -1)))) {
1.7 foxr 1227: $col_align = $col_align.'|';
1228: }
1.19 foxr 1229: if ($part_border) {
1230: if ($cell == $cgstart) {
1231: $col_align = '|' . $col_align;
1232: if ($cgroup < $colgroup_count) {
1233: $cgstart = $cgends[$cgroup];
1234: $cgroup++;
1235: } else {
1236: $cgstart = 1000000; # TODO: Get this logic right
1237: }
1238: if ($cell == ($cell_count - 1) &&
1239: ($cell == ($cgstart-1))) {
1240: $col_align = $col_align . '|'; # last col ends colgrp.
1241: }
1242: }
1243: }
1.7 foxr 1244:
1245: #factor in spans:
1246:
1.6 foxr 1247: my $cspan = $cells->[$cell]->{'colspan'};
1248: my $nextcol = $startcol + $cspan;
1.19 foxr 1249:
1250: # At this point this col is the start of the span.
1251: # nextcol is the end of the span.
1.8 foxr 1252:
1253: # If we can avoid the \multicolumn directive that's best as
1254: # that makes some things like \parpic invalid in LaTeX which
1255: # screws everything up.
1256:
1257: if (($cspan > 1) || !($col_align =~ /l/)) {
1258:
1259: $contents = '\multicolumn{'.$cspan.'}{'.$col_align.'}{'.$contents.'}';
1260:
1261: # A nasty edge case. If there's only one cell, the software will assume
1262: # we're in complete control of the row so we need to end the row ourselves.
1263:
1264: if ($cell_count == 1) {
1265: $contents .= ' \\\\';
1266: }
1267: }
1.16 foxr 1268: if ($cell_ul_border && ($cells->[$cell]->{'rowspan'} == 1)) {
1.6 foxr 1269: my $lastcol = $nextcol -1;
1270: push(@underlines, "\\cline{$startcol-$lastcol}");
1271: }
1272: $startcol = $nextcol;
1.19 foxr 1273:
1.6 foxr 1274: # Rowspans should take care of themselves.
1275:
1276: push(@row, $contents);
1277:
1278: }
1279: push(@data, \@row);
1.16 foxr 1280: if ($cell_ul_border) {
1.6 foxr 1281: for (my $i =0; $i < scalar(@underlines); $i++) {
1282: push(@data, [$underlines[$i]]);
1283: }
1284: }
1285:
1286: }
1.11 foxr 1287: #
1288: # Add bottom border if necessary: if the inner border was on, the loops above
1289: # will have done a bottom line under the last cell.
1290: #
1.17 foxr 1291: if (($outer_border || $part_border) && !$cell_ul_border) {
1.11 foxr 1292: push(@data, ["\\cline{1-$column_count}"]);
1293:
1294: }
1.17 foxr 1295: $table->set_data(\@data);
1.6 foxr 1296: }
1.14 foxr 1297:
1.6 foxr 1298: #----------------------------------------------------------------------------
1.5 foxr 1299: # The following methods allow for testability.
1.4 foxr 1300:
1301:
1302: sub get_object_attribute {
1303: my ($self, $attribute) = @_;
1.8 foxr 1304: if ($tracing > 1) { &Apache::lonnet::logthis("get_object_attribute: $attribute"); }
1.4 foxr 1305: return $self->{$attribute};
1306: }
1307:
1.5 foxr 1308: sub get_row {
1309: my ($self, $row) = @_;
1.8 foxr 1310: if ($tracing > 1) { &Apache::lonnet::logthis("get_row"); }
1311:
1.17 foxr 1312: my $rows = $self->{'rows'}->{$self->{'part'}}; # ref to an array....
1.5 foxr 1313: return $rows->[$row]; # ref to the row hash for the selected row.
1314: }
1.14 foxr 1315:
1.1 foxr 1316: # Mandatory initialization.
1.4 foxr 1317: BEGIN{
1318: }
1.1 foxr 1319:
1320: 1;
1321: __END__
1.17 foxr 1322:
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>