diff options
author | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
commit | e0c6872cf40896c7be36b11dcc744620f10adf1d (patch) | |
tree | 60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/latexindent/LatexIndent/AlignmentAtAmpersand.pm |
Initial commit
Diffstat (limited to 'support/latexindent/LatexIndent/AlignmentAtAmpersand.pm')
-rw-r--r-- | support/latexindent/LatexIndent/AlignmentAtAmpersand.pm | 525 |
1 files changed, 525 insertions, 0 deletions
diff --git a/support/latexindent/LatexIndent/AlignmentAtAmpersand.pm b/support/latexindent/LatexIndent/AlignmentAtAmpersand.pm new file mode 100644 index 0000000000..8b8e387d09 --- /dev/null +++ b/support/latexindent/LatexIndent/AlignmentAtAmpersand.pm @@ -0,0 +1,525 @@ +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 utf8; +use Unicode::GCString; +use Data::Dumper; +use Exporter qw/import/; +use List::Util qw(max); +use LatexIndent::TrailingComments qw/$trailingCommentRegExp/; +use LatexIndent::Switches qw/$is_t_switch_active $is_tt_switch_active/; +use LatexIndent::GetYamlSettings qw/%masterSettings/; +use LatexIndent::Tokens qw/%tokens/; +use LatexIndent::LogFile qw/$logger/; +our @ISA = "LatexIndent::Document"; # class inheritance, Programming Perl, pg 321 +our @EXPORT_OK = qw/align_at_ampersand find_aligned_block double_back_slash_else/; +our $alignmentBlockCounter; + +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(\%{$masterSettings{lookForAlignDelims}})) if($is_tt_switch_active); + while( my ($alignmentBlock,$yesno)= each %{$masterSettings{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 + ) + ( + .*? + ) + \R + \h* + ( + (?!<\\) + %\* # % + \h* # possible horizontal spaces + \\end\{$alignmentBlock\} # \end{alignmentBlock} + ) # %* \end{<something>} statement + #\R + /sx; + + while( ${$self}{body} =~ m/$alignmentRegExp/sx){ + + ${$self}{body} =~ s/ + $alignmentRegExp + / + # create a new Environment object + my $alignmentBlockObj = LatexIndent::AlignmentAtAmpersand->new( begin=>$1, + body=>$2, + end=>$3, + name=>$alignmentBlock, + modifyLineBreaksYamlName=>"environments", + linebreaksAtEnd=>{ + begin=>1, + body=>1, + end=>0, + }, + ); + + # log file output + $logger->trace("*Alignment block found: %*\\begin\{$alignmentBlock\}") 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); + + # calculate the maximum number of ampersands in a row in the body + my $maximumNumberOfAmpersands = 0; + foreach(split("\n",${$self}{body})){ + my $numberOfAmpersands = () = $_ =~ /(?<!\\)&/g; + $maximumNumberOfAmpersands = $numberOfAmpersands if($numberOfAmpersands>$maximumNumberOfAmpersands); + } + + # create an array of zeros + my @maximumColumnWidth = (0) x ($maximumNumberOfAmpersands+1); + my @maximumColumnWidthMC = (0) x ($maximumNumberOfAmpersands+1); + + # array for the new body + my @formattedBody; + + # now loop back through the body, and store the maximum column size + foreach(split("\n",${$self}{body})){ + # remove \\ and anything following it + my $endPiece; + if($_ =~ m/(\\\\.*)/){ + $_ =~ s/(\\\\.*)//; + $endPiece = $1; + } + + # remove any trailing comments + my $trailingComments; + if($_ =~ m/$trailingCommentRegExp/ ){ + $_ =~ s/($trailingCommentRegExp)//; + $trailingComments = $1; + } + + # count the number of ampersands in the current row + my $numberOfAmpersands = () = $_ =~ /(?<!\\)&/g; + + # switch for multiColumGrouping + my $multiColumnGrouping = ($_ =~ m/\\multicolumn/ and ${$self}{multiColumnGrouping}); + my $alignRowsWithoutMaxDelims = ${$self}{alignRowsWithoutMaxDelims}; + + # by default, the stripped row is simply the current row + my $strippedRow = $_; + + # loop through the columns + my $columnCount = 0; + + # format switch off by default + my $formatRow = 0; + + # store the column sizes for measuring and comparison purposes + my @columnSizes = (); + + # we will store the columns in each row + my @columns; + + # need to have at least one ampersand, or contain a \multicolumn command + if( ($_ =~ m/(?<!\\)&/ and ( ($numberOfAmpersands == $maximumNumberOfAmpersands)||$multiColumnGrouping||$alignRowsWithoutMaxDelims ) ) + or + ($multiColumnGrouping and $alignRowsWithoutMaxDelims) ){ + # remove space at the beginning of a row, surrounding &, and at the end of the row + $_ =~ s/(?<!\\)\h*(?<!\\)&\h*/&/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/(?<!\\)&$/ ? " ":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) + @columns = ($_ =~ m/(?<!\\)&/ ? split(/(?<!\\)&/,$_) : $_); + + # empty the white-space-stripped row + $strippedRow = ''; + foreach my $column (@columns){ + # 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()); + + # store the column size + # reference: http://www.perl.com/pub/2012/05/perlunicook-unicode-column-width-for-printing.html + my $gcs = Unicode::GCString->new($column); + my $columnWidth = $gcs->columns(); + + # multicolumn cells need a bit of special care + if($multiColumnGrouping and $column =~ m/\\multicolumn\{(\d+)\}/ and $1>1){ + $maximumColumnWidthMC[$columnCount] = $columnWidth if( defined $maximumColumnWidthMC[$columnCount] and ($columnWidth > $maximumColumnWidthMC[$columnCount]) ); + $columnWidth = 1 if($multiColumnGrouping and ($column =~ m/\\multicolumn\{(\d+)\}/)); + } + + # store the maximum column width + $maximumColumnWidth[$columnCount] = $columnWidth if( defined $maximumColumnWidth[$columnCount] and ($columnWidth > $maximumColumnWidth[$columnCount]) ); + + # put the row back together, using " " if the column is empty + $strippedRow .= ($columnCount>0 ? "&" : q() ).($columnWidth > 0 ? $column: " "); + + # store the column width + $columnSizes[$columnCount] = $columnWidth; + + # move on to the next column + if($multiColumnGrouping and ($column =~ m/\\multicolumn\{(\d+)\}/)){ + # columns that are within the multiCol statement receive a width of -1 + for my $i (($columnCount+1)..($columnCount+$1)){ + $columnSizes[$i] = -1; + } + # update the columnCount to account for the multiColSpan + $columnCount += $1; + } else { + $columnCount++; + } + } + + # toggle the formatting switch + $formatRow = 1; + } elsif($endPiece and ${$self}{alignDoubleBackSlash}){ + # otherwise a row could contain no ampersands, but would still desire + # the \\ to be aligned, see test-cases/alignment/multicol-no-ampersands.tex + @columns = $_; + $formatRow = 1; + } + + # store the information + push(@formattedBody,{ + row=>$strippedRow, + format=>$formatRow, + multiColumnGrouping=>$multiColumnGrouping, + columnSizes=>\@columnSizes, + columns=>\@columns, + endPiece=>($endPiece ? $endPiece :q() ), + trailingComment=>($trailingComments ? $trailingComments :q() )}); + } + + # output some of the info so far to the log file + $logger->trace("*Alignment at ampersand routine for ${$self}{name} (see lookForAlignDelims)") if $is_t_switch_active; + $logger->trace("Maximum column sizes of horizontally stripped formatted block (${$self}{name}): @maximumColumnWidth") if $is_t_switch_active; + $logger->trace("align at ampersand: ${$self}{lookForAlignDelims}") if $is_t_switch_active; + $logger->trace("align at \\\\: ${$self}{alignDoubleBackSlash}") if $is_t_switch_active; + $logger->trace("spaces before \\\\: ${$self}{spacesBeforeDoubleBackSlash}") if $is_t_switch_active; + $logger->trace("multi column grouping: ${$self}{multiColumnGrouping}") if $is_t_switch_active; + $logger->trace("align rows without maximum delimeters: ${$self}{alignRowsWithoutMaxDelims}") if $is_t_switch_active; + $logger->trace("spaces before ampersand: ${$self}{spacesBeforeAmpersand}") if $is_t_switch_active; + $logger->trace("spaces after ampersand: ${$self}{spacesAfterAmpersand}") if $is_t_switch_active; + $logger->trace("justification: ${$self}{justification}") if $is_t_switch_active; + + # acount for multicolumn grouping, if the appropriate switch is set + if(${$self}{multiColumnGrouping}){ + foreach(@formattedBody){ + if(${$_}{format} and ${$_}{row} !~ m/^\h*$/){ + + # set a columnCount, which will vary depending on multiColumnGrouping settings or not + my $columnCount=0; + + # loop through the columns + foreach my $column (@{${$_}{columns}}){ + # calculate the width of the current column + my $gcs = Unicode::GCString->new($column); + my $columnWidth = $gcs->columns(); + + # check for multiColumnGrouping + if(${$_}{multiColumnGrouping} and $column =~ m/\\multicolumn\{(\d+)\}/ and $1>1){ + my $multiColSpan = $1; + + # for example, \multicolumn{3}{c}{<stuff>} spans 3 columns, so + # the maximum column needs to account for this (subtract 1 because of 0 index in perl arrays) + my $columnMax = $columnCount+$multiColSpan-1; + + # groupingWidth contains the total width of column sizes grouped + # underneath the \multicolumn{} statement + my $groupingWidth = 0; + my $maxGroupingWidth = 0; + foreach (@formattedBody){ + $groupingWidth = 0; + + # loop through the columns covered by the multicolumn statement + foreach my $j ($columnCount..$columnMax){ + if( defined @{${$_}{columnSizes}}[$j] + and + @{${$_}{columnSizes}}[$j] >= 0 + and + ${$_}{format} + ){ + $groupingWidth += (defined $maximumColumnWidth[$j] ? $maximumColumnWidth[$j] : 0); + } else { + $groupingWidth = 0; + } + } + + # update the maximum grouping width + $maxGroupingWidth = $groupingWidth if($groupingWidth > $maxGroupingWidth); + + # the cells that receive multicolumn grouping need extra padding; in particular + # if the justification is *left*: + # the *last* cell of the multicol group receives the padding + # if the justification is *right*: + # the *first* cell of the multicol group receives the padding + # + # this motivates the introduction of $columnOffset, which is + # 0 if justification is left + # $multiColSpan if justification is right + my $columnOffset = (${$self}{justification} eq "left") ? $columnMax : $columnCount; + if(defined @{${$_}{columnSizes}}[$columnMax] and ($columnWidth > ($groupingWidth+(${$self}{spacesBeforeAmpersand}+1+${$self}{spacesAfterAmpersand})*($multiColSpan-1)) ) and @{${$_}{columnSizes}}[$columnMax] >= 0){ + my $multiColPadding = $columnWidth-$groupingWidth-(${$self}{spacesBeforeAmpersand}+1+${$self}{spacesAfterAmpersand})*($multiColSpan-1); + + # it's possible that multiColPadding might already be assigned; in which case, + # we need to check that the current value of $multiColPadding is greater than the existing one + if(defined @{${$_}{multiColPadding}}[$columnOffset]){ + @{${$_}{multiColPadding}}[$columnOffset] = max($multiColPadding,@{${$_}{multiColPadding}}[$columnOffset]); + } else { + @{${$_}{multiColPadding}}[$columnOffset] = $multiColPadding; + } + + # also need to account for maximum column width *including* other multicolumn statements + if($maximumColumnWidthMC[$columnCount]>$columnWidth and $column !~ m/\\multicolumn\{(\d+)\}/){ + @{${$_}{multiColPadding}}[$columnOffset] += ($maximumColumnWidthMC[$columnCount]-$columnWidth); + } + } + } + # update it to account for the ampersands and the spacing either side of ampersands + $maxGroupingWidth += ($multiColSpan-1)*(${$self}{spacesBeforeAmpersand}+1+${$self}{spacesAfterAmpersand}); + + # store the maxGroupingWidth for use in the next loop + @{${$_}{maxGroupingWidth}}[$columnCount] = $maxGroupingWidth; + + # update the columnCount to account for the multiColSpan + $columnCount += $multiColSpan - 1; + } + + # increase the column count + $columnCount++; + } + } + } + } + + # the maximum row width will be used in aligning (or not) the \\ + my $maximumRowWidth = 0; + + # now that the multicolumn widths have been accounted for, loop through the body + foreach(@formattedBody){ + if(${$_}{format} and ${$_}{row} !~ m/^\h*$/){ + + # set a columnCount, which will vary depending on multiColumnGrouping settings or not + my $columnCount=0; + my $tmpRow = q(); + + # loop through the columns + foreach my $column (@{${$_}{columns}}){ + # calculate the width of the current column + my $gcs = Unicode::GCString->new($column); + my $columnWidth = $gcs->columns(); + + # reset the column padding + my $padding = q(); + + # check for multiColumnGrouping + if(${$_}{multiColumnGrouping} and $column =~ m/\\multicolumn\{(\d+)\}/ and $1>1){ + my $multiColSpan = $1; + + # groupingWidth contains the total width of column sizes grouped + # underneath the \multicolumn{} statement + my $maxGroupingWidth = ${${$_}{maxGroupingWidth}}[$columnCount]; + + # it's possible to have situations such as + # + # \multicolumn{3}{l}{one} & \multicolumn{3}{l}{two} & \\ + # \multicolumn{6}{l}{one} & \\ + # + # in which case we need to loop through the @maximumColumnWidthMC + my $groupingWidthMC = 0; + my $multicolsEncountered =0; + for ($columnCount..($columnCount + ($multiColSpan-1))){ + if(defined $maximumColumnWidthMC[$_]){ + $groupingWidthMC += $maximumColumnWidthMC[$_]; + $multicolsEncountered++ if $maximumColumnWidthMC[$_]>0; + } + } + + # need to account for (spacesBeforeAmpersands) + length of ampersands (which is 1) + (spacesAfterAmpersands) + $groupingWidthMC += ($multicolsEncountered-1)*(${$self}{spacesBeforeAmpersand}+1+${$self}{spacesAfterAmpersand}); + + # set the padding; we need + # maximum( $maxGroupingWidth, $maximumColumnWidthMC[$columnCount] ) + my $maxValueToUse = 0; + if(defined $maximumColumnWidthMC[$columnCount]){ + $maxValueToUse = max($maxGroupingWidth,$maximumColumnWidthMC[$columnCount],$groupingWidthMC); + } else { + $maxValueToUse = $maxGroupingWidth; + } + + # calculate the padding + $padding = " " x ( $maxValueToUse >= $columnWidth ? $maxValueToUse - $columnWidth : 0 ); + + # to the log file + if($is_tt_switch_active){ + $logger->trace("*---------column-------------"); + $logger->trace($column); + $logger->trace("multiColSpan: $multiColSpan"); + $logger->trace("groupingWidthMC: $groupingWidthMC"); + $logger->trace("padding length: ",$maxValueToUse - $columnWidth); + $logger->trace("multicolsEncountered: $multicolsEncountered"); + $logger->trace("maxValueToUse: $maxValueToUse"); + $logger->trace("maximumColumnWidth: ",join(",",@maximumColumnWidth)); + $logger->trace("maximumColumnWidthMC: ",join(",",@maximumColumnWidthMC)); + } + + # update the columnCount to account for the multiColSpan + $columnCount += $multiColSpan - 1; + } else { + # compare the *current* column width with the *maximum* column width + $padding = " " x (defined $maximumColumnWidth[$columnCount] and $maximumColumnWidth[$columnCount] >= $columnWidth ? $maximumColumnWidth[$columnCount] - $columnWidth : 0); + } + + # either way, the row is formed of "COLUMN + PADDING" + if(${$self}{justification} eq "left"){ + $tmpRow .= $column.$padding.(defined @{${$_}{multiColPadding}}[$columnCount] ? " " x @{${$_}{multiColPadding}}[$columnCount]: q()).(" " x ${$self}{spacesBeforeAmpersand})."&".(" " x ${$self}{spacesAfterAmpersand}); + } else { + $tmpRow .= $padding.(defined @{${$_}{multiColPadding}}[$columnCount] ? " " x @{${$_}{multiColPadding}}[$columnCount]: q()).$column.(" " x ${$self}{spacesBeforeAmpersand})."&".(" " x ${$self}{spacesAfterAmpersand}); + } + $columnCount++; + } + + # remove the final & + $tmpRow =~ s/\h*&\h*$/ /; + my $finalSpacing = q(); + $finalSpacing = " " x (${$self}{spacesBeforeDoubleBackSlash}) if ${$self}{spacesBeforeDoubleBackSlash}>=1; + $tmpRow =~ s/\h*$/$finalSpacing/; + + # replace the row with the formatted row + ${$_}{row} = $tmpRow; + + # update the maximum row width + my $gcs = Unicode::GCString->new($tmpRow); + ${$_}{rowWidth} = $gcs->columns(); + $maximumRowWidth = ${$_}{rowWidth} if(${$_}{rowWidth} > $maximumRowWidth); + } + } + + # final loop through to get \\ aligned + foreach (@formattedBody){ + # reset the padding + my $padding = q(); + + # possibly adjust the padding + if(${$_}{format} and ${$_}{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 ($maximumRowWidth - ${$_}{rowWidth}); + } + } + + # format the row, and put the trailing \\ and trailing comments back into the row + ${$_}{row} .= $padding.(${$_}{endPiece} ? ${$_}{endPiece} :q() ).(${$_}{trailingComment}? ${$_}{trailingComment} : q() ); + } + + # to the log file + if($is_tt_switch_active){ + $logger->trace(${$_}{row}) for @formattedBody; + } + + # 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}; +} + +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/${${$masterSettings{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)", + ); +} +1; |