File Coverage

blib/lib/Text/MustacheTemplate/Parser.pm
Criterion Covered Total %
statement 152 156 100.0
branch 76 80 100.0
condition 19 20 100.0
subroutine 15 15 100.0
pod 1 1 100.0
total 263 272 100.0


line stmt bran cond sub pod time code
1             package Text::MustacheTemplate::Parser;
2 14     14   667 use 5.022000;
  14         61  
3 14     14   84 use strict;
  14         42  
  14         524  
4 14     14   64 use warnings;
  14         19  
  14         736  
5              
6 14     14   115 use Carp qw/croak/;
  14         116  
  14         1061  
7 14     14   85 use Exporter 5.57 'import';
  14         244  
  14         600  
8              
9 14     14   69 use Text::MustacheTemplate::Lexer qw/:types/;
  14         30  
  14         2265  
10 14     14   6372 use Text::MustacheTemplate::Generator;
  14         49  
  14         1588  
11              
12             our %EXPORT_TAGS = (
13             syntaxes => [qw/SYNTAX_RAW_TEXT SYNTAX_VARIABLE SYNTAX_BOX SYNTAX_COMMENT SYNTAX_PARTIAL SYNTAX_DELIMITER/],
14             variables => [qw/VARIABLE_HTML_ESCAPE VARIABLE_RAW/],
15             boxes => [qw/BOX_SECTION BOX_INVERTED_SECTION BOX_BLOCK BOX_PARENT/],
16             references => [qw/REFERENCE_STATIC REFERENCE_DYNAMIC/],
17             );
18             our @EXPORT_OK = map @$_, values %EXPORT_TAGS;
19              
20             use constant {
21             # enum
22 14         1884 SYNTAX_RAW_TEXT => 0,
23             SYNTAX_VARIABLE => 1,
24             SYNTAX_BOX => 2,
25             SYNTAX_COMMENT => 3,
26             SYNTAX_PARTIAL => 4,
27             SYNTAX_DELIMITER => 5,
28 14     14   136 };
  14         24  
29              
30             use constant {
31             # enum
32 14         1068 VARIABLE_HTML_ESCAPE => 0,
33             VARIABLE_RAW => 1,
34 14     14   83 };
  14         23  
35              
36             use constant {
37             # enum
38 14         1141 BOX_SECTION => 0,
39             BOX_INVERTED_SECTION => 1,
40             BOX_BLOCK => 2,
41             BOX_PARENT => 3,
42 14     14   111 };
  14         69  
43              
44             use constant {
45             # enum
46 14         29346 REFERENCE_STATIC => 0,
47             REFERENCE_DYNAMIC => 1,
48 14     14   80 };
  14         26  
49              
50             my %TRIM_INNER_WHITESPACE_TYPES = map { $_ => 1 } (qw({ & ^ $ < >), '#', '/'); # set
51             my %TRIM_AROUND_WHITESPACE_TYPES = map { $_ => 1 } (qw(! ^ $ < >), '#', '/');
52              
53             our $SOURCE; # optional for error reporting and optimized lambda support
54              
55             sub parse {
56 546     546 1 255615 my $class = shift;
57 546         1457 my $ast = _parse(@_);
58 536         1629 return $ast;
59             }
60              
61             sub _parse {
62 546     546   1492 my @tokens = @_;
63              
64 546         1423 my $last_delimiter_token;
65             my @root;
66 546         0 my @stack;
67 546         1042 my $ast = \@root;
68 546         2037 for my $i (0..$#tokens) {
69 3156         5531 my $token = $tokens[$i];
70              
71 3156         5940 my ($type, $pos) = @$token;
72 3156 100       7873 if ($type == TOKEN_RAW_TEXT) { # uncoverable branch false count:4
    100          
    100          
    50          
73 1226         2591 my (undef, undef, $text) = @$token;
74 1226 100       2730 if ($i != 1) { # optimized $i >= 2: comes delimiter token when $i == 0 always, so the first raw text token should be $i == 1.
75 850         1394 my $prev = $i-1; # $prev >= 1
76 850 100       2008 if (_needs_trim_around_whitespaces($tokens[$prev])) {
77 621   100     2133 while ($prev != 1 && _needs_trim_around_whitespaces($tokens[$prev-1])) {
78 54         131 $prev--;
79             }
80 621 100       1522 my $before_prev_token = $prev != 1 ? $tokens[$prev-1] : undef;
81 621   100     4191 my $is_empty_before_prev = !defined $before_prev_token || $before_prev_token->[0] == TOKEN_PADDING || (
82             $before_prev_token->[0] == TOKEN_RAW_TEXT && $before_prev_token->[2] =~ /[\r\n]\z/mano
83             );
84 621 100       1490 if ($is_empty_before_prev) {
85 270         1096 $text =~ s/\A(\r\n|[\r\n])//mano;
86             }
87             }
88             }
89 1226 100       5028 push @$ast => [SYNTAX_RAW_TEXT, $text] if $text;
90             } elsif ($type == TOKEN_PADDING) {
91 91         226 my (undef, undef, $padding) = @$token;
92 91         189 my $needs_padding = 1;
93 91 50       291 if ($i == $#tokens) { # uncoverable branch true
94 0         0 _error($token, 'Syntax Error: Padding token should not be last'); # uncoverable statement
95             }
96              
97 91         215 my $next = $i+1;
98 91 100       268 if (_needs_trim_around_whitespaces($tokens[$next])) {
99 73   100     356 while ($next != $#tokens && _needs_trim_around_whitespaces($tokens[$next+1])) {
100 1         4 $next++;
101             }
102 73 100       242 my $after_next_token = $next != $#tokens ? $tokens[$next+1] : undef;
103 73   100     515 my $is_empty_after_next = !defined $after_next_token || (
104             $after_next_token->[0] == TOKEN_RAW_TEXT && $after_next_token->[2] =~ /\A[\r\n]/mano
105             );
106 73         218 $needs_padding = !$is_empty_after_next;
107             }
108 91 100       292 if ($needs_padding) {
109 36         136 push @$ast => [SYNTAX_RAW_TEXT, $padding];
110             }
111             } elsif ($type == TOKEN_TAG) {
112 1256 100       2960 if (@$token == 3) { # uncoverable branch false count:2
    50          
113 325         710 my (undef, undef, $tag_body) = @$token;
114 325 100       1030 _error($token, 'Syntax Error: Must not contain newlines') if $tag_body =~ /[\r\n]/mo;
115 323         820 $tag_body =~ s/^\s+//ano;
116 323         749 $tag_body =~ s/\s+$//ano;
117 323         1141 push @$ast => [SYNTAX_VARIABLE, VARIABLE_HTML_ESCAPE, $tag_body];
118             } elsif (@$token == 4) {
119 931         2103 my (undef, undef, $tag_type, $tag_body) = @$token;
120 931 100       2557 if ($TRIM_INNER_WHITESPACE_TYPES{$tag_type}) {
121 897 100       2414 _error($token, 'Syntax Error: Must not contain newlines') if $tag_body =~ /[\r\n]/mo;
122 895         2058 $tag_body =~ s/^\s+//ano;
123 895         1933 $tag_body =~ s/\s+$//ano;
124             }
125              
126 929 100 100     5142 if ($tag_type eq '{' || $tag_type eq '&') { # uncoverable branch false count:8
    100          
    100          
    100          
    100          
    100          
    100          
    50          
127 65         212 push @$ast => [SYNTAX_VARIABLE, VARIABLE_RAW, $tag_body];
128             } elsif ($tag_type eq '!') {
129 34         281 push @$ast => [SYNTAX_COMMENT, $tag_body];
130             } elsif ($tag_type eq '#') {
131 154         633 push @stack => [$i, $tag_body, [SYNTAX_BOX, BOX_SECTION, $tag_body], $ast];
132 154         437 $ast = [];
133             } elsif ($tag_type eq '^') {
134 64         253 push @stack => [$i, $tag_body, [SYNTAX_BOX, BOX_INVERTED_SECTION, $tag_body], $ast];
135 64         257 $ast = [];
136             } elsif ($tag_type eq '$') {
137 105         306 push @stack => [$i, $tag_body, [SYNTAX_BOX, BOX_BLOCK, $tag_body], $ast];
138 105         191 $ast = [];
139             } elsif ($tag_type eq '<') {
140 53         118 my $name = $tag_body;
141 53         68 my $syntax = do {
142 53         106 my $is_dynamic = substr($name,0,1) eq '*';
143 53 100       125 if ($is_dynamic) {
144 3         11 $name = substr $name, 1;
145 3         13 [SYNTAX_BOX, BOX_PARENT, REFERENCE_DYNAMIC, $name];
146             } else {
147 50         135 [SYNTAX_BOX, BOX_PARENT, REFERENCE_STATIC, $name];
148             }
149             };
150 53         143 push @stack => [$i, $tag_body, $syntax, $ast];
151 53         118 $ast = [];
152             } elsif ($tag_type eq '/') {
153 372 100       835 _error($token, 'Syntax Error: Unbalanced Section') unless @stack;
154 371         641 my $item = pop @stack;
155 371         884 my ($open_idx, $open_tag_body, $syntax, $parent) = @$item;
156 371         4907 s/\s*//go for ($open_tag_body, $tag_body);
157 371 100       1044 if ($open_tag_body ne $tag_body) {
158 2         31 _error($token, "Syntax Error: Unbalanced Section: open=$open_tag_body close=$tag_body");
159             }
160 369 100       895 if ($syntax->[1] == BOX_SECTION) {
161             # keep inner template to support lambda
162 147 100       720 my $inner_template = defined $SOURCE
163             ? substr $SOURCE, $tokens[$open_idx+1][1], $pos-$tokens[$open_idx+1][1]
164             : Text::MustacheTemplate::Generator->generate_from_tokens($last_delimiter_token, @tokens[$open_idx+1..$i-1]);
165 147         447 push @$syntax => $inner_template;
166             }
167 369         793 push @$syntax => $ast;
168 369         540 $ast = $parent;
169 369         1205 push @$ast => $syntax;
170             } elsif ($tag_type eq '>') {
171 82         183 my $name = $tag_body;
172 82         960 $name =~ s/\s*//go;
173              
174             # comes delimiter token when $i == 0 always, so the first tag token should be $i == 1.
175 82         177 my $padding;
176 82 100       252 if ($i != 1) {
177 75         191 my $prev = $tokens[$i-1];
178 75 100       281 if ($prev->[0] == TOKEN_PADDING) {
179 14         34 $padding = $prev->[2] ;
180 14         69 push @$ast => [SYNTAX_RAW_TEXT, $padding];
181             }
182             }
183              
184 82         250 my $is_dynamic = substr($name,0,1) eq '*';
185 82 100       218 if ($is_dynamic) {
186 48         167 $name = substr $name, 1;
187 48         217 push @$ast => [SYNTAX_PARTIAL, REFERENCE_DYNAMIC, $name, $padding];
188             } else {
189 34         149 push @$ast => [SYNTAX_PARTIAL, REFERENCE_STATIC, $name, $padding];
190             }
191             } else {
192 0         0 _error($token, "Syntax Error: Unknown Tag Type: '$tag_type'"); # uncoverable statement
193             }
194             } else {
195 0         0 _error($token, 'Syntax Error: Unknown Token'); # uncoverable statement
196             }
197             } elsif ($type == TOKEN_DELIMITER) {
198 583         1671 my @delimiters = @$token[3,4];
199 583         965 $last_delimiter_token = $token;
200 583         2448 push @$ast => [SYNTAX_DELIMITER, @delimiters];
201             } else {
202 0         0 _error($token, 'Syntax Error: Unknown Token'); # uncoverable statement
203             }
204             }
205 539 100       1366 if (@stack) {
206 3         5 my $item = pop @stack;
207 3         8 my (undef, undef, $open_token) = @$item;
208 3         13 _error($open_token, 'Syntax Error: Unbalanced Section');
209             }
210 536         1535 return \@root;
211             }
212              
213             sub _needs_trim_around_whitespaces {
214 1580     1580   2596 my $token = shift;
215 1580         2637 my ($type) = @$token;
216              
217 1580 100       3965 if ($type == TOKEN_DELIMITER) {
    100          
218 40         84 my (undef, undef, $tag_body) = @$token;
219 40         140 return defined $tag_body; ## tag body is undef when first implicit TOKEN_DELIMITER
220             } elsif ($type == TOKEN_TAG) {
221 984 100       1932 if (@$token == 4) {
222 771         1546 my (undef, undef, $tag_type, undef) = @$token;
223 771         2972 return !!$TRIM_AROUND_WHITESPACE_TYPES{$tag_type};
224             } else {
225 213         728 return !!0;
226             }
227             }
228 556         2061 return !!0;
229             }
230              
231             sub _error {
232 10     10   26 my ($token, $msg) = @_;
233 10 100       210 croak $msg unless $SOURCE;
234              
235 9         17 my $src = $SOURCE;
236 9         16 my $curr = $token->[1];
237 9         14 my $line = 1;
238 9         17 my $start = 0;
239 9   66     82 while ($src =~ /$/smgco and pos $src <= $curr) {# uncoverable condition left
240 2         5 $start = pos $src;
241 2         7 $line++;
242             }
243 9         49 my $end = pos $src;
244 9         18 my $len = $curr - $start;
245 9 100       19 $len-- if $len > 0;
246              
247 9   100     67 my $trace = join "\n",
248             "${msg}: line:$line",
249             substr($src, $start || 0, $end - $start),
250             (' ' x $len) . '^';
251 9         1556 croak $trace, "\n";
252             }
253              
254             1;
255             __END__