#!/usr/bin/env perl # 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/. # # For details of how to use this file, please see readme.txt # load packages/modules: assume strict and warnings are part of every perl distribution use strict; use warnings; # list of modules my @listOfModules = ('FindBin','YAML::Tiny','File::Copy','File::Basename','Getopt::Long','File::HomeDir'); # check the other modules are available foreach my $moduleName (@listOfModules) { # references: # http://stackoverflow.com/questions/251694/how-can-i-check-if-i-have-a-perl-module-before-using-it # http://stackoverflow.com/questions/1917261/how-can-i-dynamically-include-perl-modules-without-using-eval eval { (my $file = $moduleName) =~ s|::|/|g; require $file . '.pm'; $moduleName->import(); 1; } or die "$moduleName Perl Module not currently installed; please install the module, and then try running latexindent.pl again; exiting"; } # now that we have confirmed the modules are available, load them use FindBin; # help find defaultSettings.yaml use YAML::Tiny; # interpret defaultSettings.yaml and other potential settings files use File::Copy; # to copy the original file to backup (if overwrite option set) use File::Basename; # to get the filename and directory path use Getopt::Long; # to get the switches/options/flags use File::HomeDir; # to get users home directory, regardless of OS # get the options my $overwrite; my $outputToFile; my $silentMode; my $tracingMode; my $tracingModeVeryDetailed; my $readLocalSettings=0; my $onlyDefault; my $showhelp; my $cruftDirectory; GetOptions ( "overwrite|w"=>\$overwrite, "outputfile|o"=>\$outputToFile, "silent|s"=>\$silentMode, "trace|t"=>\$tracingMode, "ttrace|tt"=>\$tracingModeVeryDetailed, "local|l:s"=>\$readLocalSettings, "onlydefault|d"=>\$onlyDefault, "help|h"=>\$showhelp, "cruft|c=s"=>\$cruftDirectory, ); # check local settings doesn't interfer with reading the file; # this can happen if the script is called as follows: # # latexindent.pl -l myfile.tex # # in which case, the GetOptions routine mistakes myfile.tex # as the optional parameter to the l flag. # # In such circumstances, we correct the mistake by assuming that # the only argument is the file to be indented, and place it in @ARGV if($readLocalSettings and scalar(@ARGV) < 1) { push(@ARGV,$readLocalSettings); $readLocalSettings = ''; } # this can also happen if the script is called as # # latexindent.pl -o -l myfile.tex outputfile.tex # # in which case, the GetOptions routine mistakes myfile.tex # as the optional parameter to the l flag. if($readLocalSettings and scalar(@ARGV) < 2 and $outputToFile) { unshift(@ARGV,$readLocalSettings); $readLocalSettings = ''; } # default value of readLocalSettings # # latexindent -l myfile.tex # # means that we wish to use localSettings.yaml if(defined($readLocalSettings) and ($readLocalSettings eq '')){ $readLocalSettings = 'localSettings.yaml'; } # detailed tracing mode also implies regular tracing mode $tracingMode = $tracingModeVeryDetailed ? 1 : $tracingMode; # version number my $versionNumber = "2.2"; # Check the number of input arguments- if it is 0 then simply # display the list of options (like a manual) if(scalar(@ARGV) < 1 or $showhelp) { print < used to specify the location of backup files and indent.log ENDQUOTE ; exit(2); } # set up default for cruftDirectory using the one from the input file, # unless it has been specified using -c="/some/directory" $cruftDirectory=dirname $ARGV[0] unless(defined($cruftDirectory)); die "Could not find directory $cruftDirectory\nExiting, no indentation done." if(!(-d $cruftDirectory)); # we'll be outputting to the logfile and to standard output my $logfile; my $out = *STDOUT; # open the log file open($logfile,">","$cruftDirectory/indent.log") or die "Can't open indent.log"; # output time to log file my $time = localtime(); print $logfile $time; # output version to log file print $logfile <2) { for my $fh ($out,$logfile) {print $fh <new; # Open defaultSettings.yaml $defaultSettings = YAML::Tiny->read( "$FindBin::RealBin/defaultSettings.yaml" ); print $logfile "\tReading defaultSettings.yaml from $FindBin::RealBin/defaultSettings.yaml\n\n" if($defaultSettings); # if latexindent.exe is invoked from TeXLive, then defaultSettings.yaml won't be in # the same directory as it; we need to navigate to it if(!$defaultSettings) { $defaultSettings = YAML::Tiny->read( "$FindBin::RealBin/../../texmf-dist/scripts/latexindent/defaultSettings.yaml"); print $logfile "\tReading defaultSettings.yaml (2nd attempt, TeXLive, Windows) from $FindBin::RealBin/../../texmf-dist/scripts/latexindent/defaultSettings.yaml\n\n" if($defaultSettings); } # if both of the above attempts have failed, we need to exit if(!$defaultSettings) { for my $fh ($out,$logfile) { print $fh <[0]}; # empty array to store the paths my @absPaths; # scalar to read user settings my $userSettings; # get information about user settings- first check if indentconfig.yaml exists my $indentconfig = File::HomeDir->my_home . "/indentconfig.yaml"; # if indentconfig.yaml doesn't exist, check for the hidden file, .indentconfig.yaml $indentconfig = File::HomeDir->my_home . "/.indentconfig.yaml" if(! -e $indentconfig); if ( -e $indentconfig and !$onlyDefault ) { print $logfile "\tReading path information from $indentconfig\n"; # if both indentconfig.yaml and .indentconfig.yaml exist if ( -e File::HomeDir->my_home . "/indentconfig.yaml" and -e File::HomeDir->my_home . "/.indentconfig.yaml") { print $logfile File::HomeDir->my_home,"/.indentconfig.yaml has been found, but $indentconfig takes priority\n"; } elsif ( -e File::HomeDir->my_home . "/indentconfig.yaml" ) { print $logfile "\tAlternatively, ",File::HomeDir->my_home,"/.indentconfig.yaml can be used\n"; } elsif ( -e File::HomeDir->my_home . "/.indentconfig.yaml" ) { print $logfile "\tAlternatively, ",File::HomeDir->my_home,"/indentconfig.yaml can be used\n"; } # read the absolute paths from indentconfig.yaml $userSettings = YAML::Tiny->read( "$indentconfig" ); # integrity check if($userSettings) { print $logfile "\t",Dump \%{$userSettings->[0]}; print $logfile "\n"; @absPaths = @{$userSettings->[0]->{paths}}; } else { print $logfile <my_home,"\n"; print $logfile "\tTo specify user settings you would put indentconfig.yaml here: \n\t",File::HomeDir->my_home,"/indentconfig.yaml\n\n"; print $logfile "\tAlternatively, you can use the hidden file .indentconfig.yaml as: \n\t",File::HomeDir->my_home,"/.indentconfig.yaml\n\n"; } } # get information about LOCAL settings, assuming that $readLocalSettings exists my $directoryName = dirname $ARGV[0]; # add local settings to the paths, if appropriate if ( (-e "$directoryName/$readLocalSettings") and $readLocalSettings and !(-z "$directoryName/$readLocalSettings")) { print $logfile "\tAdding $directoryName/$readLocalSettings to paths\n\n"; push(@absPaths,"$directoryName/$readLocalSettings"); } elsif ( !(-e "$directoryName/$readLocalSettings") and $readLocalSettings) { print $logfile "\tWARNING yaml file not found: \n\t$directoryName/$readLocalSettings not found\n"; print $logfile "\t\tcarrying on without it.\n"; } # read in the settings from each file foreach my $settings (@absPaths) { # check that the settings file exists and that it isn't empty if (-e $settings and !(-z $settings)) { print $logfile "\tReading USER settings from $settings\n"; $userSettings = YAML::Tiny->read( "$settings" ); # if we can read userSettings if($userSettings) { # update the MASTER setttings to include updates from the userSettings while(my($userKey, $userValue) = each %{$userSettings->[0]}) { # the update approach is slightly different for hashes vs scalars/arrays if (ref($userValue) eq "HASH") { while(my ($userKeyFromHash,$userValueFromHash) = each %{$userSettings->[0]{$userKey}}) { $masterSettings{$userKey}{$userKeyFromHash} = $userValueFromHash; } } else { $masterSettings{$userKey} = $userValue; } } # output settings to $logfile if($masterSettings{logFilePreferences}{showEveryYamlRead}){ print $logfile Dump \%{$userSettings->[0]}; print $logfile "\n"; } else { print $logfile "\t\tNot showing settings in the log file, see showEveryYamlRead.\n"; } } else { # otherwise print a warning that we can not read userSettings.yaml print $logfile "WARNING\n\t$settings \n\t contains invalid yaml format- not reading from it\n"; } } else { # otherwise keep going, but put a warning in the log file print $logfile "\nWARNING\n\t",File::HomeDir->my_home,"/indentconfig.yaml\n"; if (-z $settings) { print $logfile "\tspecifies $settings \n\tbut this file is EMPTY- not reading from it\n\n" } else { print $logfile "\tspecifies $settings \n\tbut this file does not exist- unable to read settings from this file\n\n" } } } # some people may wish to see showAlmagamatedSettings # which details the overall state of the settings modified # from the default in various user files if($masterSettings{logFilePreferences}{showAlmagamatedSettings}){ print $logfile "Almagamated/overall settings to be used:\n"; print $logfile Dump \%masterSettings ; } # scalar variables my $defaultIndent = $masterSettings{defaultIndent}; my $alwaysLookforSplitBraces = $masterSettings{alwaysLookforSplitBraces}; my $alwaysLookforSplitBrackets = $masterSettings{alwaysLookforSplitBrackets}; my $backupExtension = $masterSettings{backupExtension}; my $indentPreamble = $masterSettings{indentPreamble}; my $onlyOneBackUp = $masterSettings{onlyOneBackUp}; my $maxNumberOfBackUps = $masterSettings{maxNumberOfBackUps}; my $removeTrailingWhitespace = $masterSettings{removeTrailingWhitespace}; my $cycleThroughBackUps = $masterSettings{cycleThroughBackUps}; # hash variables my %lookForAlignDelims= %{$masterSettings{lookForAlignDelims}}; my %indentRules= %{$masterSettings{indentRules}}; my %verbatimEnvironments= %{$masterSettings{verbatimEnvironments}}; my %noIndentBlock= %{$masterSettings{noIndentBlock}}; my %checkunmatched= %{$masterSettings{checkunmatched}}; my %checkunmatchedELSE= %{$masterSettings{checkunmatchedELSE}}; my %checkunmatchedbracket= %{$masterSettings{checkunmatchedbracket}}; my %noAdditionalIndent= %{$masterSettings{noAdditionalIndent}}; my %indentAfterHeadings= %{$masterSettings{indentAfterHeadings}}; my %indentAfterItems= %{$masterSettings{indentAfterItems}}; my %itemNames= %{$masterSettings{itemNames}}; my %constructIfElseFi= %{$masterSettings{constructIfElseFi}}; my %fileExtensionPreference= %{$masterSettings{fileExtensionPreference}}; my %fileContentsEnvironments= %{$masterSettings{fileContentsEnvironments}}; # original name of file my $fileName = $ARGV[0]; # sort the file extensions by preference my @fileExtensions = sort { $fileExtensionPreference{$a} <=> $fileExtensionPreference{$b} } keys(%fileExtensionPreference); # get the base file name, allowing for different extensions (possibly no extension) my ($dir, $name, $ext) = fileparse($fileName, @fileExtensions); # quick check to make sure given file type is supported if( -e $ARGV[0] and !$ext ){ for my $fh ($out,$logfile) {print $fh <1) { print $logfile "\t WARNING: onlyOneBackUp=$onlyOneBackUp and maxNumberOfBackUps: $maxNumberOfBackUps\n"; print $logfile "\t\t setting onlyOneBackUp=0 which will allow you to reach $maxNumberOfBackUps back ups\n"; $onlyOneBackUp = 0; } # if the user has specified that $maxNumberOfBackUps = 1 then # they only want one backup if($maxNumberOfBackUps==1) { $onlyOneBackUp=1 ; print $logfile "\t FYI: you set maxNumberOfBackUps=1, so I'm setting onlyOneBackUp: 1 \n"; } elsif($maxNumberOfBackUps<=0 and !$onlyOneBackUp) { $onlyOneBackUp=0 ; $maxNumberOfBackUps=-1; } # if onlyOneBackUp is set, then the backup file will # be overwritten each time if($onlyOneBackUp) { $backupFile .= $backupExtension; print $logfile "\tcopying $fileName to $backupFile\n"; print $logfile "\t$backupFile was overwritten\n\n" if (-e $backupFile); } else { # start with a backup file .bak0 (or whatever $backupExtension is present) my $backupCounter = 0; $backupFile .= $backupExtension.$backupCounter; # if it exists, then keep going: .bak0, .bak1, ... while (-e $backupFile or $maxNumberOfBackUps>1) { if($backupCounter==$maxNumberOfBackUps) { print $logfile "\t maxNumberOfBackUps reached ($maxNumberOfBackUps)\n"; # some users may wish to cycle through back up files, e.g: # copy myfile.bak1 to myfile.bak0 # copy myfile.bak2 to myfile.bak1 # copy myfile.bak3 to myfile.bak2 # # current back up is stored in myfile.bak4 if($cycleThroughBackUps) { print $logfile "\t cycleThroughBackUps detected (see cycleThroughBackUps) \n"; for(my $i=1;$i<=$maxNumberOfBackUps;$i++) { # remove number from backUpFile my $oldBackupFile = $backupFile; $oldBackupFile =~ s/$backupExtension.*/$backupExtension/; my $newBackupFile = $oldBackupFile; # add numbers back on $oldBackupFile .= $i; $newBackupFile .= $i-1; # check that the oldBackupFile exists if(-e $oldBackupFile){ print $logfile "\t\t copying $oldBackupFile to $newBackupFile \n"; copy($oldBackupFile,$newBackupFile) or die "Could not write to backup file $backupFile. Please check permissions. Exiting.\n"; } } } # rest maxNumberOfBackUps $maxNumberOfBackUps=1 ; last; # break out of the loop } elsif(!(-e $backupFile)) { $maxNumberOfBackUps=1 ; last; # break out of the loop } print $logfile "\t $backupFile already exists, incrementing by 1...\n"; $backupCounter++; $backupFile =~ s/$backupExtension.*/$backupExtension$backupCounter/; } print $logfile "\n\t copying $fileName to $backupFile\n\n"; } # output these lines to the log file print $logfile "\tBackup file: ",$backupFile,"\n"; print $logfile "\tOverwriting file: ",$fileName,"\n\n"; copy($fileName,$backupFile) or die "Could not write to backup file $backupFile. Please check permissions. Exiting.\n"; } if(!($outputToFile or $overwrite)) { print $logfile "Just out put to the terminal :)\n\n" if !$silentMode ; } # scalar variables my $line; # $line: takes the $line of the file my $inpreamble=!$indentPreamble; # $inpreamble: switch to determine if in # preamble or not my $inverbatim=0; # $inverbatim: switch to determine if in # a verbatim environment or not my $delimiters=0; # $delimiters: switch that governs if # we need to check for & or not my $trailingcomments; # $trailingcomments stores the comments at the end of # a line my $lineCounter=0; # $lineCounter keeps track of the line number my $inIndentBlock=0; # $inindentblock: switch to determine if in # a inindentblock or not my $inFileContents=0; # $inFileContents: switch to determine if we're in a filecontents environment # array variables my @lines; # @lines: stores the newly indented lines my @mainfile; # @mainfile: stores input file; used to # grep for \documentclass # array of hashes, containing details of commands & environments my @masterIndentationArrayOfHashes; # check to see if the current file has \documentclass, if so, then # it's the main file, if not, then it doesn't have preamble open(MAINFILE, $fileName) or die "Could not open input file, $fileName"; @mainfile=; close(MAINFILE); # if the MAINFILE doesn't have a \documentclass statement, then # it shouldn't have preamble if(scalar(@{[grep(m/^\s*\\documentclass/, @mainfile)]})==0) { $inpreamble=0; print $logfile "Trace:\tNo documentclass detected, assuming no preamble\n" if($tracingMode); } else { print $logfile "Trace:\t documentclass detected, assuming preamble\n" if($tracingMode); } # the previous OPEN command puts us at the END of the file open(MAINFILE, $fileName) or die "Could not open input file, $fileName"; # loop through the lines in the INPUT file while() { # increment the line counter $lineCounter++; # very detailed output to logfile if($tracingModeVeryDetailed){ if( @masterIndentationArrayOfHashes){ print $logfile "\nLine $lineCounter\t (detailed trace) indentation hash: \n" if($tracingMode); for my $href ( @masterIndentationArrayOfHashes) { print $logfile Dump \%{$href}; } } } # tracing mode print $logfile $masterSettings{logFilePreferences}{traceModeBetweenLines} if($tracingMode and !($inpreamble or $inverbatim or $inIndentBlock)); # check to see if we're still in the preamble # or in a verbatim environment or in IndentBlock if(!($inpreamble or $inverbatim or $inIndentBlock)) { # if not, remove all leading spaces and tabs # from the current line, assuming it isn't empty s/^\t*// if($_ !~ /^((\s*)|(\t*))*$/); s/^\s*// if($_ !~ /^((\s*)|(\t*))*$/); # tracing mode print $logfile "Line $lineCounter\t removing leading spaces\n" if($tracingMode); } else { # otherwise check to see if we've reached the main # part of the document if(m/^\s*\\begin\{document\}/ and !$inFileContents and !$inverbatim) { $inpreamble = 0; # tracing mode print $logfile "Line $lineCounter\t \\begin{document} found, switching indentation searches on. \n" if($tracingMode); } else { # tracing mode if($inpreamble) { print $logfile "Line $lineCounter\t still in PREAMBLE, leaving exisiting leading space (see indentPreamble)\n" if($tracingMode); } elsif($inverbatim) { print $logfile "Line $lineCounter\t in VERBATIM-LIKE environment, leaving exisiting leading space\n" if($tracingMode); } elsif($inIndentBlock) { print $logfile "Line $lineCounter\t in NO INDENT BLOCK, leaving exisiting leading space\n" if($tracingMode); } } } # \END{ENVIRONMENTS}, or CLOSING } or CLOSING ] # \END{ENVIRONMENTS}, or CLOSING } or CLOSING ] # \END{ENVIRONMENTS}, or CLOSING } or CLOSING ] # check to see if we're ending a filecontents environment if( $_ =~ m/^\s*\\end\{(.*?)\}/ and $fileContentsEnvironments{$1} and $inFileContents){ print $logfile "Line $lineCounter\t Found END of filecontents environment (see fileContentsEnvironments)\n" if($tracingMode); $inFileContents = 0; } # set the delimiters switch $delimiters = @masterIndentationArrayOfHashes?$masterIndentationArrayOfHashes[-1]{alignmentDelimiters}:0; if($inverbatim){ print $logfile "Line $lineCounter\t $masterSettings{logFilePreferences}{traceModeDecreaseIndent} PHASE 1: in VERBATIM-LIKE environment, looking for $masterIndentationArrayOfHashes[-1]{end} \n" if($tracingMode); } elsif($inIndentBlock) { print $logfile "Line $lineCounter\t in NO INDENT BLOCK, doing nothing\n" if($tracingMode); } elsif($delimiters) { print $logfile "Line $lineCounter\t $masterSettings{logFilePreferences}{traceModeDecreaseIndent} PHASE 1: in ALIGNMENT BLOCK environment, looking for $masterIndentationArrayOfHashes[-1]{end}\n" if($tracingMode); } elsif($inpreamble and !$inFileContents) { print $logfile "Line $lineCounter\t In preamble, looking for \\begin{document}\n" if($tracingMode); } elsif($inpreamble and $inFileContents) { print $logfile "Line $lineCounter\t In preamble, in filecontents environment\n" if($tracingMode); } else { print $logfile "Line $lineCounter\t $masterSettings{logFilePreferences}{traceModeDecreaseIndent} PHASE 1: looking for reasons to DECREASE indentation of CURRENT line \n" if($tracingMode); } # check to see if we have \end{something} or \] &at_end_of_env_or_eq() unless ($inpreamble or $inIndentBlock); # check to see if we have %* \end{something} for alignment blocks # outside of environments &end_command_with_alignment(); # check to see if we're at the end of a noindent # block %\end{noindent} &at_end_noindent(); # only check for unmatched braces if we're not in # a verbatim-like environment or in the preamble or in a # noIndentBlock or in a delimiter block if(!($inverbatim or $inpreamble or $inIndentBlock or $delimiters)) { # The check for closing } and ] relies on counting, so # we have to remove trailing comments so that any {, }, [, ] # that are found after % are not counted # # note that these lines are NOT in @lines, so we # have to store the $trailingcomments to put # back on after the counting # # note the use of (?",$fileName); print OUTPUTFILE @lines; close(OUTPUTFILE); } # if -o is active then output to $ARGV[1] if($outputToFile) { open(OUTPUTFILE,">",$ARGV[1]); print OUTPUTFILE @lines; close(OUTPUTFILE); print $logfile "Output from indentation written to $ARGV[1].\n"; } # final line of the logfil print $logfile "\n",$masterSettings{logFilePreferences}{endLogFileWith}; # close the log file close($logfile); exit(0); sub indent_if_else_fi{ # PURPOSE: set indentation of line that contains \else, \fi command # # # @masterIndentationArrayOfHashes could be empty -- if so, exit return unless @masterIndentationArrayOfHashes; return unless $constructIfElseFi{$masterIndentationArrayOfHashes[-1]{name}}; # look for \fi if( $_ =~ m/^\s*\\fi/) { # tracing mode print $logfile "Line $lineCounter\t \\fi command found, matching: \\",$masterIndentationArrayOfHashes[-1]{name}, "\n" if($tracingMode); &decrease_indent($masterIndentationArrayOfHashes[-1]{name}); } # look for \else or \or elsif( $_ =~ m/^\s*\\else/ or $_ =~ m/^\s*\\or/ ) { # tracing mode print $logfile "Line $lineCounter\t \\else command found, matching: \\",$masterIndentationArrayOfHashes[-1]{name}, "\n" if($tracingMode); print $logfile "Line $lineCounter\t decreasing indent, still looking for \\fi to match \\",¤t_indentation_names(), "\n" if($tracingMode); # finding an \else or \or command removes the *indentation*, but not the entry from the master hash $masterIndentationArrayOfHashes[-1]{indent}=""; } } sub indent_after_if_else_fi{ # PURPOSE: set indentation *after* \if construct such as # # \ifnum\x=2 # # # \else # # # \fi # # How to read /^\s*\\(if.*?)(\s|\\|\#) # # ^\s* begins with multiple spaces (possibly none) # \\(if.*?)(\s|\\|\#) matches \if... up to either a # space, a \, or a # # Note: this won't match \if.*\fi if( $_ =~ m/^\s*\\(if.*?)(\s|\\|\#)/ and $_ !~ m/^\s*\\(if.*?\\fi)/ and $constructIfElseFi{$1}) { # tracing mode print $logfile "Line $lineCounter\t ifelsefi construct found: $1 \n" if($tracingMode); &increase_indent({name=>$1,type=>"ifelseif"}); } elsif(@masterIndentationArrayOfHashes) { if( ($_ =~ m/^\s*\\else/ or $_ =~ m/^\s*\\or/ ) and $constructIfElseFi{$masterIndentationArrayOfHashes[-1]{name}}) { # tracing mode print $logfile "Line $lineCounter\t setting indent *after* \\else or \\or command found for $masterIndentationArrayOfHashes[-1]{name} \n" if($tracingMode); # recover the indentation to be implemented *after* the \else or \or $masterIndentationArrayOfHashes[-1]{indent}=$indentRules{$masterIndentationArrayOfHashes[-1]{name}}||$defaultIndent unless ($noAdditionalIndent{$masterIndentationArrayOfHashes[-1]{name}}); } } } sub indent_item{ # PURPOSE: when considering environments that can contain items, such # as enumerate, itemize, etc, this subroutine sets the indentation for the item *itself* return unless(scalar(@masterIndentationArrayOfHashes)>1); return unless $indentAfterItems{$masterIndentationArrayOfHashes[-2]{name}}; if( $_ =~ m/^\s*\\(.*?)(\[|\s)/ and $itemNames{$1}){ # tracing mode print $logfile "Line $lineCounter\t $1 found within ",$masterIndentationArrayOfHashes[-1]{name}," environment (see indentAfterItems and itemNames)\n" if($tracingMode); if($itemNames{$masterIndentationArrayOfHashes[-1]{name}}) { print $logfile "Line $lineCounter\t $1 found - neutralizing indentation from previous ",$masterIndentationArrayOfHashes[-1]{name},"\n" if($tracingMode); &decrease_indent($1); } } } sub indent_after_item{ # PURPOSE: Set the indentation *after* the item # This matches a line that begins with # # \item # \item[ # \myitem # \myitem[ # # or anything else specified in itemNames # return unless @masterIndentationArrayOfHashes; return unless $indentAfterItems{$masterIndentationArrayOfHashes[-1]{name}}; if( $_ =~ m/^\s*\\(.*?)(\[|\s)/ and $itemNames{$1}) { # tracing mode print $logfile "Line $lineCounter\t $1 found within ",$masterIndentationArrayOfHashes[-1]{name}," environment (see indentAfterItems and itemNames)\n" if($tracingMode); &increase_indent({name=>$1,type=>"item"}); } } sub begin_command_with_alignment{ # PURPOSE: This matches # %* \begin{tabular} # with any number of spaces (possibly none) between # the * and \begin{noindent}. # # the comment symbol IS indended! # # This is to align blocks that contain delimeters that # are NOT contained in an alignment block in the usual way, e.g # \matrix{ # %* \begin{tabular} # 1 & 2 \\ # 3 & 4 \\ # %* \end{tabular} # } if( $_ =~ m/^\s*%\*\s*\\begin\{(.*?)\}/ and $lookForAlignDelims{$1}) { # increase the indentation &increase_indent({name=>$1, alignmentDelimiters=>1, type=>"environment", begin=>"\\begin{$1}", end=>"\\end{$1}"}); # tracing mode print $logfile "Line $lineCounter\t Delimiter environment started: $1 (see lookForAlignDelims)\n" if($tracingMode); } } sub end_command_with_alignment{ # PURPOSE: This matches # %* \end{tabular} # with any number of spaces (possibly none) between # the * and \end{tabular} (or any other name used from # lookFroAlignDelims) # # Note: the comment symbol IS indended! # # This is to align blocks that contain delimeters that # are NOT contained in an alignment block in the usual way, e.g # \matrix{ # %* \begin{tabular} # 1 & 2 \\ # 3 & 4 \\ # %* \end{tabular} # } return unless @masterIndentationArrayOfHashes; return unless $masterIndentationArrayOfHashes[-1]{alignmentDelimiters}; if( $_ =~ m/^\s*%\*\s*\\end\{(.*?)\}/ and $lookForAlignDelims{$1}) { # same subroutine used at the end of regular tabular, align, etc # environments if($delimiters) { &print_aligned_block(); &decrease_indent($1); } else { # tracing mode print $logfile "Line $lineCounter\t FYI: did you mean to start a delimiter block on a previous line? \n" if($tracingMode); print $logfile "Line $lineCounter\t perhaps using %* \\begin{$1}\n" if($tracingMode); } } } sub indent_heading{ # PURPOSE: This matches # \part # \chapter # \section # \subsection # \subsubsection # \paragraph # \subparagraph # # and anything else listed in indentAfterHeadings # # This subroutine specifies the indentation for the # heading itself, i.e the line that has \chapter, \section etc if( $_ =~ m/^\s*\\(.*?)(\[|{)/ and $indentAfterHeadings{$1}){ # tracing mode print $logfile "Line $lineCounter\t Heading found: $1 \n" if($tracingMode); # get the heading settings, it's a hash within a hash my %currentHeading = %{$indentAfterHeadings{$1}}; # $previousHeadingLevel: scalar that stores which heading # we are under: \part, \chapter, etc my $previousHeadingLevel=0; # form an array of the headings available my @headingStore=(); foreach my $env (@masterIndentationArrayOfHashes){ if($env->{type} eq 'heading'){ push(@headingStore,$env->{name}); # update heading level $previousHeadingLevel= $env->{headinglevel}; } } # if current heading level < old heading level, if($currentHeading{level}<$previousHeadingLevel) { # decrease indentation, but only if # specified in indentHeadings. Note that this check # needs to be done here- decrease_indent won't # check a nested hash if(scalar(@headingStore)) { while($currentHeading{level}<$previousHeadingLevel and scalar(@headingStore)) { my $higherHeadingName = pop(@headingStore); my %higherLevelHeading = %{$indentAfterHeadings{$higherHeadingName}}; # tracing mode print $logfile "Line $lineCounter\t stepping UP heading level from $higherHeadingName \n" if($tracingMode); &decrease_indent($higherHeadingName) if($higherLevelHeading{indent}); $previousHeadingLevel=$higherLevelHeading{level}; } # put the heading name back in to storage push(@headingStore,$1); } } elsif($currentHeading{level}==$previousHeadingLevel) { if(scalar(@headingStore)) { my $higherHeadingName = pop(@headingStore); my %higherLevelHeading = %{$indentAfterHeadings{$higherHeadingName}}; &decrease_indent($higherHeadingName) if($higherLevelHeading{indent}); } } } } sub indent_after_heading{ # PURPOSE: This matches # \part # \chapter # \section # \subsection # \subsubsection # \paragraph # \subparagraph # # and anything else listed in indentAfterHeadings # # This subroutine is specifies the indentation for # the text AFTER the heading, i.e the body of conent # in each \chapter, \section, etc if( $_ =~ m/^\s*\\(.*?)(\[|{)/ and $indentAfterHeadings{$1}) { # get the heading settings- it's a hash within a hash my %currentHeading = %{$indentAfterHeadings{$1}}; &increase_indent({name=>$1,type=>"heading",headinglevel=>$currentHeading{level}}) if($currentHeading{indent}); } } sub at_end_noindent{ # PURPOSE: This matches # % \end{noindent} # with any number of spaces (possibly none) between # the comment and \end{noindent}. # # the comment symbol IS indended! # # This is for blocks of code that the user wants # to leave untouched- similar to verbatim blocks if( $_ =~ m/^%\s*\\end\{(.*?)\}/ and $noIndentBlock{$1}) { $inIndentBlock=0; # tracing mode print $logfile "Line $lineCounter\t % \\end{no indent block} found, switching inIndentBlock OFF \n" if($tracingMode); } } sub at_beg_noindent{ # PURPOSE: This matches # % \begin{noindent} # with any number of spaces (possibly none) between # the comment and \begin{noindent}. # # the comment symbol IS indended! # # This is for blocks of code that the user wants # to leave untouched- similar to verbatim blocks if( $_ =~ m/^%\s*\\begin\{(.*?)\}/ and $noIndentBlock{$1}) { $inIndentBlock = 1; # tracing mode print $logfile "Line $lineCounter\t % \\begin{no indent block} found, switching inIndentBlock ON \n" if($tracingMode); } } sub start_command_or_key_unmatched_brackets{ # PURPOSE: This matches # \pgfplotstablecreatecol[... # # or any other command/key that has brackets [ ] # split across lines specified in the # hash tables, %checkunmatchedbracket # # How to read: ^\s*(\\)?(.*?)(\[\s*) # # ^ line begins with # \s* any (or no)spaces # (\\)? matches a \ backslash but not necessarily # (.*?) non-greedy character match and store the result # ((?$commandname,matchedBRACKETS=>$matchedBRACKETS,type=>'splitBrackets'}); } } } sub end_command_or_key_unmatched_brackets{ # PURPOSE: Check for the closing BRACKET of a command that # splits its BRACKETS across lines, such as # # \pgfplotstablecreatecol[ ... # # It works by checking if we have any entries # in the array @masterIndentationArrayOfHashes, and making # sure that we're not starting another command/key # that has split BRACKETS (nesting). # # It also checks that the line is not commented. # # We count the number of [ and ADD to the counter # ] and SUBTRACT to the counter return unless @masterIndentationArrayOfHashes; return unless ($masterIndentationArrayOfHashes[-1]{type} eq 'splitBrackets'); print $logfile "Line $lineCounter\t Searching for closing BRACKET ] $masterIndentationArrayOfHashes[-1]{name}\n" if($tracingMode); if(!($_ =~ m/^\s*(\\)?(.*?)(\s*\[)/ and ($checkunmatchedbracket{$2} or $alwaysLookforSplitBrackets)) and $_ !~ m/^\s*%/) { # get the details of the most recent command name my $commandname = $masterIndentationArrayOfHashes[-1]{name}; my $matchedBRACKETS = $masterIndentationArrayOfHashes[-1]{matchedBRACKETS}; # match [ but don't match \[ $matchedBRACKETS++ while ($_ =~ m/(? 0 ) { # tracing mode print $logfile "Line $lineCounter\t Found opening BRACE { $commandname\n" if($tracingMode); &increase_indent({name=>$commandname, matchedbraces=>$matchedbraces, lookforelse=>$lookforelse, countzeros=>0, type=>"splitbraces"}); } elsif($matchedbraces<0) { # if $matchedbraces < 0 then we must be matching # braces from a previous split-braces command # keep matching { OR }, and don't match \{ or \} while ($_ =~ m/(((?$2, type=>"environment", begin=>"\\begin{$2}", end=>"\\end{$2}"}); # check for verbatim-like environments if($verbatimEnvironments{$2}){ $inverbatim = 1; # tracing mode print $logfile "Line $lineCounter\t \\begin{verbatim-like} found, $2, switching ON verbatim \n" if($tracingMode); # remove the key and value from %lookForAlignDelims hash # to avoid any further confusion if($lookForAlignDelims{$2}) { print $logfile "WARNING\n\t Line $lineCounter\t $2 is in *both* lookForAlignDelims and verbatimEnvironments\n"; print $logfile "\t\t\t ignoring lookForAlignDelims and prioritizing verbatimEnvironments\n"; print $logfile "\t\t\t Note that you only get this message once per environment\n"; delete $lookForAlignDelims{$2}; } } } } sub at_end_of_env_or_eq{ # PURPOSE: Check if we're at the END of an environment # or at the END of a displayed equation \] # # This subroutine checks for matches of the form # # \end{environmentname} # or # \] # # Note: environmentname can begin with a backslash # which might happen in a sty or cls file. # # It also checks to see if the current environment # had alignment delimiters; if so, we need to turn # OFF the $delimiter switch return unless @masterIndentationArrayOfHashes; print $logfile "Line $lineCounter\t looking for \\end{$masterIndentationArrayOfHashes[-1]{name}} \n" if($tracingMode); if( ($_ =~ m/^\s*\\end\{\\?(.*?)\}/ or $_=~ m/^(\\\])/) and $_ !~ m/\s*^%/) { # check if we're at the end of a verbatim-like environment if($verbatimEnvironments{$1}) { $inverbatim = 0; # tracing mode print $logfile "Line $lineCounter\t \\end{verbatim-like} found: $1, switching off verbatim \n" if($tracingMode); print $logfile "Line $lineCounter\t removing leading spaces \n" if($tracingMode); #s/^\ *//; s/^\t+// if($_ ne ""); s/^\s+// if($_ ne ""); } # check if we're in an environment that is looking # to indent after each \item if(scalar(@masterIndentationArrayOfHashes) and $itemNames{$masterIndentationArrayOfHashes[-1]{name}}) { &decrease_indent($masterIndentationArrayOfHashes[-1]{name}); } # if we're at the end of an environment that receives no additional indent, log it, and move on if($noAdditionalIndent{$1}){ print $logfile "Line $lineCounter\t \\end{$1} finished a no-additional-indent environment (see noAdditionalIndent)\n" if($tracingMode); } # some commands contain \end{environmentname}, which # can cause a problem if \begin{environmentname} was not # started previously; if @masterIndentationArrayOfHashes is empty, # then we don't need to check for \end{environmentname} if(@masterIndentationArrayOfHashes) { # check to see if \end{environment} fits with most recent \begin{...} my %previousEnvironment = %{$masterIndentationArrayOfHashes[-1]}; # check to see if we need to turn off alignment # delimiters and output the current block if($masterIndentationArrayOfHashes[-1]{alignmentDelimiters} and ($previousEnvironment{name} eq $1)) { &print_aligned_block(); } # tracing mode print $logfile "Line $lineCounter\t \\end{environment} found: $1 \n" if($tracingMode and !$verbatimEnvironments{$1}); # check to see if \end{environment} fits with most recent \begin{...} if($previousEnvironment{name} eq $1) { # decrease the indentation (if appropriate) print $logfile "Line $lineCounter\t removed $1 from Indentation array\n" if($tracingMode); &decrease_indent($1); } else { # otherwise put the environment name back on the stack print $logfile "Line $lineCounter\t WARNING: \\end{$1} found on its own line, not matched to \\begin{$previousEnvironment{name}}\n" unless ($delimiters or $inverbatim or $inIndentBlock or $1 eq "\\\]"); } # need a special check for \[ and \] if($1 eq "\\\]") { &decrease_indent($1); } } # if we're at the end of the document, we remove all current # indentation- this is especially prominent in examples that # have headings, and the user has chosen to indentAfterHeadings if($1 eq "document" and !$inFileContents and !$inpreamble and !$delimiters and !$inverbatim and !$inIndentBlock and @masterIndentationArrayOfHashes) { @masterIndentationArrayOfHashes=(); # tracing mode if($tracingMode) { print $logfile "Line $lineCounter\t \\end{$1} found, emptying indentation array \n" unless ($delimiters or $inverbatim or $inIndentBlock or $1 eq "\\\]"); } } } } sub print_aligned_block{ # PURPOSE: this subroutine does a few things related # to printing blocks of code that contain # delimiters, such as align, tabular, etc # # It does the following # - turns off delimiters switch # - processes the block # - deletes the block $delimiters=0; # tracing mode print $logfile "Line $lineCounter\t Delimiter body FINISHED: $masterIndentationArrayOfHashes[-1]{name}\n" if($tracingMode); # print the current FORMATTED block my @block = &format_block(@{$masterIndentationArrayOfHashes[-1]{block}}); foreach $line (@block) { # add the indentation and add the # each line of the formatted block # to the output # unless this would only create trailing whitespace and the # corresponding option is set unless ($line =~ m/^$/ and $removeTrailingWhitespace) { $line =¤t_indentation().$line; } push(@lines,$line); } } sub format_block{ # PURPOSE: Format a delimited environment such as the # tabular or align environment that contains & # # INPUT: @block array containing unformatted block # from, for example, align, or tabular # OUTPUT: @formattedblock array containing FORMATTED block # @block is the input my @block=@_; # tracing mode print $logfile "\t\tFormatting alignment block: $masterIndentationArrayOfHashes[-1]{name}\n" if($tracingMode); # step the line counter back to the beginning of the block- # it will be increased back to the end of the block in the # loop later on: foreach $row (@tmpblock) $lineCounter -= scalar(@block); # local array variables my @formattedblock; my @tmprow=(); my @tmpblock=(); my @maxmstringsize=(); my @ampersandCount=(); # local scalar variables my $alignrowcounter=-1; my $aligncolcounter=-1; my $tmpstring; my $row; my $column; my $maxmcolstrlength; my $i; my $j; my $fmtstring; my $linebreak; my $maxNumberAmpersands = 0; my $currentNumberAmpersands; my $trailingcomments; # local hash table my %stringsize=(); # loop through the block and count & per line- store the biggest # NOTE: this needs to be done in its own block so that # we can know what the maximum number of & in the block is foreach $row (@block) { # delete trailing comments $trailingcomments=''; if($row =~ m/((? $maxNumberAmpersands ); # put trailing comments back on if($trailingcomments){ $row =~ s/%TC/$trailingcomments/; } } # tracing mode print $logfile "\t\tmaximum number of & in any row: $maxNumberAmpersands\n" if($tracingMode); # loop through the lines in the @block foreach $row (@block){ # get the ampersand count $currentNumberAmpersands = shift(@ampersandCount); # increment row counter $alignrowcounter++; # clear the $linebreak variable $linebreak=''; # check for line break \\ # and don't mess with a line that doesn't have the maximum # number of & if($row =~ m/\\\\/ and $currentNumberAmpersands==$maxNumberAmpersands ) { # remove \\ and all characters that follow # and put it back in later, once the measurement # has been done $row =~ s/(\\\\.*)//; $linebreak = $1; } if($currentNumberAmpersands==$maxNumberAmpersands) { # remove trailing comments $trailingcomments=''; if($row =~ m/((?$maxmcolstrlength) { $maxmcolstrlength = $stringsize{$i.$j}; } } } push(@maxmstringsize,$maxmcolstrlength); } # README: printf( formatting, expression) # # formatting has the form %-50s & %-20s & %-19s # (the numbers have been made up for example) # the - symbols mean that each column should be left-aligned # the numbers represent how wide each column is # the s represents string # the & needs to be inserted # join up the maximum string lengths using "s %-" $fmtstring = join("s & %-",@maxmstringsize); # add an s to the end, and a newline $fmtstring .= "s "; # add %- to the beginning $fmtstring = "%-".$fmtstring; # process the @tmpblock of aligned material foreach $row (@tmpblock) { $linebreak=''; # check for line break \\ if($row =~ m/\\\\/) { # remove \\ and all characters that follow # and put it back in later $row =~ s/(\\\\.*$)//; $linebreak = $1; } if($row =~ m/NOFORMATTING/) { $row =~ s/NOFORMATTING//; $tmpstring=$row; # tracing mode print $logfile "\t\tLine $lineCounter\t maximum number of & NOT found- not aligning delimiters \n" if($tracingMode); } else { # remove trailing comments $trailingcomments=''; if($row =~ m/((?1?"spaces":"space"," before \\\\ (see $masterIndentationArrayOfHashes[-1]{name} spacesBeforeDoubleBackSlash)\n" if($tracingMode); $tmpstring =~ s/\\\\/$spaceString\\\\/; } } # put trailing comments back on if($trailingcomments) { $tmpstring =~ s/%TC/$trailingcomments/; } # tracing mode print $logfile "\t\tLine $lineCounter\t Found maximum number of & so aligning delimiters\n" if($tracingMode); } # remove trailing whitespace if ($removeTrailingWhitespace) { print $logfile "\t\tLine $lineCounter\t removing trailing whitespace from delimiter aligned line\n" if ($tracingMode); $tmpstring =~ s/\s+$/\n/; } push(@formattedblock,$tmpstring); # increase the line counter $lineCounter++; } # return the formatted block @formattedblock; } sub increase_indent{ # PURPOSE: Adjust the indentation # of the current environment, command, etc; # check that it's not an environment # that doesn't want indentation. my %infoHash = %{pop(@_)}; my $command = $infoHash{name}; # check for conflicting hash keys &check_conflicting_keys($command); # quick check for verbatim Environment if($inverbatim){ print $logfile "Line $lineCounter\t currently inverbatim environment, not increasing indentation\n" if($tracingMode); return; } if($indentRules{$command}) { # tracing mode print $logfile "Line $lineCounter\t increasing indent using rule for $command (see indentRules)\n" if($tracingMode); } else { # default indentation if(!($noAdditionalIndent{$command} or $verbatimEnvironments{$command})) { # tracing mode print $logfile "Line $lineCounter\t increasing indent using defaultIndent\n" if($tracingMode); } elsif($noAdditionalIndent{$command}) { # tracing mode print $logfile "Line $lineCounter\t no additional indent added for $command (see noAdditionalIndent)\n" if($tracingMode); } } # add to the master array of hashes push(@masterIndentationArrayOfHashes,\%infoHash); # handle the keys slightly different when dealing with environments or commands if($infoHash{type} eq 'environment'){ # environments if(!$noAdditionalIndent{$command}){ $masterIndentationArrayOfHashes[-1]{indent} = $indentRules{$command}||$defaultIndent; } # check to see if we need to look for alignment delimiters if($lookForAlignDelims{$command}){ # there are two ways to complete the lookForAlignDelims field, either as a scalar # or as a hash, so that we can check for alignDoubleBackSlash. # # tabular: # delims: 1 # alignDoubleBackSlash: 1 # # or, simply, # # tabular: 1 # # We need to perform a check to see which has been done. if(ref($lookForAlignDelims{$command}) eq 'HASH'){ # tabular: # delims: 1 # alignDoubleBackSlash: 1 $masterIndentationArrayOfHashes[-1]{alignmentDelimiters}=defined $lookForAlignDelims{$command}{delims}?$lookForAlignDelims{$command}{delims}:1; $masterIndentationArrayOfHashes[-1]{alignDoubleBackSlash}=defined $lookForAlignDelims{$command}{alignDoubleBackSlash}?$lookForAlignDelims{$command}{alignDoubleBackSlash}:1; $masterIndentationArrayOfHashes[-1]{spacesBeforeDoubleBackSlash}=$lookForAlignDelims{$command}{spacesBeforeDoubleBackSlash}||0; } else { # tabular: 1 $masterIndentationArrayOfHashes[-1]{alignmentDelimiters}=1; $masterIndentationArrayOfHashes[-1]{alignDoubleBackSlash}=1; } if($masterIndentationArrayOfHashes[-1]{alignmentDelimiters}==1){ # tracing mode print $logfile "Line $lineCounter\t Delimiter environment started: $command (see lookForAlignDelims)\n" if($tracingMode); } } } else { # commands, headings, etc if(!$noAdditionalIndent{$command}){ $masterIndentationArrayOfHashes[-1]{indent} = $indentRules{$command}||$defaultIndent; } } # details of noAdditionalIndent to the main hash if($noAdditionalIndent{$command}){ $masterIndentationArrayOfHashes[-1]{noAdditionalIndent} = 'yes'; } } sub decrease_indent{ # PURPOSE: Adjust the indentation # of the current environment; # check that it's not an environment # that doesn't want indentation. # if there is no evidence of indentation, then return return unless(scalar(@masterIndentationArrayOfHashes)); # otherwise get details of the most recent command, environment, item, if, heading, etc my $command = pop(@_); if(!$inverbatim) { print $logfile "Line $lineCounter\t removing ", $masterIndentationArrayOfHashes[-1]{name}, " from masterIndentationArrayOfHashes\n" if($tracingMode); pop(@masterIndentationArrayOfHashes); # tracing mode if($tracingMode) { if(@masterIndentationArrayOfHashes) { print $logfile "Line $lineCounter\t decreasing masterIndentationArrayOfHashes to: ",¤t_indentation_names(),"\n"; } else { print $logfile "Line $lineCounter\t masterIndentationArrayOfHashes now empty \n"; } } } } sub current_indentation{ # PURPOSE: loop through masterIndentationArrayOfHashes and # pull out the indentation, and join it together # if the masterIndentationArrayOfHashes is empty, return an empty string return "" unless(@masterIndentationArrayOfHashes); my $indent; foreach my $env (@masterIndentationArrayOfHashes){ $indent .= defined($env->{indent})?$env->{indent}:''; } return $indent; } sub current_indentation_names{ # PURPOSE: loop through masterIndentationArrayOfHashes and # pull out the list of environment/command names return "masterIndentationArrayOfHashes empty" unless(@masterIndentationArrayOfHashes); my $listOfNames; foreach my $env (@masterIndentationArrayOfHashes){ $listOfNames .= $env->{name}; $listOfNames .= "," unless $env == $masterIndentationArrayOfHashes[-1]; } return $listOfNames; } sub check_conflicting_keys{ # PURPOSE: users may sometimes put an environment in two # hash keys; for example, they might put lstlistings # in both indentRules and in noAdditionalIndent; # in which case, we need a hierachy. # # This subroutine implements such a hierachy, # and deletes the redundant key. # if the user has specified $indentRules{$command} and # $noAdditionalIndent{$command} then they are a bit confused- # we remove the $indentRules{$command} and assume that they # want $noAdditionalIndent{$command} my $command = pop(@_); if(scalar($indentRules{$command}) and $noAdditionalIndent{$command}) { print $logfile "WARNING\n\t Line $lineCounter\t $command is in *both* indentRules and noAdditionalIndent\n"; print $logfile "\t\t\t ignoring indentRules and prioritizing noAdditionalIndent\n"; print $logfile "\t\t\t Note that you only get this message once per command/environment\n"; # remove the key and value from %indentRules hash # to avoid any further confusion delete $indentRules{$command}; } # if the command is in verbatimEnvironments and in indentRules then # remove it from %indentRules hash # to avoid any further confusion if($indentRules{$command} and $verbatimEnvironments{$command}) { # remove the key and value from %indentRules hash # to avoid any further confusion print $logfile "WARNING\n\t Line $lineCounter\t $command is in *both* indentRules and verbatimEnvironments\n"; print $logfile "\t\t\t ignoring indentRules and prioritizing verbatimEnvironments\n"; print $logfile "\t\t\t Note that you only get this message once per environment\n"; delete $indentRules{$command}; } }