package LatexIndent::Wrap; # 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 Text::Wrap; use LatexIndent::Tokens qw/%tokens/; use LatexIndent::TrailingComments qw/$trailingCommentRegExp/; 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/; use LatexIndent::Verbatim qw/%verbatimStorage/; use Exporter qw/import/; our @ISA = "LatexIndent::Document"; # class inheritance, Programming Perl, pg 321 our @EXPORT_OK = qw/text_wrap/; our $sentenceCounter; sub text_wrap{ my $self = shift; $logger->trace("*Text wrap regular expression construction: (see textWrapOptions: )") if $is_t_switch_active; # textWrap Blocks FOLLOW # textWrap Blocks FOLLOW # textWrap Blocks FOLLOW my $blocksFollow = q(); my $blocksFollowHash = \%{${$mainSettings{modifyLineBreaks}{textWrapOptions}}{blocksFollow}}; foreach my $blocksFollowEachPart (sort keys %{$blocksFollowHash}) { my $yesNo = $blocksFollowHash->{$blocksFollowEachPart}; if($yesNo){ if($blocksFollowEachPart eq "par"){ $blocksFollowEachPart = qr/\R?\\par/s; } elsif ($blocksFollowEachPart eq "blankLine"){ $blocksFollowEachPart = qr/ (?:\A(?:$tokens{blanklines}\R)+) # the order of each of these | # is important, as (like always) the first (?:\G(?:$tokens{blanklines}\R)+) # thing to be matched will | # be accepted (?:(?:$tokens{blanklines}\h*\R)+) | \R{2,} | \G /sx; } elsif ($blocksFollowEachPart eq "commentOnPreviousLine"){ $blocksFollowEachPart = qr/^$trailingCommentRegExp/m; } elsif ($blocksFollowEachPart eq "verbatim"){ $blocksFollowEachPart = qr/$tokens{verbatim}\d+$tokens{endOfToken}/; } elsif ($blocksFollowEachPart eq "filecontents"){ $blocksFollowEachPart = qr/$tokens{filecontents}\d+$tokens{endOfToken}/; } elsif ($blocksFollowEachPart eq "headings"){ $blocksFollowEachPart = q(); my %headingsLevels = %{$mainSettings{indentAfterHeadings}}; while( my ($headingName,$headingInfo)= each %headingsLevels){ # check for a * in the name $headingName =~ s/\*/\\\*/g; # make the regex as # # headingName # # possible horizontal space # []? # possible optional argument # # possible horizontal space # {} # mandatory argument # $headingName = qr/\\$headingName\h*(?:\[[^]]*?\])?\h*\{[^}]*?\}\h*(?:\\label\{[^}]*?\})?/m; # put starred headings at the front of the regexp if($headingName =~ m/\*/){ $blocksFollowEachPart = $headingName.($blocksFollowEachPart eq '' ?q():"|$blocksFollowEachPart"); } else { $blocksFollowEachPart .= ($blocksFollowEachPart eq '' ?q():"|").$headingName ; } } } elsif ($blocksFollowEachPart eq "other"){ $blocksFollowEachPart = qr/$yesNo/x; } $blocksFollow .= ($blocksFollow eq '' ? q() : "|").qr/$blocksFollowEachPart/sx; } } # if blankLine is not active from blocksFollow then we need to set up the # beginning of the string, but make sure that it is *not* followed by a # blank line token, or a blank line if(!${${$mainSettings{modifyLineBreaks}{textWrapOptions}}{blocksFollow}}{blankLine}){ $blocksFollow .= ($blocksFollow eq '' ? q() : "|"). qr/ \G (?!$tokens{blanklines}) /sx; } # followed by 0 or more h-space and line breaks $blocksFollow = ($blocksFollow eq '' ? q() : qr/(?:$blocksFollow)(?:\h|\R)*/sx ); $logger->trace("textWrap blocks follow regexp:") if $is_tt_switch_active; $logger->trace($blocksFollow) if $is_tt_switch_active; # textWrap Blocks BEGIN with # textWrap Blocks BEGIN with # textWrap Blocks BEGIN with my $blocksBeginWith = q(); my $blocksBeginWithHash = \%{${$mainSettings{modifyLineBreaks}{textWrapOptions}}{blocksBeginWith}}; foreach my $blocksBeginWithEachPart (sort keys %{$blocksBeginWithHash}) { my $yesNo = $blocksBeginWithHash->{$blocksBeginWithEachPart}; if($yesNo){ if($blocksBeginWithEachPart eq "A-Z"){ $logger->trace("textWrap Blocks BEGINS with capital letters (see textWrap:blocksBeginWith:A-Z)") if $is_t_switch_active; $blocksBeginWithEachPart = qr/(?!(?:$tokens{blanklines}|$tokens{verbatim}|$tokens{preamble}))[A-Z]/; } elsif ($blocksBeginWithEachPart eq "a-z"){ $logger->trace("textWrap Blocks BEGINS with lower-case letters (see textWrap:blocksBeginWith:a-z)") if $is_t_switch_active; $blocksBeginWithEachPart = qr/[a-z]/; } elsif ($blocksBeginWithEachPart eq "0-9"){ $logger->trace("textWrap Blocks BEGINS with numbers (see textWrap:blocksBeginWith:0-9)") if $is_t_switch_active; $blocksBeginWithEachPart = qr/[0-9]/; } elsif ($blocksBeginWithEachPart eq "other"){ $logger->trace("textWrap Blocks BEGINS with other $yesNo (reg exp) (see textWrap:blocksBeginWith:other)") if $is_t_switch_active; $blocksBeginWithEachPart = qr/$yesNo/; } $blocksBeginWith .= ($blocksBeginWith eq "" ? q(): "|" ).$blocksBeginWithEachPart; } } $blocksBeginWith = qr/$blocksBeginWith/; # textWrap Blocks END with # textWrap Blocks END with # textWrap Blocks END with my $blocksEndBefore = q(); my $blocksEndBeforeHash = \%{${$mainSettings{modifyLineBreaks}{textWrapOptions}}{blocksEndBefore}}; foreach my $blocksEndBeforeEachPart (sort keys %{$blocksEndBeforeHash}) { my $yesNo = $blocksEndBeforeHash->{$blocksEndBeforeEachPart}; if($yesNo){ if ($blocksEndBeforeEachPart eq "other"){ $logger->trace("textWrap Blocks ENDS with other $yesNo (reg exp) (see textWrap:blocksEndBefore:other)") if $is_t_switch_active; $blocksEndBeforeEachPart = qr/\R?$yesNo/; } elsif ($blocksEndBeforeEachPart eq "commentOnOwnLine"){ $logger->trace("textWrap Blocks ENDS with commentOnOwnLine (see textWrap:blocksEndBefore:commentOnOwnLine)") if $is_t_switch_active; $blocksEndBeforeEachPart = qr/^$trailingCommentRegExp/m; } elsif ($blocksEndBeforeEachPart eq "verbatim"){ $logger->trace("textWrap Blocks ENDS with verbatim (see textWrap:blocksEndBefore:verbatim)") if $is_t_switch_active; $blocksEndBeforeEachPart = qr/$tokens{verbatim}\d/; } elsif ($blocksEndBeforeEachPart eq "filecontents"){ $logger->trace("textWrap Blocks ENDS with filecontents (see textWrap:blocksEndBefore:filecontents)") if $is_t_switch_active; $blocksEndBeforeEachPart = qr/$tokens{filecontents}/; } $blocksEndBefore .= ($blocksEndBefore eq "" ? q(): "|" ).$blocksEndBeforeEachPart; } } $blocksEndBefore = qr/$blocksEndBefore/; # the OVERALL textWrap Blocks regexp # the OVERALL textWrap Blocks regexp # the OVERALL textWrap Blocks regexp $logger->trace("Overall textWrap Blocks end with regexp:") if $is_tt_switch_active; $logger->trace($blocksEndBefore) if $is_tt_switch_active; # store the text wrap blocks my @textWrapBlockStorage = split(/($blocksFollow)/,${$self}{body}); # sentences need special treatment if (${$self}{modifyLineBreaksYamlName} eq 'sentence'){ @textWrapBlockStorage = (${$self}{body}); } # call the text wrapping routine my $columns = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{columns}; # vital Text::Wrap options $Text::Wrap::columns=$columns; $Text::Wrap::huge = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{huge}; # all other Text::Wrap options not usually needed/helpful, but available $Text::Wrap::separator = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{separator} if(${$mainSettings{modifyLineBreaks}{textWrapOptions}}{separator} ne ''); $Text::Wrap::break = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{break} if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{break}; $Text::Wrap::unexpand = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{unexpand} if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{unexpand}; $Text::Wrap::tabstop = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{tabstop} if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{tabstop}; # clear the body, which will be updated with newly wrapped body ${$self}{body} = q(); # loop back through the text wrap block storage foreach my $textWrapBlockStorageValue (@textWrapBlockStorage){ if ($textWrapBlockStorageValue =~ m/^\s*$blocksBeginWith/s or ${$self}{modifyLineBreaksYamlName} eq 'sentence') { # LIMIT is one greater than the maximum number of times EXPR may be split my @textWrapBeforeEndWith = split(/($blocksEndBefore)/,$textWrapBlockStorageValue,2); # sentences need special treatment if (${$self}{modifyLineBreaksYamlName} eq 'sentence'){ @textWrapBeforeEndWith = (); } # if we have an occurence of blocksEndBefore, then grab the stuff before it if (scalar @textWrapBeforeEndWith > 1){ $textWrapBlockStorageValue = $textWrapBeforeEndWith[0]; } $logger->trace("TEXTWRAP BLOCK: $textWrapBlockStorageValue") if $is_tt_switch_active; # initiate the trailing comments storage my $trailingComments = q(); # grab all *internal* trailing comments from the block while($textWrapBlockStorageValue =~ m|$trailingCommentRegExp|){ $textWrapBlockStorageValue =~ s|(\h*$trailingCommentRegExp)||s; $trailingComments .= $1; } # remove internal line breaks $textWrapBlockStorageValue =~ s/\R(?!\Z)/ /sg; # convert multiple spaces into single $textWrapBlockStorageValue =~ s/\h{2,}/ /sg if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{multipleSpacesToSingle}; # goal: get an accurate measurement of verbatim objects; # # example: # Lorem \verb!x+y! ipsum dolor sit amet # # is represented as # # Lorem LTXIN-TK-VERBATIM1-END ipsum dolor sit amet # # so we *measure* the verbatim token and replace it with # an appropriate-length string # # Lorem a2A41233rt ipsum dolor sit amet # # and then put the body back to # # Lorem LTXIN-TK-VERBATIM1-END ipsum dolor sit amet # # following the text wrapping my @putVerbatimBackIn; # check body for verbatim and get measurements if ($textWrapBlockStorageValue =~ m/$tokens{verbatim}/s and ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{huge} eq "overflow"){ # reference: https://stackoverflow.com/questions/10336660/in-perl-how-can-i-generate-random-strings-consisting-of-eight-hex-digits my @set = ('0' ..'9', 'A' .. 'Z', 'a' .. 'z'); # loop through verbatim objects while( my ($verbatimID,$child)= each %verbatimStorage){ my $verbatimThing = ${$child}{begin}.${$child}{body}.${$child}{end}; # if the object has line breaks, don't measure it next if $verbatimThing =~ m/\R/s; if($textWrapBlockStorageValue =~m/$verbatimID/s){ # measure length my $verbatimLength = Unicode::GCString->new($verbatimThing)->columns(); # create temporary ID, and check that it is not contained in the body my $verbatimTmpID = join '' => map $set[rand @set], 1 .. $verbatimLength; while($textWrapBlockStorageValue=~m/$verbatimTmpID/s){ $verbatimTmpID = join '' => map $set[rand @set], 1 .. $verbatimLength; } # store for use after the text wrapping push(@putVerbatimBackIn,{origVerbatimID=>$verbatimID,tmpVerbatimID=>$verbatimTmpID}); # make the substitution $textWrapBlockStorageValue =~ s/$verbatimID/$verbatimTmpID/s; } } } # perform the text wrap routine $textWrapBlockStorageValue = wrap('','',$textWrapBlockStorageValue) if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{columns} > 0; # append trailing comments from WITHIN the block $textWrapBlockStorageValue =~ s/\R?$/$trailingComments\n/s if ($trailingComments ne ''); # append blocksEndBefore and the stuff following it if (scalar @textWrapBeforeEndWith > 1){ $textWrapBlockStorageValue .= $textWrapBeforeEndWith[1].$textWrapBeforeEndWith[2]; } # put blank line tokens on their own lines, should only happen when the following is used: # blocksFollow: # blankLine: 0 while ($textWrapBlockStorageValue =~ m/^\H.*?$tokens{blanklines}/m ){ $textWrapBlockStorageValue =~ s/^(\H.*?)$tokens{blanklines}/$1\n$tokens{blanklines}/m; } # remove surrounding spaces from blank line tokens $textWrapBlockStorageValue =~ s/^\h*$tokens{blanklines}\h*/$tokens{blanklines}/mg; # put verbatim back in $textWrapBlockStorageValue=~ s/${$_}{tmpVerbatimID}/${$_}{origVerbatimID}/s foreach (@putVerbatimBackIn); } # update the body ${$self}{body} .= $textWrapBlockStorageValue; } } 1;