package LaTeX::Table; use strict; use warnings; use Moose; use Moose::Util::TypeConstraints; use MooseX::FollowPBP; use version; our $VERSION = qv('1.0.6'); use LaTeX::Table::Types::Std; use LaTeX::Table::Types::Xtab; use LaTeX::Table::Types::Ctable; use LaTeX::Table::Types::Longtable; use Carp; use Scalar::Util qw(reftype); use English qw( -no_match_vars ); use Module::Pluggable search_path => 'LaTeX::Table::Themes', sub_name => 'themes', except => 'LaTeX::Table::Themes::ThemeI', instantiate => 'new'; # Scalar options: # Str for my $attr ( qw(label maincaption shortcaption caption caption_top coldef custom_template width maxwidth width_environment custom_tabular_environment position tabletail star) ) { has $attr => ( is => 'rw', isa => 'Str', default => 0 ); } has 'filename' => ( is => 'rw', isa => 'Str', default => 'latextable.tex' ); has 'foottable' => ( is => 'rw', isa => 'Str', default => q{} ); has 'eor' => ( is => 'rw', isa => 'Str', default => q{\\\\} ); has 'environment' => ( is => 'rw', isa => 'Str', default => 1 ); has 'theme' => ( is => 'rw', isa => 'Str', default => 'Meyrin' ); has 'continuedmsg' => ( is => 'rw', isa => 'Str', default => '(continued)' ); has 'tabletailmsg' => ( is => 'rw', isa => 'Str', default => 'Continued on next page' ); has 'tableheadmsg' => ( is => 'rw', isa => 'Str', default => 'Continued from previous page' ); has 'tablelasttail' => ( is => 'rw', isa => 'Str', default => q{} ); # Num has 'xentrystretch' => ( is => 'rw', isa => 'Num', default => 0 ); # Bool for my $attr (qw(center left right _default_align continued sideways)) { has $attr => ( is => 'rw', isa => 'Bool', predicate => "has_$attr", clearer => "clear_$attr", ); } # enum has 'type' => ( is => 'rw', isa => enum( [qw( std ctable xtab longtable )] ), default => 'std', ); has 'fontfamily' => ( is => 'rw', isa => enum( [qw( 0 rm sf tt )] ), default => 0, ); has 'fontsize' => ( is => 'rw', isa => enum( [ qw(0 tiny scriptsize footnotesize small normal large Large LARGE huge Huge) ] ), default => 0, ); # Reference/Object options has 'coldef_strategy' => ( is => 'rw', isa => 'HashRef' ); has 'callback' => ( is => 'rw', isa => 'CodeRef' ); has 'resizebox' => ( is => 'rw', isa => 'ArrayRef[Str]' ); has 'columns_like_header' => ( is => 'rw', isa => 'ArrayRef[Int]' ); has 'header' => ( is => 'rw', isa => 'ArrayRef[ArrayRef[Value]]', default => sub { [] } ); has 'data' => ( is => 'rw', isa => 'ArrayRef[ArrayRef[Value]]', default => sub { [] } ); has 'predef_themes' => ( is => 'rw', isa => 'HashRef[HashRef]', default => sub { {} } ); has 'custom_themes' => ( is => 'rw', isa => 'HashRef[HashRef]', default => sub { {} } ); # private has '_data_summary' => ( is => 'rw', isa => 'ArrayRef[Str]' ); has '_type_obj' => ( is => 'rw', isa => 'LaTeX::Table::Types::TypeI' ); has '_RULE_TOP_ID' => ( is => 'ro', default => 0 ); has '_RULE_MID_ID' => ( is => 'ro', default => 1 ); has '_RULE_INNER_ID' => ( is => 'ro', default => 2 ); has '_RULE_BOTTOM_ID' => ( is => 'ro', default => 3 ); __PACKAGE__->meta->make_immutable; sub generate_string { my ( $self, @args ) = @_; # analyze the data $self->_calc_data_summary( $self->get_data ); my $type_obj_name = 'LaTeX::Table::Types::' . uc( substr $self->get_type, 0, 1 ) . substr $self->get_type, 1; $self->_set_type_obj( $type_obj_name->new( _table_obj => $self ) ); return $self->_get_type_obj->generate_latex_code(); } sub generate { my ( $self, $header, $data ) = @_; open my $LATEX, '>', $self->get_filename or $self->_ioerror( 'open', $OS_ERROR ); print {$LATEX} $self->generate_string( $header, $data ) or $self->_ioerror( 'write', $OS_ERROR ); close $LATEX or $self->_ioerror( 'close', $OS_ERROR ); return 1; } sub get_available_themes { my ($self) = @_; my %defs; for my $theme_obj ( $self->themes ) { %defs = ( %defs, %{ $theme_obj->_definition } ); } $self->set_predef_themes( \%defs ); return { ( %{ $self->get_predef_themes }, %{ $self->get_custom_themes } ) }; } sub _invalid_option_usage { my ( $self, $option, $msg ) = @_; croak "Invalid usage of option $option: $msg."; } sub _ioerror { my ( $self, $function, $error ) = @_; croak "IO error: Can't $function '" . $self->get_filename . "': $error"; } sub _default_coldef_strategy { my ($self) = @_; my $STRATEGY = { MISSING_VALUE => qr{\A \s* \z}xms, NUMBER => qr{\A\s*([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\s*\z}xms, NUMBER_MUST_MATCH_ALL => 1, LONG => qr{\A \s* (?=\w+\s+\w+).{29,}? \S}xms, LONG_MUST_MATCH_ALL => 0, NUMBER_COL => 'r', NUMBER_COL_X => 'r', LONG_COL => 'p{5cm}', LONG_COL_X => 'X', LONG_COL_Y => 'L', DEFAULT_COL => 'l', DEFAULT_COL_X => 'l', }; $self->set_coldef_strategy($STRATEGY); return $STRATEGY; } sub _get_coldef_types { my ($self) = @_; # everything that does not contain an underscore is a coltype my @coltypes = sort grep {m{ \A [^_]+ \z }xms} keys %{ $self->get_coldef_strategy }; return @coltypes; } sub _check_coldef_strategy { my ( $self, $strategy ) = @_; my $default = $self->_default_coldef_strategy; for my $key ( keys %{$default} ) { if ( !defined $strategy->{$key} ) { $strategy->{$key} = $default->{$key}; } } $self->set_coldef_strategy($strategy); my @coltypes = $self->_get_coldef_types(); for my $type (@coltypes) { if ( !defined $strategy->{"${type}_COL"} ) { $self->_invalid_option_usage( 'coldef_strategy', "Missing column attribute ${type}_COL for $type" ); } if ( !defined $strategy->{"${type}_MUST_MATCH_ALL"} ) { $strategy->{"${type}_MUST_MATCH_ALL"} = 1; } } return; } sub _extract_number_columns { my ( $self, $col ) = @_; my $def = $self->_get_mc_def($col); return defined $def->{cols} ? $def->{cols} : 1; } sub _row_is_latex_command { my ( $self, $row ) = @_; if ( scalar( @{$row} ) == 1 && $row->[0] =~ m{\A \s* \\ }xms ) { return 1; } return 0; } sub _calc_data_summary { my ( $self, $data ) = @_; my $max_col_number = 0; my $strategy = $self->get_coldef_strategy; if ( !$strategy ) { $strategy = $self->_default_coldef_strategy; } else { $self->_check_coldef_strategy($strategy); } my %matches; my %cells; my @coltypes = $self->_get_coldef_types(); ROW: for my $row ( @{$data} ) { if ( scalar @{$row} == 0 || $self->_row_is_latex_command($row) ) { next ROW; } if ( scalar @{$row} > $max_col_number ) { $max_col_number = scalar @{$row}; } my $i = 0; COL: for my $col ( @{$row} ) { next COL if $col =~ $strategy->{MISSING_VALUE}; for my $coltype (@coltypes) { if ( $col =~ $strategy->{$coltype} ) { $matches{$i}{$coltype}++; } } $cells{$i}++; $i += $self->_extract_number_columns($col); } } my @summary; for my $i ( 0 .. $max_col_number - 1 ) { my $type_of_this_col = 'DEFAULT'; for my $coltype (@coltypes) { if (defined $matches{$i}{$coltype} && ( !$strategy->{"${coltype}_MUST_MATCH_ALL"} || $cells{$i} == $matches{$i}{$coltype} ) ) { $type_of_this_col = $coltype; } } push @summary, $type_of_this_col; } $self->_set_data_summary( \@summary ); return; } sub _apply_callback_cell { my ( $self, $i, $j, $value, $is_header ) = @_; my $col_cb = $self->_get_mc_def($value); $col_cb->{value} = &{ $self->get_callback }( $i, $j, $col_cb->{value}, $is_header ); return $self->_get_mc_value($col_cb); } # formats the data/header as latex code sub _get_matrix_latex_code { my ( $self, $data_ref, $is_header ) = @_; my $theme = $self->get_theme_settings; my $i = 0; my $row_id = 0; my @code = $is_header ? ( $self->_get_hline_code( $self->_get_RULE_TOP_ID ) ) : (); ROW: for my $row ( @{$data_ref} ) { $i++; my @cols = @{$row}; # empty rows produce a horizontal line if ( !@cols ) { push @code, $self->_get_hline_code( $self->_get_RULE_INNER_ID, 1 ); next ROW; } # single column rows that start with a backslash are just # printed out if ( $self->_row_is_latex_command($row) ) { push @code, $cols[0] . "\n"; next ROW; } if ( $self->get_callback ) { my $k = 0; for my $col (@cols) { $col = $self->_apply_callback_cell( $row_id, $k, $col, $is_header ); $k += $self->_extract_number_columns($col); } } if ($is_header) { my $j = 0; for my $col (@cols) { $col = $self->_apply_header_formatting( $col, ( !defined $theme->{STUB_ALIGN} || $j > 0 ) ); $j += $self->_extract_number_columns($col); } } $row_id++; # now print the row LaTeX code my $bgcolor = $is_header ? $theme->{'HEADER_BG_COLOR'} : ( $row_id % 2 ) ? $theme->{'DATA_BG_COLOR_ODD'} : $theme->{'DATA_BG_COLOR_EVEN'}; push @code, $self->_get_row_array( \@cols, $bgcolor, $is_header ); next ROW if $is_header; # do we have to draw a horizontal line? if ( $i == scalar @{$data_ref} ) { push @code, $self->_get_hline_code( $self->_get_RULE_BOTTOM_ID ); } else { push @code, $self->_get_hline_code( $self->_get_RULE_INNER_ID ); } } # without header, just draw the topline, not this midline if ( $is_header && $i ) { push @code, $self->_get_hline_code( $self->_get_RULE_MID_ID ); } return $self->_align_code( \@code ); } sub _align_code { my ( $self, $code_ref ) = @_; my %max; for my $row ( @{$code_ref} ) { next if ( !defined reftype $row); for my $i ( 0 .. scalar( @{$row} ) - 1 ) { $row->[$i] =~ s{^\s+|\s+$}{}gxms; my $l = length $row->[$i]; if ( !defined $max{$i} || $max{$i} < $l ) { $max{$i} = $l; } } } my $code = q{}; ROW: for my $row ( @{$code_ref} ) { if ( !defined reftype $row) { $code .= $row; next ROW; } for my $i ( 0 .. scalar( @{$row} ) - 1 ) { $row->[$i] = sprintf '%-*s', $max{$i}, $row->[$i]; } $code .= join( ' & ', @{$row} ) . q{ } . $self->get_eor . "\n"; } return $code; } sub _get_hline_code { my ( $self, $id, $single ) = @_; my $theme = $self->get_theme_settings; my $hlines = $theme->{'HORIZONTAL_RULES'}; my $line = '\hline'; if ( defined $theme->{RULES_CMD} && reftype $theme->{RULES_CMD} eq 'ARRAY' ) { $line = $theme->{RULES_CMD}->[$id]; } if ( $id == $self->_get_RULE_BOTTOM_ID ) { $id = 0; } # just one line? if ( defined $single && $single ) { return "$line\n"; } return "$line\n" x $hlines->[$id]; } sub _apply_header_formatting { my ( $self, $col, $aligning ) = @_; my $theme = $self->get_theme_settings; if ( $aligning && defined $theme->{'HEADER_CENTERED'} && $theme->{'HEADER_CENTERED'} ) { $col = $self->_add_mc_def( { value => $col, align => 'c', cols => '1' } ); } if ( length $col ) { if ( defined $theme->{'HEADER_FONT_STYLE'} ) { $col = $self->_add_font_family( $col, $theme->{'HEADER_FONT_STYLE'} ); } if ( defined $theme->{'HEADER_FONT_COLOR'} ) { $col = $self->_add_font_color( $col, $theme->{'HEADER_FONT_COLOR'} ); } } return $col; } sub _get_cell_bg_color { my ( $self, $row_bg_color, $col_id ) = @_; my $cell_bg_color = $row_bg_color; if ( $self->get_columns_like_header ) { HEADER_COLUMN: for my $i ( @{ $self->get_columns_like_header } ) { if ( $i == $col_id ) { $cell_bg_color = $self->get_theme_settings->{'HEADER_BG_COLOR'}; last HEADER_COLUMN; } } } return $cell_bg_color; } sub _get_row_array { my ( $self, $cols_ref, $bgcolor, $is_header ) = @_; my @cols; my @cols_defs = map { $self->_get_mc_def($_) } @{$cols_ref}; my $vlines = $self->get_theme_settings->{'VERTICAL_RULES'}; my $v0 = q{|} x $vlines->[0]; my $v1 = q{|} x $vlines->[1]; my $v2 = q{|} x $vlines->[2]; my $j = 0; my $col_id = 0; for my $col_def (@cols_defs) { if ( !$is_header && $self->get_columns_like_header ) { HEADER_COLUMN: for my $i ( @{ $self->get_columns_like_header } ) { next HEADER_COLUMN if $i != $col_id; $col_def = $self->_get_mc_def( $self->_apply_header_formatting( $self->_get_mc_value($col_def), 0 ) ); if ( !defined $col_def->{cols} ) { my @summary = @{ $self->_get_data_summary() }; $col_def->{cols} = 1; $col_def->{align} = $self->get_coldef_strategy->{ $summary[$col_id] . $self->_get_coldef_type_col_suffix }; } } } if ( defined $col_def->{cols} ) { my $vl_pre = $j == 0 ? $v0 : q{}; my $vl_post = $j == $#cols_defs ? $v0 : $j == 0 && $col_def->{cols} == 1 ? $v1 : $v2; my $color_code = q{}; my $cell_bg_color = $self->_get_cell_bg_color( $bgcolor, $col_id ); if ( defined $cell_bg_color ) { $color_code = '>{\columncolor{' . $cell_bg_color . '}}'; } push @cols, '\\multicolumn{' . $col_def->{cols} . '}{' . $vl_pre . $color_code . $col_def->{align} . $vl_post . '}{' . $col_def->{value} . '}'; $col_id += $col_def->{cols}; } else { push @cols, $col_def->{value}; $col_id++; } $j++; } if ( defined $bgcolor ) { # @cols has always at least one element, otherwise we draw a line $cols[0] = "\\rowcolor{$bgcolor}" . $cols[0]; } return \@cols; } sub _add_mc_def { my ( $self, $arg_ref ) = @_; my $def = $self->_get_mc_def( $arg_ref->{value} ); return defined $def->{cols} ? $arg_ref->{value} : $self->_get_mc_value($arg_ref); } sub _get_mc_value { my ( $self, $def ) = @_; return defined $def->{cols} ? $def->{value} . q{:} . $def->{cols} . $def->{align} : $def->{value}; } sub _get_mc_def { my ( $self, $value ) = @_; return $value =~ m{ \A (.*)\:(\d+)([clr]) \s* \z }xms ? { value => $1, cols => $2, align => $3 } : { value => $value }; } sub _add_font_family { my ( $self, $col, $family ) = @_; my %know_families = ( tt => 1, bf => 1, it => 1, sc => 1 ); if ( !defined $know_families{$family} ) { $self->_invalid_option_usage( 'custom_themes', "Family not known: $family. Valid families are: " . join ', ', sort keys %know_families ); } my $col_def = $self->_get_mc_def($col); $col_def->{value} = "\\text$family" . '{' . $col_def->{value} . '}'; return $self->_get_mc_value($col_def); } sub _add_font_color { my ( $self, $col, $color ) = @_; my $col_def = $self->_get_mc_def($col); $col_def->{value} = "\\color{$color}" . $col_def->{value}; return $self->_get_mc_value($col_def); } sub _get_coldef_type_col_suffix { my ($self) = @_; if ( $self->get_width_environment eq 'tabularx' ) { return '_COL_X'; } elsif ( $self->get_width_environment eq 'tabulary' ) { return '_COL_Y'; } return '_COL'; } sub _get_coldef_code { my ( $self, $data ) = @_; my @cols = @{ $self->_get_data_summary() }; my $vlines = $self->get_theme_settings->{'VERTICAL_RULES'}; my $v0 = q{|} x $vlines->[0]; my $v1 = q{|} x $vlines->[1]; my $v2 = q{|} x $vlines->[2]; my $table_def = q{}; my $i = 0; my $strategy = $self->get_coldef_strategy(); my $typesuffix = $self->_get_coldef_type_col_suffix(); my @attributes = grep {m{ _COL }xms} keys %{$strategy}; for my $col (@cols) { # align text right, numbers left, first col always left my $align; for my $attribute ( sort @attributes ) { if ( $attribute =~ m{ \A $col $typesuffix \z }xms ) { $align = $strategy->{$attribute}; # for _X and _Y, use default if no special defs are found } elsif ( ( $typesuffix eq '_COL_X' || $typesuffix eq '_COL_Y' ) && $attribute =~ m{ \A $col _COL \z }xms ) { $align = $strategy->{$attribute}; } } if ( $i == 0 ) { if ( defined $self->get_theme_settings->{'STUB_ALIGN'} ) { $align = $self->get_theme_settings->{'STUB_ALIGN'}; } $table_def .= $v0 . $align . $v1; } elsif ( $i == ( scalar(@cols) - 1 ) ) { $table_def .= $align . $v0; } else { $table_def .= $align . $v2; } $i++; if ( $i == 1 && $self->get_width && !$self->get_width_environment ) { $table_def .= '@{\extracolsep{\fill}}'; } } return $table_def; } sub get_theme_settings { my ($self) = @_; my $themes = $self->get_available_themes(); if ( defined $themes->{ $self->get_theme } ) { return $themes->{ $self->get_theme }; } $self->_invalid_option_usage( 'theme', 'Not known: ' . $self->get_theme ); return; } no Moose::Util::TypeConstraints; no Moose; 1; # Magic true value required at end of module __END__ =head1 NAME LaTeX::Table - Perl extension for the automatic generation of LaTeX tables. =head1 VERSION This document describes LaTeX::Table version 1.0.6 =head1 SYNOPSIS use LaTeX::Table; use Number::Format qw(:subs); # use mighty CPAN to format values my $header = [ [ 'Item:2c', '' ], [ '\cmidrule(r){1-2}' ], [ 'Animal', 'Description', 'Price' ], ]; my $data = [ [ 'Gnat', 'per gram', '13.65' ], [ '', 'each', '0.0173' ], [ 'Gnu', 'stuffed', '92.59' ], [ 'Emu', 'stuffed', '33.33' ], [ 'Armadillo', 'frozen', '8.99' ], ]; my $table = LaTeX::Table->new( { filename => 'prices.tex', maincaption => 'Price List', caption => 'Try our special offer today!', label => 'table:prices', position => 'tbp', header => $header, data => $data, } ); # write LaTeX code in prices.tex $table->generate(); # callback functions help you to format values easily (as # a great alternative to LaTeX packages like rccol) # # Here, the first colum and the header is printed in upper # case and the third colum is formatted with format_price() $table->set_callback(sub { my ($row, $col, $value, $is_header ) = @_; if ($col == 0 || $is_header) { $value = uc $value; } elsif ($col == 2 && !$is_header) { $value = format_price($value, 2, ''); } return $value; }); print $table->generate_string(); Now in your LaTeX document: \documentclass{article} % for multi-page tables (xtab or longtable) \usepackage{xtab} %\usepackage{longtable} % for publication quality tables (Meyrin theme, the default) \usepackage{booktabs} % for the NYC theme \usepackage{array} \usepackage{colortbl} \usepackage{xcolor} \begin{document} \input{prices} \end{document} =head1 DESCRIPTION LaTeX makes professional typesetting easy. Unfortunately, this is not entirely true for tables and the standard LaTeX table macros have a rather limited functionality. This module supports many CTAN packages and hides the complexity of using them behind an easy and intuitive API. =head1 FEATURES This module supports multi-page tables via the C or the C package. For publication quality tables, it uses the C package. It also supports the C and C packages for nicer fixed-width tables. Furthermore, it supports the C package for colored tables optimized for presentations. The powerful new C package is supported and especially recommended when footnotes are needed. C ships with some predefined, good looking L<"THEMES">. The program I makes it possible to use this module from within a text editor. =head1 INTERFACE =over =item Cnew($arg_ref)> Constructs a C object. The parameter is an hash reference with options (see below). =item C<$table-Egenerate()> Generates the LaTeX table code. The generated LaTeX table can be included in a LaTeX document with the C<\input> command: % include prices.tex, generated by LaTeX::Table \input{prices} =item C<$table-Egenerate_string()> Same as generate() but instead of creating a LaTeX file, this returns the LaTeX code as string. my $latexcode = $table->generate_string(); =item C<$table-Eget_available_themes()> Returns an hash reference to all available themes. See L<"THEMES"> for details. for my $theme ( keys %{ $table->get_available_themes } ) { ... } =item C<$table-Esearch_path( add =E "MyThemes" );> C will search under the C namespace for themes. You can add here an additional search path. Inherited from L. =back =head1 OPTIONS Options can be defined in the constructor hash reference or with the setter C. Additionally, getters of the form C are created. =head2 BASIC OPTIONS =over =item C The name of the LaTeX output file. Default is 'latextable.tex'. =item C Can be 'std' (default) for standard LaTeX tables, 'ctable' for tables using the C package or 'xtab' and 'longtable' for multi-page tables (requires the C and C LaTeX packages, respectively). =item C
The header. It is a reference to an array (the rows) of array references (the columns). $table->set_header([ [ 'Animal', 'Price' ] ]); will produce following header: +--------+-------+ | Animal | Price | +--------+-------+ Here an example for a multirow header: $table->set_header([ [ 'Animal', 'Price' ], ['', '(roughly)' ] ]); This code will produce this header: +--------+-----------+ | Animal | Price | | | (roughly) | +--------+-----------+ Single column rows that start with a backslash are treated as LaTeX commands and are not further formatted. So, my $header = [ [ 'Item:2c', '' ], ['\cmidrule{1-2}'], [ 'Animal', 'Description', 'Price' ] ]; will produce following LaTeX code in the Zurich theme: \multicolumn{2}{c}{\textbf{Item}} & \\ \cmidrule{1-2} \textbf{Animal} & \multicolumn{1}{c}{\textbf{Description}} & \multicolumn{1}{c}{\textbf{Price}}\\ Note that there is no C<\multicolumn>, C<\textbf> or C<\\> added to the second row. =item C The data. Once again a reference to an array (rows) of array references (columns). $table->set_data([ [ 'Gnu', '92.59' ], [ 'Emu', '33.33' ] ]); And you will get a table like this: +-------+---------+ | Gnu | 92.59 | | Emu | 33.33 | +-------+---------+ An empty column array will produce a horizontal rule (line): $table->set_data([ [ 'Gnu', '92.59' ], [], [ 'Emu', '33.33' ] ]); Now you will get such a table: +-------+---------+ | Gnu | 92.59 | +-------+---------+ | Emu | 33.33 | +-------+---------+ This works also in C
. Single column rows starting with a backslash are again printed without any formatting. So, $table->set_data([ [ 'Gnu', '92.59' ], ['\hline'], [ 'Emu', '33.33' ] ]); is equivalent to the example above (except that there always the correct rule command is used, i.e. C<\midrule> vs. C<\hline>). =item C The table types listed above use the L