--- loncom/xml/lontable.pm 2010/11/18 17:12:14 1.15 +++ loncom/xml/lontable.pm 2014/12/15 00:52:40 1.22 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Generating TeX tables. # -# $Id: lontable.pm,v 1.15 2010/11/18 17:12:14 raeburn Exp $ +# $Id: lontable.pm,v 1.22 2014/12/15 00:52:40 raeburn Exp $ # # # Copyright Michigan State University Board of Trustees @@ -108,7 +108,7 @@ modified by this. These configuration i my $table = lontable::new(\%config_hash) -=over3 +=over 3 =item alignment @@ -211,11 +211,10 @@ 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 @@ -248,14 +247,13 @@ The contents of the cell. =back -=back =cut sub new { my ($class, $configuration) = @_; - + if ($tracing) {&Apache::lonnet::logthis("new table"); } # Initialize the object member data with the default values # then override with any stuff in $configuration. @@ -267,11 +265,18 @@ sub new { theme => "plain", column_count => 0, row_open => 0, - rows => [], - col_widths => {} + rows => { + 'body' => [], + 'head' => [], + 'foot' => [] + }, + col_widths => {}, + part => 'body', # one of 'body', 'head', 'foot'. + colgroups => [] # Stores information about column groups. + }; - foreach my $key (keys %$configuration) { + foreach my $key (keys(%$configuration)) { $self->{$key} = $$configuration{$key}; } @@ -347,6 +352,22 @@ Set or get the presence of a request for 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. +Valid values for the parameter are: + +=over 2 + +=item 0 - no borders present. + +=item 1 - All borders (borders around all four sides of the cell. + +=item 2 - Border at top and bottom of the cell. + +=item 3 - Border at the left and right sides of the cell. + +=item 4 - Border around groups (colgroups as well as thead/tfoot/tbody). + + +=back =head3 Examples: @@ -416,11 +437,11 @@ sub theme { =pod -=head 2 width +=head2 width Gets and optionally sets the width of the table. -=head 3 Examples: +=head3 Examples: my $newwidth = $table->width("10cm"); # 10cm width returns "10cm". @@ -479,13 +500,13 @@ sub start_row { # Override the defaults if the config hash is present: if (defined($config)) { - foreach my $key (keys %$config) { + foreach my $key (keys(%$config)) { $row_hash->{$key} = $config->{$key}; } } - my $rows = $self->{'rows'}; + my $rows = $self->{'rows'}->{$self->{'part'}}; push(@$rows, $row_hash); $self->{"row_open"} = 1; # Row is now open and ready for business. @@ -511,8 +532,9 @@ sub end_row { # 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 $row = $self->{'rows'}->{$self->{'part'}}->[-1]; my $cells = $row->{'cells'}; if ($row->{'cell_width'} > $self->{'column_count'}) { @@ -560,8 +582,8 @@ sub configure_row { $self->start_row(); } - my $row = $self->{'rows'}[-1]; - foreach my $config_item (keys %$config) { + my $row = $self->{'rows'}->{$self->{'part'}}->[-1]; + foreach my $config_item (keys(%$config)) { $row->{$config_item} = $config->{$config_item}; } } @@ -615,6 +637,8 @@ Note as well that if width specification =back +=back + =cut sub add_cell { @@ -627,7 +651,7 @@ sub add_cell { if (!$self->{'row_open'}) { $self->start_row(); } - my $rows = $self->{'rows'}; + my $rows = $self->{'rows'}->{$self->{'part'}}; my $current_row = $rows->[-1]; my $current_cells = $current_row->{'cells'}; my $last_coord = $current_row->{'cell_width'}; @@ -740,14 +764,180 @@ sub append_cell_text { my ($this, $text) = @_; if($tracing) {&Apache::lonnet::logthis("append_cell_text: $text"); } - my $rows = $this->{'rows'}; + my $rows = $this->{'rows'}->{$this->{'part'}}; my $current_row = $rows->[-1]; my $cells = $current_row->{'cells'}; my $current_cell = $cells->[-1]; $current_cell->{'contents'} .= $text; } +#-------------------------- Support for row/column groups. ---- + +=pod + +=head2 start_head + +starts the table head. This corresponds to the tag in +html/xml. All rows defined in this group will be +collected and placed at the front of the table come rendering time. +Furthermore, if the table has group borders enabled, a rule will be +rendered following and preceding this group of rows. + +=cut + +sub start_head { + my ($this) = @_; + if ($tracing) { &Apache::lonnet::logthis("start_head"); } + $this->{'part'} = 'head'; +} + +=pod + +=head2 end_head + +Ends a table head. This corresponds to the + closing tag in html/xml. + +=cut + +sub end_head { + my ($this) = @_; + if ($tracing) { &Apache::lonnet::logthis("end_head"); } + $this->{'part'} = 'body'; +} + +=pod + +=head2 start_foot + +Starts the table footer. All of the rows generated in the footer will +be rendered at the bottom of the table. This sub corresponds to the + tag in html/xml. If the table has group borders enabled, a rule +will be rendered at the top and bottom of the set of columns in this +group + +=cut + +sub start_foot { + my ($this) = @_; + if ($tracing) { &Apache::lonnet::logthis("start_foot"); } + $this->{'part'} = 'foot'; +} + +=pod + +=head2 end_foot + +Ends the set of rows in the table footer. This corresponds to the + end tag in xml/html. + +=cut + +sub end_foot { + my ($this) = @_; + if ($tracing) { &Apache::lonnet::logthis("end_foot") } + $this->{'part'} = 'body'; +} + +=pod + +=head2 start_body + +Starts the set of rows that will be in the table body. Note that if +we are not in the header or footer, body rows are implied. +This correspondes to the presence of a tag in html/xml. +If group borders are on, a rule will be rendered at the top and bottom +of the body rows. + +=cut + +sub start_body { + my ($this) = @_; + if ($tracing) { &Apache::lonnet::logthis("start_body"); } + $this->{'part'} = 'body'; +} + +=pod + +=head2 end_body +Ends the set of rows in a table body. Note that in the event we are not +in the header or footer groups this code assumes we are in the body +group. I believe this is a good match to how mot browsers render. + +=cut + +sub end_body { + my ($this) = @_; + if ($tracing) { &Apache::lonnet::logthis("end_body"); } + +} + +=pod + +=head2 define_colgroup + +Define a column group a column group corresponds to the + tag in Html/Xml. A column group as we implement it has +the following properties tht will be shared amongst all cells in the +columns in the group unless overidden in the specific oell definition: + +=over 2 + +=item span + +The number of columns in the column group. This defaults to 1. + +=item halign + +Horizontal alignment of the cells. This defaults to left. +Other values are left, center, right (justify and char are +accepted but treated as left). + +=item valign + +Vertical alignment of the cells. This defaults to middle. +Other values are top middle, bottom, (baseline is accepted and +treated as top). + +=back + +If group borders are turned on, a rule will be rendered +at the left and right side of the column group. + +=head3 parameters + +=over 2 + +=item definition + +This is a hash that contains any of the keys described above that +define the column group. + +=back + + +=head3 Example + + $table->define_colgroup({ + 'span' => 2, + 'halign' => 'center' + }) + + + +=cut + +sub define_colgroup { + my ($this, $attributes) = @_; + if ($tracing) { &Apache::lonnet::logthis("col_group"); } + my $colgroups = $this->{'colgroups'}; + push(@$colgroups, $attributes); # Colgroups always add at end. + + +} + +#------------------------- Render the table --------------------- =pod @@ -760,6 +950,7 @@ a certain amount of testing to be done o The caller can then ask the table object to generate LaTeX. =cut + sub generate { my ($this) = @_; my $useP = 0; @@ -770,8 +961,16 @@ sub generate { if($tracing) {&Apache::lonnet::logthis("generate"); } my $table = Apache::lonlatextable->new(); + my $inner_border = $this->{'inner_border'}; + my $outer_border = $this->{'outer_border'}; + my $column_count = $this->{'column_count'}; - # Add the caption if supplied. + my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0; + my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0; + my $part_border = ($inner_border == 4); + + + # Add the caption if supplied. if ($this->{'caption'} ne "") { $table->set_caption($this->caption); @@ -797,7 +996,7 @@ sub generate { my $specified_width = 0.0; my $specified_cols = 0; - foreach my $col (keys %$colwidths) { + foreach my $col (keys(%$colwidths)) { $specified_width = $specified_width + $colwidths->{$col}; $specified_cols++; } @@ -827,28 +1026,139 @@ sub generate { $default_width = ($tablewidth/$unspecified_cols); # 'fair' default width. my $width_remaining = $tablewidth - $default_width*$unspecified_cols; # What's left for the specified cols. my $reduction = $tablewidth/$width_remaining; # Reduction fraction for specified cols - foreach my $col (keys %$colwidths) { + foreach my $col (keys(%$colwidths)) { $colwidths->{$col} = $colwidths->{$col}/$reduction; } } } + # If rule is groups. we need to have a + # list of the column numbers at which a column ends... + # and the coldef needs to start with a | + # + my @colgroup_ends; + my $colgroup_col = 0; + my $group = 0; + my $coldef = ""; + if ($outer_border || $cell_lr_border) { + $coldef .= '|'; + } + if ($part_border) { + $coldef .= '|'; + my $colgroup_col = 0; + my $colgroups = $this->{'colgroups'}; + foreach my $group (@$colgroups) { + if (defined $group->{'span'}) { + $colgroup_col += $group->{'span'}; + } else { + $colgroup_col++; + } + push(@colgroup_ends, $colgroup_col); + } + + } + $this->render_part('head', $table, $useP, $default_width, + \@colgroup_ends); + $this->render_part('body', $table, $useP, $default_width, + \@colgroup_ends); + $this->render_part('foot', $table, $useP, $default_width, + \@colgroup_ends); - + + + for (my $i =0; $i < $column_count; $i++) { + if ($useP) { + $coldef .= "p{$default_width $colunits}"; + } else { + $coldef .= 'l'; + } + if ($cell_lr_border || + ($outer_border && ($i == $column_count-1))) { + $coldef .= '|'; + } + if ($part_border && ($i == ($colgroup_ends[$group]-1))) { + $coldef .= '|'; + $group++; + } + } + $table->{'coldef'} = $coldef; + + # Return the table: + + if ($tracing) { &Apache::lonnet::logthis("Leaving generate"); } + + + return $table; + +} + + +#--------------------------------------------------------------------------- +# +# Private methods: +# + +# +# Convert size with units -> size in cm. +# The resulting size is floating point with no units so that it can be used in +# computation. Note that an illegal or missing unit is treated silently as +# cm for now. +# +sub size_to_cm { + my ($this, $size_spec) = @_; + my ($size, $units) = split(/ /, $size_spec); + if (lc($units) eq 'mm') { + return $size / 10.0; + } + if (lc($units) eq 'in') { + return $size * 2.54; + } + + return $size; # Default is cm. +} + +# +# Render a part of the table. The valid table parts are +# head, body and foot. These corresopnd to the set of rows +# define within , and +# respectively. +# +sub render_part { + my ($this, $part, $table, $useP, + $default_width, $colgroup_ends) = @_; + + if ($tracing) { &Apache::lonnet::logthis("render_part: $part") }; + + # Do nothing if that part of the table is empty: + + if ($this->{'rows'}->{$part} == undef) { + if ($tracing) {&Apache::lonnet::logthis("$part is empty"); } + return; + } + + my @cgends = @$colgroup_ends; # Build up the data: my @data; - my $rows = $this->{'rows'}; + my $colwidths = $this->{'col_widths'}; + my $rows = $this->{'rows'}->{$part}; my $row_count = scalar(@$rows); my $inner_border = $this->{'inner_border'}; my $outer_border = $this->{'outer_border'}; my $column_count = $this->{'column_count'}; + my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0; + my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0; + my $part_border = ($inner_border == 4); + my $colunits = 'cm'; # All units in cm. + # Add a top line if the outer or inner border is enabled: + # or if group rules are on. + # - if ($outer_border || $inner_border) { + if ($outer_border || $cell_ul_border || $part_border) { push(@data, ["\\cline{1-$column_count}"]); } @@ -861,11 +1171,13 @@ sub generate { my $startcol = 1; my @underlines; # Array of \cline cells if cellborder on. - + my $colgroup_count = @cgends; # Number of column groups. + my $cgroup = 0; # Group we are on. + my $cgstart = 0; # Where the next cgroup starts. 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. @@ -908,17 +1220,35 @@ sub generate { $contents = $embeddedAlignStart . $contents . $embeddedAlignEnd; } - if ($inner_border || ($outer_border && ($cell == 0))) { + if ($cell_lr_border || ($outer_border && ($cell == 0))) { $col_align = '|'.$col_align; } - if ($inner_border || ($outer_border && ($cell == ($cell_count -1)))) { + if ($cell_lr_border || ($outer_border && ($cell == ($cell_count -1)))) { $col_align = $col_align.'|'; } + if ($part_border) { + if ($cell == $cgstart) { + $col_align = '|' . $col_align; + if ($cgroup < $colgroup_count) { + $cgstart = $cgends[$cgroup]; + $cgroup++; + } else { + $cgstart = 1000000; # TODO: Get this logic right + } + if ($cell == ($cell_count - 1) && + ($cell == ($cgstart-1))) { + $col_align = $col_align . '|'; # last col ends colgrp. + } + } + } #factor in spans: my $cspan = $cells->[$cell]->{'colspan'}; my $nextcol = $startcol + $cspan; + + # At this point this col is the start of the span. + # nextcol is the end of the span. # If we can avoid the \multicolumn directive that's best as # that makes some things like \parpic invalid in LaTeX which @@ -935,18 +1265,19 @@ sub generate { $contents .= ' \\\\'; } } - if ($inner_border && ($cells->[$cell]->{'rowspan'} == 1)) { + if ($cell_ul_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) { + if ($cell_ul_border) { for (my $i =0; $i < scalar(@underlines); $i++) { push(@data, [$underlines[$i]]); } @@ -957,60 +1288,13 @@ sub generate { # Add bottom border if necessary: if the inner border was on, the loops above # will have done a bottom line under the last cell. # - if ($outer_border && !$inner_border) { + if (($outer_border || $part_border) && !$cell_ul_border) { push(@data, ["\\cline{1-$column_count}"]); } - $table->set_data(\@data); - - my $coldef = ""; - if ($outer_border || $inner_border) { - $coldef .= '|'; - } - for (my $i =0; $i < $column_count; $i++) { - if ($useP) { - $coldef .= "p{$default_width $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; - + $table->set_data(\@data); } -#--------------------------------------------------------------------------- -# -# Private methods: -# -# -# Convert size with units -> size in cm. -# The resulting size is floating point with no units so that it can be used in -# computation. Note that an illegal or missing unit is treated silently as -# cm for now. -# -sub size_to_cm { - my ($this, $size_spec) = @_; - my ($size, $units) = split(/ /, $size_spec); - if (lc($units) eq 'mm') { - return $size / 10.0; - } - if (lc($units) eq 'in') { - return $size * 2.54; - } - - return $size; # Default is cm. -} #---------------------------------------------------------------------------- # The following methods allow for testability. @@ -1025,7 +1309,7 @@ sub get_row { my ($self, $row) = @_; if ($tracing > 1) { &Apache::lonnet::logthis("get_row"); } - my $rows = $self->{'rows'}; # ref to an array.... + my $rows = $self->{'rows'}->{$self->{'part'}}; # ref to an array.... return $rows->[$row]; # ref to the row hash for the selected row. } @@ -1035,4 +1319,4 @@ BEGIN{ 1; __END__ - +