diff options
Diffstat (limited to 'support/latexindent/LatexIndent/Wrap.pm')
-rw-r--r-- | support/latexindent/LatexIndent/Wrap.pm | 317 |
1 files changed, 317 insertions, 0 deletions
diff --git a/support/latexindent/LatexIndent/Wrap.pm b/support/latexindent/LatexIndent/Wrap.pm new file mode 100644 index 0000000000..a2b8383d94 --- /dev/null +++ b/support/latexindent/LatexIndent/Wrap.pm @@ -0,0 +1,317 @@ +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 + # <hspace> # possible horizontal space + # []? # possible optional argument + # <hspace> # 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; |