package LatexIndent::HiddenChildren; # 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 LatexIndent::Switches qw/$is_t_switch_active $is_tt_switch_active $is_m_switch_active /; use LatexIndent::Tokens qw/%tokens/; use LatexIndent::LogFile qw/$logger/; use LatexIndent::GetYamlSettings qw/%mainSettings/; use Data::Dumper; use Exporter qw/import/; our @EXPORT_OK = qw/find_surrounding_indentation_for_children update_family_tree get_family_tree check_for_hidden_children %familyTree hidden_children_preparation_for_alignment unpack_children_into_body/; # hiddenChildren can be stored in a global array, it doesn't matter what level they're at our %familyTree; our %allChildren; #---------------------------------------------------------- # Discussion surrounding hidden children # # Consider the following latex code # # \begin{one} # body of one # body of one # body of one # \begin{two} # body of two # body of two # body of two # body of two # \end{two} # \end{one} # # From the visual perspective, we might say that and are *nested* children; # from the perspective of latexindent.pl, however, they actually have *the same level*. # # Graphically, you might represent it as follows # # * # / \ # / \ # / \ # O O # # where * represents the 'root' document object, and each 'O' is an environment object; the # first one, on the left, represents and the second one, on the right, represents . # (Remember that the environment regexp does not allow \begin within its body.) # # When processing the document, will be processed *before* . Furthermore, because # and are at the same level, they are not *natural* ancestors of each other; as such, # we say that is a *hidden* child, and that its 'adopted' ancestor is . # # We need to go to a lot of effort to make sure that knows about its ancestors and its # surrounding indentation ( in this case). The subroutines in this file do that effort. #---------------------------------------------------------- sub find_surrounding_indentation_for_children { my $self = shift; # output to logfile $logger->trace("*FamilyTree before update:") if $is_tt_switch_active; $logger->trace( Dumper( \%familyTree ) ) if ($is_tt_switch_active); # update the family tree with ancestors $self->update_family_tree; # output information to the logfile $logger->trace("*FamilyTree after update:") if $is_tt_switch_active; $logger->trace( Dumper( \%familyTree ) ) if ($is_tt_switch_active); while ( my ( $idToSearch, $ancestorToSearch ) = each %familyTree ) { $logger->trace("*Hidden child ID: ,$idToSearch, here are its ancestors:") if $is_t_switch_active; foreach ( @{ ${$ancestorToSearch}{ancestors} } ) { $logger->trace("ID: ${$_}{ancestorID}") if ($is_t_switch_active); my $tmpIndentation = ref( ${$_}{ancestorIndentation} ) eq 'SCALAR' ? ${ ${$_}{ancestorIndentation} } : ${$_}{ancestorIndentation}; $tmpIndentation = $tmpIndentation ? $tmpIndentation : q(); $logger->trace("indentation: '$tmpIndentation'") if ($is_t_switch_active); } } return; } sub update_family_tree { my $self = shift; # loop through the hash $logger->trace("*Updating FamilyTree...") if $is_t_switch_active; while ( my ( $idToSearch, $ancestorToSearch ) = each %familyTree ) { $logger->trace("*current ID: $idToSearch") if ($is_t_switch_active); foreach ( @{ ${$ancestorToSearch}{ancestors} } ) { my $ancestorID = ${$_}{ancestorID}; # construct the natural ancestors my $naturalAncestors = q(); foreach ( @{ ${ $familyTree{$idToSearch} }{ancestors} } ) { $naturalAncestors .= "---" . ${$_}{ancestorID} if ( ${$_}{type} eq "natural" ); } # we only need to update the family tree if the $ancestorID is *not* a natural # ancestor, otherwise everything will be taken care of by the natural ancestor if ( $naturalAncestors !~ m/$ancestorID/ ) { $logger->trace("ancestor: $ancestorID") if ($is_t_switch_active); if ( $familyTree{$ancestorID} ) { $logger->trace("$ancestorID is a key within familyTree, grabbing its ancestors") if ($is_t_switch_active); foreach ( @{ ${ $familyTree{$ancestorID} }{ancestors} } ) { $logger->trace("ancestor of *hidden* child: ${$_}{ancestorID}") if ($is_t_switch_active); my $newAncestorId = ${$_}{ancestorID}; my $type; if ( $naturalAncestors =~ m/$ancestorID/ ) { $type = "natural"; } else { $type = "adopted"; } my $matched = grep { $_->{ancestorID} eq $newAncestorId } @{ ${ $familyTree{$idToSearch} }{ancestors} }; push( @{ ${ $familyTree{$idToSearch} }{ancestors} }, { ancestorID => ${$_}{ancestorID}, ancestorIndentation => ${$_}{ancestorIndentation}, type => $type } ) unless ($matched); } } else { $logger->trace("natural ancestors of $ancestorID: $naturalAncestors") if ($is_t_switch_active); foreach ( @{ ${ $allChildren{$ancestorID} }{ancestors} } ) { my $newAncestorId = ${$_}{ancestorID}; my $type; if ( $naturalAncestors =~ m/$newAncestorId/ ) { $type = "natural"; } else { $type = "adopted"; } my $matched = grep { $_->{ancestorID} eq $newAncestorId } @{ ${ $familyTree{$idToSearch} }{ancestors} }; unless ($matched) { $logger->trace("ancestor of UNHIDDEN child: ${$_}{ancestorID}") if ($is_t_switch_active); push( @{ ${ $familyTree{$idToSearch} }{ancestors} }, { ancestorID => ${$_}{ancestorID}, ancestorIndentation => ${$_}{ancestorIndentation}, type => $type } ); } } } } } } } sub check_for_hidden_children { my $self = shift; my @matched; # grab the matches if ( $is_m_switch_active and ${ $mainSettings{modifyLineBreaks}{textWrapOptions} }{huge} ne "overflow" ) { # if modifyLineBreaks is active, then the IDS can be split across lines my $ifElseFiSpecialBegin = join( "\\R?\\h*", split( //, $tokens{ifelsefiSpecial} ) ); my $BeginwithLineBreaks = join( "\\R?\\h*", split( //, $tokens{beginOfToken} ) ); my $EndwithLineBreaks = join( "\\R?\\h*", split( //, $tokens{endOfToken} ) ); my $BlankLinesWithLineBreaks = join( "\\R?\\h*", split( //, $tokens{blanklines} ) ); @matched = ( ${$self}{body} =~ /(?!$BlankLinesWithLineBreaks)((?:$ifElseFiSpecialBegin)?$BeginwithLineBreaks[-a-z0-9\n]+?$EndwithLineBreaks)/ig ); # remove line breaks and horizontal space from matches $_ =~ s/\R|\h//gs foreach (@matched); } else { @matched = ( ${$self}{body} =~ /((?:$tokens{ifelsefiSpecial})?$tokens{beginOfToken}.[-a-z0-9]+?$tokens{endOfToken})/igs ); } # log file $logger->trace("*Hidden children check for ${$self}{name}") if $is_t_switch_active; $logger->trace( join( "|", @matched ) ) if $is_t_switch_active; my $naturalAncestors = ${$self}{naturalAncestors}; # loop through the hidden children foreach my $match (@matched) { next if $match =~ m/$tokens{verbatim}/; # update the family tree with ancestors of self if ( ${$self}{ancestors} ) { foreach ( @{ ${$self}{ancestors} } ) { my $newAncestorId = ${$_}{ancestorID}; unless ( grep { $_->{ancestorID} eq $newAncestorId } @{ ${ $familyTree{$match} }{ancestors} } ) { my $type = ( $naturalAncestors =~ m/${$_}{ancestorID}/ ) ? "natural" : "adopted"; $logger->trace("Adding ${$_}{ancestorID} to the $type family tree of $match") if ($is_t_switch_active); push( @{ $familyTree{$match}{ancestors} }, { ancestorID => ${$_}{ancestorID}, ancestorIndentation => ${$_}{ancestorIndentation}, type => $type } ); } } } # update the family tree with self unless ( grep { $_->{ancestorID} eq ${$self}{id} } @{ ${ $familyTree{$match} }{ancestors} } ) { my $type = ( $naturalAncestors =~ m/${$self}{id}/ ) ? "natural" : "adopted"; $logger->trace("Adding ${$self}{id} to the $type family tree of hiddenChild $match") if ($is_t_switch_active); push( @{ $familyTree{$match}{ancestors} }, { ancestorID => ${$self}{id}, ancestorIndentation => ${$self}{indentation}, type => $type } ); if ( ${$self}{lookForAlignDelims} ) { $logger->trace("$match needs measuring for ${$self}{name} (see lookForAlignDelims)") if ($is_t_switch_active); push( @{ ${$self}{measureHiddenChildren} }, $match ); } } } } # # AlignmentAtAmpersand routine calculations are below # # PURPOSE: # # Consider the following example, which contains hidden children # # \begin{align} # A & =\begin{array}{cc} %