File Coverage

blib/lib/LaTeX/TOM/Parser.pm
Criterion Covered Total %
statement 494 647 76.3
branch 148 226 65.4
condition 133 199 66.8
subroutine 40 49 81.6
pod 0 3 0.0
total 815 1124 72.5


line stmt bran cond sub pod time code
1             ###############################################################################
2             #
3             # LaTeX::TOM::Parser
4             #
5             # The parsing class
6             #
7             ###############################################################################
8              
9             package LaTeX::TOM::Parser;
10              
11 10     10   80 use strict;
  10         20  
  10         364  
12 10         4306 use base qw(
13             LaTeX::TOM::Node
14             LaTeX::TOM::Tree
15 10     10   60 );
  10         20  
16 10     10   73 use constant true => 1;
  10         23  
  10         570  
17 10     10   61 use constant false => 0;
  10         20  
  10         458  
18              
19 10     10   57 use Carp qw(carp croak);
  10         20  
  10         533  
20 10     10   61 use File::Basename qw(fileparse);
  10         19  
  10         1891  
21              
22             our $VERSION = '0.12';
23              
24             my %error_handlers = (
25             0 => sub { warn "parse error: $_[0].\n" },
26             1 => sub { die "parse error: $_[0].\n" },
27             2 => sub {},
28             );
29              
30             # Constructor
31             #
32             sub _new {
33 17     17   41 my $class = shift;
34              
35 10     10   73 no strict 'refs';
  10         22  
  10         78502  
36              
37             my $self = bless {
38             config => {
39 17         63 BRACELESS => \%{'LaTeX::TOM::BRACELESS'},
40 17         45 INNERCMDS => \%{'LaTeX::TOM::INNERCMDS'},
41 17         46 MATHENVS => \%{'LaTeX::TOM::MATHENVS'},
42 17         45 MATHBRACKETS => \%{'LaTeX::TOM::MATHBRACKETS'},
43 17         43 PARSE_ERRORS_FATAL => ${'LaTeX::TOM::PARSE_ERRORS_FATAL'},
44 17         37 TEXTENVS => \%{'LaTeX::TOM::TEXTENVS'},
  17         100  
45             },
46             };
47              
48 17         73 $self->_init(@_);
49              
50 17         47 return $self;
51             }
52              
53             sub new {
54             # XXX deprecated as of 2023-01-30
55 0     0 0 0 carp 'Direct use of LaTeX::TOM::Parser constructor is deprecated and will be removed in future version';
56 0         0 shift->_new(@_);
57             }
58              
59             # Set/reset "globals"
60             #
61             sub _init {
62 17     17   32 my $parser = shift;
63 17         45 my ($parse_errors_fatal, $readinputs, $applymappings) = @_;
64              
65             my $retrieve_opt_default = sub
66             {
67 51     51   89 my ($opt, $default) = @_;
68 51 100       160 return $opt if defined $opt;
69 23         85 return $default;
70 17         84 };
71              
72             # set user options
73             #
74 17         51 $parser->{readinputs} = $retrieve_opt_default->($readinputs, 0);
75 17         52 $parser->{applymappings} = $retrieve_opt_default->($applymappings, 0);
76 17         49 $parser->{PARSE_ERRORS_FATAL} = $retrieve_opt_default->($parse_errors_fatal, $parser->{config}{PARSE_ERRORS_FATAL});
77              
78             # init internal stuff
79             #
80 17         48 $parser->{MATHBRACKETS} = $parser->{config}{MATHBRACKETS};
81              
82             # this will hold a running list/hash of commands that have been remapped
83 17         35 $parser->{MAPPEDCMDS} = {};
84              
85             # this will hold a running list/hash of commands that have been used. We dont
86             # bother apply mappings except to commands that have been used.
87 17         36 $parser->{USED_COMMANDS} = {};
88              
89             # no file yet
90 17         87 $parser->{file} = undef;
91             }
92              
93             # Parse a LaTeX file, return a tree. You probably want this method.
94             #
95             sub parseFile {
96 9     9 0 560 my $parser = shift;
97 9         18 my $filename = shift;
98              
99             # init variables
100             #
101 9         20 $parser->{file} = $filename; # file name member data
102 9         19 my $tree = {}; # init output tree
103              
104             # read in text from file or bomb out
105             #
106 9         29 my $text = _readFile($filename, true);
107              
108             # do the parse
109             #
110 9         40 $tree = $parser->parse($text);
111              
112 9         31 return $tree;
113             }
114              
115             # main parsing entrypoint
116             #
117             sub parse {
118 19     19 0 4363 my $parser = shift;
119 19         52 my ($text) = @_;
120              
121             # first half of parsing (goes up to finding commands, reading inputs)
122             #
123 19         64 my ($tree, $bracehash) = $parser->_parseA($text);
124             _debug(
125             'done with _parseA',
126 0     0   0 sub { $tree->_warn() },
127 19         140 );
128              
129             # handle mappings
130             #
131 19 100       98 $parser->_applyMappings($tree) if $parser->{applymappings};
132             _debug(
133             'done with _applyMappings',
134 0     0   0 sub { $tree->_warn() },
135 19         112 );
136              
137             # second half of parsing (environments)
138             #
139 19         73 $parser->_parseB($tree);
140             _debug(
141             'done with _parseB',
142 0     0   0 sub { $tree->_warn() },
143 19         144 );
144              
145             # once all the above is done we can propegate math/plaintext modes down
146             #
147 19         100 $parser->_propegateModes($tree, 0, 0); # math = 0, plaintext = 0
148             _debug(
149             'done with _propegateModes',
150 0     0   0 sub { $tree->_warn() },
151 19         108 );
152              
153             # handle kooky \[ \] math mode
154             #
155 19 50       81 if (not exists $parser->{MAPPEDCMDS}->{'\\['}) {
156             # math mode (\[ \], \( \))
157 19         130 $parser->_stage5($tree, {'\\[' => '\\]', '\\(' => '\\)'}, 1);
158 19         91 $parser->_propegateModes($tree, 0, 0); # have to do this again of course
159 19         44 $parser->{MATHBRACKETS}->{'\\['} = '\\]'; # put back in brackets list for
160 19         46 $parser->{MATHBRACKETS}->{'\\('} = '\\)'; # printing purposes.
161             }
162             _debug(
163             undef,
164 0     0   0 sub { $tree->_warn() },
165 19         117 );
166              
167 19         122 $tree->listify; # add linked-list stuff
168              
169 19         98 return $tree;
170             }
171              
172             # Parsing with no mappings and no externally accessible parser object.
173             #
174             sub _basicparse {
175 8     8   20 my $parser = shift; # @_ would break code
176 8         15 my $text = shift;
177              
178 8 50       24 my $parse_errors_fatal = (defined $_[0] ? $_[0] : $parser->{config}{PARSE_ERRORS_FATAL});
179 8 100       29 my $readinputs = (defined $_[1] ? $_[1] : 1);
180              
181 8         39 $parser = LaTeX::TOM::Parser->_new($parse_errors_fatal, $readinputs);
182 8         27 my ($tree, $bracehash) = $parser->_parseA($text);
183              
184 8         27 $parser->_parseB($tree);
185              
186 8         42 $tree->listify; # add linked-list stuff
187              
188 8         63 return ($tree, $bracehash);
189             }
190              
191             # start the tree. separate out comment and text nodes.
192             #
193             sub _stage1 {
194 27     27   58 my $parser = shift;
195 27         39 my $text = shift;
196              
197 27         81 my @nodes = _getTextAndCommentNodes($text, 0, length($text));
198              
199 27         134 return LaTeX::TOM::Tree->_new([@nodes]);
200             }
201              
202             # this stage parses the braces ({}) and adds the corresponding structure to
203             # the tree.
204             #
205             sub _stage2 {
206 146     146   210 my $parser = shift;
207              
208 146         195 my $tree = shift;
209 146   100     321 my $bracehash = shift || undef;
210 146   50     419 my $startidx = shift || 0; # last two params for starting at some specific
211 146   50     366 my $startpos = shift || 0; # node and offset.
212              
213 146         195 my %blankhash;
214              
215 146 100       281 if (not defined $bracehash) {
216 27         64 $bracehash = {%blankhash};
217             }
218              
219 146         203 my $leftidx = -1;
220 146         233 my $leftpos = -1;
221 146         197 my $leftcount = 0;
222              
223             # loop through the nodes
224 146         231 for (my $i = $startidx; $i < @{$tree->{nodes}}; $i++) {
  410         868  
225 264         380 my $node = $tree->{nodes}[$i];
226 264         370 my $spos = $node->{start}; # get text start position
227              
228             # set position placeholder within the text block
229 264 100       462 my $pos = ($i == $startidx) ? $startpos : 0;
230              
231 264 50       617 if ($node->{type} eq 'TEXT') {
232              
233 264         835 _debug("parseStage2: looking at text node: [$node->{content}]", undef);
234              
235 264         530 my ($nextpos, $brace) = _findbrace($node->{content}, $pos);
236 264         601 while ($nextpos != -1) {
237              
238 244         312 $pos = $nextpos + 1; # update position pointer
239              
240             # handle left brace
241 244 100       551 if ($brace eq '{') {
    50          
242 122         377 _debug("found '{' at position $nextpos, leftcount is $leftcount", undef);
243 122 100       247 if ($leftcount == 0) {
244 119         170 $leftpos = $nextpos;
245 119         162 $leftidx = $i
246             }
247 122         185 $leftcount++;
248             }
249              
250             # handle right brance
251             elsif ($brace eq '}') {
252              
253 122         346 _debug("found '}' at position $nextpos, leftcount is $leftcount", undef);
254 122         183 my $rightpos = $nextpos;
255 122         184 $leftcount--;
256              
257             # found the corresponding right brace to our starting left brace
258 122 100       228 if ($leftcount == 0) {
259              
260             # see if we have to split the text node into 3 parts
261             #
262 119 50       217 if ($leftidx == $i) {
263              
264 119         325 my ($leftside, $textnode3) = $node->split($rightpos, $rightpos);
265 119         322 my ($textnode1, $textnode2) = $leftside->split($leftpos, $leftpos);
266              
267             # make the new GROUP node
268             my $groupnode = LaTeX::TOM::Node->_new(
269             {type => 'GROUP',
270             start => $textnode2->{start} - 1,
271 119         426 end => $textnode2->{end} + 1,
272             children => LaTeX::TOM::Tree->_new([$textnode2]),
273             });
274              
275             # splice the new subtree into the old location
276 119         190 splice @{$tree->{nodes}}, $i, 1, $textnode1, $groupnode, $textnode3;
  119         365  
277              
278             # add to the brace-pair lookup table
279 119         350 $bracehash->{$groupnode->{start}} = $groupnode->{end};
280 119         363 $bracehash->{$groupnode->{end}} = $groupnode->{start};
281              
282             # recur into new child node
283 119         405 $parser->_stage2($groupnode->{children}, $bracehash);
284              
285 119         345 $i++; # skip to textnode3 for further processing
286             }
287              
288             # split across nodes
289             #
290             else {
291              
292 0         0 my ($textnode1, $textnode2) = $tree->{nodes}[$leftidx]->split($leftpos, $leftpos);
293 0         0 my ($textnode3, $textnode4) = $node->split($rightpos, $rightpos);
294              
295             # remove nodes in between the node we found '{' in and the node
296             # we found '}' in
297             #
298 0         0 my @removed = splice @{$tree->{nodes}}, $leftidx+1, $i-$leftidx-1;
  0         0  
299              
300             # create a group node that contains the text after the left brace,
301             # then all the nodes up until the next text node, then the text
302             # before the right brace.
303             #
304             my $groupnode = LaTeX::TOM::Node->_new(
305             {type => 'GROUP',
306             start => $textnode2->{start} - 1,
307 0         0 end => $textnode3->{end} + 1,
308             children => LaTeX::TOM::Tree->_new(
309             [$textnode2,
310             @removed,
311             $textnode3]),
312             });
313              
314             # replace the two original text nodes with the leftover left and
315             # right portions, as well as the group node with everything in
316             # the middle.
317             #
318 0         0 splice @{$tree->{nodes}}, $leftidx, 2, $textnode1, $groupnode, $textnode4;
  0         0  
319              
320             # add to the brace-pair lookup table
321 0         0 $bracehash->{$groupnode->{start}} = $groupnode->{end};
322 0         0 $bracehash->{$groupnode->{end}} = $groupnode->{start};
323              
324             # recur into new child nodes
325 0         0 $parser->_stage2($groupnode->{children}, $bracehash);
326              
327             # step back to textnode4 on this level for further processing
328 0         0 $i -= scalar @removed;
329             }
330              
331 119         168 $leftpos = -1; # reset left data
332 119         155 $leftidx = -1;
333 119         279 last;
334             } # $leftcount == 0
335              
336             # check for '}'-based error
337             #
338 3 50       8 if ($leftcount < 0) {
339 0         0 $error_handlers{$parser->{PARSE_ERRORS_FATAL}}->("'}' before '{' at " . ($spos + $rightpos));
340 0         0 $leftcount = 0; # reset and continue
341             }
342             } # right brace
343              
344 125         247 ($nextpos, $brace) = _findbrace($node->{content}, $pos);
345              
346             } # while (braces left)
347              
348             } # if TEXT
349              
350             } # loop over all nodes
351              
352             # check for extra '{' parse error
353             #
354 146 50       290 if ($leftcount > 0) {
355 0         0 my $spos = $tree->{nodes}[$leftidx]->{start}; # get text start position
356 0         0 $error_handlers{$parser->{PARSE_ERRORS_FATAL}}->("unmatched '{' at " . ($spos + $leftpos));
357              
358             # try to continue on, after the offending brace
359 0         0 $parser->_stage2($tree, $bracehash, $leftidx, $leftpos + 1);
360             }
361              
362 146         316 return $bracehash;
363             }
364              
365             # this stage finds LaTeX commands and accordingly turns GROUP nodes into
366             # command nodes, labeled with the command
367             #
368             sub _stage3 {
369 146     146   200 my $parser = shift;
370              
371 146         192 my $tree = shift;
372 146         191 my $parent = shift;
373              
374 146         208 for (my $i = 0; $i< @{$tree->{nodes}}; $i++) {
  531         1057  
375              
376 385         508 my $node = $tree->{nodes}[$i];
377              
378             # check text node for command tag
379 385 100       691 if ($node->{type} eq 'TEXT') {
380 266         387 my $text = $node->{content};
381              
382             # inner command (such as {\command text text}). our regexp checks to see
383             # if this text chunk begins with \command, since that would be the case
384             # due to the previous parsing stages. if found, the parent node is
385             # promoted to a command.
386             #
387 266 100 100     946 if ($text =~ /^\s*\\(\w+\*?)/ && defined $parent && $parser->{config}{INNERCMDS}->{$1}) {
      100        
388 2         4 my $command = $1;
389              
390             # if the parent is already a command node, we have to make a new
391             # nested command node
392             #
393 2 50       10 if ($parent->{type} eq 'COMMAND') {
    50          
394              
395             # make a new command node
396             my $newnode = LaTeX::TOM::Node->_new(
397             {type => 'COMMAND',
398             command => $command,
399             start => $parent->{start},
400             end => $parent->{end},
401             position => 'inner',
402 0         0 children => $parent->{children} });
403              
404             # point parent to it
405 0         0 $parent->{children} = LaTeX::TOM::Tree->_new([$newnode]);
406              
407             # start over at this level (get additional inner commands)
408 0         0 $parent = $newnode;
409 0         0 $i = -1;
410              
411 0         0 $parser->{USED_COMMANDS}->{$newnode->{command}} = 1;
412             }
413              
414             # parent is a naked group, we can make it into a command node
415             #
416             elsif ($parent->{type} eq 'GROUP') {
417 2         5 $parent->{type} = 'COMMAND';
418 2         4 $parent->{command} = $command;
419 2         4 $parent->{position} = 'inner';
420              
421             # start over at this level
422 2         3 $i = -1;
423              
424 2         4 $parser->{USED_COMMANDS}->{$parent->{command}} = 1;
425             }
426              
427 2         21 $node->{content} =~ s/^\s*\\(?:\w+\*?)//o;
428             }
429              
430             # outer command (such as \command{parameters}). our regexp checks to
431             # see if this text chunk ends in \command, since that would be the case
432             # due to the previous parsing stages.
433             #
434 266 100 100     1291 if ($text =~ /(?:^|[^\\])(\\\w+\*?(\s*\[.*?\])?)\s*$/os &&
      66        
435             defined $tree->{nodes}[$i+1] &&
436             $tree->{nodes}[$i+1]->{type} eq 'GROUP') {
437              
438 112         293 my $tag = $1;
439              
440 112         384 _debug("found text node [$text] with command tag [$tag]", undef);
441              
442             # remove the text
443 112         611 $node->{content} =~ s/\\\w+\*?\s*(?:\[.*?\])?\s*$//os;
444              
445             # parse it for command and ops
446 112         348 $tag =~ /^\\(\w+\*?)\s*(?:\[(.*?)\])?$/os;
447              
448 112         234 my $command = $1;
449 112         186 my $opts = $2;
450              
451             # make the next node a command node with the above data
452 112         187 my $next = $tree->{nodes}[$i+1];
453              
454 112         208 $next->{type} = 'COMMAND';
455 112         233 $next->{command} = $command;
456 112         177 $next->{opts} = $opts;
457 112         190 $next->{position} = 'outer';
458              
459 112         262 $parser->{USED_COMMANDS}->{$next->{command}} = 1;
460             }
461              
462             # recognize braceless commands
463             #
464 266 100 66     1333 if ($text =~ /(\\(\w+\*?)[ \t]+(\S+))/gso || $text =~ /(\\(\w+)(\d+))/gso) {
465 2         6 my $all = $1;
466 2         6 my $command = $2;
467 2         6 my $param = $3;
468              
469 2 50       8 if ($parser->{config}{BRACELESS}->{$command}) {
470             # warn "found braceless command $command with param $param";
471              
472             # get location to split from node text
473 0         0 my $a = index $node->{content}, $all, 0;
474 0         0 my $b = $a + length($all) - 1;
475              
476             # make all the new nodes
477              
478             # new left and right text nodes
479 0         0 my ($leftnode, $rightnode) = $node->split($a, $b);
480              
481             # param contents node
482 0         0 my $pstart = index $node->{content}, $param, $a;
483             my $newchild = LaTeX::TOM::Node->_new(
484             {type => 'TEXT',
485             start => $node->{start} + $pstart,
486 0         0 end => $node->{start} + $pstart + length($param) - 1,
487             content => $param });
488              
489             # new command node
490             my $commandnode = LaTeX::TOM::Node->_new(
491             {type => 'COMMAND',
492             braces => 0,
493             command => $command,
494             start => $node->{start} + $a,
495 0         0 end => $node->{start} + $b,
496             children => LaTeX::TOM::Tree->_new([$newchild]),
497             });
498              
499 0         0 $parser->{USED_COMMANDS}->{$commandnode->{command}} = 1;
500              
501             # splice these all into the original array
502 0         0 splice @{$tree->{nodes}}, $i, 1, $leftnode, $commandnode, $rightnode;
  0         0  
503              
504             # make the rightnode the node we're currently analyzing
505 0         0 $node = $rightnode;
506              
507             # make sure outer loop will continue parsing *after* rightnode
508 0         0 $i += 2;
509             }
510             }
511             }
512              
513             # recur
514 385 100 100     1250 if ($node->{type} eq 'GROUP' ||
515             $node->{type} eq 'COMMAND') {
516              
517 119         316 $parser->_stage3($node->{children}, $node);
518             }
519             }
520             }
521              
522             # this stage finds \begin{x} \end{x} environments and shoves their contents
523             # down into a new child node, with a parent node of ENVIRONMENT type.
524             #
525             # this has the effect of making the tree deeper, since much of the structure
526             # is in environment tags and will now be picked up.
527             #
528             # for ENVIRONMENTs, "start" means the ending } on the \begin tag,
529             # "end" means the starting \ on the \end tag,
530             # "ostart" is the starting \ on the "begin" tag,
531             # "oend" is the ending } on the "end" tag, and
532             # and "class" is the "x" from above.
533             #
534             sub _stage4 {
535 144     144   214 my $parser = shift;
536 144         179 my $tree = shift;
537              
538 144         185 my $bcount = 0; # \begin "stack count"
539 144         185 my $class = ""; # environment class
540 144         171 my $bidx = 0; # \begin array index.
541              
542 144         199 for (my $i = 0; $i < @{$tree->{nodes}}; $i++) {
  604         1155  
543 460         592 my $node = $tree->{nodes}->[$i];
544              
545             # see if this is a "\begin" command node
546 460 100 100     1879 if ($node->{type} eq 'COMMAND' && $node->{command} eq 'begin') {
    100 100        
    100 100        
547              
548 32         150 _debug("parseStage4: found a begin COMMAND node, $node->{children}->{nodes}[0]->{content} @ $node->{start}", undef);
549              
550             # start a new "stack"
551 32 100       95 if ($bcount == 0) {
    50          
552 28         52 $bidx = $i;
553 28         37 $bcount++;
554 28         69 $class = $node->{children}->{nodes}->[0]->{content};
555 28         74 _debug("parseStage4: opening environment tag found, class = $class", undef);
556             }
557              
558             # add to the "stack"
559             elsif ($node->{children}->{nodes}->[0]->{content} eq $class) {
560 0         0 $bcount++;
561 0         0 _debug("parseStage4: incrementing tag count for $class", undef);
562             }
563             }
564              
565             # handle "\end" command nodes
566             elsif ($node->{type} eq 'COMMAND' &&
567             $node->{command} eq 'end' &&
568             $node->{children}->{nodes}->[0]->{content} eq $class) {
569              
570 27         66 $bcount--;
571 27         94 _debug("parseStage4: decrementing tag count for $class", undef);
572              
573             # we found our closing "\end" tag. replace everything with the proper
574             # ENVIRONMENT tag and subtree.
575             #
576 27 50       65 if ($bcount == 0) {
577              
578 27         79 _debug("parseStage4: closing environment $class", undef);
579              
580             # first we must take everything between the "\begin" and "\end"
581             # nodes and put them in a new array, removing them from the old one
582 27         46 my @newarray = splice @{$tree->{nodes}}, $bidx+1, $i - ($bidx + 1);
  27         106  
583              
584             # make the ENVIRONMENT node
585 27         67 my $start = $tree->{nodes}[$bidx]->{end};
586 27         52 my $end = $node->{start};
587 27         159 my $envnode = LaTeX::TOM::Node->_new(
588             {type => 'ENVIRONMENT',
589             class => $class,
590             start => $start, # "inner" start and end
591             end => $end,
592             ostart => $start - length('begin') - length($class) - 2,
593             oend => $end + length('end') + length($class) + 2,
594             children => LaTeX::TOM::Tree->_new([@newarray]),
595             });
596              
597 27 50       136 if ($parser->{config}{MATHENVS}->{$envnode->{class}}) {
598 0         0 $envnode->{math} = 1;
599             }
600              
601             # replace the \begin and \end COMMAND nodes with the single
602             # environment node
603 27         48 splice @{$tree->{nodes}}, $bidx, 2, $envnode;
  27         166  
604              
605 27         57 $class = ""; # reset class.
606              
607             # i is going to change by however many nodes we removed
608 27         47 $i -= scalar @newarray;
609              
610             # recur into the children
611 27         75 $parser->_stage4($envnode->{children});
612             }
613             }
614              
615             # recur in general
616             elsif ($node->{children}) {
617 90         296 $parser->_stage4($node->{children});
618             }
619             }
620              
621             # parse error if we're missing an "\end" tag.
622 144 100       409 if ($bcount > 0) {
623 1         10 $error_handlers{$parser->{PARSE_ERRORS_FATAL}}->(
624             "missing \\end{$class} for \\begin{$class} at position $tree->{nodes}[$bidx]->{end}"
625             );
626             }
627             }
628              
629             # This is the "math" stage: here we grab simple-delimiter math modes from
630             # the text they are embedded in, and turn those into new groupings, with the
631             # "math" flag set.
632             #
633             # having this top level to go over all the bracket types prevents some pretty
634             # bad combinatorial explosion
635             #
636             sub _stage5 {
637 46     46   70 my $parser = shift;
638              
639 46         68 my $tree = shift;
640 46   100     145 my $caremath = shift || 0;
641              
642 46         78 my $brackets = $parser->{MATHBRACKETS};
643              
644             # loop through all the different math mode bracket types
645 46         230 foreach my $left (sort {length($b) <=> length($a)} keys %$brackets) {
  125         278  
646 140         252 my $right = $brackets->{$left};
647              
648 140         289 $parser->_stage5_r($tree, $left, $right, $caremath);
649             }
650             }
651              
652             # recursive meat of above
653             #
654             sub _stage5_r {
655 554     554   740 my $parser = shift;
656              
657 554         685 my $tree = shift;
658 554         694 my $left = shift;
659 554         671 my $right = shift;
660 554   100     1139 my $caremath = shift || 0; # do we care if we're already in math mode?
661             # this matters for \( \), \[ \]
662              
663 554         690 my $leftpos = -1; # no text pos for found left brace yet.
664 554         624 my $leftidx = -1; # no array index for found left brace yet.
665              
666             # loop through the nodes
667 554         726 for (my $i = 0; $i < scalar @{$tree->{nodes}}; $i++) {
  2120         3870  
668 1566         2062 my $node = $tree->{nodes}[$i];
669 1566         1908 my $pos = 0; # position placeholder within the text block
670 1566         2146 my $spos = $node->{start}; # get text start position
671              
672 1566 100 66     6957 if ($node->{type} eq 'TEXT' &&
    100 66        
      66        
      66        
673             (!$caremath || (!$node->{math} && $caremath))) {
674              
675             # search for left brace if we haven't started a pair yet
676 1114 50       1817 if ($leftidx == -1) {
677 1114         1775 $leftpos = _findsymbol($node->{content}, $left, $pos);
678              
679 1114 50       2010 if ($leftpos != -1) {
680 0         0 _debug("found (left) $left in [$node->{content}]", undef);
681 0         0 $leftidx = $i;
682 0         0 $pos = $leftpos + 1; # next pos to search from
683             }
684             }
685              
686             # search for a right brace
687 1114 50       1685 if ($leftpos != -1) {
688 0         0 my $rightpos = _findsymbol($node->{content}, $right, $pos);
689              
690             # found
691 0 0       0 if ($rightpos != -1) {
692              
693             # we have to split the text node into 3 parts
694 0 0       0 if ($leftidx == $i) {
695 0         0 _debug("splitwithin: found (right) $right in [$node->{content}]", undef);
696              
697 0         0 my ($leftnode, $textnode3) = $node->split($rightpos, $rightpos + length($right) - 1);
698 0         0 my ($textnode1, $textnode2) = $leftnode->split($leftpos, $leftpos + length($left) - 1);
699              
700 0         0 my $startpos = $spos; # get text start position
701              
702             # make the math ENVIRONMENT node
703 0         0 my $mathnode = LaTeX::TOM::Node->_new(
704             {type => 'ENVIRONMENT',
705             class => $left, # use left delim as class
706             math => 1,
707             start => $startpos + $leftpos,
708             ostart => $startpos + $leftpos - length($left) + 1,
709             end => $startpos + $rightpos,
710             oend => $startpos + $rightpos + length($right) - 1,
711             children => LaTeX::TOM::Tree->_new([$textnode2]),
712             });
713              
714 0         0 splice @{$tree->{nodes}}, $i, 1, $textnode1, $mathnode, $textnode3;
  0         0  
715              
716 0         0 $i++; # skip ahead two nodes, so we'll be parsing textnode3
717             }
718              
719             # split across nodes
720             else {
721              
722 0         0 _debug("splitacross: found (right) $right in [$node->{content}]", undef);
723              
724             # create new set of 4 smaller text nodes from the original two
725             # that contain the left and right delimiters
726             #
727 0         0 my ($textnode1, $textnode2) = $tree->{nodes}[$leftidx]->split($leftpos, $leftpos + length($left) - 1);
728 0         0 my ($textnode3, $textnode4) = $tree->{nodes}[$i]->split($rightpos, $rightpos + length($right) - 1);
729              
730             # nodes to remove "from the middle" (between the left and right
731             # text nodes which contain the delimiters)
732             #
733 0         0 my @remnodes = splice @{$tree->{nodes}}, $leftidx+1, $i - $leftidx - 1;
  0         0  
734              
735             # create a math node that contains the text after the left brace,
736             # then all the nodes up until the next text node, then the text
737             # before the right brace.
738             #
739             my $mathnode = LaTeX::TOM::Node->_new(
740             {type => 'ENVIRONMENT',
741             class => $left,
742             math => 1,
743             start => $textnode2->{start} - 1,
744             end => $textnode3->{end} + 1,
745             ostart => $textnode2->{start} - 1 - length($left) + 1,
746 0         0 oend => $textnode3->{end} + 1 + length($right) - 1,
747             children => LaTeX::TOM::Tree->_new(
748             [$textnode2,
749             @remnodes,
750             $textnode3]),
751             });
752              
753             # replace (TEXT_A, ... , TEXT_B) with the mathnode created above
754 0         0 splice @{$tree->{nodes}}, $leftidx, 2, $textnode1, $mathnode, $textnode4;
  0         0  
755              
756             # do all nodes again but the very leftmost
757             #
758 0         0 $i = $leftidx;
759             }
760              
761 0         0 $leftpos = -1; # reset left data
762 0         0 $leftidx = -1;
763             } # right brace
764             } # left brace
765             else {
766              
767 1114         1732 my $rightpos = _findsymbol($node->{content}, $right, $pos);
768              
769 1114 50       2211 if ($rightpos != -1) {
770 0         0 my $startpos = $node->{start}; # get text start position
771 0         0 $error_handlers{$parser->{PARSE_ERRORS_FATAL}}->("unmatched '$right' at " . ($startpos + $rightpos));
772             }
773             }
774             } # if TEXT
775              
776             # recur, but not into verbatim environments!
777             #
778             elsif ($node->{children} &&
779             !(
780             ($node->{type} eq 'COMMAND' && $node->{command} =~ /^verb/) ||
781             ($node->{type} eq 'ENVIRONMENT' && $node->{class} =~ /^verbatim/))) {
782              
783 414 50       746 if ($LaTeX::TOM::DEBUG) {
784 0         0 my $message = "Recurring into $node->{type} node ";
785 0 0       0 $message .= $node->{command} if ($node->{type} eq 'COMMAND');
786 0 0       0 $message .= $node->{class} if ($node->{type} eq 'ENVIRONMENT');
787 0         0 _debug($message, undef);
788             }
789              
790 414         998 $parser->_stage5_r($node->{children}, $left, $right, $caremath);
791             }
792              
793             } # loop over text blocks
794              
795 554 50       1244 if ($leftpos != -1) {
796 0         0 my $startpos = $tree->{nodes}[$leftidx]->{start}; # get text start position
797 0         0 $error_handlers{$parser->{PARSE_ERRORS_FATAL}}->("unmatched '$left' at " . ($startpos + $leftpos));
798             }
799             }
800              
801             # This stage propegates the math mode flag and plaintext flags downward.
802             #
803             # After this is done, we can make the claim that only text nodes marked with
804             # the plaintext flag should be printed. math nodes will have the "math" flag,
805             # and also plantext = 0.
806             #
807             sub _propegateModes {
808 190     190   264 my $parser = shift;
809              
810 190         241 my $tree = shift;
811 190         232 my $math = shift; # most likely want to call this with 0
812 190         231 my $plaintext = shift; # ditto this-- default to nothing visible.
813              
814 190         233 foreach my $node (@{$tree->{nodes}}) {
  190         338  
815              
816             # handle text nodes on this level. set flags.
817             #
818 536 100       948 if ($node->{type} eq 'TEXT') {
    50          
819 384         546 $node->{math} = $math;
820 384         642 $node->{plaintext} = $plaintext;
821             }
822              
823             # propegate flags downward, possibly modified
824             #
825             elsif (defined $node->{children}) {
826              
827 152         194 my $mathflag = $math; # math propegates down by default
828 152         196 my $plaintextflag = 0; # plaintext flag does NOT propegate by default
829              
830             # handle math or plain text forcing envs
831             #
832 152 100 100     486 if ($node->{type} eq 'ENVIRONMENT' || $node->{type} eq 'COMMAND') {
    50          
833 148 50 33     1280 if (defined $node->{class} && (
    100 66        
      66        
      66        
      33        
      66        
      66        
834             $parser->{config}{MATHENVS}->{$node->{class}} ||
835             $parser->{config}{MATHENVS}->{"$node->{class}*"})
836             )
837             {
838 0         0 $mathflag = 1;
839 0         0 $plaintextflag = 0;
840             }
841             elsif (($node->{type} eq 'COMMAND' &&
842             ($parser->{config}{TEXTENVS}->{$node->{command}} ||
843             $parser->{config}{TEXTENVS}->{"$node->{command}*"})) ||
844             ($node->{type} eq 'ENVIRONMENT' &&
845             ($parser->{config}{TEXTENVS}->{$node->{class}} ||
846             $parser->{config}{TEXTENVS}{"$node->{command}*"}))
847             ) {
848              
849 80         124 $mathflag = 0;
850 80         103 $plaintextflag = 1;
851             }
852             }
853              
854             # groupings change nothing
855             #
856             elsif ($node->{type} eq 'GROUP') {
857 4         6 $mathflag = $math;
858 4         6 $plaintextflag = $plaintext;
859             }
860              
861             # recur
862 152         427 $parser->_propegateModes($node->{children}, $mathflag, $plaintextflag);
863             }
864             }
865             }
866              
867             # apply a mapping to text nodes in a tree
868             #
869             # for newcommands and defs: mapping is a hash:
870             #
871             # {name, nparams, template, type}
872             #
873             # name is a string
874             # nparams is an integer
875             # template is a tree fragement containing text nodes with #x flags, where
876             # parameters will be replaced.
877             # type is "command"
878             #
879             # for newenvironments:
880             #
881             # {name, nparams, btemplate, etemplate, type}
882             #
883             # same as above, except type is "environment" and there are two templates,
884             # btemplate and etemplate.
885             #
886             sub _applyMapping {
887 12     12   18 my $parser = shift;
888              
889 12         16 my $tree = shift;
890 12         18 my $mapping = shift;
891 12   100     29 my $i = shift || 0; # index to start with, in tree.
892              
893 12         13 my $applications = 0; # keep track of # of applications
894              
895 12         16 for (; $i < @{$tree->{nodes}}; $i++) {
  54         109  
896              
897 42         54 my $node = $tree->{nodes}[$i];
898              
899             # begin environment nodes
900             #
901 42 100 100     333 if ($node->{type} eq 'COMMAND'
    100 100        
    100 100        
    100 100        
    100 100        
      66        
      100        
902             && $node->{command} eq 'begin'
903             && $node->{children}->{nodes}[0]->{content} eq $mapping->{name}
904             ) {
905             # grab the nparams next group nodes as parameters
906             #
907 1         3 my @params = ();
908              
909 1         2 my $remain = $mapping->{nparams};
910 1         2 my $j = 1;
911 1   33     20 while ($remain > 0 && ($i + $j) < scalar @{$tree->{nodes}}) {
  0         0  
912              
913 0         0 my $node = $tree->{nodes}[$i + $j];
914              
915             # grab group node
916 0 0       0 if ($node->{type} eq 'GROUP') {
917 0         0 push @params, $node->{children};
918 0         0 $remain--;
919             }
920              
921 0         0 $j++;
922             }
923              
924             # if we didn't get enough group nodes, bomb out
925 1 50       7 next if $remain;
926              
927             # otherwise make new subtree
928 1         4 my $applied = _applyParamsToTemplate($mapping->{btemplate}, @params);
929              
930             # splice in the result
931 1         3 splice @{$tree->{nodes}}, $i, $j, @{$applied->{nodes}};
  1         9  
  1         4  
932              
933             # skip past all the new stuff
934 1         3 $i += scalar @{$applied->{nodes}} - 1;
  1         6  
935             }
936              
937             # end environment nodes
938             #
939             elsif ($node->{type} eq 'COMMAND'
940             && $node->{command} eq 'end'
941             && $node->{children}->{nodes}[0]->{content} eq $mapping->{name}
942             ) {
943             # make new subtree (no params)
944 1         4 my $applied = $mapping->{etemplate}->copy();
945              
946             # splice in the result
947 1         3 splice @{$tree->{nodes}}, $i, 1, @{$applied->{nodes}};
  1         2  
  1         13  
948              
949             # skip past all the new stuff
950 1         3 $i += scalar @{$applied->{nodes}} - 1;
  1         4  
951              
952 1         4 $applications++; # only count end environment nodes
953             }
954              
955             # newcommand nodes
956             #
957             elsif ($node->{type} eq 'COMMAND'
958             && $node->{command} eq $mapping->{name}
959             && $mapping->{nparams}
960             ) {
961 1         4 my @params = ();
962              
963             # children of COMMAND node will be first parameter
964 1         2 push @params, $node->{children};
965              
966             # find next nparams GROUP nodes and push their children onto @params
967 1         3 my $remain = $mapping->{nparams} - 1;
968 1         1 my $j = 1;
969 1   33     5 while ($remain > 0 && ($i + $j) < scalar @{$tree->{nodes}}) {
  0         0  
970              
971 0         0 my $node = $tree->{nodes}[$i + $j];
972              
973             # grab group node
974 0 0       0 if ($node->{type} eq 'GROUP') {
975 0         0 push @params, $node->{children};
976 0         0 $remain--;
977             }
978              
979 0         0 $j++;
980             }
981              
982             # if we didn't get enough group nodes, bomb out
983 1 50       5 next if ($remain > 0);
984              
985             # apply the params to the template
986 1         3 my $applied = _applyParamsToTemplate($mapping->{template}, @params);
987              
988             # splice in the result
989 1         3 splice @{$tree->{nodes}}, $i, $j, @{$applied->{nodes}};
  1         3  
  1         2  
990              
991             # skip past all the new stuff
992 1         2 $i += scalar @{$applied->{nodes}} - 1;
  1         2  
993              
994 1         4 $applications++;
995             }
996              
997             # find 0-param mappings
998             elsif ($node->{type} eq 'TEXT' && !$mapping->{nparams}) {
999              
1000 15         26 my $text = $node->{content};
1001 15         22 my $command = $mapping->{name};
1002              
1003             # find occurrences of the mapping command
1004             #
1005 15 50       43 my $wordend = ($command =~ /\w$/ ? 1 : 0);
1006 15   33     128 while (($wordend && $text =~ /\\\Q$command\E(\W|$)/g) ||
      33        
      33        
1007             (!$wordend && $text =~ /\\\Q$command\E/g)) {
1008              
1009 0         0 _debug("found occurrence of mapping $command", undef);
1010              
1011 0         0 my $idx = index $node->{content}, '\\' . $command, 0;
1012              
1013             # split the text node at that command
1014 0         0 my ($leftnode, $rightnode) = $node->split($idx, $idx + length($command));
1015              
1016             # copy the mapping template
1017 0         0 my $applied = $mapping->{template}->copy();
1018              
1019             # splice the new nodes in
1020 0         0 splice @{$tree->{nodes}}, $i, 1, $leftnode, @{$applied->{nodes}}, $rightnode;
  0         0  
  0         0  
1021              
1022             # adjust i so we end up on rightnode when we're done
1023 0         0 $i += scalar @{$applied->{nodes}} + 1;
  0         0  
1024              
1025             # get the next node
1026 0         0 $node = $tree->{nodes}[$i];
1027              
1028             # count application
1029 0         0 $applications++;
1030             }
1031             }
1032              
1033             # recur
1034             elsif ($node->{children}) {
1035              
1036 10         43 $applications += $parser->_applyMapping($node->{children}, $mapping);
1037             }
1038             }
1039              
1040 12         26 return $applications;
1041             }
1042              
1043             # find and apply all mappings in the tree, progressively and recursively.
1044             # a mapping applies to the entire tree and subtree consisting of nodes AFTER
1045             # itself in the level array.
1046             #
1047             sub _applyMappings {
1048 14     14   21 my $parser = shift;
1049              
1050 14         18 my $tree = shift;
1051              
1052 14         29 for (my $i = 0; $i < @{$tree->{nodes}}; $i++) {
  63         116  
1053              
1054 49         71 my $prev = $tree->{nodes}[$i-1];
1055 49         61 my $node = $tree->{nodes}[$i];
1056              
1057             # find newcommands
1058 49 100 100     298 if ($node->{type} eq 'COMMAND' &&
    100 100        
    50 66        
    100 66        
      33        
1059             $node->{command} =~ /^(re)?newcommand$/) {
1060              
1061 1         4 my $mapping = _makeMapping($tree, $i);
1062 1 50       5 next if (!$mapping->{name}); # skip fragged commands
1063              
1064 1 50       4 if ($parser->{USED_COMMANDS}->{$mapping->{name}}) {
1065 1         6 _debug("applying (nc) mapping $mapping->{name}", undef);
1066             } else {
1067 0         0 _debug("NOT applying (nc) mapping $mapping->{name}", undef);
1068 0         0 next;
1069             }
1070              
1071             # add to mappings list
1072             #
1073 1         5 $parser->{MAPPEDCMDS}->{"\\$mapping->{name}"} = 1;
1074              
1075 1         4 _debug("found a mapping with name $mapping->{name}, $mapping->{nparams} params", undef);
1076              
1077             # remove the mapping declaration
1078             #
1079 1         3 splice @{$tree->{nodes}}, $i, $mapping->{skip} + 1;
  1         5  
1080              
1081             # apply the mapping
1082 1         4 my $count = $parser->_applyMapping($tree, $mapping, $i);
1083              
1084 1 50       5 if ($count > 0) {
1085 1     0   6 _debug("printing altered subtree", sub { $tree->_warn() });
  0         0  
1086             }
1087              
1088 1         7 $i--; # since we removed the cmd node, check this index again
1089             }
1090              
1091             # handle "\newenvironment" mappings
1092             elsif ($node->{type} eq 'COMMAND' &&
1093             $node->{command} =~ /^(re)?newenvironment$/) {
1094              
1095             # make a mapping hash
1096             #
1097 1         6 my $mapping = $parser->_makeEnvMapping($tree, $i);
1098 1 50       4 next if (!$mapping->{name}); # skip fragged commands.
1099              
1100 1         13 _debug("applying (ne) mapping $mapping->{name}", undef);
1101              
1102             # remove the mapping declaration
1103             #
1104 1         5 splice @{$tree->{nodes}}, $i, $mapping->{skip} + 1;
  1         8  
1105              
1106             # apply the mapping
1107             #
1108 1         5 my $count = $parser->_applyMapping($tree, $mapping, $i);
1109             }
1110              
1111             # handle "\def" stype commands.
1112             elsif ($node->{type} eq 'COMMAND' &&
1113             defined $prev &&
1114             $prev->{type} eq 'TEXT' &&
1115             $prev->{content} =~ /\\def\s*$/o) {
1116              
1117 0         0 _debug("found def style mapping $node->{command}", undef);
1118              
1119             # remove the \def
1120 0         0 $prev->{content} =~ s/\\def\s*$//o;
1121              
1122             # make the mapping
1123             my $mapping = {name => $node->{command},
1124             nparams => 0,
1125 0         0 template => $node->{children}->copy(),
1126             type => 'command'};
1127              
1128 0 0       0 next if (!$mapping->{name}); # skip fragged commands
1129              
1130 0 0       0 if ($parser->{USED_COMMANDS}->{$mapping->{name}}) {
1131 0         0 _debug("applying (def) mapping $mapping->{name}", undef);
1132             } else {
1133 0         0 _debug("NOT applying (def) mapping $mapping->{name}", undef);
1134 0         0 next;
1135             }
1136              
1137             # add to mappings list
1138             #
1139 0         0 $parser->{MAPPEDCMDS}->{"\\$mapping->{name}"} = 1;
1140              
1141 0     0   0 _debug("template is", sub { $mapping->{template}->_warn() });
  0         0  
1142              
1143             # remove the command node
1144 0         0 splice @{$tree->{nodes}}, $i, 1;
  0         0  
1145              
1146             # apply the mapping
1147 0         0 my $count = $parser->_applyMapping($tree, $mapping, $i);
1148              
1149 0         0 $i--; # check this index again
1150             }
1151              
1152             # recur
1153             elsif ($node->{children}) {
1154              
1155 12         27 $parser->_applyMappings($node->{children});
1156             }
1157             }
1158             }
1159              
1160             # read files from \input commands and place into the tree, parsed
1161             #
1162             # also include bibliographies
1163             #
1164             sub _addInputs {
1165 89     89   123 my $parser = shift;
1166              
1167 89         118 my $tree = shift;
1168              
1169 89         130 for (my $i = 0; $i < @{$tree->{nodes}}; $i++) {
  326         616  
1170              
1171 237         304 my $node = $tree->{nodes}[$i];
1172              
1173 237 100 100     816 if ($node->{type} eq 'COMMAND'
    100 100        
1174             && $node->{command} eq 'input'
1175             ) {
1176 7         15 my $file = $node->{children}->{nodes}[0]->{content};
1177 7 100       24 next if $file =~ /pstex/; # ignore pstex images
1178              
1179 6         23 _debug("reading input file $file", undef);
1180              
1181 6         8 my $contents;
1182 6         140 my $filename = fileparse($file);
1183 6         41 my $has_extension = qr/\.\S+$/;
1184              
1185             # read in contents of file
1186 6 100 66     191 if (-e $file && $filename =~ $has_extension) {
    50          
1187 5         21 $contents = _readFile($file);
1188             }
1189             elsif ($filename !~ $has_extension) {
1190 1         5 $file = "$file.tex";
1191 1 50       17 $contents = _readFile($file) if -e $file;
1192             }
1193              
1194             # dump Psfig/TeX files, they aren't useful to us and have
1195             # nonconforming syntax. Use declaration line as our heuristic.
1196             #
1197 6 100 66     43 if (defined $contents
1198             && $contents =~ m!^ \% \s*? Psfig/TeX \s* $!mx
1199             ) {
1200 1         3 undef $contents;
1201 1         244 carp "ignoring Psfig input `$file'";
1202             }
1203              
1204             # actually do the parse of the sub-content
1205             #
1206 6 100       95 if (defined $contents) {
1207             # parse into a tree
1208 5         20 my ($subtree,) = $parser->_basicparse($contents, $parser->{PARSE_ERRORS_FATAL});
1209              
1210             # replace \input command node with subtree
1211 5         14 splice @{$tree->{nodes}}, $i, 1, @{$subtree->{nodes}};
  5         12  
  5         12  
1212              
1213             # step back
1214 5         21 $i--;
1215             }
1216             }
1217             elsif ($node->{type} eq 'COMMAND'
1218             && $node->{command} eq 'bibliography'
1219             ) {
1220             # try to find a .bbl file
1221             #
1222 1         96 foreach my $file (<*.bbl>) {
1223              
1224 1         6 my $contents = _readFile($file);
1225              
1226 1 50       6 if (defined $contents) {
1227              
1228 1         9 my ($subtree,) = $parser->_basicparse($contents, $parser->{PARSE_ERRORS_FATAL});
1229 1         4 splice @{$tree->{nodes}}, $i, 1, @{$subtree->{nodes}};
  1         3  
  1         4  
1230 1         3 $i--;
1231             }
1232             }
1233             }
1234              
1235             # recur
1236 236 100       421 if ($node->{children}) {
1237 71         158 $parser->_addInputs($node->{children});
1238             }
1239             }
1240             }
1241              
1242             # do pre-mapping parsing
1243             #
1244             sub _parseA {
1245 27     27   49 my $parser = shift;
1246 27         46 my $text = shift;
1247              
1248 27         100 my $tree = $parser->_stage1($text);
1249 27         77 my $bracehash = $parser->_stage2($tree);
1250              
1251 27         90 $parser->_stage3($tree);
1252              
1253 27 100       114 $parser->_addInputs($tree) if $parser->{readinputs};
1254              
1255 27         72 return ($tree, $bracehash);
1256             }
1257              
1258             # do post-mapping parsing (make environments)
1259             #
1260             sub _parseB {
1261 27     27   48 my $parser = shift;
1262 27         61 my $tree = shift;
1263              
1264 27         77 $parser->_stage4($tree);
1265              
1266 27         74 _debug("done with parseStage4", undef);
1267              
1268 27         89 $parser->_stage5($tree, 0);
1269              
1270 27         80 _debug("done with parseStage5", undef);
1271             }
1272              
1273             ###############################################################################
1274             #
1275             # Parser "Static" Subroutines
1276             #
1277             ###############################################################################
1278              
1279             # find next unescaped char in some text
1280             #
1281             sub _uindex {
1282 0     0   0 my $text = shift;
1283 0         0 my $char = shift;
1284 0         0 my $pos = shift;
1285              
1286 0         0 my $realbrace = 0;
1287 0         0 my $idx = -1;
1288              
1289             # get next opening brace
1290 0         0 do {
1291 0         0 $realbrace = 1;
1292 0         0 $idx = index $text, $char, $pos;
1293              
1294 0 0       0 if ($idx != -1) {
1295 0         0 $pos = $idx + 1;
1296 0         0 my $prevchar = substr $text, $idx - 1, 1;
1297 0 0       0 if ($prevchar eq '\\') {
1298 0         0 $realbrace = 0;
1299 0         0 $idx = -1;
1300             }
1301             }
1302             } while (!$realbrace);
1303              
1304 0         0 return $idx;
1305             }
1306              
1307             sub _find {
1308 3006     3006   4827 my ($text, $symbol, $pos) = @_;
1309              
1310 3006         3714 my ($found, $index);
1311              
1312             # get next occurrence of the symbol
1313 3006         3534 do {
1314 3006         3554 $found = true;
1315 3006         4113 $index = index $text, $symbol, $pos;
1316              
1317 3006 50 100     6015 if ($symbol eq '}' && $index - 1 >= 0 && substr($text, $index - 1, 1) eq ' ') {
      66        
1318             #$pos = $index + 1;
1319 0         0 $index = -1;
1320             }
1321              
1322 3006 100       5955 if ($index != -1) {
1323 460         567 $pos = $index + 1;
1324              
1325             # make sure this occurrence isn't escaped. this is imperfect.
1326 460 100       862 my $prev_char = ($index - 1 >= 0) ? (substr $text, $index - 1, 1) : '';
1327 460 100       789 my $pprev_char = ($index - 2 >= 0) ? (substr $text, $index - 2, 1) : '';
1328              
1329 460 50 33     1288 if ($prev_char eq '\\' && $pprev_char ne '\\') {
1330 0         0 $found = false;
1331             }
1332             }
1333             } until ($found);
1334              
1335 3006         4669 return $index;
1336             }
1337              
1338             # support function: find the next occurrence of some symbol which is
1339             # not escaped.
1340             #
1341             sub _findsymbol {
1342 2228     2228   3282 return _find(@_);
1343             }
1344              
1345             # support function: find the earliest next brace in some (flat) text
1346             #
1347             sub _findbrace {
1348 389     389   649 my ($text, $pos) = @_;
1349              
1350 389         650 my $index_o = _find($text, '{', $pos);
1351 389         615 my $index_c = _find($text, '}', $pos);
1352              
1353             # handle all find cases
1354 389 100 100     1865 if ($index_o == -1 && $index_c == -1) {
    100 100        
    50 33        
      66        
1355 145         340 return (-1, '');
1356             }
1357             elsif ($index_c == -1 || ($index_o != -1 && $index_o < $index_c)) {
1358 122         311 return ($index_o, '{');
1359             }
1360             elsif ($index_o == -1 || $index_c < $index_o) {
1361 122         406 return ($index_c, '}');
1362             }
1363             }
1364              
1365              
1366             # skip "blank nodes" in a tree, starting at some position. will finish
1367             # at the first non-blank node. (ie, not a comment or whitespace TEXT node.
1368             #
1369             sub _skipBlankNodes {
1370 2     2   4 my ($tree, $i) = @_;
1371              
1372 2         4 my $node = $tree->{nodes}[$$i];
1373              
1374 2   66     18 while ($node->{type} eq 'COMMENT'
      66        
1375             || ($node->{type} eq 'TEXT'
1376             && $node->{content} =~ /^\s*$/s)
1377             ) {
1378 1         5 $node = $tree->{nodes}[++$$i];
1379             }
1380             }
1381              
1382             # is the passed-in node a valid parameter node? for this to be true, it must
1383             # either be a GROUP or a position = inner command.
1384             #
1385             sub _validParamNode {
1386 2     2   4 my ($node) = @_;
1387              
1388 2 50 0     7 if ($node->{type} eq 'GROUP'
      33        
1389             || ($node->{type} eq 'COMMAND' && $node->{position} eq 'inner')) {
1390 2         5 return true;
1391             }
1392 0         0 return false;
1393             }
1394              
1395             # duplicate a valid param node. This means for a group, copy the child tree.
1396             # for a command, make a new tree with just the command node and its child tree.
1397             #
1398             sub _duplicateParam {
1399 2     2   4 my $parser = shift;
1400 2         3 my $node = shift;
1401              
1402 2 50       5 if ($node->{type} eq 'GROUP') {
    0          
1403 2         16 return $node->{children}->copy();
1404             }
1405             elsif ($node->{type} eq 'COMMAND') {
1406              
1407 0         0 my $subtree = $node->{children}->copy(); # copy child subtree
1408 0         0 my $nodecopy = $node->copy(); # make a new node with old data
1409 0         0 $nodecopy->{children} = $subtree; # set the child pointer to new subtree
1410              
1411             # return a new tree with the new node (subtree) as its only element
1412 0         0 return LaTeX::TOM::Tree->_new([$nodecopy]);
1413             }
1414              
1415 0         0 return undef;
1416             }
1417              
1418             sub _getMapping {
1419 2     2   6 my ($type, $tree, $i) = @_;
1420              
1421 2         5 my $node = $tree->{nodes}[$$i];
1422              
1423 2 50 33     12 if ($node->{type} ne 'COMMAND'
      33        
1424             || ($node->{command} ne "new$type"
1425             && $node->{command} ne "renew$type")
1426             ) {
1427 0         0 return ();
1428             }
1429              
1430             # figure out command (first child, text node)
1431 2         5 my $command = $node->{children}->{nodes}[0]->{content};
1432 2 100       10 if ($command =~ /^\s* \\(\S+) \s*$/x) {
1433 1         3 $command = $1;
1434             }
1435              
1436 2         4 $node = $tree->{nodes}[++$$i];
1437              
1438             # figure out number of params
1439 2         6 my $nparams = 0;
1440 2 50       5 if ($node->{type} eq 'TEXT') {
1441 2         4 my $text = $node->{content};
1442              
1443 2 100       11 if ($text =~ /^\s* \[\s* ([0-9]+) \s*\] \s*$/x) {
1444 1         2 $nparams = $1;
1445             }
1446              
1447 2         5 $$i++;
1448             }
1449              
1450 2         19 return ($command, $nparams);
1451             }
1452              
1453             # make a mapping from a newenvironment fragment
1454             #
1455             # newenvironments have the following syntax:
1456             #
1457             # \newenvironment{name}[nparams]?{beginTeX}{endTeX}
1458             #
1459             sub _makeEnvMapping {
1460 1     1   2 my $parser = shift;
1461 1         11 my ($tree, $index) = @_;
1462              
1463 1         3 my $i = $index;
1464              
1465 1 50       4 my ($command, $nparams) = _getMapping('environment', $tree, \$i) or return undef;
1466              
1467             # default templates-- just repeat the declarations
1468             #
1469 1         8 my ($btemplate) = $parser->_basicparse("\\begin{$command}", 2, 0);
1470 1         7 my ($etemplate) = $parser->_basicparse("\\end{$command}", 2, 0);
1471              
1472 1         8 my $end_pos = $i;
1473              
1474             # get two group subtrees... one for the begin and one for the end
1475             # templates. we only ignore whitespace TEXT nodes and comments
1476             #
1477 1         6 _skipBlankNodes($tree, \$i);
1478 1         3 my $node = $tree->{nodes}[$i];
1479              
1480 1 50       6 if (_validParamNode($node)) {
1481 1         5 $btemplate = $parser->_duplicateParam($node);
1482              
1483 1         5 $i++;
1484 1         4 _skipBlankNodes($tree, \$i);
1485 1         4 $node = $tree->{nodes}[$i];
1486              
1487 1 50       3 if (_validParamNode($node)) {
1488 1         5 $etemplate = $parser->_duplicateParam($node);
1489 1         3 $end_pos = $i;
1490             }
1491             }
1492              
1493             # build and return the mapping hash
1494             #
1495             return {
1496 1         15 type => 'environment',
1497             name => $command,
1498             nparams => $nparams,
1499             btemplate => $btemplate, # begin template
1500             etemplate => $etemplate, # end template
1501             skip => $end_pos - $index,
1502             };
1503             }
1504              
1505             # make a mapping from a newcommand fragment
1506             # takes tree pointer and index of command node
1507             #
1508             # newcommands have the following syntax:
1509             #
1510             # \newcommand{\name}[nparams]?{anyTeX}
1511             #
1512             sub _makeMapping {
1513 1     1   5 my ($tree, $index) = @_;
1514              
1515 1         2 my $i = $index;
1516              
1517 1 50       4 my ($command, $nparams) = _getMapping('command', $tree, \$i) or return undef;
1518              
1519             # grab subtree template (array ref)
1520             #
1521 1         3 my $node = $tree->{nodes}[$i];
1522 1         1 my $template;
1523              
1524 1 50       3 if ($node->{type} eq 'GROUP') {
1525 1         6 $template = $node->{children}->copy();
1526             }
1527             else {
1528 0         0 return undef;
1529             }
1530              
1531             # build and return the mapping hash
1532             #
1533             return {
1534 1         7 type => 'command',
1535             name => $command,
1536             nparams => $nparams,
1537             template => $template,
1538             skip => $i - $index,
1539             };
1540             }
1541              
1542             # this sub is the main entry point for the sub that actually takes a set of
1543             # parameter trees and inserts them into a template tree. the return result,
1544             # newly allocated, should be plopped back into the original tree where the
1545             # parameters (along with the initial command invocation)
1546             #
1547             sub _applyParamsToTemplate {
1548 2     2   5 my $template = shift;
1549 2         4 my @params = @_;
1550              
1551             # have to copy the template to a freshly allocated tree
1552             #
1553 2         6 my $applied = $template->copy();
1554              
1555             # now recursively apply the params.
1556             #
1557 2         80 _applyParamsToTemplate_r($applied, @params);
1558              
1559 2         6 return $applied;
1560             }
1561              
1562             # recursive helper for above
1563             #
1564             sub _applyParamsToTemplate_r {
1565 4     4   7 my $template = shift;
1566 4         9 my @params = @_;
1567              
1568 4         5 for (my $i = 0; $i < @{$template->{nodes}}; $i++) {
  13         32  
1569              
1570 9         14 my $node = $template->{nodes}[$i];
1571              
1572 9 100       26 if ($node->{type} eq 'TEXT') {
    50          
1573              
1574 7         11 my $text = $node->{content};
1575              
1576             # find occurrences of the parameter flags
1577             #
1578 7 100       20 if ($text =~ /(#([0-9]+))/) {
1579              
1580 1         3 my $all = $1;
1581 1         3 my $num = $2;
1582              
1583             # get the index of the flag we just found
1584             #
1585 1         3 my $idx = index $text, $all, 0;
1586              
1587             # split the node on the location of the flag
1588             #
1589 1         5 my ($leftnode, $rightnode) = $node->split($idx, $idx + length($all) - 1);
1590              
1591             # make a copy of the param we want
1592             #
1593 1         16 my $param = $params[$num - 1]->copy();
1594              
1595             # splice the new text nodes, along with the parameter subtree, into
1596             # the old location
1597             #
1598 1         3 splice @{$template->{nodes}}, $i, 1, $leftnode, @{$param->{nodes}}, $rightnode;
  1         5  
  1         3  
1599              
1600             # skip forward to where $rightnode is in $template on next iteration
1601             #
1602 1         2 $i += scalar @{$param->{nodes}};
  1         5  
1603             }
1604             }
1605              
1606             # recur
1607             elsif (defined $node->{children}) {
1608              
1609 2         9 _applyParamsToTemplate_r($node->{children}, @params);
1610             }
1611             }
1612             }
1613              
1614              
1615             # This sub takes a chunk of the document text between two points and makes
1616             # it into a list of TEXT nodes and COMMENT nodes, as we would expect from
1617             # '%' prefixed LaTeX comment lines
1618             #
1619             sub _getTextAndCommentNodes {
1620 27     27   65 my ($text, $begins, $ends) = @_;
1621              
1622 27         80 my $node_text = substr $text, $begins, $ends - $begins;
1623              
1624 27         117 _debug("getTextAndCommentNodes: looking at [$node_text]", undef);
1625              
1626             my $make_node = sub {
1627 26     26   66 my ($mode_type, $begins, $start_pos, $output) = @_;
1628              
1629 26         273 return LaTeX::TOM::Node->_new({
1630             type => uc $mode_type,
1631             start => $begins + $start_pos,
1632             end => $begins + $start_pos + length($output) - 1,
1633             content => $output,
1634             });
1635 27         135 };
1636              
1637 27         108 my @lines = split (/(
1638             (?:\s* # whitespace
1639             (?
1640             \%[^\n]* # comment
1641             \n)+ # newline
1642             )/mx, $node_text);
1643              
1644 27         52 my @nodes;
1645              
1646 27         49 my $start_pos = 0;
1647 27         48 my $output;
1648             my $mode_type;
1649 27         42 my $first = true;
1650              
1651 27         64 foreach my $line (@lines) {
1652              
1653 26 50 33     123 my $line_type = (
1654             $line =~ /^\s*\%/
1655             && $node_text !~ /
1656             \\begin\{verbatim\}
1657             .* \Q$line\E .*
1658             \\end\{verbatim\}
1659             /sx
1660             ) ? 'comment' : 'text';
1661              
1662             # if type stays the same, add to output and do nothing
1663 26 50 33     88 if ($first || $line_type eq $mode_type) {
1664              
1665 26         58 $output .= $line;
1666              
1667             # handle turning off initialization stuff
1668 26   50     133 $first &&= false;
1669 26   33     123 $mode_type ||= $line_type;
1670             }
1671              
1672             # if type changes, make new node from current chunk, change mode type
1673             # and start a new chunk
1674             else {
1675 0         0 push @nodes, $make_node->($mode_type, $begins, $start_pos, $output);
1676              
1677 0         0 $start_pos += length($output); # update start position
1678 0         0 $output = $line;
1679              
1680 0         0 $mode_type = $line_type;
1681             }
1682             }
1683              
1684 27 100       126 push @nodes, $make_node->($mode_type, $begins, $start_pos, $output) if defined $output;
1685              
1686 27         157 return @nodes;
1687             }
1688              
1689             # Read in the contents of a text file on disk. Return in string scalar.
1690             #
1691             sub _readFile {
1692 16     16   45 my ($file, $raise_error) = @_;
1693              
1694 16   100     76 $raise_error ||= false;
1695              
1696 16         658 my $opened = open(my $fh, '<', $file);
1697              
1698 16 50       83 unless ($opened) {
1699 0 0       0 croak "Cannot open `$file': $!" if $raise_error;
1700 0         0 return undef;
1701             }
1702              
1703 16         30 my $contents = do { local $/; <$fh> };
  16         73  
  16         661  
1704 16         196 close($fh);
1705              
1706 16         101 return $contents;
1707             }
1708              
1709             sub _debug {
1710 920     920   1564 my ($message, $code) = @_;
1711              
1712 920         1204 my $DEBUG = $LaTeX::TOM::DEBUG;
1713              
1714 920 50 33     2059 return unless $DEBUG >= 1 && $DEBUG <= 2;
1715              
1716 0           my ($filename, $line) = (caller)[1,2];
1717 0           my $caller = join ':', (fileparse($filename))[0], $line;
1718              
1719 0 0 0       warn "$caller: $message\n" if $DEBUG >= 1 && defined $message;
1720 0 0 0       $code->() if $DEBUG == 2 && defined $code;
1721             }
1722              
1723             1;