package LatexIndent::Heading; # 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::Tokens qw/%tokens/; use LatexIndent::Switches qw/$is_m_switch_active $is_t_switch_active $is_tt_switch_active/; use LatexIndent::TrailingComments qw/$trailingCommentRegExp/; use LatexIndent::GetYamlSettings qw/%masterSettings/; use LatexIndent::LogFile qw/$logger/; use LatexIndent::Special qw/$specialBeginBasicRegExp/; use Exporter qw/import/; our @ISA = "LatexIndent::Document"; # class inheritance, Programming Perl, pg 321 our @EXPORT_OK = qw/find_heading construct_headings_levels $allHeadingsRegexp/; our $headingCounter; our @headingsRegexpArray; our $allHeadingsRegexp = q(); sub construct_headings_levels{ my $self = shift; # grab the heading levels my %headingsLevels = %{$masterSettings{indentAfterHeadings}}; # output to log file $logger->trace("*Constructing headings reg exp for example, chapter, section, etc (see indentAfterThisHeading)") if $is_t_switch_active ; # delete the values that have indentAfterThisHeading set to 0 while( my ($headingName,$headingInfo)= each %headingsLevels){ if(!${$headingsLevels{$headingName}}{indentAfterThisHeading}){ $logger->trace("Not indenting after $headingName (see indentAfterThisHeading)") if $is_t_switch_active; delete $headingsLevels{$headingName}; } else { # *all heading* regexp, remembering to put starred headings at the front of the regexp if($headingName =~ m/\*/){ $logger->trace("Putting $headingName at the beginning of the allHeadings regexp, as it contains a *") if $is_t_switch_active ; $allHeadingsRegexp = $headingName.($allHeadingsRegexp eq '' ?q():"|$allHeadingsRegexp"); } else { $logger->trace("Putting $headingName at the END of the allHeadings regexp") if $is_t_switch_active ; $allHeadingsRegexp .= ($allHeadingsRegexp eq '' ?q():"|").$headingName ; } } } # check for a * in the name $allHeadingsRegexp =~ s/\*/\\\*/g; # sort the file extensions by preference my @sortedByLevels = sort { ${$headingsLevels{$a}}{level} <=> $headingsLevels{$b}{level} } keys(%headingsLevels); # it could be that @sortedByLevels is empty; return if !@sortedByLevels; $logger->trace("*All headings regexp: $allHeadingsRegexp") if $is_tt_switch_active; $logger->trace("*Now to construct headings regexp for each level:") if $is_t_switch_active; # loop through the levels, and create a regexp for each (min and max values are the first and last values respectively from sortedByLevels) for(my $i = ${$headingsLevels{$sortedByLevels[0]}}{level}; $i <= ${$headingsLevels{$sortedByLevels[-1]}}{level}; $i++ ){ # level regexp my @tmp = grep { ${$headingsLevels{$_}}{level} == $i } keys %headingsLevels; if(@tmp){ my $headingsAtThisLevel = q(); foreach(@tmp){ # put starred headings at the front of the regexp if($_ =~ m/\*/){ $logger->trace("Putting $_ at the beginning of this regexp (level $i), as it contains a *") if $is_t_switch_active; $headingsAtThisLevel = $_.($headingsAtThisLevel eq '' ?q():"|$headingsAtThisLevel"); } else { $logger->trace("Putting $_ at the END of this regexp (level $i)") if $is_t_switch_active; $headingsAtThisLevel .= ($headingsAtThisLevel eq '' ?q():"|").$_ ; } } # make the stars escaped correctly $headingsAtThisLevel =~ s/\*/\\\*/g; push(@headingsRegexpArray,$headingsAtThisLevel); $logger->trace("Heading level regexp for level $i will contain: $headingsAtThisLevel") if $is_t_switch_active; } } } sub find_heading{ # if there are no headings regexps, there's no point going any further return if!@headingsRegexpArray; my $self = shift; # otherwise loop through the headings regexp $logger->trace("*Searching ${$self}{name} for headings ") if $is_t_switch_active; # loop through each headings match; note that we need to # do it in *reverse* so as to ensure that the lower level headings get matched first of all foreach(reverse(@headingsRegexpArray)){ # the regexp my $headingRegExp = qr/ ( \\($_) # name stored into $2 ) # beginning bit into $1 ( .*? ) # body into $3 (\R*)? # linebreaks at end of body into $4 ((?:\\(?:$allHeadingsRegexp))|$) # up to another heading, or else the end of the file /sx; while(${$self}{body} =~ m/$headingRegExp/){ # log file output $logger->trace("heading found: $2") if $is_t_switch_active; ${$self}{body} =~ s/ $headingRegExp / # create a new heading object my $headingObject = LatexIndent::Heading->new(begin=>q(), body=>$1.$3, end=>q(), afterbit=>($4?$4:q()).($5?$5:q()), name=>$2.":heading", parent=>$2, nameForIndentationSettings=>$2, linebreaksAtEnd=>{ begin=>0, body=>0, end=>0, }, modifyLineBreaksYamlName=>"afterHeading", endImmediatelyFollowedByComment=>0, ); # the settings and storage of most objects has a lot in common $self->get_settings_and_store_new_object($headingObject); ${@{${$self}{children}}[-1]}{replacementText}; /xse; } } } sub get_replacement_text{ my $self = shift; # the replacement text for a heading (chapter, section, etc) needs to put the trailing part back in $logger->trace("Custom replacement text routine for ${$self}{name}") if $is_t_switch_active; ${$self}{replacementText} = ${$self}{id}.${$self}{afterbit}; delete ${$self}{afterbit}; } sub create_unique_id{ my $self = shift; $headingCounter++; ${$self}{id} = "$tokens{afterHeading}$headingCounter"; return; } sub adjust_replacement_text_line_breaks_at_end{ return; } sub yaml_get_object_attribute_for_indentation_settings{ # when looking for noAdditionalIndent or indentRules, we may need to determine # which thing we're looking for, e.g # # chapter: # body: 0 # optionalArguments: 1 # mandatoryArguments: 1 # afterHeading: 0 # # this method returns 'body' by default, but the other objects (optionalArgument, mandatoryArgument, afterHeading) # return their appropriate identifier. my $self = shift; return ${$self}{modifyLineBreaksYamlName}; } sub tasks_particular_to_each_object{ my $self = shift; # search for commands, keys, named grouping braces $self->find_commands_or_key_equals_values_braces; # we need to transfer the details from the modifyLineBreaks of the command # child object to the heading object. # # for example, if we have # # \chapter{some heading here} # # and we want to modify the linebreak before the \chapter command using, for example, # # commands: # CommandStartsOnOwnLine: 1 # # then we need to transfer this information to the heading object if($is_m_switch_active){ $logger->trace("Searching for linebreak preferences immediately infront of ${$self}{parent}") if $is_t_switch_active; foreach(@{${$self}{children}}){ if(${$_}{name} eq ${$self}{parent}){ $logger->trace("Named child found: ${$_}{name}") if $is_t_switch_active; if(defined ${$_}{BeginStartsOnOwnLine}){ $logger->trace("Transferring information from ${$_}{id} (${$_}{name}) to ${$self}{id} (${$self}{name}) for BeginStartsOnOwnLine") if $is_t_switch_active; ${$self}{BeginStartsOnOwnLine} = ${$_}{BeginStartsOnOwnLine}; } else { $logger->trace("No information found in ${$_}{name} for BeginStartsOnOwnLine") if $is_t_switch_active; } last; } } } # search for special begin/end $self->find_special if ${$self}{body} =~ m/$specialBeginBasicRegExp/s; return; } sub add_surrounding_indentation_to_begin_statement{ # almost all of the objects add surrounding indentation to the 'begin' statements, # but some (e.g HEADING) have their own method my $self = shift; $logger->trace("Adding surrounding indentation after (empty, by design!) begin statement of ${$self}{name} (${$self}{id})") if $is_t_switch_active; ${$self}{begin} .= ${$self}{surroundingIndentation}; # add indentation } 1;