summaryrefslogtreecommitdiff
path: root/support/latexindent/LatexIndent/Indent.pm
blob: fcf15fc1407b321cd5e9fcb72c384932602caefc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
package LatexIndent::Indent;
#	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::HiddenChildren qw/%familyTree/;
use LatexIndent::GetYamlSettings qw/%mainSettings/;
use LatexIndent::LogFile qw/$logger/;
use Text::Tabs;
use Data::Dumper;
use Exporter qw/import/;
our @EXPORT_OK = qw/indent wrap_up_statement determine_total_indentation indent_begin indent_body indent_end_statement final_indentation_check push_family_tree_to_indent get_surrounding_indentation indent_children_recursively check_for_blank_lines_at_beginning put_blank_lines_back_in_at_beginning add_surrounding_indentation_to_begin_statement post_indentation_check/;
our %familyTree;

sub indent{
    my $self = shift;

    # determine the surrounding and current indentation
    $self->determine_total_indentation;

    # indent the begin statement
    $self->indent_begin;

    # indent the body
    $self->indent_body;

    # indent the end statement
    $self->indent_end_statement;

    # output the completed object to the log file
    $logger->trace("Complete indented object (${$self}{name}) after indentation:\n${$self}{begin}${$self}{body}${$self}{end}") if $is_tt_switch_active;

    # wrap-up statement
    $self->wrap_up_statement;
    return $self;
}

sub wrap_up_statement{
    my $self = shift;
    $logger->trace("*Finished indenting ${$self}{name}") if $is_t_switch_active;
    return $self;
  }

sub determine_total_indentation{
    my $self = shift;

    # calculate and grab the surrounding indentation
    $self->get_surrounding_indentation;

    # logfile information
    my $surroundingIndentation = ${$self}{surroundingIndentation};
    $logger->trace("indenting object ${$self}{name}") if($is_t_switch_active);
    (my $during = $surroundingIndentation) =~ s/\t/TAB/g;
    $logger->trace("indentation *surrounding* object: '$during'") if($is_t_switch_active);
    ($during = ${$self}{indentation}) =~ s/\t/TAB/g;
    $logger->trace("indentation *of* object: '$during'") if($is_t_switch_active);
    ($during = $surroundingIndentation.${$self}{indentation}) =~ s/\t/TAB/g;
    $logger->trace("*total* indentation to be added: '$during'") if($is_t_switch_active);

    # form the total indentation of the object
    ${$self}{indentation} = $surroundingIndentation.${$self}{indentation};

}

sub get_surrounding_indentation{
    my $self = shift;

    my $surroundingIndentation = q();

    if($familyTree{${$self}{id}}){
        $logger->trace("Adopted ancestors found!") if($is_t_switch_active);
        foreach(@{${$familyTree{${$self}{id}}}{ancestors}}){
            if(${$_}{type} eq "adopted"){
                my $newAncestorId = ${$_}{ancestorID};
                $logger->trace("ancestor ID: $newAncestorId, adding indentation of $newAncestorId to surroundingIndentation of ${$self}{id}") if($is_t_switch_active);
                $surroundingIndentation .= ref(${$_}{ancestorIndentation}) eq 'SCALAR'
                                                    ?
                                            (${${$_}{ancestorIndentation}}?${${$_}{ancestorIndentation}}:q())
                                                    :
                                            (${$_}{ancestorIndentation}?${$_}{ancestorIndentation}:q());
            }
        }
    }
    ${$self}{surroundingIndentation} = $surroundingIndentation;

}

sub indent_begin{
    # for most objects, the begin statement is just one line, but there are exceptions, e.g KeyEqualsValuesBraces
    return;
}

sub indent_body{
    my $self = shift;

    # output to the logfile
    $logger->trace("Body (${$self}{name}) before indentation:\n${$self}{body}") if $is_tt_switch_active;

    # last minute check for modified bodyLineBreaks
    $self->count_body_line_breaks if $is_m_switch_active;

    # some objects need to check for blank line tokens at the beginning
    $self->check_for_blank_lines_at_beginning if $is_m_switch_active; 

    # some objects can format their body to align at the & character
    $self->align_at_ampersand if (${$self}{lookForAlignDelims} and !${$self}{dontMeasure});

    # grab the indentation of the object
    # NOTE: we need this here, as ${$self}{indentation} can be updated by the align_at_ampersand routine, 
    #       see https://github.com/cmhughes/latexindent.pl/issues/223 for example
    my $indentation = ${$self}{indentation};

    # possibly remove paragraph line breaks
    $self->remove_paragraph_line_breaks if ($is_m_switch_active and ${$self}{removeParagraphLineBreaks} and !${$mainSettings{modifyLineBreaks}{removeParagraphLineBreaks}}{beforeTextWrap});

    # body indendation
    if(${$self}{linebreaksAtEnd}{begin}==1){
        if(${$self}{body} =~ m/^\h*$/s){
            $logger->trace("Body of ${$self}{name} is empty, not applying indentation") if $is_t_switch_active;
        } else {
            # put any existing horizontal space after the current indentation
            $logger->trace("Entire body of ${$self}{name} receives indendentation") if $is_t_switch_active;
            ${$self}{body} =~ s/^(\h*)/$indentation$1/mg;  # add indentation
        }
    } elsif(${$self}{linebreaksAtEnd}{begin}==0 and ${$self}{bodyLineBreaks}>0) {
        if(${$self}{body} =~ m/
                            (.*?)      # content of first line
                            \R         # first line break
                            (.*$)      # rest of body
                            /sx){
            my $bodyFirstLine = $1;
            my $remainingBody = $2;
            $logger->trace("first line of body: $bodyFirstLine") if $is_tt_switch_active;
            $logger->trace("remaining body (before indentation):\n'$remainingBody'") if($is_tt_switch_active);
    
            # add the indentation to all the body except first line
            $remainingBody =~ s/^/$indentation/mg unless($remainingBody eq '');  # add indentation
            $logger->trace("remaining body (after indentation):\n$remainingBody'") if($is_tt_switch_active);
    
            # put the body back together
            ${$self}{body} = $bodyFirstLine."\n".$remainingBody; 
        }
    }

    # some objects need a post-indentation check, e.g ifElseFi
    $self->post_indentation_check;
    # if the routine check_for_blank_lines_at_beginning has been called, then the following routine
    # puts blank line tokens back in 
    $self->put_blank_lines_back_in_at_beginning if $is_m_switch_active; 

    # the final linebreak can be modified by a child object; see test-cases/commands/figureValign-mod5.tex, for example
    if($is_m_switch_active and defined ${$self}{linebreaksAtEnd}{body} and ${$self}{linebreaksAtEnd}{body}==1 and ${$self}{body} !~ m/\R$/ and ${$self}{body} ne ''){
        $logger->trace("Adding a linebreak at end of body for ${$self}{name} to contain a linebreak at the end (linebreaksAtEnd is 1, but there isn't currently a linebreak)") if($is_t_switch_active);
        ${$self}{body} .= "\n";
    }

    # output to the logfile
    $logger->trace("Body (${$self}{name}) after indentation:\n${$self}{body}") if $is_tt_switch_active;
    return $self;
}

sub post_indentation_check{
    return;
}

sub check_for_blank_lines_at_beginning{
    # some objects need this routine
    return;
}

sub put_blank_lines_back_in_at_beginning{
    # some objects need this routine
    return;
}

sub indent_end_statement{
    my $self = shift;
    my $surroundingIndentation = (${$self}{surroundingIndentation} and $familyTree{${$self}{id}})
                                            ?
                                 (ref(${$self}{surroundingIndentation}) eq 'SCALAR'?${${$self}{surroundingIndentation}}:${$self}{surroundingIndentation})
                                            :q();

    # end{statement} indentation, e.g \end{environment}, \fi, }, etc
    if(${$self}{linebreaksAtEnd}{body}){
        ${$self}{end} =~ s/^\h*/$surroundingIndentation/mg;  # add indentation
        $logger->trace("Adding surrounding indentation to ${$self}{end} (${$self}{name}: '$surroundingIndentation')") if($is_t_switch_active);
     }
    return $self;
}

sub final_indentation_check{
    # problem:
    #       if a tab is appended to spaces, it will look different 
    #       from spaces appended to tabs (see test-cases/items/spaces-and-tabs.tex)
    # solution:
    #       move all of the tabs to the beginning of ${$self}{indentation}
    # notes;
    #       this came to light when studying test-cases/items/items1.tex

    my $self = shift;

    my $indentation;
    my $numberOfTABS; 
    my $after;
    ${$self}{body} =~ s/
                        ^((\h*|\t*)((\h+)(\t+))+)
                        /   
                        # fix the indentation
                        $indentation = $1;

                        # count the number of tabs
                        $numberOfTABS = () = $indentation=~ \/\t\/g;
                        $logger->trace("Number of tabs: $numberOfTABS") if($is_t_switch_active);

                        # log the after
                        ($after = $indentation) =~ s|\t||g;
                        $after = "TAB"x$numberOfTABS.$after;
                        $logger->trace("Indentation after: '$after'") if($is_t_switch_active);
                        ($indentation = $after) =~s|TAB|\t|g;

                        $indentation;
                       /xsmeg;

    return unless($mainSettings{maximumIndentation} =~ m/^\h+$/);

    # maximum indentation check
    $logger->trace("*Maximum indentation check") if($is_t_switch_active);

    # replace any leading tabs with spaces, and update the body
    my @expanded_lines = expand(${$self}{body});
    ${$self}{body} = join("",@expanded_lines);

    # grab the maximum indentation
    my $maximumIndentation = $mainSettings{maximumIndentation};
    my $maximumIndentationLength = length($maximumIndentation)+1;

    # replace any leading space that is greater than the 
    # specified maximum indentation with the maximum indentation
    ${$self}{body} =~ s/^\h{$maximumIndentationLength,}/$maximumIndentation/smg;
}

sub indent_children_recursively{
    my $self = shift;

    unless(defined ${$self}{children}) {
        $logger->trace("No child objects (${$self}{name})") if $is_t_switch_active;
        return;
    }

    $logger->trace('Pre-processed body:') if $is_tt_switch_active;
    $logger->trace(${$self}{body}) if($is_tt_switch_active);

    # send the children through this indentation routine recursively
    if(defined ${$self}{children}){
        foreach my $child (@{${$self}{children}}){
            $logger->trace("Indenting child objects on ${$child}{name}") if $is_t_switch_active;
            $child->indent_children_recursively;
        }
    } 

    $logger->trace("*Replacing ids with begin, body, and end statements:") if $is_t_switch_active;

    # loop through document children hash
    while( scalar (@{${$self}{children}}) > 0 ){
          my $index = 0;
          # we work through the array *in order*
          foreach my $child (@{${$self}{children}}){
            $logger->trace("Searching ${$self}{name} for ${$child}{id}...") if $is_t_switch_active;
            if(${$self}{body} =~ m/${$child}{idRegExp}/s){
                # we only care if id is first non-white space character 
                # and if followed by line break 
                # if m switch is active 
                my $IDFirstNonWhiteSpaceCharacter = 0;
                my $IDFollowedImmediatelyByLineBreak = 0;

                # update the above two, if necessary
                if ($is_m_switch_active){
                    $IDFirstNonWhiteSpaceCharacter = (${$self}{body} =~ m/^${$child}{idRegExp}/m 
                                                            or 
                                                         ${$self}{body} =~ m/^\h\h*${$child}{idRegExp}/m
                                                        ) ?1:0;
                    $IDFollowedImmediatelyByLineBreak = (${$self}{body} =~ m/${$child}{idRegExp}\h*\R+/m) ?1:0;
                    ${$child}{IDFollowedImmediatelyByLineBreak} = $IDFollowedImmediatelyByLineBreak; 
               }

                # log file info
                $logger->trace("${$child}{id} found!") if($is_t_switch_active);
                $logger->trace("*Indenting  ${$child}{name} (id: ${$child}{id})") if $is_t_switch_active;
                $logger->trace("looking up indentation scheme for ${$child}{name}") if($is_t_switch_active);

                # line break checks *after* <end statement>
                if (defined ${$child}{EndFinishesWithLineBreak}
                    and ${$child}{EndFinishesWithLineBreak}==-1 
                    and $IDFollowedImmediatelyByLineBreak) {
                    # remove line break *after* <end statement>, if appropriate
                    my $EndStringLogFile = ${$child}{aliases}{EndFinishesWithLineBreak}||"EndFinishesWithLineBreak";
                    $logger->trace("Removing linebreak after ${$child}{end} (see $EndStringLogFile)") if $is_t_switch_active;
                    ${$self}{body} =~ s/${$child}{idRegExp}(\h*)?(\R|\h)*/${$child}{id}$1/s;
                    ${$child}{linebreaksAtEnd}{end} = 0;
                }

                # perform indentation
                $child->indent;

                # surrounding indentation is now up to date
                my $surroundingIndentation = (${$child}{surroundingIndentation} and ${$child}{hiddenChildYesNo})
                                                        ?
                                             (ref(${$child}{surroundingIndentation}) eq 'SCALAR'?${${$child}{surroundingIndentation}}:${$child}{surroundingIndentation})
                                                        :q();

                # line break checks before <begin statement>
                if(defined ${$child}{BeginStartsOnOwnLine} and ${$child}{BeginStartsOnOwnLine} !=0 ){
                    my $BeginStringLogFile = ${$child}{aliases}{BeginStartsOnOwnLine}||"BeginStartsOnOwnLine";

                    # 
                    # Blank line poly-switch notes (==4)
                    #
                    # when BeginStartsOnOwnLine=4 we adopt the following approach:
                    #   temporarily change BeginStartsOnOwnLine to -1, make adjustments
                    #   temporarily change BeginStartsOnOwnLine to 3, make adjustments
                    #
                    # we use an array, @polySwitchValues to facilitate this
                    my @polySwitchValues = (${$child}{BeginStartsOnOwnLine}==4)?(-1,3):(${$child}{BeginStartsOnOwnLine});

                    foreach(@polySwitchValues){
                        # if BeginStartsOnOwnLine is 4, then we hack 
                        #       $IDFirstNonWhiteSpaceCharacter 
                        # to be 0 on the second time through (poly-switch set to 3)
                        $IDFirstNonWhiteSpaceCharacter = 0 if (${$child}{BeginStartsOnOwnLine}==4 and $_==3);

                        # if the child ID is not the first character and BeginStartsOnOwnLine>=1 
                        # then we will need to add a line break (==1), a comment (==2) or another blank line (==3)
                        if($_>=1 and !$IDFirstNonWhiteSpaceCharacter){
                            # by default, assume that no trailing comment token is needed
                            my $trailingCharacterToken = q();
                            if($_==2){
                                $logger->trace("Removing space immediately before ${$child}{id}, in preparation for adding % ($BeginStringLogFile == 2)") if $is_t_switch_active;
                                ${$self}{body} =~ s/\h*${$child}{idRegExp}/${$child}{id}/s;
                                $logger->trace("Adding a % at the end of the line that ${$child}{begin} is on, then a linebreak ($BeginStringLogFile == 2)") if $is_t_switch_active;
                                $trailingCharacterToken = "%".$self->add_comment_symbol;
                            } elsif ($_==3){
                                $logger->trace("Adding a blank line at the end of the line that ${$child}{begin} is on, then a linebreak ($BeginStringLogFile == 3)") if $is_t_switch_active;
                                $trailingCharacterToken = "\n".(${$mainSettings{modifyLineBreaks}}{preserveBlankLines}?$tokens{blanklines}:q());
                            } else {
                                $logger->trace("Adding a linebreak at the beginning of ${$child}{begin} (see $BeginStringLogFile)") if $is_t_switch_active;
                            }

                            # the trailing comment/linebreak magic
                            ${$child}{begin} = "$trailingCharacterToken\n".${$child}{begin};
                            $child->add_surrounding_indentation_to_begin_statement;

                            # remove surrounding indentation ahead of %
                            ${$child}{begin} =~ s/^(\h*)%/%/ if($_==2);
                        } elsif ($_==-1 and $IDFirstNonWhiteSpaceCharacter){
                            # finally, if BeginStartsOnOwnLine == -1 then we might need to *remove* a blank line(s)
                            # important to check we don't move the begin statement next to a blank-line-token
                            my $blankLineToken = $tokens{blanklines};
                            if(${$self}{body} !~ m/$blankLineToken\R*\h*${$child}{idRegExp}/s){
                                $logger->trace("Removing linebreak before ${$child}{begin} (see $BeginStringLogFile in ${$child}{modifyLineBreaksYamlName} YAML)") if $is_t_switch_active;
                                ${$self}{body} =~ s/(\h*)(?:\R*|\h*)+${$child}{idRegExp}/$1${$child}{id}/s;
                            } else {
                                $logger->trace("Not removing linebreak ahead of ${$child}{begin}, as blank-line-token present (see preserveBlankLines)") if $is_t_switch_active;
                            }
                        }
                     }
                }

                $logger->trace(Dumper(\%{$child})) if($is_tt_switch_active);

                # replace ids with body
                ${$self}{body} =~ s/${$child}{idRegExp}/${$child}{begin}${$child}{body}${$child}{end}/;

                # log file info
                $logger->trace("Body (${$self}{name}) now looks like:") if $is_tt_switch_active;
                $logger->trace(${$self}{body}) if($is_tt_switch_active);

                # remove element from array: http://stackoverflow.com/questions/174292/what-is-the-best-way-to-delete-a-value-from-an-array-in-perl
                splice(@{${$self}{children}}, $index, 1);

                # output to the log file
                $logger->trace("deleted child key ${$child}{name} (parent is: ${$self}{name})") if $is_t_switch_active;

                # restart the loop, as the size of the array has changed
                last;
              } else {
                $logger->trace("${$child}{id} not found") if($is_t_switch_active);
              }

              # increment the loop counter
              $index++;
            }
    }

    # logfile info
    $logger->trace("${$self}{name} has this many children:") if $is_tt_switch_active;
    $logger->trace(scalar @{${$self}{children}}) if $is_tt_switch_active;
    $logger->trace("Post-processed body (${$self}{name}):") if($is_tt_switch_active);
    $logger->trace(${$self}{body}) if($is_tt_switch_active);

}

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;

    my $surroundingIndentation = ${$self}{surroundingIndentation};
    ${$self}{begin} =~ s/^(\h*)?/$surroundingIndentation/mg;  # add indentation

}

1;