package LatexIndent::Verbatim; # 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 Data::Dumper; use Exporter qw/import/; use LatexIndent::Tokens qw/%tokens/; use LatexIndent::GetYamlSettings qw/%mainSettings/; use LatexIndent::Switches qw/$is_t_switch_active $is_tt_switch_active $is_m_switch_active/; use LatexIndent::LogFile qw/$logger/; our @EXPORT_OK = qw/put_verbatim_back_in find_verbatim_environments find_noindent_block find_verbatim_commands find_verbatim_special verbatim_common_tasks %verbatimStorage/; our @ISA = "LatexIndent::Document"; # class inheritance, Programming Perl, pg 321 our $verbatimCounter; our %verbatimStorage; sub find_noindent_block { my $self = shift; # noindent block $logger->trace('*Searching for NOINDENTBLOCK (see noIndentBlock)') if $is_t_switch_active; $logger->trace( Dumper( \%{ $mainSettings{noIndentBlock} } ) ) if ($is_tt_switch_active); while ( my ( $noIndentBlock, $yesno ) = each %{ $mainSettings{noIndentBlock} } ) { # integrity check on the field for noIndentBlock if ( ref($yesno) eq "HASH" ) { if ( defined ${$yesno}{lookForThis} and !${$yesno}{lookForThis} ) { $logger->trace(" *not* looking for $noIndentBlock as lookForThis: 0") if $is_t_switch_active; next; } if ( not defined ${$yesno}{name} ) { if ( not defined ${$yesno}{begin} ) { $logger->trace(" *not* looking for $noIndentBlock as $noIndentBlock:begin not specified") if $is_t_switch_active; next; } elsif ( not defined ${$yesno}{end} ) { $logger->trace(" *not* looking for $noIndentBlock as $noIndentBlock:end not specified") if $is_t_switch_active; next; } } elsif ( defined ${$yesno}{begin} or defined ${$yesno}{end} ) { $logger->trace( " *not* looking for $noIndentBlock as $noIndentBlock:name specified with begin and/or end") if $is_t_switch_active; next; } } elsif ( ref($yesno) ne "HASH" and !$yesno ) { $logger->trace(" *not* looking for $noIndentBlock as $noIndentBlock:$yesno") if $is_t_switch_active; next; } # if we've made it this far, then we're good to go my $noIndentRegExp; my $noIndentBlockObj; if ( ref($yesno) eq "HASH" ) { # default value of begin and end if ( defined ${$yesno}{name} and not defined ${$yesno}{begin} and not defined ${$yesno}{end} ) { ${$yesno}{begin} = "\\\\begin\\{(${$yesno}{name})\\}"; ${$yesno}{end} = "\\\\end\\{\\2\\}"; $logger->trace("looking for regex based $noIndentBlock, name: ${$yesno}{name}") if $is_t_switch_active; $logger->trace("begin not specified for $noIndentBlock, setting default ${$yesno}{begin}") if $is_t_switch_active; $logger->trace("end not specified for $noIndentBlock, setting default ${$yesno}{end}") if $is_t_switch_active; } # default value of body if ( not defined ${$yesno}{body} ) { $logger->trace("looking for regex based $noIndentBlock, begin: ${$yesno}{begin}, end: ${$yesno}{end}") if $is_t_switch_active; $logger->trace("body not specified for $noIndentBlock, setting default .*?") if $is_t_switch_active; ${$yesno}{body} = qr/.*?/sx; } else { $logger->trace("looking for regex based $noIndentBlock") if $is_t_switch_active; $logger->trace("begin: ${$yesno}{begin}") if $is_t_switch_active; $logger->trace("body: ${$yesno}{body}") if $is_t_switch_active; $logger->trace("end: ${$yesno}{end}") if $is_t_switch_active; } $noIndentRegExp = qr/ (${$yesno}{begin}) (${$yesno}{body}) (${$yesno}{end}) /sx; } else { $logger->trace("looking for $noIndentBlock:$yesno noIndentBlock") if $is_t_switch_active; ( my $noIndentBlockSpec = $noIndentBlock ) =~ s/\*/\\*/sg; $noIndentRegExp = qr/ ( (?!<\\) % (?:\h|(?!<\\)%)* # possible horizontal spaces \\begin\{ ($noIndentBlockSpec) # environment name captured into $2 \} # % \begin{noindentblock} statement ) # begin statement captured into $1 ( .*? # non-greedy match (body) into $3 ) ( (?!<\\) % # % (?:\h|(?!<\\)%)* # possible horizontal spaces \\end\{\2\} # % \end{noindentblock} statement ) # end statement captured into $4 /sx; } while ( ${$self}{body} =~ m/$noIndentRegExp/sx ) { # create a new Verbatim object if ( ref($yesno) eq "HASH" and not defined ${$yesno}{name} ) { # user defined begin and end statements $noIndentBlockObj = LatexIndent::Verbatim->new( begin => $1, body => $2, end => $3, name => $noIndentBlock, type => "noindentblock", modifyLineBreaksYamlName => "verbatim", ); } else { # specified by name (entry:1 or entry: name: regex) $noIndentBlockObj = LatexIndent::Verbatim->new( begin => $1, body => $3, end => $4, name => $2, type => "noindentblock", modifyLineBreaksYamlName => "verbatim", ); } # give unique id $noIndentBlockObj->create_unique_id; # verbatim children go in special hash $verbatimStorage{ ${$noIndentBlockObj}{id} } = $noIndentBlockObj; # log file output $logger->trace("NOINDENTBLOCK found: ${$noIndentBlockObj}{name}") if $is_t_switch_active; # remove the environment block, and replace with unique ID ${$self}{body} =~ s/$noIndentRegExp/${$noIndentBlockObj}{id}/sx; $logger->trace("replaced with ID: ${$noIndentBlockObj}{id}") if $is_t_switch_active; # possible decoration in log file $logger->trace( ${ $mainSettings{logFilePreferences} }{showDecorationFinishCodeBlockTrace} ) if ${ $mainSettings{logFilePreferences} }{showDecorationFinishCodeBlockTrace}; } } return; } sub find_verbatim_environments { my $self = shift; # verbatim environments $logger->trace('*Searching for VERBATIM environments (see verbatimEnvironments)') if $is_t_switch_active; $logger->trace( Dumper( \%{ $mainSettings{verbatimEnvironments} } ) ) if ($is_tt_switch_active); while ( my ( $verbEnv, $yesno ) = each %{ $mainSettings{verbatimEnvironments} } ) { my $verbEnvSpec; # integrity check on the field for noIndentBlock if ( ref($yesno) eq "HASH" ) { if ( defined ${$yesno}{lookForThis} and !${$yesno}{lookForThis} ) { $logger->trace(" *not* looking for $verbEnv as lookForThis: 0") if $is_t_switch_active; next; } elsif ( not defined ${$yesno}{name} ) { $logger->trace(" *not* looking for $verbEnv as $verbEnv:name not specified") if $is_t_switch_active; next; } else { $logger->trace("looking for VERBATIM-environments $verbEnv, name: ${$yesno}{name}") if $is_t_switch_active; $verbEnvSpec = ${$yesno}{name}; } } elsif ( ref($yesno) ne "HASH" and $yesno ) { $logger->trace("looking for $verbEnv:$yesno environments") if $is_t_switch_active; ( $verbEnvSpec = $verbEnv ) =~ s/\*/\\*/sg; } else { $logger->trace(" *not* looking for $verbEnv as $verbEnv:$yesno") if $is_t_switch_active; next; } # if we've made it this far, then we're good to go my $verbatimRegExp = qr/ ( \\begin\{ ($verbEnvSpec) # environment name captured into $2 \} # \begin{} statement captured into $1 ) ( .*? # non-greedy match (body) into $3 ) # any character, but not \\begin ( \\end\{\2\} # \end{} statement captured into $4 ) (\h*)? # possibly followed by horizontal space (\R)? # possibly followed by a line break /sx; while ( ${$self}{body} =~ m/$verbatimRegExp/sx ) { # create a new Verbatim object my $verbatimBlock = LatexIndent::Verbatim->new( begin => $1, body => $3, end => $4, name => $2, type => "environment", modifyLineBreaksYamlName => "verbatim", linebreaksAtEnd => { end => $6 ? 1 : 0, }, horizontalTrailingSpace => $5 ? $5 : q(), aliases => { # begin statements BeginStartsOnOwnLine => "VerbatimBeginStartsOnOwnLine", # after end statements EndFinishesWithLineBreak => "VerbatimEndFinishesWithLineBreak", }, ); # there are common tasks for each of the verbatim objects $verbatimBlock->verbatim_common_tasks; # verbatim children go in special hash $verbatimStorage{ ${$verbatimBlock}{id} } = $verbatimBlock; # log file output $logger->trace("*VERBATIM environment found: ${$verbatimBlock}{name}") if $is_t_switch_active; # remove the environment block, and replace with unique ID ${$self}{body} =~ s/$verbatimRegExp/${$verbatimBlock}{replacementText}/sx; $logger->trace("replaced with ID: ${$verbatimBlock}{id}") if $is_t_switch_active; # possible decoration in log file $logger->trace( ${ $mainSettings{logFilePreferences} }{showDecorationFinishCodeBlockTrace} ) if ${ $mainSettings{logFilePreferences} }{showDecorationFinishCodeBlockTrace}; } } return; } sub find_verbatim_commands { my $self = shift; # verbatim commands need to be treated separately to verbatim environments; # note that, for example, we could quite reasonably have \lstinline!%!, which # would need to be found *before* trailing comments have been removed. Similarly, # verbatim commands need to be put back in *after* trailing comments have been put # back in $logger->trace('*Searching for VERBATIM commands (see verbatimCommands)') if $is_t_switch_active; $logger->trace( Dumper( \%{ $mainSettings{verbatimCommands} } ) ) if ($is_tt_switch_active); while ( my ( $verbCommand, $yesno ) = each %{ $mainSettings{verbatimCommands} } ) { my $verbCommandSpec; # integrity check on the field for noIndentBlock if ( ref($yesno) eq "HASH" ) { if ( defined ${$yesno}{lookForThis} and !${$yesno}{lookForThis} ) { $logger->trace(" *not* looking for $verbCommand as lookForThis: 0") if $is_t_switch_active; next; } elsif ( not defined ${$yesno}{name} ) { $logger->trace(" *not* looking for $verbCommand as $verbCommand:name not specified") if $is_t_switch_active; next; } else { $logger->trace("looking for regex based VERBATIM-commands $verbCommand, name: ${$yesno}{name}") if $is_t_switch_active; $verbCommandSpec = ${$yesno}{name}; } } elsif ( ref($yesno) ne "HASH" and $yesno ) { $logger->trace("looking for $verbCommand:$yesno Commands") if $is_t_switch_active; $verbCommandSpec = $verbCommand; } else { $logger->trace("*not* looking for $verbCommand as $verbCommand:$yesno") if $is_t_switch_active; next; } # if we've made it this far, then we're good to go my $verbatimRegExp = qr/ ( \\($verbCommandSpec) # name of command into $2 \h* ) ( \[ (?: (?! (?:(?new( begin => $1 . ( $3 ? $3 : q() ) . $4, body => $5, end => $4, name => $2, type => "command", modifyLineBreaksYamlName => "verbatim", linebreaksAtEnd => { end => $7 ? 1 : 0, }, horizontalTrailingSpace => $6 ? $6 : q(), aliases => { # begin statements BeginStartsOnOwnLine => "VerbatimBeginStartsOnOwnLine", # after end statements EndFinishesWithLineBreak => "VerbatimEndFinishesWithLineBreak", }, optArg => $3 ? $3 : q(), ); # there are common tasks for each of the verbatim objects $verbatimCommand->verbatim_common_tasks; # output, if desired $logger->trace( Dumper($verbatimCommand), 'ttrace' ) if ($is_tt_switch_active); # check for nested verbatim commands if ( ${$verbatimCommand}{body} =~ m/($tokens{verbatimInline}\d+$tokens{endOfToken})/s ) { my $verbatimNestedID = $1; my $verbatimBody = ${ $verbatimStorage{$verbatimNestedID} }{begin} . ${ $verbatimStorage{$verbatimNestedID} }{body} . ${ $verbatimStorage{$verbatimNestedID} }{end}; ${$verbatimCommand}{body} =~ s/$verbatimNestedID/$verbatimBody/s; delete $verbatimStorage{$verbatimNestedID}; } # verbatim children go in special hash $verbatimStorage{ ${$verbatimCommand}{id} } = $verbatimCommand; # log file output $logger->trace("*VERBATIM command found: ${$verbatimCommand}{name}") if $is_t_switch_active; # remove the environment block, and replace with unique ID ${$self}{body} =~ s/$verbatimRegExp/${$verbatimCommand}{replacementText}/sx; $logger->trace("replaced with ID: ${$verbatimCommand}{id}") if $is_t_switch_active; # possible decoration in log file $logger->trace( ${ $mainSettings{logFilePreferences} }{showDecorationFinishCodeBlockTrace} ) if ${ $mainSettings{logFilePreferences} }{showDecorationFinishCodeBlockTrace}; } } return; } sub find_verbatim_special { my $self = shift; # loop through specialBeginEnd while ( my ( $specialName, $BeginEnd ) = each %{ $mainSettings{specialBeginEnd} } ) { # only classify special Verbatim if lookForThis is 'verbatim' if ( ( ref($BeginEnd) eq "HASH" ) and ${$BeginEnd}{lookForThis} =~ m/v/s and ${$BeginEnd}{lookForThis} eq 'verbatim' ) { $logger->trace('*Searching for VERBATIM special (see specialBeginEnd)') if $is_t_switch_active; my $verbatimRegExp = qr/ ( ${$BeginEnd}{begin} ) ( .*? ) ( ${$BeginEnd}{end} ) (\h*)? # possibly followed by horizontal space (\R)? # possibly followed by a line break /sx; while ( ${$self}{body} =~ m/$verbatimRegExp/sx ) { # create a new Verbatim object my $verbatimBlock = LatexIndent::Verbatim->new( begin => $1, body => $2, end => $3, name => $specialName, modifyLineBreaksYamlName => "specialBeginEnd", linebreaksAtEnd => { end => $5 ? 1 : 0, }, horizontalTrailingSpace => $4 ? $4 : q(), type => "special", aliases => { # begin statements BeginStartsOnOwnLine => "SpecialBeginStartsOnOwnLine", # after end statements EndFinishesWithLineBreak => "SpecialEndFinishesWithLineBreak", }, ); # there are common tasks for each of the verbatim objects $verbatimBlock->verbatim_common_tasks; # verbatim children go in special hash $verbatimStorage{ ${$verbatimBlock}{id} } = $verbatimBlock; # log file output $logger->trace("*VERBATIM special found: $specialName") if $is_t_switch_active; # remove the special block, and replace with unique ID ${$self}{body} =~ s/$verbatimRegExp/${$verbatimBlock}{replacementText}/sx; $logger->trace("replaced with ID: ${$verbatimBlock}{id}") if $is_t_switch_active; # possible decoration in log file $logger->trace( ${ $mainSettings{logFilePreferences} }{showDecorationFinishCodeBlockTrace} ) if ${ $mainSettings{logFilePreferences} }{showDecorationFinishCodeBlockTrace}; } } } } sub put_verbatim_back_in { my $self = shift; my %input = @_; my $verbatimCount = 0; my $toMatch = q(); if ( $input{match} eq "everything-except-commands" ) { $toMatch = "noindentblockenvironmentspeciallinesprotect"; } else { $toMatch = "command"; } # count the number of non-command verbatim objects while ( my ( $key, $child ) = each %verbatimStorage ) { ${$child}{type} = "environment" if !( defined ${$child}{type} ); $verbatimCount++ if ( $toMatch =~ m/${$child}{type}/ ); } return unless ( $verbatimCount > 0 ); # search for environments/commands $logger->trace('*Putting verbatim back in') if $is_t_switch_active; $logger->trace('pre-processed body:') if $is_tt_switch_active; $logger->trace( ${$self}{body} ) if ($is_tt_switch_active); # loop through document children hash my $verbatimFound = 0; while ( $verbatimFound < $verbatimCount ) { while ( my ( $verbatimID, $child ) = each %verbatimStorage ) { if ( $toMatch =~ m/${$child}{type}/ ) { if ( ${$self}{body} =~ m/$verbatimID/m ) { # possibly remove trailing line break if ( $is_m_switch_active and defined ${$child}{EndFinishesWithLineBreak} and ${$child}{EndFinishesWithLineBreak} == -1 and ${$self}{body} =~ m/$verbatimID\h*\R/s ) { $logger->trace("m-switch active, removing trailing line breaks from ${$child}{name}") if $is_t_switch_active; ${$self}{body} =~ s/$verbatimID(\h*)?(\R|\h)*/$verbatimID /s; } # line protection mode can allow line breaks to be removed # at end of verbatim; these need to be added back in # # see # # test-cases/line-switch-test-cases/environments-simple-nested-mod13.tex # if ( ${$child}{type} eq "linesprotect" ) { # remove leading space ahead of verbatim ID ${$self}{body} =~ s/^\h*$verbatimID/$verbatimID/m; if ( $is_m_switch_active and ${$self}{body} =~ m/$verbatimID\h*\S/s ) { ${$self}{body} =~ s/$verbatimID\h*(\S)/$verbatimID\n$1/s; } } # replace ids with body ${$self}{body} =~ s/$verbatimID/${$child}{begin}${$child}{body}${$child}{end}/s; # log file info $logger->trace('Body now looks like:') if $is_tt_switch_active; $logger->trace( ${$self}{body}, 'ttrace' ) if ($is_tt_switch_active); # delete the child so it won't be operated upon again delete $verbatimStorage{$verbatimID}; $verbatimFound++; } elsif ( $is_m_switch_active and ${ $mainSettings{modifyLineBreaks}{textWrapOptions} }{columns} > 1 and ${ $mainSettings{modifyLineBreaks}{textWrapOptions} }{huge} ne "overflow" and ${$self}{body} !~ m/${$child}{id}/ ) { $logger->trace( "$verbatimID not found in body using /m matching, it may have been split across line (see modifyLineBreaks: textWrapOptions)" ) if ($is_t_switch_active); # search for a version of the verbatim ID that may have line breaks my $verbatimIDwithLineBreaks = join( "\\R?", split( //, $verbatimID ) ); my $verbatimIDwithLineBreaksRegExp = qr/$verbatimIDwithLineBreaks/s; # replace the line-broken verbatim ID with a non-broken verbatim ID ${$self}{body} =~ s/$verbatimIDwithLineBreaksRegExp/${$child}{id}/s; # note: we do *not* label this as found, as we need to go back through # and search for the newly modified ID } } # logfile info $logger->trace('*Post-processed body:') if $is_tt_switch_active; $logger->trace( ${$self}{body} ) if ($is_tt_switch_active); } } return; } sub verbatim_common_tasks { my $self = shift; # get yaml settings $self->yaml_modify_line_breaks_settings if $is_m_switch_active; # give unique id $self->create_unique_id; # the replacement text can be just the ID, but the ID might have a line break at the end of it $self->get_replacement_text; # the above regexp, when used below, will remove the trailing linebreak in ${$self}{linebreaksAtEnd}{end} # so we compensate for it here $self->adjust_replacement_text_line_breaks_at_end; # modify line breaks end statements $self->modify_line_breaks_end if ( $is_m_switch_active and defined ${$self}{EndStartsOnOwnLine} and ${$self}{EndStartsOnOwnLine} != 0 ); $self->modify_line_breaks_end_after if ($is_m_switch_active and defined ${$self}{EndFinishesWithLineBreak} and ${$self}{EndFinishesWithLineBreak} != 0 ); } sub create_unique_id { my $self = shift; $verbatimCounter++; ${$self}{id} = ( ${$self}{type} eq 'command' ? $tokens{verbatimInline} : $tokens{verbatim} ) . $verbatimCounter . $tokens{endOfToken}; return; } sub yaml_get_textwrap_removeparagraphline_breaks { my $self = shift; $logger->trace("No text wrap or remove paragraph line breaks for verbatim code blocks, ${$self}{name}") if ${ $mainSettings{logFilePreferences} }{showDecorationFinishCodeBlockTrace}; } 1;