JezK
Edit File: AlignmentAtAmpersand.pm
package LatexIndent::AlignmentAtAmpersand; # This program 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 3 of the License, or # (at your option) any later version. # # This program 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. # # See http://www.gnu.org/licenses/. # # Chris Hughes, 2017 # # For all communication, please visit: https://github.com/cmhughes/latexindent.pl use strict; use warnings; use Data::Dumper; use Exporter qw/import/; use List::Util qw/max min sum/; use LatexIndent::TrailingComments qw/$trailingCommentRegExp/; use LatexIndent::Switches qw/$is_t_switch_active $is_tt_switch_active %switches/; use LatexIndent::GetYamlSettings qw/%mainSettings/; use LatexIndent::Tokens qw/%tokens/; use LatexIndent::LogFile qw/$logger/; use LatexIndent::HiddenChildren qw/%familyTree/; use LatexIndent::Verbatim qw/%verbatimStorage/; our @ISA = "LatexIndent::Document"; # class inheritance, Programming Perl, pg 321 our @EXPORT_OK = qw/align_at_ampersand find_aligned_block double_back_slash_else main_formatting individual_padding multicolumn_padding multicolumn_pre_check multicolumn_post_check dont_measure hidden_child_cell_row_width hidden_child_row_width get_column_width/; our $alignmentBlockCounter; our @cellStorage; # two-dimensional storage array containing the cell information our @formattedBody; # array for the new body our @minMultiColSpan; our @maxColumnWidth; our @maxDelimiterWidth; sub find_aligned_block { my $self = shift; return unless ( ${$self}{body} =~ m/(?!<\\)%\*\h*\\begin\{/s ); # aligned block # %* \begin{tabular} # 1 & 2 & 3 & 4 \\ # 5 & & 6 & \\ # %* \end{tabular} $logger->trace('*Searching for ALIGNED blocks marked by comments') if ($is_t_switch_active); $logger->trace( Dumper( \%{ $mainSettings{lookForAlignDelims} } ) ) if ($is_tt_switch_active); while ( my ( $alignmentBlock, $yesno ) = each %{ $mainSettings{lookForAlignDelims} } ) { if ( ref $yesno eq "HASH" ) { $yesno = ( defined ${$yesno}{delims} ) ? ${$yesno}{delims} : 1; } if ($yesno) { $logger->trace("looking for %*\\begin\{$alignmentBlock\} environments"); my $alignmentRegExp = qr/ ( (?!<\\) % \* \h* # possible horizontal spaces \\begin\{ ($alignmentBlock) # environment name captured into $2 \} # \begin{alignmentBlock} statement captured into $1 ) ( .*? # non-greedy match (body) into $3 ) \R # a line break \h* # possible horizontal spaces ( (?!<\\) %\* # % \h* # possible horizontal spaces \\end\{\2\} # \end{alignmentBlock} statement captured into $4 ) /sx; while ( ${$self}{body} =~ m/$alignmentRegExp/sx ) { ${$self}{body} =~ s/ $alignmentRegExp / # create a new Environment object my $alignmentBlockObj = LatexIndent::AlignmentAtAmpersand->new( begin=>$1, body=>$3, end=>$4, name=>$2, modifyLineBreaksYamlName=>"environments", linebreaksAtEnd=>{ begin=>1, body=>1, end=>0, }, ); # log file output $logger->trace("*Alignment block found: %*\\begin\{${$alignmentBlock}{name}\}") if $is_t_switch_active; # the settings and storage of most objects has a lot in common $self->get_settings_and_store_new_object($alignmentBlockObj); ${@{${$self}{children}}[-1]}{replacementText}; /xseg; } } else { $logger->trace("*not* looking for $alignmentBlock as $alignmentBlock:$yesno"); } } return; } sub yaml_modify_line_breaks_settings { return; } sub tasks_particular_to_each_object { return; } sub create_unique_id { my $self = shift; $alignmentBlockCounter++; ${$self}{id} = "$tokens{alignmentBlock}$alignmentBlockCounter"; return; } sub align_at_ampersand { my $self = shift; return if ( ${$self}{bodyLineBreaks} == 0 ); # some blocks may contain verbatim to be measured ${$self}{measureVerbatim} = ( ${$self}{body} =~ m/$tokens{verbatim}/ ? 1 : 0 ); my $maximumNumberOfAmpersands = 0; # clear the global arrays @formattedBody = (); @cellStorage = (); @minMultiColSpan = (); @maxColumnWidth = (); @maxDelimiterWidth = (); # maximum column widths my @maximumColumnWidths; my $rowCounter = -1; my $columnCounter = -1; $logger->trace("*dontMeasure routine, row mode") if ( ${$self}{dontMeasure} and $is_t_switch_active ); # initial loop for column storage and measuring # initial loop for column storage and measuring # initial loop for column storage and measuring foreach ( split( "\n", ${$self}{body} ) ) { $rowCounter++; # default is to measure this row, but it can be switched off by the dont_measure routine ${$self}{measureRow} = 1; # call the dont_measure routine $self->dont_measure( mode => "row", row => $_ ) if ${$self}{dontMeasure}; # remove \\ and anything following it my $endPiece = q(); if ( $_ =~ m/(\\\\.*)/ ) { if ( ${$self}{alignFinalDoubleBackSlash} ) { # for example, if we want: # # Name & \shortstack{Hi \\ Lo} \\ <!--- Note this row! # Foo & Bar \\ # # in the first row, note that the first \\ # needs to be ignored, and we align by # the final double back slash $_ =~ s/(\\\\ (?: (?! (?:\\\\) ). # any character, but not \\ )*?$ # non-greedy )//sx; $endPiece = $1; } else { $_ =~ s/(\\\\.*)//; $endPiece = $1; } } # remove any trailing comments my $trailingComments; if ( $_ =~ m/$trailingCommentRegExp/ ) { $_ =~ s/($trailingCommentRegExp)//; $trailingComments = $1; } # some rows shouldn't be formatted my $unformattedRow = $_; # delimiters regex, default is: # # (?<!\\)& # # which is set in GetYamlSettings.pm, but can be set # by the user using, for example # # lookForAlignDelims: # tabular: # delimiter: '\<' # my $delimiterRegEx = qr/${$self}{delimiterRegEx}/; my $numberOfAmpersands = () = $_ =~ /$delimiterRegEx/g; $maximumNumberOfAmpersands = $numberOfAmpersands if ( $numberOfAmpersands > $maximumNumberOfAmpersands ); # remove space at the beginning of a row, surrounding &, and at the end of the row $_ =~ s/(?<!\\)\h*($delimiterRegEx)\h*/$1/g; $_ =~ s/^\h*//g; $_ =~ s/\h*$//g; # if the line finishes with an &, then add an empty space, # otherwise the column count is off $_ .= ( $_ =~ m/$delimiterRegEx$/ ? " " : q() ); # store the columns, which are either split by & # or otherwise simply the current line, if for example, the current line simply # contains \multicolumn{8}... \\ (see test-cases/texexchange/366841-zarko.tex, for example) my @columns = ( $_ =~ m/$delimiterRegEx/ ? split( /($delimiterRegEx)/, $_ ) : $_ ); $columnCounter = -1; my $spanning = 0; foreach my $column (@columns) { # if a column contains only the delimiter, then we need to # - measure it # - add it and its length to the previous cell # - remove it from the columns array # if ( $column =~ m/$delimiterRegEx/ ) { # update the delimiter to be used, and its associated length # for the *previous* cell my $spanningOffSet = ( $spanning > 0 ? $spanning - 1 : 0 ); ${ $cellStorage[$rowCounter][ $columnCounter - $spanningOffSet ] }{delimiter} = $1; ${ $cellStorage[$rowCounter][ $columnCounter - $spanningOffSet ] }{delimiterLength} = &get_column_width($1); # keep track of maximum delimiter width $maxDelimiterWidth[ $columnCounter - $spanningOffSet ] = ( defined $maxDelimiterWidth[ $columnCounter - $spanningOffSet ] ? max( $maxDelimiterWidth[ $columnCounter - $spanningOffSet ], ${ $cellStorage[$rowCounter][ $columnCounter - $spanningOffSet ] }{delimiterLength} ) : ${ $cellStorage[$rowCounter][ $columnCounter - $spanningOffSet ] }{delimiterLength} ); # importantly, move on to the next column! next; } # otherwise increment the column counter, and proceed $columnCounter++; # reset spanning (only applicable if multiColumnGrouping) $spanning = 0; # if a column has finished with a \ then we need to add a trailing space, # otherwise the \ can be put next to &. See test-cases/texexchange/112343-gonzalo for example $column .= ( $column =~ m/\\$/ ? " " : q() ); # basic cell storage $cellStorage[$rowCounter][$columnCounter] = ( { width => &get_column_width($column), entry => $column, type => ( $numberOfAmpersands > 0 ? "X" : "*" ), groupPadding => 0, colSpan => ".", delimiter => "", delimiterLength => 0, measureThis => ( $numberOfAmpersands > 0 ? ${$self}{measureRow} : 0 ) } ); # possible hidden children, see https://github.com/cmhughes/latexindent.pl/issues/85 if ( ( ${$self}{measureHiddenChildren} or ${$self}{measureVerbatim} ) and $column =~ m/.*?$tokens{beginOfToken}/s ) { $self->hidden_child_cell_row_width( $column, $rowCounter, $columnCounter ); } # store the maximum column width $maxColumnWidth[$columnCounter] = ( defined $maxColumnWidth[$columnCounter] ? max( $maxColumnWidth[$columnCounter], ${ $cellStorage[$rowCounter][$columnCounter] }{width} ) : ${ $cellStorage[$rowCounter][$columnCounter] }{width} ) if ${ $cellStorage[$rowCounter][$columnCounter] }{type} eq "X"; # \multicolumn cell if ( ${$self}{multiColumnGrouping} and $column =~ m/\\multicolumn\{(\d+)\}/ and $1 > 1 ) { $spanning = $1; # adjust the type ${ $cellStorage[$rowCounter][$columnCounter] }{type} = "$spanning"; # some \multicol cells can have their spanning information removed from type # so we store it in colSpan as well ${ $cellStorage[$rowCounter][$columnCounter] }{colSpan} = $spanning; # and don't measure it ${ $cellStorage[$rowCounter][$columnCounter] }{measureThis} = 0; # create 'gap' columns for ( my $j = $columnCounter + 1; $j <= $columnCounter + ( $spanning - 1 ); $j++ ) { $cellStorage[$rowCounter][$j] = ( { type => "-", entry => '', width => 0, individualPadding => 0, groupPadding => 0, colSpan => ".", delimiter => "", delimiterLength => 0, measureThis => 0 } ); } # store the minimum spanning value $minMultiColSpan[$columnCounter] = ( defined $minMultiColSpan[$columnCounter] ? min( $minMultiColSpan[$columnCounter], $spanning ) : $spanning ); # adjust the column counter $columnCounter += $spanning - 1; } } # store the information push( @formattedBody, { row => $_, endPiece => $endPiece, trailingComment => $trailingComments, numberOfAmpersands => $numberOfAmpersands, unformattedRow => $unformattedRow } ); } # store the maximum number of ampersands ${$self}{maximumNumberOfAmpersands} = $maximumNumberOfAmpersands; # blocks with nested multicolumns need some pre checking $self->multicolumn_pre_check if ${$self}{multiColumnGrouping}; # maximum column width loop, and individual padding $self->individual_padding; # multi column rearrangement and multicolumn padding $self->multicolumn_padding if ${$self}{multiColumnGrouping}; # multi column post check to ensure that multicolumn commands have received appropriate padding $self->multicolumn_post_check if ${$self}{multiColumnGrouping}; # output to log file if ($is_t_switch_active) { &pretty_print_cell_info($_) for ( "entry", "type", "colSpan", "width", "measureThis", "maximumColumnWidth", "individualPadding", "groupPadding", "delimiter", "delimiterLength" ); } # main formatting loop $self->main_formatting; # final \\ loop # final \\ loop # final \\ loop foreach (@formattedBody) { # reset the padding my $padding = q(); # possibly adjust the padding if ( ${$_}{row} !~ m/^\h*$/ ) { # remove trailing horizontal space if ${$self}{alignDoubleBackSlash} is set to 0 ${$_}{row} =~ s/\h*$// if ( !${$self}{alignDoubleBackSlash} ); # format spacing infront of \\ if ( defined ${$self}{spacesBeforeDoubleBackSlash} and ${$self}{spacesBeforeDoubleBackSlash} < 0 and !${$self}{alignDoubleBackSlash} ) { # zero spaces (possibly resulting in un-aligned \\) $padding = q(); } elsif ( defined ${$self}{spacesBeforeDoubleBackSlash} and ${$self}{spacesBeforeDoubleBackSlash} >= 0 and !${$self}{alignDoubleBackSlash} ) { # specified number of spaces (possibly resulting in un-aligned \\) $padding = " " x ( ${$self}{spacesBeforeDoubleBackSlash} ); } else { # aligned \\ $padding = " " x max( 0, ( ${$self}{maximumRowWidth} - ${$_}{rowWidth} ) ); } } # format the row, and put the trailing \\ and trailing comments back into the row ${$_}{row} .= $padding . ( ${$_}{endPiece} ? ${$_}{endPiece} : q() ) . ( ${$_}{trailingComment} ? ${$_}{trailingComment} : q() ); # some rows shouldn't be formatted, and may only have trailing comments; # see test-cases/alignment/table4.tex for example if (( ${$_}{numberOfAmpersands} == 0 and !${$_}{endPiece} ) or ( ${$_}{numberOfAmpersands} < ${$self}{maximumNumberOfAmpersands} and !${$self}{alignRowsWithoutMaxDelims} and !${$_}{endPiece} ) ) { ${$_}{row} = ( ${$_}{unformattedRow} ne "" ? ${$_}{unformattedRow} : q() ) . ( ${$_}{trailingComment} ? ${$_}{trailingComment} : q() ); } # spaces for leadingBlankColumn in operation if ( ${$self}{leadingBlankColumn} > -1 ) { $padding = " " x ( ${$self}{leadingBlankColumn} ); ${$_}{row} =~ s/^\h*/$padding/s; } } # delete the original body ${$self}{body} = q(); # update the body ${$self}{body} .= ${$_}{row} . "\n" for @formattedBody; # if the \end{} statement didn't originally have a line break before it, we need to remove the final # line break added by the above ${$self}{body} =~ s/\h*\R$//s if !${$self}{linebreaksAtEnd}{body}; # if the \begin{} statement doesn't finish with a line break, then we adjust the indentation # to be the length of the begin statement. # # example: # # \begin{align*}1&2\\ # 3&4\\ # 5 & 6 # \end{align*} # # goes to # # \begin{align*}1 & 2 \\ # 3 & 4 \\ # 5 & 6 # \end{align*} # # see https://github.com/cmhughes/latexindent.pl/issues/223 for example if ( !${ ${$self}{linebreaksAtEnd} }{begin} and ${ $cellStorage[0][0] }{type} eq "X" and ${ $cellStorage[0][0] }{measureThis} ) { my $lengthOfBegin = ${$self}{begin}; if ( ( ${$self}{begin} eq '{' | ${$self}{begin} eq '[' ) and ${$self}{parentBegin} ) { $lengthOfBegin = ${$self}{parentBegin} . "{"; } ${$self}{indentation} = " " x ( &get_column_width($lengthOfBegin) ); $logger->trace("Adjusting indentation of ${$self}{name} in AlignAtAmpersand routine") if ($is_t_switch_active); } } sub main_formatting { # PURPOSE: # (1) perform the *padding* operations # by adding # # <spacesBeforeAmpersand> # & # <spacesEndAmpersand> # # to the cell entries, accounting for # the justification being LEFT or RIGHT # # (2) measure the row width and store # the maximum row width for use with # the (possible) alignment of the \\ # my $self = shift; ${$self}{maximumRowWidth} = 0; # objective (1): padding # objective (1): padding # objective (1): padding $logger->trace("*formatted rows for: ${$self}{name}") if ($is_t_switch_active); my $rowCount = -1; # row loop foreach my $row (@cellStorage) { $rowCount++; # clear the temporary row my $tmpRow = q(); # column loop foreach my $cell (@$row) { if ( ${$cell}{type} eq "*" or ${$cell}{type} eq "-" ) { $tmpRow .= ${$cell}{entry}; next; } # the placement of the padding is dependent on the value of justification if ( ${$self}{justification} eq "left" ) { # LEFT: # LEFT: # LEFT: # <cell entry> <individual padding> <group padding> ... $tmpRow .= ${$cell}{entry}; $tmpRow .= " " x ${$cell}{individualPadding}; $tmpRow .= " " x ${$cell}{groupPadding}; } else { # RIGHT: # RIGHT: # RIGHT: # <group padding> <individual padding> <cell entry> ... $tmpRow .= " " x ${$cell}{groupPadding}; $tmpRow .= " " x ${$cell}{individualPadding}; $tmpRow .= ${$cell}{entry}; } # either way, finish with: <spacesBeforeAmpersand> & <spacesAfterAmpersand> $tmpRow .= " " x ${$self}{spacesBeforeAmpersand}; $tmpRow .= ${$cell}{delimiter}; $tmpRow .= " " x ${$self}{spacesAfterAmpersand}; } # if alignRowsWithoutMaxDelims = 0 # and there are *less than* the maximum number of ampersands, then # we undo all of the work above! if ( !${$self}{alignRowsWithoutMaxDelims} and ${ $formattedBody[$rowCount] }{numberOfAmpersands} < ${$self}{maximumNumberOfAmpersands} ) { $tmpRow = ${ $formattedBody[$rowCount] }{unformattedRow}; } # spacing before \\ my $finalSpacing = q(); $finalSpacing = " " x ( ${$self}{spacesBeforeDoubleBackSlash} ) if ${$self}{spacesBeforeDoubleBackSlash} >= 1; $tmpRow =~ s/\h*$/$finalSpacing/; # if $tmpRow is made up of only horizontal space, then empty it $tmpRow = q() if ( $tmpRow =~ m/^\h*$/ ); # to the log file $logger->trace($tmpRow) if ($is_t_switch_active); # store this formatted row ${ $formattedBody[$rowCount] }{row} = $tmpRow; # objective (2): calculate row width and update maximumRowWidth # objective (2): calculate row width and update maximumRowWidth # objective (2): calculate row width and update maximumRowWidth my $rowWidth = &get_column_width($tmpRow); # possibly update rowWidth if there are hidden children; see test-cases/alignment/hidden-child1.tex and friends $rowWidth = $self->hidden_child_row_width( $tmpRow, $rowCount, $rowWidth ) if ( ${$self}{measureHiddenChildren} or ${$self}{measureVerbatim} ); ${ $formattedBody[$rowCount] }{rowWidth} = $rowWidth; # update the maximum row width if ( $rowWidth > ${$self}{maximumRowWidth} and !( ${ $formattedBody[$rowCount] }{numberOfAmpersands} == 0 and !${ $formattedBody[$rowCount] }{endPiece} ) ) { ${$self}{maximumRowWidth} = $rowWidth; } } # log file information if ( $is_tt_switch_active and ${$self}{measureHiddenChildren} ) { $logger->info('*FamilyTree after align for ampersand'); $logger->trace( Dumper( \%familyTree ) ) if ($is_tt_switch_active); $rowCount = -1; # row loop foreach my $row (@cellStorage) { $rowCount++; $logger->trace("row $rowCount row width: ${$formattedBody[$rowCount]}{rowWidth}"); } } } sub dont_measure { my $self = shift; my %input = @_; if ( $input{mode} eq "cell" and ref( \${$self}{dontMeasure} ) eq "SCALAR" and ${$self}{dontMeasure} eq "largest" and ${ $cellStorage[ $input{row} ][ $input{column} ] }{width} == $maxColumnWidth[ $input{column} ] ) { # dontMeasure stored as largest, for example # # lookForAlignDelims: # tabular: # dontMeasure: largest $logger->trace( "CELL FOUND with maximum column width, $maxColumnWidth[$input{column}], and will not be measured (largest mode)" ) if ($is_t_switch_active); $logger->trace( "column: ", $input{column}, " width: ", ${ $cellStorage[ $input{row} ][ $input{column} ] }{width} ) if ($is_t_switch_active); $logger->trace( "entry: ", ${ $cellStorage[ $input{row} ][ $input{column} ] }{entry} ) if ($is_t_switch_active); $logger->trace("--------------------------") if ($is_t_switch_active); ${ $cellStorage[ $input{row} ][ $input{column} ] }{measureThis} = 0; ${ $cellStorage[ $input{row} ][ $input{column} ] }{type} = "X"; } elsif ( $input{mode} eq "cell" and ( ref( ${$self}{dontMeasure} ) eq "ARRAY" ) ) { # loop through the entries in dontMeasure foreach ( @{ ${$self}{dontMeasure} } ) { if ( ref( \$_ ) eq "SCALAR" and ${ $cellStorage[ $input{row} ][ $input{column} ] }{entry} eq $_ ) { # dontMeasure stored as *strings*, for example: # # lookForAlignDelims: # tabular: # dontMeasure: # - \multicolumn{1}{c}{Expiry} # - Tenor # - \multicolumn{1}{c}{$\Delta_{\text{call},10}$} $logger->trace("CELL FOUND (this): $_ and will not be measured") if ($is_t_switch_active); ${ $cellStorage[ $input{row} ][ $input{column} ] }{measureThis} = 0; ${ $cellStorage[ $input{row} ][ $input{column} ] }{type} = "X"; } elsif ( ref($_) eq "HASH" and ${$_}{this} and ${ $cellStorage[ $input{row} ][ $input{column} ] }{entry} eq ${$_}{this} ) { # for example: # # lookForAlignDelims: # tabular: # dontMeasure: # - # this: \multicolumn{1}{c}{Expiry} # applyTo: cell # # OR (note that applyTo is optional): # # lookForAlignDelims: # tabular: # dontMeasure: # - # this: \multicolumn{1}{c}{Expiry} next if ( defined ${$_}{applyTo} and !${$_}{applyTo} eq "cell" ); $logger->trace("CELL FOUND (this): ${$_}{this} and will not be measured") if ($is_t_switch_active); ${ $cellStorage[ $input{row} ][ $input{column} ] }{measureThis} = 0; ${ $cellStorage[ $input{row} ][ $input{column} ] }{type} = "X"; } elsif ( ref($_) eq "HASH" and ${$_}{regex} ) { # for example: # # lookForAlignDelims: # tabular: # dontMeasure: # - # regex: \multicolumn{1}{c}{Expiry} # applyTo: cell # # OR (note that applyTo is optional): # # lookForAlignDelims: # tabular: # dontMeasure: # - # regex: \multicolumn{1}{c}{Expiry} next if ( defined ${$_}{applyTo} and !${$_}{applyTo} eq "cell" ); my $regex = qr/${$_}{regex}/; next unless ${ $cellStorage[ $input{row} ][ $input{column} ] }{entry} =~ m/${$_}{regex}/; $logger->trace("CELL FOUND (regex): ${$_}{regex} and will not be measured") if ($is_t_switch_active); ${ $cellStorage[ $input{row} ][ $input{column} ] }{measureThis} = 0; ${ $cellStorage[ $input{row} ][ $input{column} ] }{type} = "X"; } } } elsif ( $input{mode} eq "row" and ( ref( ${$self}{dontMeasure} ) eq "ARRAY" ) ) { foreach ( @{ ${$self}{dontMeasure} } ) { # move on, unless we have specified applyTo as row: # # lookForAlignDelims: # tabular: # dontMeasure: # - # this: \multicolumn{1}{c}{Expiry} # applyTo: row # # note: *default value* of applyTo is cell next unless ( ref($_) eq "HASH" and defined ${$_}{applyTo} and ${$_}{applyTo} eq "row" ); if ( ${$_}{this} and $input{row} eq ${$_}{this} ) { $logger->trace("ROW FOUND (this): ${$_}{this}") if ($is_t_switch_active); $logger->trace("and will not be measured") if ($is_t_switch_active); ${$self}{measureRow} = 0; } elsif ( ${$_}{regex} and $input{row} =~ ${$_}{regex} ) { $logger->trace("ROW FOUND (regex): ${$_}{regex}") if ($is_t_switch_active); $logger->trace("and will not be measured") if ($is_t_switch_active); ${$self}{measureRow} = 0; } } } } sub individual_padding { # PURPOSE # (1) the *primary* purpose of this routine is to # measure the *individual padding* of # each cell. # # for example, for # # 111 & 2 & 33333 \\ # 4 & 55 & 66\\ # # then the individual padding will be # # 0 1 0 # 2 0 3 # # this is calculated by looping # through the rows & columns # and finding the maximum column widths # # (2) the *secondary* purpose of this routine is to # fill in any gaps in the @cellStorage array # for any entries that don't yet exist; # # for example, # 111 & 2 & 33333 \\ # 4 & 55 & 66\\ # 77 & 8 <------ GAP HERE # # there is a gap in @cellStorage in the final row, # which is completed by the second loop in the below my $self = shift; # array to store maximum column widths my @maximumColumnWidths; # we count the maximum number of columns my $maximumNumberOfColumns = 0; # maximum column width loop # maximum column width loop # maximum column width loop $logger->trace("*dontMeasure routine, cell mode") if ( ${$self}{dontMeasure} and $is_t_switch_active ); # row loop my $rowCount = -1; foreach my $row (@cellStorage) { $rowCount++; # column loop my $j = -1; foreach my $cell (@$row) { $j++; # if alignRowsWithoutMaxDelims = 0 # and there are *less than* the maximum number of ampersands, then # don't measure this column if ( !${$self}{alignRowsWithoutMaxDelims} and ${ $formattedBody[$rowCount] }{numberOfAmpersands} < ${$self}{maximumNumberOfAmpersands} ) { ${$cell}{measureThis} = 0; ${$cell}{type} = "*"; } # check if the cell shouldn't be measured $self->dont_measure( mode => "cell", row => $rowCount, column => $j ) if ${$self}{dontMeasure}; # it's possible to have delimiters of different lengths, for example # # \begin{tabbing} # 1 # 22 \> 333 # 4444 \\ # xxx # aaa # yyyyy # zzzzzzzz \\ # . # # & # \\ # # ^^ # || # \end{tabbing} # # note that this has a delimiter of \> (length 2) and # (length 1) # # furthermore, it's possible to specify the delimiter justification as "left" or "right" if ( ${$cell}{delimiterLength} > 0 and ${$cell}{delimiterLength} < $maxDelimiterWidth[$j] ) { if ( ${$self}{delimiterJustification} eq "left" ) { ${$cell}{delimiter} .= " " x ( $maxDelimiterWidth[$j] - ${$cell}{delimiterLength} ); } elsif ( ${$self}{delimiterJustification} eq "right" ) { ${$cell}{delimiter} = " " x ( $maxDelimiterWidth[$j] - ${$cell}{delimiterLength} ) . ${$cell}{delimiter}; } # update the delimiterLength ${$cell}{delimiterLength} = $maxDelimiterWidth[$j]; } # to keep leadingBlankColumn on, we need to check: # # - are we in the first column? # - is leadingBlankColumn 0 or more? # - cell width of first column equal 0? # - are we measuring this cell? # # see test-cases/alignment/issue-275a.tex and the associated logfile # if ( $j == 0 and ${$self}{leadingBlankColumn} > -1 and ${$cell}{width} > 0 and ${$cell}{measureThis} == 1 ) { ${$self}{leadingBlankColumn} = -1; } # there are some cells that shouldn't be accounted for in measuring, # for example {ccc} next if !${$cell}{measureThis}; # otherwise, make the measurement $maximumColumnWidths[$j] = ( defined $maximumColumnWidths[$j] ? max( $maximumColumnWidths[$j], ${$cell}{width} ) : ${$cell}{width} ); } # update the maximum number of columns $maximumNumberOfColumns = $j if ( $j > $maximumNumberOfColumns ); } # individual padding and gap filling loop # individual padding and gap filling loop # individual padding and gap filling loop # row loop foreach my $row (@cellStorage) { # column loop foreach ( my $j = 0; $j <= $maximumNumberOfColumns; $j++ ) { if ( defined ${$row}[$j] ) { # individual padding my $maximum = ( defined $maximumColumnWidths[$j] ? $maximumColumnWidths[$j] : 0 ); my $cellWidth = ${$row}[$j]{width}; ${$row}[$j]{individualPadding} += ( $maximum > $cellWidth ? $maximum - $cellWidth : 0 ); } else { # gap filling ${$row}[$j] = ( { type => "-", entry => '', width => 0, individualPadding => 0, groupPadding => 0, measureThis => 0, colSpan => ".", delimiter => "", delimiterLength => 0, } ); } # now the gaps have been filled, store the maximumColumnWidth for future reference ${$row}[$j]{maximumColumnWidth} = ( defined $maximumColumnWidths[$j] ? $maximumColumnWidths[$j] : 0 ); } } } sub multicolumn_pre_check { # PURPOSE: # ensure that multiple multicolumn commands are # handled appropriately # # example 1 # # \multicolumn{2}{c}{thing} & \\ # 111 & 2 & 33333 \\ # 4 & 55 & 66 \\ # \multicolumn{2}{c}{a} & \\ # # ^^^^^^^^ # |||||||| # # the second multicolumn command should not be measured, but # *should* receive individual padding my $self = shift; # loop through minMultiColSpan and add empty entries as necessary foreach (@minMultiColSpan) { $_ = "." if !( defined $_ ); } # ensure that only the *MINIMUM* multicolumn commands are designated # to be measured; for example: # # \multicolumn{2}{c|}{Ótimo humano} & Abuso da pontuação & Recompensas densas \\ # \multicolumn{3}{c||}{Exploração Fácil} & second \\ # Assault & Asterix & Beam Rider & Alien \\ # # the \multicolumn{2}{c|}{Ótimo humano} *is* the minimum multicolumn command # for the first column, and the \multicolumn{3}{c||}{Exploração Fácil} *is not* # to be measured # row loop my $rowCount = -1; foreach my $row (@cellStorage) { $rowCount++; # column loop my $j = -1; foreach my $cell (@$row) { $j++; if ( ${$cell}{type} =~ m/(\d)/ and ( $1 > $minMultiColSpan[$j] ) ) { ${$cell}{type} = "X"; ${$cell}{measureThis} = 0; } } } # now loop back through and ensure that each of the \multicolumn commands # are measured correctly # # row loop $rowCount = -1; foreach my $row (@cellStorage) { $rowCount++; # column loop my $j = -1; foreach my $cell (@$row) { $j++; # multicolumn entry if ( ${$cell}{type} =~ m/(\d+)/ ) { my $multiColumnSpan = $1; # *inner* row loop my $innerRowCount = -1; foreach my $innerRow (@cellStorage) { $innerRowCount++; # we only want to measure the *other* rows next if ( $innerRowCount == $rowCount ); # column loop my $innerJ = -1; foreach my $innerCell (@$innerRow) { $innerJ++; if ( $innerJ == $j and ${$innerCell}{type} =~ m/(\d)/ and $1 >= $multiColumnSpan ) { if ( ${$cell}{width} < ${$innerCell}{width} ) { ${$cell}{type} = "X"; ${$cell}{measureThis} = 0; ${$cell}{individualPadding} = ( ${$innerCell}{width} - ${$cell}{width} ); } else { ${$innerCell}{type} = "X"; ${$innerCell}{measureThis} = 0; ${$innerCell}{individualPadding} = ( ${$cell}{width} - ${$innerCell}{width} ); } } } } } } } } sub multicolumn_padding { # PURPOSE: # assign multi column padding, for example: # # \multicolumn{2}{c}{thing}&\\ # 111 &2&33333 \\ # 4& 55&66\\ # # needs to be transformed into # # \multicolumn{2}{c}{thing} & \\ # 111 & 2 & 33333 \\ # 4 & 55 & 66 \\ # # ^^^^^^^^^^^^^^^ # ||||||||||||||| # # and we need to compute the multi column padding, # illustrated by the up arrows in the above # # Approach: # (1) measure the "grouping widths" under/above each of the # \multicolumn entries; these are stored within # # groupingWidth # # for the relevant column entries; in the # above example, they would be stored in the # # 2 # 55 # # entries when justification is LEFT, and in the # # 111 # 4 # # entries when justification is RIGHT. We also calculate # maximum grouping width and store it within $maxGroupingWidth # # (2) loop back through and update # # groupPadding # # for each of the relevant column entries; in the # above example, they would be stored in the # # 2 # 55 # # entries when justification is LEFT, and in the # # 111 # 4 # # (3) finally, account for the \multicolumn command itself; # ensuring that we account for it being wider or narrower # than its spanning columns my $self = shift; # row loop my $rowCount = -1; foreach my $row (@cellStorage) { $rowCount++; # column loop my $j = -1; foreach my $cell (@$row) { $j++; # multicolumn entry next unless ( ${$cell}{type} =~ m/(\d+)/ ); my $multiColumnSpan = $1; my $maxGroupingWidth = 0; # depending on the # # justification # # setting (left or right), we store the # # groupingWidth # # and groupPadding accordingly my $justificationOffset = ( ${$self}{justification} eq "left" ? $j + $multiColumnSpan - 1 : $j ); # phase (1) # phase (1) # phase (1) # *inner* row loop my $innerRowCount = -1; foreach my $innerRow (@cellStorage) { $innerRowCount++; # we only want to measure the *other* rows next if ( $innerRowCount == $rowCount ); # we will store the width of columns spanned by the multicolumn command my $groupingWidth = 0; # *inner* column loop for ( my $innerJ = 0; $innerJ < $multiColumnSpan; $innerJ++ ) { # some entries should not be measured in the grouping width, # for example # # \multicolumn{2}{c}{thing} & \\ # 111 & 2 & 33333 \\ # 4 & 55 & 66 \\ # \multicolumn{2}{c}{a} & \\ # # ^^^^^^^^ # |||||||| # # the second \multicolumn entry shouldn't be measured, and it will # have had # # measureThis # # switched off during multicolumn_pre_check next if !${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{measureThis}; $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{width}; $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{individualPadding}; $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{delimiterLength} if ( $innerJ < $multiColumnSpan - 1 ); } # adjust for # <spacesBeforeAmpersand> # & # <spacesEndAmpersand> # note: # we need to multiply this by the appropriate multicolumn # spanning; in the above example: # # \multicolumn{2}{c}{thing} & \\ # 111 & 2 & 33333 \\ # 4 & 55 & 66 \\ # # we multiply by (2-1) = 1 because there is *1* ampersand # underneath the multicolumn command # # note: # the & will have been accounted for in the above section using delimiterLength $groupingWidth += ( $multiColumnSpan - 1 ) * ( ${$self}{spacesBeforeAmpersand} + ${$self}{spacesAfterAmpersand} ); # store the grouping width for the next phase ${ $cellStorage[$innerRowCount][$justificationOffset] }{groupingWidth} = $groupingWidth; # update the maximum grouping width $maxGroupingWidth = max( $maxGroupingWidth, $groupingWidth ); } # phase (2) # phase (2) # phase (2) # now that the maxGroupingWidth has been established, loop back # through and update groupingWidth for the appropriate cells # *inner* row loop $innerRowCount = -1; foreach my $innerRow (@cellStorage) { $innerRowCount++; # we only want to make adjustment on the *other* rows next if ( $innerRowCount == $rowCount ); # it's possible that we have multicolumn commands that were *not* measured, # and are *narrower* than the maxGroupingWidth, for example: # # \multicolumn{3}{l}{aaaa} & \\ <------ this entry won't have been measured! # $S_N$ & $=$ & $\SI{1000}{\kV\A}$ & $U_0$ & \\ # \multicolumn{3}{l}{bbbbbbb} & \\ # if ( ${ $cellStorage[$innerRowCount][$j] }{colSpan} ne "." and ${ $cellStorage[$innerRowCount][$j] }{colSpan} == $multiColumnSpan and !${ $cellStorage[$innerRowCount][$j] }{measureThis} and $maxGroupingWidth > ${ $cellStorage[$innerRowCount][$j] }{width} and ${ $cellStorage[$innerRowCount][$j] }{width} >= ${$cell}{width} ) { ${ $cellStorage[$innerRowCount][$j] }{individualPadding} = 0; ${ $cellStorage[$innerRowCount][$j] }{groupPadding} = ( $maxGroupingWidth - ${ $cellStorage[$innerRowCount][$j] }{width} ); } my $groupingWidth = ${ $cellStorage[$innerRowCount][$justificationOffset] }{groupingWidth}; # phase (3) # phase (3) # phase (3) # there are two possible cases: # # (1) the \multicolumn statement is *WIDER* than its grouped columns, e.g.: # # \multicolumn{2}{c}{thing} & \\ # 111 & 2 & 33333 \\ # 4 & 55 & 66 \\ # ^^^^^^^^^^^^^^^ # ||||||||||||||| # # in this situation, we need to adjust each of the group paddings for the cells # marked with an up arrow # # (2) the \multicolumn statement is *NARROWER* than its grouped columns, e.g.: # # || # ** # \multicolumn{2}{c}{thing} & \\ # 111 & bbbbbbbbbbbbbbbbbbbbbb & 33333 \\ # 4 & 55 & 66 \\ # # in this situation, we need to adjust the groupPadding of the multicolumn # entry, which happens ***once the row loop has finished*** # if ( ${$cell}{width} > $maxGroupingWidth ) { ${ $cellStorage[$innerRowCount][$justificationOffset] }{groupPadding} = ( ${$cell}{width} - $maxGroupingWidth ); } } # case (2) from above when \multicolumn statement is *NARROWER* than its grouped columns if ( ${$cell}{width} < $maxGroupingWidth ) { ${$cell}{groupPadding} += ( $maxGroupingWidth - ${$cell}{width} ); ${$cell}{individualPadding} = 0; } } } } sub multicolumn_post_check { # PURPOSE: # ensure that multiple multicolumn commands are # handled appropriately # # example 1 # # \multicolumn{2}{c}{thing} & aaa & bbb \\ # 111 & 2 & 33333 & \\ # 4 & 55 & 66 & \\ # \multicolumn{3}{c}{a} & \\ # # ^^^^^^^^ # |||||||| # # the second multicolumn command needs to have its group padding # accounted for. # # *Note* this will not have been done previously, as # this second multicolumn command will have had its type changed to # X, because it spans 3 columns, and there is a cell within its # column that spans *less than 3 columns* my $self = shift; # row loop my $rowCount = -1; foreach my $row (@cellStorage) { $rowCount++; # column loop my $j = -1; foreach my $cell (@$row) { $j++; # we only need to account for X-type columns, with an integer colSpan next unless ( ${$cell}{type} eq "X" and ${$cell}{colSpan} =~ m/(\d+)/ ); # multicolumn entry my $multiColumnSpan = $1; # we store the maximum grouping width my $maxGroupingWidth = 0; # *inner* row loop my $innerRowCount = -1; foreach my $innerRow (@cellStorage) { $innerRowCount++; # we only want to measure the *other* rows next if ( $innerRowCount == $rowCount ); # we will store the width of columns spanned by the multicolumn command my $groupingWidth = 0; # we need to keep track of the number of ampersands encountered; # # for example: # # \multicolumn{2}{c}{thing} & aaa & bbb \\ # 111 & 2 & 33333 & \\ # 4 & 55 & 66 & \\ # \multicolumn{3}{c}{a} & \\ <!----- focus on the multicolumn entry in this row # # focusing on the final multicolumn entry (spanning 3 columns), # when we loop back through the rows, we will encounter: # # row 1: one ampersand # # \multicolumn{2}{c}{thing} & aaa ... # # row 2: two ampersands # # 111 & 2 & 33333 ... # # row 3: two ampersands # # 4 & 55 & 66 ... # # note: we start the counter at -1, because the count of ampersands # is always one behind the cell count; for example, looking # at row 3 in the above, in the above, in cell 1 (entry: 4) # we have 0 ampersands, in cell 2 (entry: 55) we now have 1 ampersands, etc my $ampersandsEncountered = -1; # column loop my @dumRow = @$innerRow; foreach ( my $innerJ = 0; $innerJ <= $#dumRow; $innerJ++ ) { # exit the loop if we've reached the multiColumn spanning width last if ( $innerJ >= ($multiColumnSpan) ); # don't include '*' or '-' cell types in the calculations my $type = ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{type}; next if ( $type eq "*" or $type eq "-" ); # update the grouping width $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{width}; $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{individualPadding}; # update the number of & encountered $ampersandsEncountered++; $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{delimiterLength} if ( $ampersandsEncountered > 0 ); # and adjust the column count, if necessary; in the above example # # \multicolumn{2}{c}{thing} & aaa & bbb \\ # 111 & 2 & 33333 & \\ # 4 & 55 & 66 & \\ # \multicolumn{3}{c}{a} & \\ <!----- focus on the multicolumn entry in this row # # the *first entry* \multicolumn{2}{c}{thing} spans 2 columns, so # we need to move the column count along accordingly if ( ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{colSpan} =~ m/(\d+)/ ) { $innerJ += $1 - 1; } } # adjust for # <spacesBeforeAmpersand> # & # <spacesEndAmpersand> # note: # we need to multiply this by the appropriate # number of *ampersandsEncountered* # # \multicolumn{2}{c}{thing} & \\ # 111 & 2 & 33333 \\ # 4 & 55 & 66 \\ $groupingWidth += $ampersandsEncountered * ( ${$self}{spacesBeforeAmpersand} + ${$self}{spacesAfterAmpersand} ); # update the maximum grouping width $maxGroupingWidth = max( $maxGroupingWidth, $groupingWidth ); } ${$cell}{individualPadding} = 0; # there are cases where the # # maxGroupingWidth # # is *less than* the cell width, for example: # # \multicolumn{4}{|c|}{\textbf{Search Results}} \\ <!------ the width of this multicolumn entry # 1 & D7 & R & \\ is larger than maxGroupingWidth # 2 & D2 & R & \\ # \multicolumn{3}{|c|}{\textbf{Avg}} & \\ # ${$cell}{groupPadding} = max( 0, $maxGroupingWidth - ${$cell}{width} ); } } } sub pretty_print_cell_info { my $thingToPrint = ( defined $_[0] ? $_[0] : "entry" ); $logger->trace("*cell information: $thingToPrint"); $logger->trace( "minimum multi col span: " . join( ",", @minMultiColSpan ) ) if (@minMultiColSpan); foreach my $row (@cellStorage) { my $tmpLogFileLine = q(); foreach my $cell (@$row) { $tmpLogFileLine .= ${$cell}{$thingToPrint} . "\t"; } $logger->trace( ' ' . $tmpLogFileLine ) if ($is_t_switch_active); } if ( $thingToPrint eq "type" ) { $logger->trace("*key to types:"); $logger->trace("\tX\tbasic cell, will be measured and aligned"); $logger->trace("\t*\t will not be measured, and no ampersand"); $logger->trace("\t-\t phantom/blank cell for gaps"); $logger->trace("\t[0-9]\tmulticolumn cell, spanning multiple columns"); } } sub double_back_slash_else { my $self = shift; # check for existence of \\ statement, and associated line break information $self->check_for_else_statement( # else name regexp elseNameRegExp => qr/${${$mainSettings{fineTuning}}{modifyLineBreaks}}{doubleBackSlash}/, # else statements name: note that DBS stands for 'Double Back Slash' ElseStartsOnOwnLine => "DBSStartsOnOwnLine", # end statements ElseFinishesWithLineBreak => "DBSFinishesWithLineBreak", # for the YAML settings storage storageNameAppend => "DBS", # logfile information logName => "double-back-slash-block (for align at ampersand, see lookForAlignDelims)", ); } # possible hidden children, see test-cases/alignment/issue-162.tex and friends # # \begin{align} # A & =\begin{array}{cc} % <!--- Hidden child # BBB & CCC \\ % <!--- Hidden child # E & F % <!--- Hidden child # \end{array} \\ % <!--- Hidden child # # Z & =\begin{array}{cc} % <!--- Hidden child # Y & X \\ % <!--- Hidden child # W & V % <!--- Hidden child # \end{array} % <!--- Hidden child # \end{align} # # PURPOSE: # # measure CELL width that has hidden children # # A & =\begin{array}{cc} # BBB & CCC \\ # E & F # \end{array} \\ # # ^^^^^^^^^^^^^^^^^^ # |||||||||||||||||| # # Cell # sub hidden_child_cell_row_width { my $self = shift; my ( $tmpCellEntry, $rowCounter, $columnCounter ) = @_; for my $hiddenChildToMeasure ( @{ ${$self}{measureHiddenChildren} } ) { if ( $tmpCellEntry =~ m/.*?$hiddenChildToMeasure/s and defined $familyTree{$hiddenChildToMeasure}{bodyForMeasure} ) { $tmpCellEntry =~ s/$hiddenChildToMeasure/$familyTree{$hiddenChildToMeasure}{bodyForMeasure}/s; } } if ( $tmpCellEntry =~ m/$tokens{verbatim}/ ) { # # verbatim example: # # \begin{tabular}{ll} # Testing & Line 1 \\ # Testing & Line 2 \\ # Testing & Line 3 \verb|X| \\ # Testing & Line 4 \\ # \end{tabular} while ( my ( $verbatimID, $child ) = each %verbatimStorage ) { if ( $tmpCellEntry =~ m/.*?$verbatimID/s ) { $tmpCellEntry =~ s/$verbatimID/${$child}{begin}${$child}{body}${$child}{end}/s; } } } my $bodyLineBreaks = 0; $bodyLineBreaks++ while ( $tmpCellEntry =~ m/\R/sg ); if ( $bodyLineBreaks > 0 ) { my $maxRowWidthWithinCell = 0; foreach ( split( "\n", $tmpCellEntry ) ) { my $currentRowWidth = &get_column_width($_); $maxRowWidthWithinCell = $currentRowWidth if ( $currentRowWidth > $maxRowWidthWithinCell ); } ${ $cellStorage[$rowCounter][$columnCounter] }{width} = $maxRowWidthWithinCell; } else { ${ $cellStorage[$rowCounter][$columnCounter] }{width} = &get_column_width($tmpCellEntry); } } # PURPOSE: # # measure ROW width that has hidden children # # A & =\begin{array}{cc} # BBB & CCC \\ # E & F # \end{array} \\ # # ^^^^^^^^^^^^^^^^^^^^^^ # |||||||||||||||||||||| # # row # sub hidden_child_row_width { my $self = shift; my ( $tmpRow, $rowCount, $rowWidth ) = @_; # some alignment blocks have the 'begin' statement on the opening line: # # this bit # |||||||||||||| # `````````````` # \begin{align*}1 & 2 \\ # 3 & 4 \\ # 5 & 6 # \end{align*} # my $lengthOfBegin = 0; if ( !${ ${$self}{linebreaksAtEnd} }{begin} and ${ $cellStorage[0][0] }{type} eq "X" and ${ $cellStorage[0][0] }{measureThis} ) { my $beginToMeasure = ${$self}{begin}; if ( ( ${$self}{begin} eq '{' | ${$self}{begin} eq '[' ) and ${$self}{parentBegin} ) { $beginToMeasure = ${$self}{parentBegin} . "{"; } $lengthOfBegin = &get_column_width($beginToMeasure); $tmpRow = $beginToMeasure . $tmpRow if $rowCount == 0; } if ( $tmpRow =~ m/.*?$tokens{beginOfToken}/s ) { $tmpRow = ( "." x $lengthOfBegin ) . $tmpRow; for my $hiddenChildToMeasure ( @{ ${$self}{measureHiddenChildren} } ) { if ( $tmpRow =~ m/(^.*)?$hiddenChildToMeasure/m and defined $familyTree{$hiddenChildToMeasure}{bodyForMeasure} ) { my $partBeforeId = $1; my $lengthPartBeforeId = &get_column_width($partBeforeId); foreach ( @{ $familyTree{$hiddenChildToMeasure}{ancestors} } ) { if ( ${$_}{ancestorID} eq ${$self}{id} ) { if ( $lengthOfBegin > 0 ) { ${$_}{ancestorIndentation} = ( " " x ($lengthPartBeforeId) ); } else { ${$_}{ancestorIndentation} = ${$_}{ancestorIndentation} . ( " " x ($lengthPartBeforeId) ); } } } my $tmpBodyToMeasure = join( "\n" . ( "." x ($lengthPartBeforeId) ), split( "\n", $familyTree{$hiddenChildToMeasure}{bodyForMeasure} ) ); # remove trailing \\ $tmpBodyToMeasure =~ s/(\\\\\h*$)//mg; $tmpRow =~ s/$hiddenChildToMeasure/$tmpBodyToMeasure/s; } } if ( $tmpRow =~ m/$tokens{verbatim}/ ) { # # verbatim example: # # \begin{tabular}{ll} # Testing & Line 1 \\ # Testing & Line 2 \\ # Testing & Line 3 \verb|X| \\ # Testing & Line 4 \\ # \end{tabular} while ( my ( $verbatimID, $child ) = each %verbatimStorage ) { if ( $tmpRow =~ m/.*?$verbatimID/s ) { $tmpRow =~ s/$verbatimID/${$child}{begin}${$child}{body}${$child}{end}/s; } } } my $bodyLineBreaks = 0; $bodyLineBreaks++ while ( $tmpRow =~ m/\R/sg ); if ( $bodyLineBreaks > 0 ) { my $maxRowWidth = 0; foreach ( split( "\n", $tmpRow ) ) { my $currentRowWidth = &get_column_width($_); $maxRowWidth = $currentRowWidth if ( $currentRowWidth > $maxRowWidth ); } $rowWidth = $maxRowWidth; } else { $rowWidth = &get_column_width($tmpRow); } } elsif (!${ ${$self}{linebreaksAtEnd} }{begin} and ${ $cellStorage[0][0] }{type} eq "X" and ${ $cellStorage[0][0] }{measureThis} ) { $rowWidth = &get_column_width($tmpRow); } # possibly draw ruler to log file &draw_ruler_to_logfile( $tmpRow, $rowWidth ) if ($is_t_switch_active); return $rowWidth; } sub draw_ruler_to_logfile { # draw a ruler to the log file, useful for debugging # # example: # # ----|----|----|----|----|----|----|----|----|----|----|----|----|----| # 5 10 15 20 25 30 35 40 45 50 55 60 65 70 # my ( $tmpRow, $maxRowWidth ) = @_; $logger->trace("*tmpRow:"); foreach ( split( "\n", $tmpRow ) ) { my $currentRowWidth = &get_column_width($_); $logger->trace("$_ \t(length: $currentRowWidth)"); } my $rulerMax = int( $maxRowWidth / 10 + 1.5 ) * 10; $logger->trace( ( "----|" x ( int( $rulerMax / 5 ) ) ) ); my $ruler = q(); for ( my $i = 1; $i <= $rulerMax / 5; $i++ ) { $ruler .= " " . $i * 5 } $logger->trace($ruler); } sub get_column_width { my $stringToBeMeasured = $_[0]; # default length measurement # credit/reference: https://perldoc.perl.org/perlunicook#%E2%84%9E-33:-String-length-in-graphemes unless ( $switches{GCString} ) { my $count = 0; while ( $stringToBeMeasured =~ /\X/g ) { $count++ } return $count; } # if GCString active, then use Unicode::GCString return Unicode::GCString->new($stringToBeMeasured)->columns(); } 1;