# The LearningOnline Network with CAPA # Generating TeX tables. # # $Id: lontable.pm,v 1.10 2009/04/17 20:17:48 raeburn Exp $ # # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ ## Copyright for TtHfunc and TtMfunc by Ian Hutchinson. # TtHfunc and TtMfunc (the "Code") may be compiled and linked into # binary executable programs or libraries distributed by the # Michigan State University (the "Licensee"), but any binaries so # distributed are hereby licensed only for use in the context # of a program or computational system for which the Licensee is the # primary author or distributor, and which performs substantial # additional tasks beyond the translation of (La)TeX into HTML. # The C source of the Code may not be distributed by the Licensee # to any other parties under any circumstances. # # This module is a support packkage that helps londefdef generate # LaTeX tables using the LaTeX::Table package. A prerequisite is that # the print generator must have added the following to the LaTeX # # \usepackage{xtab} # \usepackage{booktabs} # \usepackage{array} # \usepackage{colortbl} # \usepackage{xcolor} # # These packages are installed in the packaged LaTeX distributions we know of as of # 11/24/2008 # package Apache::lontable; use strict; use LaTeX::Table; use Apache::lonnet; # for trace logging. my $tracing = 0; # Set to 1 to enable log tracing. 2 for local sub tracing. =pod =head1 lontable Table generation assistant for the LaTeX target This module contains support software for generating tables in LaTeX output mode In this implementation, we use the LaTeX::Table package to do the actual final formatting. Each table creates a new object. Table objects can have global properties configured. The main operations on a table object are: =over 3 =item start_row Opens a new table row. =item end_row Closes a table row. =item configure_row Modifies a configuration item in the currently open row. =item generate Returns the generated table string. =item configure Configures a table's global configuration. =item add_cell Add and configure a cell to the current row.6 =back =cut =pod =head2 new - create a new object. Create a new table object. Any of the raw table configuration items can be modified by this. These configuration items include: my $table = lontable::new(\%config_hash) =over3 =item alignment Table alignment. Some table styles support this but not all. =item tableborder If true, a border is drawn around the table. =item cellborder If true, borders are drawn around the cells inside a table. =item caption The table caption text. =item theme The theme of the table to use. Defaults to Zurich. Themes we know about are: NYC, NYC2, Zurich, Berlin, Dresden, Houston, Miami, plain, Paris. Other themes can be added to the LaTeX::Table package, and they will become supported automatically, as theme names are not error checked. Any use of a non-existent theme is reported by the LaTeX::Table package when the table text is generated. =item width The width of the table. in any TeX unit measure e.g. 10.8cm This forces the table to the tabularx environment. It also forces the declarations for cells to be paragraph mode which supports more internal formatting. =back =head3 Member data The object hash has the following members: =over 3 =item column_count Maintained internally, the number of colums in the widest row. =item alignment Table alignment (configurable) "left", "center", or "right". =item outer_border True if a border should be drawn around the entire table (configurable) =item inner_borders True if a border should be drawn around all cells (configurable). =item caption Table caption (configurable). =item theme Theme desired (configurable). =item width If defined, the width of the table (should be supplied in fraction of column width e.g. .75 for 75%. =item row_open True if a row is open and not yet closed. =item rows Array of row data. This is an array of hashes described below. =back =head3 Row data. Each row of table data is an element of the rows hash array. Hash elements are =over 3 =item default_halign 0 Default horizontal alignment for cells in this row. =item default_valign Default vertical alignment for cells in this row (may be ignored). =item cell_width The width of the row in cells. This is the sum of the column spans of the cells in the row. =item cells Array of hashes where each element represents the data for a cell. The contents of each element of this hash are described below: =over 3 =item header If present, the row is a 'header' that is it was made via the tag. =item halign If present, overrides the row default horizontal alignment. =item valign if present, override the row default vertical alignment. =item rowspan If present, indicates the number of rows this cell spans. =item colspan If present indicates the number of columns this cell spans. Note that a cell can span both rows and columns. =item start_col The starting column of the cell in the table grid. =item contents The contents of the cell. =back =back =cut sub new { my ($class, $configuration) = @_; if($tracing) {&Apache::lonnet::logthis("new table object");} # Initialize the object member data with the default values # then override with any stuff in $configuration. my $self = { alignment => "left", outer_border => 0, inner_border => 0, caption => "", theme => "Zurich", column_count => 0, row_open => 0, rows => [], }; foreach my $key (keys %$configuration) { $self->{$key} = $$configuration{$key}; } bless($self, $class); return $self; } #------------------------------------------------------------------------- # # Methods that get/set table global configuration. # =pod =head2 Gets/set alignment. If the method is passed a new alignment value, that replaces the current one. Regardless, the current alignment is used: =head3 Examples: my $align = $table->alignment(); # Return current alignment $table->alignment("center"); # Attempt centered alignment. =cut sub alignment { my ($self, $new_value) = @_; if ($tracing) {&Apache::lonnet::logthis("alignment = $new_value");} if (defined($new_value)) { $self->{'alignment'} = $new_value; } return $self->{'alignment'}; } =pod =head2 table_border Set or get the presence of an outer border in the table. If passed a parameter, that parameter replaces the current request for or not for an outer border. Regardless, the function returns the final value of the outer_border request. =head3 Examples: $table->table_border(1); # Request an outer border. my $outer_requested = $table->table_border(); =cut sub table_border { my ($self, $new_value) = @_; if ($tracing) {&Apache::lonnet::logthis("table_border $new_value");} if (defined($new_value)) { $self->{'outer_border'} = $new_value; } return $self->{'outer_border'}; } =pod =head2 cell_border Set or get the presence of a request for cells to have borders drawn around them. If a paramter is passed, it will be treated as a new value for the cell border configuration. Regardless,the final value of that configuration parameter is returned. =head3 Examples: my $cell_border = $table->cell_border(); # ask if cell borders are requested. $table->cell_border(1); # Request cell borders. =cut sub cell_border { my ($self, $new_value) = @_; if($tracing) {&Apache::lonnet::logthis("cell_border: $new_value"); } if (defined($new_value)) { $self->{'inner_border'} = $new_value; } return $self->{'inner_border'}; } =pod =head2 caption Gets and/or sets the caption string for the table. The caption string appears to label the table. If a parameter is supplied it will become the new caption string.k =head3 Examples: $my caption = $table->caption(); $table->caption("This is the new table caption"); =cut sub caption { my ($self, $new_value) = @_; if($tracing) {&Apache::lonnet::logthis("caption: $new_value"); } if (defined($new_value)) { $self->{'caption'} = $new_value; } return $self->{'caption'}; } =pod =head2 theme Gets and optionally sets the table theme. The table theme describes how the table will be typset by the table package. If a parameter is supplied it will be the new theme selection. =head3 Examples: my $theme = $table->theme(); $table->theme("Dresden"); =cut sub theme { my ($self, $new_value) = @_; if($tracing) {&Apache::lonnet::logthis("theme $new_value"); } if (defined($new_value)) { $self->{'theme'} = $new_value; } return $self->{'theme'}; } =pod =head 2 width Gets and optionally sets the width of the table. =head 3 Examples: my $newwidth = $table->width("10cm"); # 10cm width returns "10cm". =cut sub width { my ($self, $new_value) = @_; if($tracing) {&Apache::lonnet::logthis("width = $new_value"); } if (defined($new_value)) { $self->{'width'} = $new_value; } return $self->{'width'}; # Could be undef. } =pod =head2 start_row Begins a new row in the table. If a row is already open, that row is closed off prior to starting the new row. Rows can have the following attributes which are specified by an optional hash passed in to this function. =over 3 =item default_halign The default horizontal alignment of the row. This can be "left", "center", or "right" =item default_valign The default vertical alignment of the row. This can be "top", "center", or "bottom" =back =head3 Examples: $table_start_row(); # no attributes. $table_start({default_halign => "center", default_valign => "bottom"}); # Create setting the attrbutes. =cut sub start_row { my ($self, $config) = @_; if($tracing) {&Apache::lonnet::logthis("start_row"); } if ($self->{'row_open'}) { $self->end_row(); } my $row_hash = { default_halign => "left", default_valign => "top", cell_width => 0, cells => [] }; # Override the defaults if the config hash is present: if (defined($config)) { foreach my $key (keys %$config) { $row_hash->{$key} = $config->{$key}; } } my $rows = $self->{'rows'}; push(@$rows, $row_hash); $self->{"row_open"} = 1; # Row is now open and ready for business. } =pod =head2 end_row Closes off a row. Once closed, cells cannot be added to this row again. =head3 Examples: $table->end_row(); =cut sub end_row { my ($self) = @_; if($tracing) {&Apache::lonnet::logthis("end_row"); } if ($self->{'row_open'}) { # Mostly we need to determine if this row has the maximum # cell count of any row in existence in the table: my $row = $self->{'rows'}->[-1]; my $cells = $row->{'cells'}; if ($row->{'cell_width'} > $self->{'column_count'}) { $self->{'column_count'} = $row->{'cell_width'}; } $self->{'row_open'} = 0;; } } =pod =head2 configure_row Modify the configuration of a row. If a row is not open, a new one will be opened. =head3 Parameters: config_hash - A hash that contains new values for the set of row confiuguration items to be modified. There is currently no check/penalty for items that are not in the set of defined configuration properties which are: =over 2 =item default_halign The default horizontal alignment for text in cells in the row. This can be any of: "left", "right" or "center". =item default_valign The default vertical alignment for text in cells in the row. This can be any of: "top", "bottom" or "center" =back =cut sub configure_row { my ($self, $config) = @_; if($tracing) {&Apache::lonnet::logthis("configure_row");} if (!$self->{'row_open'}) { $self->start_row(); } my $row = $self->{'rows'}[-1]; foreach my $config_item (keys %$config) { $row->{$config_item} = $config->{$config_item}; } } =pod =head2 add_cell Add a new cell to a row. If there is a row above us, we need to watch out for row spans that may force additional blank cell entries to fill in the span. =head3 Parameters: =over 2 =item text Text to put in the cell. =item cell_config Hash of configuration options that override the defaults. The recognized options, and their defaults are: =over 2 =item halign If nonblank overrides the row's default for the cell's horizontal alignment. =item valign If nonblank, overrides the row's default for the cdell's vertical alignment. =item rowspan Number of rows the cell spans. =item colspan Number of columns the cell spans. =back =cut sub add_cell { my ($self, $text, $config) = @_; if($tracing) {&Apache::lonnet::logthis("add_cell : $text"); } # If a row is not open, we must open it: if (!$self->{'row_open'}) { $self->start_row(); } my $rows = $self->{'rows'}; my $current_row = $rows->[-1]; my $current_cells = $current_row->{'cells'}; my $last_coord = $current_row->{'cell_width'}; # We have to worry about row spans if there is a prior row: if (scalar(@$rows) > 1) { my $last_row = $rows->[-2]; if ($last_coord < $last_row->{'cell_width'}) { my $prior_coord = 0; my $prior_cell_index = 0; while ($prior_coord <= $last_coord) { # Pull a cell down if it's coord matches our start coord # And there's a row span > 1. # Having done so, we adjust our $last_coord to match the # end point of the pulled down cell. my $prior_cell = $last_row->{'cells'}->[$prior_cell_index]; if (!defined($prior_cell)) { last; } if (($prior_cell->{'start_col'} == $last_coord) && ($prior_cell->{'rowspan'} > 1)) { # Need to drop the cell down my %dropped_down_cell = %$prior_cell; $dropped_down_cell{'rowspan'}--; $dropped_down_cell{'contents'} = ''; push(@$current_cells, \%dropped_down_cell); $last_coord += $dropped_down_cell{'colspan'}; $current_row->{'cell_width'} = $last_coord; } $prior_coord += $prior_cell->{'colspan'}; $prior_cell_index++; } } } # # Now we're ready to build up our cell: my $cell = { rowspan => 1, colspan => 1, start_col => $last_coord, contents => $text }; if (defined($config)) { foreach my $key (keys(%$config)) { $cell->{$key} = $config->{$key}; } } $current_row->{'cell_width'} += $cell->{'colspan'}; push(@$current_cells, $cell); if ($tracing) { &Apache::lonnet::logthis("add_cell done"); } } =pod =head2 append_cell_text Sometimes it's necessary to create/configure the cell and then later add text to it. This sub allows text to be appended to the most recently created cell. =head3 Parameters The text to add to the cell. =cut sub append_cell_text { my ($this, $text) = @_; if($tracing) {&Apache::lonnet::logthis("append_cell_text: $text"); } my $rows = $this->{'rows'}; my $current_row = $rows->[-1]; my $cells = $current_row->{'cells'}; my $current_cell = $cells->[-1]; $current_cell->{'contents'} .= $text; } =pod =head2 generate Call this when the structures for the table have been built. This will generate and return the table object that can be used to generate the table. Returning the table object allows for a certain amount of testing to be done on the generated table. The caller can then ask the table object to generate LaTeX. =cut sub generate { my ($this) = @_; my $useP = 0; my $colwidth; my $colunits; if($tracing) {&Apache::lonnet::logthis("generate"); } my $table = LaTeX::Table->new(); $table->set_center(0); # loncapa tables don't float. $table->set_environment(0); # Add the caption if supplied. if ($this->{'caption'} ne "") { $table->set_caption($this->caption); } # Set the width if defined: if (defined ($this->{'width'})) { # $table->set_width($this->{'width'}); # $table->set_width_environment('tabularx'); $useP = 1; ($colwidth, $colunits) = split(/ /, $this->{'width'}); $colwidth = $colwidth/$this->{'column_count'}; } # Build up the data: my @data; my $rows = $this->{'rows'}; my $row_count = scalar(@$rows); my $inner_border = $this->{'inner_border'}; my $outer_border = $this->{'outer_border'}; my $column_count = $this->{'column_count'}; for (my $row = 0; $row < $row_count; $row++) { my @row; my $cells = $rows->[$row]->{'cells'}; my $def_halign = $rows->[$row]->{'default_halign'}; my $cell_count = scalar(@$cells); my $startcol = 1; my @underlines; # Array of \cline cells if cellborder on. for (my $cell = 0; $cell < $cell_count; $cell++) { my $contents = $cells->[$cell]->{'contents'}; # # Cell alignment is the default alignment unless # explicitly specified in the cell. # NOTE: at this point I don't know how to do vert alignment. # my $halign = $def_halign; if (defined ($cells->[$cell]->{'halign'})) { $halign = $cells->[$cell]->{'halign'}; } # Create the horizontal alignment character: my $col_align = 'l'; my $embeddedAlignStart = ""; my $embeddedAlignEnd = ""; if ($halign eq 'right') { $col_align = 'r'; $embeddedAlignStart = '\begin{flushright} '; $embeddedAlignEnd = ' \end{flushright}'; } if ($halign eq 'center') { $col_align = 'c'; $embeddedAlignStart = '\begin{center}'; $embeddedAlignEnd = '\end{center}'; } # If the width has been specified, turn these into # para mode; and wrap the contents in the start/stop stuff: if ($useP) { my $cw = $colwidth * $cells->[$cell]->{'colspan'}; $col_align = "p{$cw $colunits}"; $contents = $embeddedAlignStart . $contents . $embeddedAlignEnd; } if ($inner_border || ($outer_border && ($cell == 0))) { $col_align = '|'.$col_align; } if ($inner_border || ($outer_border && ($cell == ($cell_count -1)))) { $col_align = $col_align.'|'; } #factor in spans: my $cspan = $cells->[$cell]->{'colspan'}; my $nextcol = $startcol + $cspan; # If we can avoid the \multicolumn directive that's best as # that makes some things like \parpic invalid in LaTeX which # screws everything up. if (($cspan > 1) || !($col_align =~ /l/)) { $contents = '\multicolumn{'.$cspan.'}{'.$col_align.'}{'.$contents.'}'; # A nasty edge case. If there's only one cell, the software will assume # we're in complete control of the row so we need to end the row ourselves. if ($cell_count == 1) { $contents .= ' \\\\'; } } if ($inner_border && ($cells->[$cell]->{'rowspan'} == 1)) { my $lastcol = $nextcol -1; push(@underlines, "\\cline{$startcol-$lastcol}"); } $startcol = $nextcol; # Rowspans should take care of themselves. push(@row, $contents); } push(@data, \@row); if ($inner_border) { for (my $i =0; $i < scalar(@underlines); $i++) { push(@data, [$underlines[$i]]); } } } $table->set_data(\@data); my $coldef = ""; if ($outer_border || $inner_border) { $coldef .= '|'; } for (my $i =0; $i < $column_count; $i++) { if ($useP) { $coldef .= "p{$colwidth $colunits}"; } else { $coldef .= 'l'; } if ($inner_border || ($outer_border && ($i == $column_count-1))) { $coldef .= '|'; } } $table->{'coldef'} = $coldef; # Return the table: if ($tracing) { &Apache::lonnet::logthis("Leaving generate"); } return $table; } #---------------------------------------------------------------------------- # The following methods allow for testability. sub get_object_attribute { my ($self, $attribute) = @_; if ($tracing > 1) { &Apache::lonnet::logthis("get_object_attribute: $attribute"); } return $self->{$attribute}; } sub get_row { my ($self, $row) = @_; if ($tracing > 1) { &Apache::lonnet::logthis("get_row"); } my $rows = $self->{'rows'}; # ref to an array.... return $rows->[$row]; # ref to the row hash for the selected row. } # Mandatory initialization. BEGIN{ } 1; __END__