summaryrefslogtreecommitdiff
path: root/support/latexindent/LatexIndent/AlignmentAtAmpersand.pm
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/latexindent/LatexIndent/AlignmentAtAmpersand.pm
Initial commit
Diffstat (limited to 'support/latexindent/LatexIndent/AlignmentAtAmpersand.pm')
-rw-r--r--support/latexindent/LatexIndent/AlignmentAtAmpersand.pm525
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;