File Coverage

blib/lib/Template/Sluz.pm
Criterion Covered Total %
statement 818 940 87.0
branch 322 434 74.1
condition 122 179 68.1
subroutine 58 64 90.6
pod 7 17 41.1
total 1327 1634 81.2


line stmt bran cond sub pod time code
1             # Copyright (C) 2025 Scott Baker
2             #
3             # This program is free software: you can redistribute it and/or modify
4             # it under the terms of the GNU General Public License as published by
5             # the Free Software Foundation, either version 3 of the License, or
6             # (at your option) any later version.
7             #
8             # This program is distributed in the hope that it will be useful,
9             # but WITHOUT ANY WARRANTY; without even the implied warranty of
10             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11             # GNU General Public License for more details.
12             #
13             # You should have received a copy of the GNU General Public License
14             # along with this program. If not, see .
15              
16             package Template::Sluz;
17              
18 11     11   2934459 use strict;
  11         23  
  11         432  
19 11     11   50 use warnings;
  11         15  
  11         542  
20 10     10   183 use 5.016;
  10         43  
21              
22 10     10   54 use File::Basename qw(dirname basename);
  10         20  
  10         739  
23 10     10   6047 use autouse 'Carp' => qw(croak);
  10         9299  
  10         59  
24              
25 10     10   1287 use constant SLUZ_INLINE => 'INLINE_TEMPLATE';
  10         17  
  10         55853  
26              
27             our $VERSION = 'v0.9.4';
28              
29             ################################################################################
30             # Built-in Sluz functions that can be used in templates
31             ################################################################################
32              
33             sub count {
34 3     3 0 5 my $v = shift;
35              
36 3 50       13 if (ref $v eq 'ARRAY') {
37 3         16 return scalar @$v;
38             }
39              
40 0 0       0 if (ref $v eq 'HASH') {
41 0         0 return scalar(keys %$v);
42             }
43              
44 0 0       0 if (defined $v) { return 1 }
  0         0  
45              
46 0         0 return 0;
47             }
48              
49             sub join {
50 4     4 0 7 my $arr = shift();
51 4   100     18 my $glue = shift() // ', ';
52              
53 4         23 return CORE::join($glue, @$arr);
54             }
55              
56             ################################################################################
57             ################################################################################
58              
59             sub new {
60 11     11 1 12167 my $class = shift;
61 11         38 my %args = @_;
62             my $self = {
63             version => $VERSION,
64             tpl_file => undef,
65             inc_tpl_file => undef,
66             debug => $args{debug} // 0,
67 11   50     226 auto_escape => $args{auto_escape} // 0,
      100        
68             tpl_vars => {},
69             parent_tpl => undef,
70             var_prefix => 'sluz_pfx',
71             perl_file => undef,
72             perl_file_dir => undef,
73             fetch_called => 0,
74             char_pos => -1,
75             open_delim => '{',
76             close_delim => '}',
77             _sub_cache => {},
78             __S => {}, # Cached prefixed var hash used by _peval
79             _convert_cache => {}, # Cached _convert_vars results (avoids re-running regex on repeated expressions)
80             _blocks_cache => {}, # Cached _get_blocks results (avoids re-tokenizing if payloads in loops)
81             _if_rules_cache => {}, # Cached parsed {if} rules (avoids re-parsing same if block in loops)
82             _verified_sub_cache => {}, # Cached subs that succeeded once — skip eval/SIG overhead
83             };
84              
85 11         32 bless $self, $class;
86 11         48 $self->_precompute_tags();
87 11         44 return $self;
88             }
89              
90             sub assign {
91 244     244 1 3110 my $self = shift;
92              
93 244         330 my $pfx = $self->{var_prefix};
94              
95             # Accept either a hashref: assign($hash_ref)
96 244 100 100     649 if (@_ == 1 && ref $_[0] eq 'HASH') {
    100          
97 9         55 my $h = shift;
98 9         32 @{$self->{tpl_vars}}{keys %$h} = values %$h;
  9         26  
99 9         30 for my $k (keys %$h) {
100 24         79 $self->{__S}{"${pfx}_$k"} = $h->{$k};
101             }
102              
103             # Or a key-value list: assign(name => 'Scott', age => 42)
104             } elsif (@_ % 2 == 0) {
105 234         483 my %h = @_;
106 234         422 @{$self->{tpl_vars}}{keys %h} = values %h;
  234         469  
107 234         385 for my $k (keys %h) {
108 258         808 $self->{__S}{"${pfx}_$k"} = $h{$k};
109             }
110             } else {
111 1         15 $self->_error_out("Invalid assign. Must be a key/value or hash", 18956);
112             }
113             }
114              
115             sub fetch {
116 3     3 1 1378 my $self = shift;
117 3   50     13 my $tpl_file = shift || '';
118 3         6 my $parent = shift;
119              
120 3 100       28 if (!$self->{perl_file}) {
121 1         6 $self->{perl_file} = $self->_get_perl_file;
122 1         58 $self->{perl_file_dir} = dirname($self->{perl_file});
123             }
124              
125 3 50       11 if (!$tpl_file) {
126 0         0 $tpl_file = $self->_guess_tpl_file($self->{perl_file});
127             }
128              
129 3         4 my $parent_tpl;
130 3 100       9 if (defined $parent) {
131 1         4 $parent_tpl = $parent;
132             } else {
133 2         101 $parent_tpl = $self->{parent_tpl};
134             }
135 3 100       13 if ($parent_tpl) {
136 2         10 $self->assign('__CHILD_TPL', $tpl_file);
137 2         5 $tpl_file = $parent_tpl;
138             }
139              
140 3         13 my $str = $self->_get_tpl_content($tpl_file);
141 3         12 my @blocks = $self->_get_blocks($str);
142 3         12 my $html = $self->_process_blocks(\@blocks);
143              
144 3         8 $self->{fetch_called} = 1;
145 3         12 return $html;
146             }
147              
148             sub parse {
149 0     0 0 0 my $self = shift;
150 0         0 return $self->fetch(@_);
151             }
152              
153             sub display {
154 0     0 0 0 my $self = shift;
155 0         0 print $self->fetch(@_);
156             }
157              
158             # Parse a string instead of a file
159             sub parse_string {
160 210     210 1 151956 my $self = shift;
161 210   50     583 my $tpl_str = shift // '';
162 210         620 my @blocks = $self->_get_blocks($tpl_str);
163              
164 210         627 return $self->_process_blocks(\@blocks);
165             }
166              
167             # Getter/setter for parent TPL
168             sub parent_tpl {
169 2     2 0 1987 my $self = shift;
170              
171 2 50       8 if (@_) {
172 2         5 $self->{parent_tpl} = shift;
173             }
174              
175 2         6 return $self->{parent_tpl};
176             }
177              
178             sub set_delimiters {
179 1     1 1 4 my $self = shift;
180 1         2 my $open = shift;
181 1         2 my $close = shift;
182              
183 1 50 33     5 if (!defined $open || !defined $close) {
184 0         0 $self->_error_out("set_delimiters requires both open and close delimiter arguments", 51234);
185             }
186              
187 1 50 33     4 if (length($open) != 1 || length($close) != 1) {
188 0         0 $self->_error_out("Delimiters must be single characters", 51235);
189             }
190              
191 1 50       3 if ($open eq $close) {
192 0         0 $self->_error_out("Open and close delimiters must be different characters", 51236);
193             }
194              
195 1         2 $self->{open_delim} = $open;
196 1         2 $self->{close_delim} = $close;
197              
198 1         2 $self->_precompute_tags();
199              
200             # Clear all caches since results are delimiter-dependent
201 1         3 $self->{_blocks_cache} = {};
202 1         2 $self->{_if_rules_cache} = {};
203 1         2 $self->{_convert_cache} = {};
204 1         3 $self->{_sub_cache} = {};
205 1         2 $self->{_verified_sub_cache} = {};
206              
207 1         4 return;
208             }
209              
210             # Dive down an array or hashref using our dotted syntax
211             sub array_dive {
212 128     128 0 164 my $self = shift;
213 128         176 my $needle = shift;
214 128         184 my $haystack = shift;
215              
216 128 50 33     416 if (!defined $needle || !defined $haystack) { return undef }
  0         0  
217              
218             # Quick path: needle is a direct key in the hash
219 128 100       269 if (exists $haystack->{$needle}) {
220 97         262 return $haystack->{$needle};
221             }
222              
223             # Walk dotted path (e.g. "user.address.city") through nested structures
224 31         88 my @parts = split /\./, $needle;
225 31         47 my $arr = $haystack;
226              
227 31         48 for my $elem (@parts) {
228 55 50       125 if (!defined $arr) { return undef }
  0         0  
229 55 100       129 if (ref $arr eq 'ARRAY') {
    50          
230 10 50 33     61 if (!($elem =~ /^\d+$/ && $elem < @$arr)) { return undef }
  0         0  
231 10         17 $arr = $arr->[$elem];
232             } elsif (ref $arr eq 'HASH') {
233 45 100       109 if (!exists $arr->{$elem}) { return undef }
  9         33  
234 36         89 $arr = $arr->{$elem};
235             } else {
236 0         0 return undef;
237             }
238             }
239 22         60 return $arr;
240             }
241              
242             # HTML-escape a string for safe output. Encodes & < > " ' to entities.
243             # Usable as a modifier: {$var|escape} or as a callable: {escape($var)}
244             sub escape {
245 32   50 32 1 71 my $str = shift // '';
246              
247 32 100       53 if (ref $str eq 'ARRAY') { return 'ARRAY' }
  1         4  
248 31 50       92 if (ref $str eq 'HASH') { return 'HASH' }
  0         0  
249              
250 31         54 $str =~ s/&/&/g;
251 31         78 $str =~ s/
252 31         59 $str =~ s/>/>/g;
253 31         45 $str =~ s/"/"/g;
254 31         40 $str =~ s/'/'/g;
255              
256 31         126 return $str;
257             }
258              
259             # Bypass auto-escaping when auto_escape is on: {$var|noescape}
260             sub noescape {
261 7     7 1 19 return shift;
262             }
263              
264             # Apply auto-escaping if enabled, otherwise return value unchanged.
265             # Ref types (ARRAY/HASH) pass through unescaped.
266             sub _esc {
267 137     137   284 my ($self, $val) = @_;
268              
269 137 100       283 if (!$self->{auto_escape}) { return $val }
  125         307  
270 12 50       52 if (ref $val) { return $val }
  0         0  
271              
272 12         26 return escape($val);
273             }
274              
275             sub ltrim_one {
276 73     73 0 87 my $self = shift;
277 73   50     154 my $str = shift // '';
278 73         94 my $char = shift;
279              
280 73 100 100     231 if (length $str && substr($str, 0, 1) eq $char) {
281 5         17 return substr($str, 1);
282             }
283              
284 68         138 return $str;
285             }
286              
287             sub find_ending_tag {
288 15     15 0 39 my $self = shift;
289 15   50     101 my $haystack = shift // '';
290 15         30 my $open_tag = shift;
291 15         23 my $close_tag = shift;
292              
293             # Find the first close tag; if there's only one open tag before it, we're done
294 15         33 my $pos = index($haystack, $close_tag);
295 15 50       31 if ($pos < 0) { return undef }
  0         0  
296              
297 15         32 my $substr = substr($haystack, 0, $pos);
298 15         129 my $open_count = () = $substr =~ /\Q$open_tag\E/g;
299 15 100       43 if ($open_count == 1) { return $pos }
  10         33  
300              
301             # Nested tags: scan forward through subsequent close tags until
302             # open/close counts balance (max 5 nesting levels)
303 5         10 my $close_len = length $close_tag;
304 5         8 my $offset = $pos + $close_len;
305              
306 5         15 for (0 .. 4) {
307 13         17 $pos = index($haystack, $close_tag, $offset);
308 13 50       26 if ($pos < 0) { return undef }
  0         0  
309              
310 13         36 $substr = substr($haystack, 0, $pos + 2);
311 13         72 $open_count = () = $substr =~ /\Q$open_tag\E/g;
312 13         54 my $close_count = () = $substr =~ /\Q$close_tag\E/g;
313 13 100       51 if ($open_count == $close_count) { return $pos }
  5         14  
314              
315 8         16 $offset = $pos + $close_len;
316             }
317              
318 0         0 return undef;
319             }
320              
321             sub get_tokens {
322 21     21 0 25 my $self = shift;
323 21   50     37 my $str = shift // '';
324 21         64 my $o = quotemeta($self->{open_delim});
325 21         47 my $c = quotemeta($self->{close_delim});
326 21         288 my @tokens = split /($o[^$c]+$c)/, $str;
327 21 50       42 @tokens = grep { defined && length } @tokens;
  172         344  
328 21         66 return @tokens;
329             }
330              
331             sub is_if_token {
332 165     165 0 163 my $self = shift;
333 165   50     279 my $str = shift // '';
334 165 100       207 if ($str eq $self->{_tag_else}) { return 1 }
  36         59  
335 129 100       163 if ($str eq $self->{_tag_if_close}) { return 1 }
  27         40  
336 102 100 100     235 if (index($str, $self->{_tag_if}) == 0 || index($str, $self->{_tag_elseif}) == 0) {
337 58         72 my $inner = substr($str, length($self->{open_delim}));
338 58         138 $inner =~ s/^\S+\s+//; # strip 'if ' or 'elseif '
339 58         210 $inner =~ s/\Q$self->{close_delim}\E$//;
340 58         136 return $inner;
341             }
342 44         89 return '';
343             }
344              
345             # -------------------------------------------------------------------
346             # Private methods
347             # -------------------------------------------------------------------
348              
349             sub _precompute_tags {
350 12     12   20 my $self = shift;
351 12         54 my $o = $self->{open_delim};
352 12         45 my $c = $self->{close_delim};
353              
354             # Tag strings
355 12         31 $self->{_tag_if} = "${o}if ";
356 12         39 $self->{_tag_if_close} = "${o}/if${c}";
357 12         25 $self->{_tag_else} = "${o}else${c}";
358 12         27 $self->{_tag_elseif} = "${o}elseif ";
359 12         26 $self->{_tag_foreach} = "${o}foreach ";
360 12         27 $self->{_tag_foreach_close} = "${o}/foreach${c}";
361 12         25 $self->{_tag_include} = "${o}include ";
362 12         34 $self->{_tag_literal} = "${o}literal${c}";
363 12         78 $self->{_tag_literal_close} = "${o}/literal${c}";
364 12         26 $self->{_tag_comment_open} = "${o}*";
365 12         23 $self->{_tag_comment_close} = "*${c}";
366              
367             # Precomputed tag lengths (avoids repeated length() calls in hot loops)
368 12         42 $self->{_tag_if_len} = length($self->{_tag_if});
369 12         25 $self->{_tag_if_close_len} = length($self->{_tag_if_close});
370 12         26 $self->{_tag_foreach_len} = length($self->{_tag_foreach});
371 12         22 $self->{_tag_include_len} = length($self->{_tag_include});
372 12         50 $self->{_tag_literal_len} = length($self->{_tag_literal});
373 12         23 $self->{_tag_literal_close_len} = length($self->{_tag_literal_close});
374              
375             # Precomputed quotemeta values (avoids per-call quotemeta in _get_blocks)
376 12         58 $self->{_od_qr} = quotemeta($o);
377 12         43 $self->{_cd_qr} = quotemeta($c);
378              
379             # Precompiled space-guard regex (avoids per-open-delimiter regex compile)
380 12         24 my $od_qr = $self->{_od_qr};
381 12         57 my $cd_qr = $self->{_cd_qr};
382 12         390 $self->{_space_guard_re} = qr/\s[$od_qr$cd_qr]\s/;
383              
384             # Precompiled variable regex: {$var} or {$var.dot.path}
385 12         1318 $self->{_re_var_simple} = qr/^\Q$o\E\$(\w[\w.]*)\Q$c\E$/;
386 12         833 $self->{_re_var_full} = qr/^\Q$o\E\$([\w|.'";\t :,!@#%^&*?_\/\\\-]+)\Q$c\E$/;
387              
388             # Precompiled foreach regex
389 12         897 $self->{_re_foreach} = qr/^\Q$o\Eforeach (\$\w[\w.]*) as \$(\w+)(?: => \$(\w+))?\Q$c\E(.+)\Q$o\E\/foreach\Q$c\E$/s;
390              
391             # Precompiled literal regex
392 12         183 $self->{_re_literal} = qr/^\Q$o\Eliteral\Q$c\E(.+)\Q$o\E\/literal\Q$c\E$/s;
393              
394             # Precompiled expression catch-all regex
395 12         124 $self->{_re_expr} = qr/^\Q$o\E(.+)\Q$c\E$/s;
396              
397             # Precompiled simple if regex (no else/elseif)
398 12         197 $self->{_re_if_simple} = qr/\Q$o\Eif (.+?)\Q$c\E(.+)\Q$o\E\/if\Q$c\E/s;
399              
400 12         37 return;
401             }
402              
403             sub _get_perl_file {
404 1     1   2 my $self = shift;
405 1         3 my $i = 0;
406 1         2 my $file;
407              
408 1         5 while (caller($i)) {
409 3         13 $file = (caller($i))[1];
410 3         10 $i++;
411             }
412              
413 1   50     5 return $file || __FILE__;
414             }
415              
416             sub _guess_tpl_file {
417 0     0   0 my $self = shift;
418 0         0 my $pfile = shift;
419              
420 0         0 my $base = basename($pfile);
421 0         0 $base =~ s/\.(pl|pm)$/.stpl/;
422              
423 0         0 return "tpls/$base";
424             }
425              
426             sub _get_tpl_content {
427 13     13   23 my $self = shift;
428 13   50     35 my $tpl_file = shift // '';
429 13         28 $self->{tpl_file} = $tpl_file;
430 13         23 my $tf = $tpl_file;
431              
432 13 50       38 if ($self->{perl_file_dir}) {
433 13         57 $tf = $self->{perl_file_dir} . "/$tf";
434             }
435              
436 13 50       39 if ($tpl_file eq SLUZ_INLINE) {
437 0         0 my $c = $self->_get_inline_content($self->{perl_file});
438 0 0       0 if (defined $c) { return $c }
  0         0  
439 0         0 return '';
440             }
441              
442 13 50 33     446 if ($tf && !-r $tf) {
443 0         0 $self->_error_out("Unable to load template file $tf", 42280);
444             }
445              
446 13 50       43 if ($tf) {
447 13         70 local $/;
448 13 50       671 open my $fh, '<', $tf or $self->_error_out("Cannot open $tf: $!", 42280);
449 13         426 my $str = <$fh>;
450 13         247 close $fh;
451 13   100     166 return $str // '';
452             }
453              
454 0         0 return '';
455             }
456              
457             sub _get_inline_content {
458 0     0   0 my $self = shift;
459 0         0 my $file = shift;
460 0         0 local $/;
461 0 0       0 open my $fh, '<', $file or return undef;
462 0         0 my $str = <$fh>;
463 0         0 close $fh;
464 0         0 my $idx = index($str, '__DATA__');
465 0 0       0 if ($idx < 0) { return undef }
  0         0  
466 0         0 return substr($str, $idx + 9);
467             }
468              
469             # -------------------------------------------------------------------
470             # Tokenizer
471             # -------------------------------------------------------------------
472              
473             sub _get_blocks {
474 317     317   16645 my $self = shift;
475 317   50     662 my $str = shift // '';
476              
477             # Check blocks cache — avoids re-tokenizing the same payload string
478             # (e.g. an {if} payload re-parsed on every iteration of a {foreach} loop)
479 317 100       882 if (exists $self->{_blocks_cache}{$str}) {
480 15         27 return @{$self->{_blocks_cache}{$str}};
  15         62  
481             }
482              
483 302         487 my $od = $self->{open_delim};
484 302         457 my $cd = $self->{close_delim};
485 302         552 my $tag_if = $self->{_tag_if};
486 302         493 my $tag_foreach = $self->{_tag_foreach};
487 302         499 my $tag_literal = $self->{_tag_literal};
488 302         436 my $slen = length $str;
489 302         430 my $start = 0;
490 302         427 my $i;
491             my @blocks;
492              
493 302         648 my $z = index($str, $od);
494 302 100       630 if ($z < 0) { $z = $slen }
  40         55  
495              
496 302         777 for ($i = $z; $i < $slen; $i++) {
497 826         1436 my $char = substr($str, $i, 1);
498 826         1185 my $is_open = $char eq $od;
499 826         1015 my $is_closed = $char eq $cd;
500              
501 826 100 100     2172 if (!$is_open && !$is_closed) {
502 284         516 my $next_open = index($str, $od, $i);
503 284 100       607 if ($next_open < 0) { $next_open = $slen }
  157         215  
504 284         372 my $next_close = index($str, $cd, $i);
505 284 100       506 if ($next_close < 0) { $next_close = $slen }
  12         24  
506 284 100       472 if ($next_open < $next_close) {
507 7         14 $i = $next_open - 1;
508             } else {
509 277         367 $i = $next_close - 1;
510             }
511 284         723 next;
512             }
513              
514 542         770 my $has_len = $start != $i;
515 542         682 my $is_comment = 0;
516              
517 542 100       922 if ($is_open) {
518 276         345 my $prev_c;
519 276 100       539 if ($i > 0) {
520 28         66 $prev_c = substr($str, $i - 1, 1);
521             } else {
522 248         365 $prev_c = ' ';
523             }
524 276         324 my $next_c;
525 276 50       597 if ($i + 1 < $slen) {
526 276         475 $next_c = substr($str, $i + 1, 1);
527             } else {
528 0         0 $next_c = ' ';
529             }
530 276         451 my $chk = $prev_c . $char . $next_c;
531 276 100       2167 if ($chk =~ $self->{_space_guard_re}) { $is_open = 0 }
  2         5  
532 276 100       840 if ($next_c eq '*') { $is_comment = 1 }
  15         32  
533             }
534              
535 542 100 100     1871 if ($is_open && $has_len) {
    100          
536 26         98 push @blocks, [substr($str, $start, $i - $start), $i];
537 26         43 $start = $i;
538             } elsif ($is_closed) {
539 266         418 my $len = $i - $start + 1;
540 266         451 my $block = substr($str, $start, $len);
541              
542 266         328 my $matched_block;
543 266 100       839 if (index($block, $tag_if) == 0) { $matched_block = 'if' }
  54 100       71  
    100          
544 39         67 elsif (index($block, $tag_foreach) == 0) { $matched_block = 'foreach' }
545 7         24 elsif (index($block, $tag_literal) == 0) { $matched_block = 'literal' }
546              
547 266 100       485 if ($matched_block) {
548 100         156 my $close_tag = "${od}/${matched_block}${cd}";
549 100         225 for (my $j = $i + 1; $j < length $str; $j++) {
550 1954 100       3539 if (substr($str, $j, 1) eq $cd) {
551 220         337 my $tmp = substr($str, $start, $j - $start + 1);
552 220         1315 my $oc = () = $tmp =~ /\Q${od}${matched_block}\E/g;
553 220         723 my $cc = () = $tmp =~ /\Q${close_tag}\E/g;
554 220 100       480 if ($oc == $cc) {
555 99         125 $block = $tmp;
556 99         189 last;
557             }
558             }
559             }
560             }
561              
562 266 50       509 if (length $block) { push @blocks, [$block, $i] }
  266         737  
563 266         392 $start += length($block);
564 266         446 $i = $start;
565             }
566              
567 542 100       1452 if ($is_comment) {
568 15         100 my $end = $self->find_ending_tag(substr($str, $start), $self->{_tag_comment_open}, $self->{_tag_comment_close});
569 15 50       45 if (!defined $end) {
570 0         0 my ($line, $col, $file) = $self->_get_char_location($i, $self->{tpl_file});
571 0         0 $self->_error_out("Missing closing $self->{_tag_comment_close} for comment in $file on line #$line", 48724);
572             }
573 15         30 $start += $end + length($self->{_tag_comment_close});
574 15         47 $i = $start;
575             }
576             }
577              
578 302 100       519 if ($start < $slen) {
579 62         199 push @blocks, [substr($str, $start), $i];
580             }
581              
582             # Strip leading newline from text blocks that follow {if} or
583             # {foreach} blocks, to avoid double-newlines when the block
584             # payload already ends with \n. For {foreach}, only strip when
585             # the payload actually ends with \n — if the payload is inline
586             # (no trailing \n), the newline is structural content, not
587             # whitespace noise.
588 302         417 my $prev_is_if = 0;
589 302         796 my $tag_foreach_close = $self->{_tag_foreach_close};
590 302         867 for my $i (0 .. $#blocks) {
591 354   50     748 my $bstr = $blocks[$i][0] // '';
592 354   100     1159 my $cur_is_if = (index($bstr, $tag_if) == 0 || index($bstr, $tag_foreach) == 0);
593 354 100       634 if ($prev_is_if) {
594 4         6 my $should_strip = 1;
595 4 100       86 if ($blocks[$i-1][0] =~ /^\Q$tag_foreach\E.+?\}(.*)\Q$tag_foreach_close\E$/s) {
596 2 100       8 $should_strip = (substr($1, -1) eq "\n") ? 1 : 0;
597             }
598 4 100       35 if ($should_strip) {
599 3         10 $blocks[$i][0] = $self->ltrim_one($bstr, "\n");
600             }
601             }
602 354         674 $prev_is_if = $cur_is_if;
603             }
604              
605 302         909 $self->{_blocks_cache}{$str} = \@blocks;
606 302         938 return @blocks;
607             }
608              
609             sub _process_blocks {
610 275     275   392 my $self = shift;
611 275         355 my $blocks = shift;
612 275         387 my $out = shift; # Optional: ref to append output to (avoids temp string + concat)
613              
614 275         464 my $od = $self->{open_delim};
615 275         361 my $cd = $self->{close_delim};
616 275         399 my $var_tag = "${od}\$";
617 275         385 my $var_re = $self->{_re_var_simple};
618              
619 275 50       504 if ($out) {
620 0         0 for my $x (@$blocks) {
621 0         0 my $block = $x->[0];
622 0 0       0 next unless length $block;
623 0         0 my $first = substr($block, 0, 1);
624 0 0       0 if ($first ne $od) {
625 0         0 $$out .= $block;
626 0         0 next;
627             }
628             # Fast path: {$var} or {$var.dot} with no modifier — inline
629             # variable resolution, skip _process_block AND _variable_block
630 0 0 0     0 if (substr($block, 0, 2) eq $var_tag && index($block, '|') < 0
      0        
631             && $block =~ $var_re) {
632 0         0 my $var = $1;
633 0         0 my $val;
634 0 0       0 if (index($var, '.') < 0) {
635 0         0 $val = $self->{tpl_vars}{$var};
636             } else {
637 0         0 $val = $self->array_dive($var, $self->{tpl_vars});
638             }
639 0 0       0 if (ref $val eq 'ARRAY') { $$out .= 'ARRAY' }
  0 0       0  
    0          
640 0         0 elsif (ref $val eq 'HASH') { $$out .= 'HASH' }
641 0         0 elsif (defined $val) { $$out .= $self->_esc($val) }
642 0         0 next;
643             }
644 0         0 $$out .= $self->_process_block($block, $x->[1]);
645             }
646 0         0 return;
647             }
648              
649 275         401 my $html = '';
650 275         570 for my $x (@$blocks) {
651 318         462 my $block = $x->[0];
652 318 50       637 next unless length $block;
653 318         534 my $first = substr($block, 0, 1);
654 318 100       622 if ($first ne $od) {
655 82         125 $html .= $block;
656 82         158 next;
657             }
658             # Fast path: {$var} or {$var.dot} with no modifier
659 236 100 100     1529 if (substr($block, 0, 2) eq $var_tag && index($block, '|') < 0
      100        
660             && $block =~ $var_re) {
661 62         188 my $var = $1;
662 62         98 my $val;
663 62 100       131 if (index($var, '.') < 0) {
664 54         146 $val = $self->{tpl_vars}{$var};
665             } else {
666 8         29 $val = $self->array_dive($var, $self->{tpl_vars});
667             }
668 62 100       220 if (ref $val eq 'ARRAY') { $html .= 'ARRAY' }
  4 50       12  
    100          
669 0         0 elsif (ref $val eq 'HASH') { $html .= 'HASH' }
670 51         129 elsif (defined $val) { $html .= $self->_esc($val) }
671 62         162 next;
672             }
673             # If block fast path — skip _process_block dispatch
674 174 100 100     639 if (substr($block, 0, $self->{_tag_if_len}) eq $self->{_tag_if}
675             && substr($block, -$self->{_tag_if_close_len}) eq $self->{_tag_if_close}) {
676 46         67 $self->{char_pos} = $x->[1];
677 46         92 $html .= $self->_if_block($block);
678 46         68 next;
679             }
680 128         332 $html .= $self->_process_block($block, $x->[1]);
681             }
682              
683 265         1059 return $html;
684             }
685              
686             sub _process_block {
687 140     140   209 my $self = shift;
688 140   50     302 my $str = shift // '';
689 140   50     273 my $char_pos = shift // -1;
690              
691 140         212 $self->{char_pos} = $char_pos;
692              
693 140         222 my $od = $self->{open_delim};
694 140         187 my $cd = $self->{close_delim};
695              
696             # 1. Variable block {$foo} or {$foo|modifier}
697 140 100 100     960 if (substr($str, 0, 2) eq "${od}\$" && $str =~ $self->{_re_var_full}) {
698 60         166 return $self->_variable_block($1);
699             }
700              
701             # 2. If block {if ...}{/if}
702 80 50 66     222 if (substr($str, 0, $self->{_tag_if_len}) eq $self->{_tag_if}
703             && substr($str, -$self->{_tag_if_close_len}) eq $self->{_tag_if_close}) {
704 0         0 return $self->_if_block($str);
705             }
706              
707             # 3. Foreach block {foreach ...}{/foreach}
708 80 100 66     597 if (substr($str, 0, $self->{_tag_foreach_len}) eq $self->{_tag_foreach} && $str =~ $self->{_re_foreach}) {
709 39         204 return $self->_foreach_block($1, $2, $3, $4);
710             }
711              
712             # 4. Include block {include ...}
713 41 100       109 if (substr($str, 0, $self->{_tag_include_len}) eq $self->{_tag_include}) {
714 9         37 return $self->_include_block($str);
715             }
716              
717             # 5. Literal block {literal}...{/literal}
718 32 100 66     157 if (substr($str, 0, $self->{_tag_literal_len}) eq $self->{_tag_literal} && $str =~ $self->{_re_literal}) {
719 7         38 return $1;
720             }
721              
722             # 6. Expression / function block
723 25 100       215 if ($str =~ $self->{_re_expr}) {
724 21         83 return $self->_expression_block($str, $1);
725             }
726              
727             # 7. Unclosed tag
728 4 100       14 if (substr($str, -1) ne $cd) {
729 3         14 my ($line, $col, $file) = $self->_get_char_location($self->{char_pos}, $self->{tpl_file});
730 3         18 $self->_error_out("Unclosed tag $str in $file on line #$line", 45821);
731             }
732              
733             # 8. Fallthrough
734 1         2 return $str;
735             }
736              
737             # -------------------------------------------------------------------
738             # Block handlers
739             # -------------------------------------------------------------------
740              
741             sub _variable_block {
742 60     60   86 my $self = shift;
743 60         148 my $str = shift;
744              
745             # Fast path: no pipe means no modifier, just resolve the variable.
746             # Avoids running the pipe-split regex on every plain variable block.
747 60 50       139 if (index($str, '|') < 0) {
748 0         0 my $ret;
749             # Inline simple key lookup (no dots) — skips array_dive method call
750 0 0       0 if (index($str, '.') < 0) {
751 0         0 $ret = $self->{tpl_vars}{$str};
752             } else {
753 0         0 $ret = $self->array_dive($str, $self->{tpl_vars});
754             }
755 0 0       0 if (ref $ret eq 'ARRAY') { return 'ARRAY' }
  0         0  
756 0 0       0 if (ref $ret eq 'HASH') { return 'HASH' }
  0         0  
757 0 0       0 if (defined $ret) { return $self->_esc($ret) }
  0         0  
758 0         0 return '';
759             }
760              
761 60 50       282 if ($str =~ /(.+?)\|(.*)/) {
762 60         108 my $key = $1;
763 60         132 my $mod = $2;
764              
765 60         173 my $tmp = $self->array_dive($key, $self->{tpl_vars});
766 60   100     396 my $is_nothing = (!defined $tmp || (defined $tmp && ref $tmp eq '' && !length $tmp && $tmp ne '0'));
767 60         99 my $is_default = index($mod, 'default:') >= 0;
768              
769 60 100 100     247 if ($is_nothing && $is_default) {
    100 100        
770 6         11 my $dval = $mod;
771 6         30 $dval =~ s/^.*?default://;
772 6         20 my ($ret) = $self->_peval($dval);
773 6 50       16 if (defined $ret) { return $ret }
  6         29  
774 0         0 return '';
775             } elsif (!$is_nothing && $is_default) {
776 3   50     9 return $self->array_dive($key, $self->{tpl_vars}) // '';
777             } else {
778 51 100       115 if ($is_nothing) {
779 6         26 return '';
780             }
781 45   50     85 my $pre = $self->array_dive($key, $self->{tpl_vars}) // '';
782              
783 45         60 my $seen_escape = 0;
784 45         57 my $seen_noescape = 0;
785              
786             # Split on | not inside double or single quotes (supports chained
787             # modifiers like {$x|uc|substr:0,3})
788 45         155 my $pipe_re = qr/\|(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)(?![^']*'(?:(?:[^']*'){2})*[^']*$)/;
789 45         233 for my $m_part (split $pipe_re, $mod) {
790 49         107 my @x = split /:/, $m_part, 2;
791 49   50     148 my $func = $x[0] // '';
792 49 100       87 if ($func eq 'escape') { $seen_escape = 1 }
  19         31  
793 49 100       90 if ($func eq 'noescape') { $seen_noescape = 1 }
  7         11  
794 49   100     119 my $param_str = $x[1] // '';
795 49         100 my @params = ($pre);
796              
797 49 100       100 if (length $param_str) {
798             # Split on commas not inside double or single quotes
799             # (parameter separator in modifier calls like substr:2,2)
800 15         39 my $comma_re = qr/,(?=(?:[^"]*"[^"]*")*[^"]*$)(?=(?:[^']*'[^']*')*[^']*$)/;
801             my @new = map {
802 15         72 my ($v) = $self->_peval($_);
  16         46  
803 16         51 $v;
804             } split $comma_re, $param_str;
805 15         50 push @params, @new;
806             }
807              
808             {
809 10     10   95 no strict 'refs';
  10         19  
  10         53604  
  49         72  
810              
811             # Priority: main::, Template::Sluz built-ins, then CORE::
812 49   66     61 my $callable = defined &{"main::$func"} || defined &{$func} || defined &{"CORE::$func"};
813              
814 49 50       96 if (!$callable) {
815 0         0 my ($line, $col, $file) = $self->_get_char_location($self->{char_pos}, $self->{tpl_file});
816 0         0 $self->_error_out("Unknown function call $func in $file on line #$line", 47204);
817             }
818              
819 49 100       62 if (defined &{"main::$func"}) {
  49 100       153  
820 11         19 $pre = eval { &{"main::$func"}(@params) };
  11         16  
  11         40  
821 38         89 } elsif (defined &{$func}) {
822 31         45 $pre = eval { &{$func}(@params) };
  31         39  
  31         74  
823             } else {
824 7         13 $pre = eval { &{"CORE::$func"}(@params) };
  7         12  
  7         49  
825             }
826             }
827              
828 49 50       267 if ($@) {
829 0         0 $self->_error_out("Exception: $@", 79134);
830             }
831             }
832              
833 45 100 100     136 if ($self->{auto_escape} && !$seen_noescape && !$seen_escape) {
      100        
834 1         4 return $self->_esc($pre);
835             }
836 44         215 return $pre;
837             }
838             }
839              
840 0         0 my $ret = $self->array_dive($str, $self->{tpl_vars});
841 0 0       0 if (ref $ret eq 'ARRAY') { return 'ARRAY' }
  0         0  
842 0 0       0 if (ref $ret eq 'HASH') { return 'HASH' }
  0         0  
843 0 0       0 if (defined $ret) { return $self->_esc($ret) }
  0         0  
844 0         0 return '';
845             }
846              
847             sub _if_block {
848 64     64   77 my $self = shift;
849 64         79 my $str = shift;
850              
851 64         73 my @rules;
852 64 100       121 if (exists $self->{_if_rules_cache}{$str}) {
853 12         16 @rules = @{$self->{_if_rules_cache}{$str}};
  12         29  
854             } else {
855 52         68 my $od = $self->{open_delim};
856 52         88 my $cd = $self->{close_delim};
857 52         82 my $isimple_start = length($od) + 1;
858 52         66 my $else_check = $od . 'else';
859 52         87 my $is_simple = index($str, $else_check, $isimple_start) < 0;
860              
861 52 100       92 if ($is_simple) {
862 31         253 $str =~ $self->{_re_if_simple};
863 31   100     158 my $cond = $1 // '';
864 31   50     76 my $payload = $2 // '';
865 31         66 $payload = $self->ltrim_one($payload, "\n");
866 31         88 @rules = ([$cond, $payload]);
867             } else {
868 21         42 my @toks = $self->get_tokens($str);
869 21         70 @rules = $self->_if_rules_from_tokens(\@toks);
870             }
871              
872 52         147 $self->{_if_rules_cache}{$str} = \@rules;
873             }
874              
875 64         85 my $ret = '';
876 64         102 for my $rule (@rules) {
877 78         107 my $raw = $rule->[0];
878             # Inline _convert_vars for cached expressions — saves method call per iteration
879             my $test = (index($raw, '$') < 0) ? $raw :
880 78 100 66     252 ($self->{_convert_cache}{$raw} // $self->_convert_vars($raw));
881 78         105 my $payload = $rule->[1];
882 78         140 my ($res) = $self->_peval($test);
883 78 100       161 if ($res) {
884             # Inline _get_blocks for cached payloads
885 53         101 my $cached = $self->{_blocks_cache}{$payload};
886 53 100       133 my @in_blocks = $cached ? @$cached : $self->_get_blocks($payload);
887 53         153 $ret .= $self->_process_blocks(\@in_blocks);
888 53         109 last;
889             }
890             }
891              
892 64         140 return $ret;
893             }
894              
895             sub _foreach_block {
896 39     39   54 my $self = shift;
897 39         85 my $src_expr = shift;
898 39         65 my $okey = shift;
899 39         63 my $oval = shift;
900 39         155 my $payload = shift;
901              
902 39         118 my $conv_src = $self->_convert_vars($src_expr);
903 39         107 $payload = $self->ltrim_one($payload, "\n");
904 39         126 my @blocks = $self->_get_blocks($payload);
905              
906             # Pre-classify blocks for fast dispatch in the loop (cached in block arrays)
907             # type: -1=empty, 0=text, 1=simple_var, 2=if_block, 99=other
908 39         96 my $od = $self->{open_delim};
909 39         61 my $cd = $self->{close_delim};
910 39         60 for my $b (@blocks) {
911 62 100       133 next if defined $b->[2];
912 56         88 my $bs = $b->[0];
913 56 50 100     499 if (!length $bs) {
    100 66        
    100 66        
    100          
914 0         0 $b->[2] = -1;
915             } elsif (substr($bs, 0, 1) ne $od) {
916 16         37 $b->[2] = 0;
917             } elsif (substr($bs, 0, 2) eq "${od}\$" && index($bs, '|') < 0
918             && $bs =~ $self->{_re_var_simple}) {
919 30         59 $b->[2] = 1;
920 30         82 $b->[3] = $1;
921             } elsif (substr($bs, 0, $self->{_tag_if_len}) eq $self->{_tag_if}
922             && substr($bs, -$self->{_tag_if_close_len}) eq $self->{_tag_if_close}) {
923 6         15 $b->[2] = 2;
924             } else {
925 4         14 $b->[2] = 99;
926             }
927             }
928              
929 39         151 my ($src) = $self->_peval($conv_src);
930              
931 39 100 100     189 if (!defined $src) {
    100          
932 2         5 $src = [];
933             } elsif (ref $src ne 'ARRAY' && ref $src ne 'HASH') {
934 1         3 $src = [$src];
935             }
936              
937 39         96 my $pfx = $self->{var_prefix};
938              
939             # Precompute __S keys for the loop variables
940 39         58 my $okey_ks = "${pfx}_$okey";
941 39 100       76 my $oval_ks = defined $oval ? "${pfx}_$oval" : undef;
942 39         54 my $first_ks = "${pfx}__FOREACH_FIRST";
943 39         50 my $last_ks = "${pfx}__FOREACH_LAST";
944 39         49 my $index_ks = "${pfx}__FOREACH_INDEX";
945              
946 39         49 my $ret = '';
947 39         50 my $idx = 0;
948              
949 39         67 my $need_first = index($payload, '__FOREACH_FIRST') >= 0;
950 39         56 my $need_last = index($payload, '__FOREACH_LAST') >= 0;
951 39         51 my $need_index = index($payload, '__FOREACH_INDEX') >= 0;
952              
953             # Save only the keys we'll modify — O(k) where k <= 5, vs O(n) for
954             # copying the entire tpl_vars/__S hashes. Big win for nested foreach.
955 39         86 my @tpl_keys = ($okey);
956 39         56 my @ks_keys = ($okey_ks);
957 39 100       82 if (defined $oval) {
958 9         17 push @tpl_keys, $oval;
959 9         14 push @ks_keys, $oval_ks;
960             }
961 39 100       69 push @tpl_keys, '__FOREACH_FIRST' if $need_first;
962 39 100       66 push @ks_keys, $first_ks if $need_first;
963 39 100       61 push @tpl_keys, '__FOREACH_LAST' if $need_last;
964 39 100       64 push @ks_keys, $last_ks if $need_last;
965 39 100       62 push @tpl_keys, '__FOREACH_INDEX' if $need_index;
966 39 100       59 push @ks_keys, $index_ks if $need_index;
967              
968 39         104 my @tpl_exists = map { exists $self->{tpl_vars}{$_} } @tpl_keys;
  52         145  
969 39         59 my @tpl_vals = map { $self->{tpl_vars}{$_} } @tpl_keys;
  52         106  
970 39         56 my @ks_exists = map { exists $self->{__S}{$_} } @ks_keys;
  52         101  
971 39         77 my @ks_vals = map { $self->{__S}{$_} } @ks_keys;
  52         93  
972              
973 39 100       97 if (ref $src eq 'ARRAY') {
    50          
974 35         64 my $last = $#$src;
975 35         86 for my $i (0 .. $last) {
976 88 100       123 if ($need_first) {
977 6 100       16 $self->{tpl_vars}{__FOREACH_FIRST} = ($idx == 0) ? 1 : 0;
978 6 100       13 $self->{__S}{$first_ks} = ($idx == 0) ? 1 : 0;
979             }
980 88 100       134 if ($need_last) {
981 3 100       13 $self->{tpl_vars}{__FOREACH_LAST} = ($idx == $last) ? 1 : 0;
982 3 100       10 $self->{__S}{$last_ks} = ($idx == $last) ? 1 : 0;
983             }
984 88 100       126 if ($need_index) {
985 3         5 $self->{tpl_vars}{__FOREACH_INDEX} = $idx;
986 3         5 $self->{__S}{$index_ks} = $idx;
987             }
988 88 100       192 if (defined $oval) {
989 16         29 $self->{tpl_vars}{$okey} = $i;
990 16         26 $self->{tpl_vars}{$oval} = $src->[$i];
991 16         28 $self->{__S}{$okey_ks} = $i;
992 16         27 $self->{__S}{$oval_ks} = $src->[$i];
993             } else {
994 72         146 $self->{tpl_vars}{$okey} = $src->[$i];
995 72         107 $self->{__S}{$okey_ks} = $src->[$i];
996             }
997             # Inline block processing with pre-classified types — no substr/regex per iteration
998 88         115 for my $b (@blocks) {
999 132         158 my $type = $b->[2];
1000 132 100       264 if ($type == 0) {
    100          
    100          
    50          
1001 34         52 $ret .= $b->[0];
1002             } elsif ($type == 1) {
1003 68         88 my $var = $b->[3];
1004 68         67 my $val;
1005 68 100       109 if (index($var, '.') < 0) {
1006 56         81 $val = $self->{tpl_vars}{$var};
1007             } else {
1008 12         36 $val = $self->array_dive($var, $self->{tpl_vars});
1009             }
1010 68 50       141 if (ref $val eq 'ARRAY') { $ret .= 'ARRAY' }
  0 50       0  
    50          
1011 0         0 elsif (ref $val eq 'HASH') { $ret .= 'HASH' }
1012 68         110 elsif (defined $val) { $ret .= $self->_esc($val) }
1013             } elsif ($type == 2) {
1014 18         32 $self->{char_pos} = $b->[1];
1015 18         42 $ret .= $self->_if_block($b->[0]);
1016             } elsif ($type == -1) {
1017 0         0 next;
1018             } else {
1019 12         34 $ret .= $self->_process_block($b->[0], $b->[1]);
1020             }
1021             }
1022 88         122 $idx++;
1023             }
1024             } elsif (ref $src eq 'HASH') {
1025 4         32 my @keys = sort keys %$src;
1026 4         10 my $last = $#keys;
1027 4         13 for my $i (0 .. $last) {
1028 11         21 my $k = $keys[$i];
1029 11 50       26 if ($need_first) {
1030 0 0       0 $self->{tpl_vars}{__FOREACH_FIRST} = ($idx == 0) ? 1 : 0;
1031 0 0       0 $self->{__S}{$first_ks} = ($idx == 0) ? 1 : 0;
1032             }
1033 11 50       22 if ($need_last) {
1034 0 0       0 $self->{tpl_vars}{__FOREACH_LAST} = ($idx == $last) ? 1 : 0;
1035 0 0       0 $self->{__S}{$last_ks} = ($idx == $last) ? 1 : 0;
1036             }
1037 11 50       25 if ($need_index) {
1038 0         0 $self->{tpl_vars}{__FOREACH_INDEX} = $idx;
1039 0         0 $self->{__S}{$index_ks} = $idx;
1040             }
1041 11 100       39 if (defined $oval) {
1042 6         12 $self->{tpl_vars}{$okey} = $k;
1043 6         13 $self->{tpl_vars}{$oval} = $src->{$k};
1044 6         11 $self->{__S}{$okey_ks} = $k;
1045 6         11 $self->{__S}{$oval_ks} = $src->{$k};
1046             } else {
1047 5         14 $self->{tpl_vars}{$okey} = $src->{$k};
1048 5         15 $self->{__S}{$okey_ks} = $src->{$k};
1049             }
1050             # Inline block processing with pre-classified types — no substr/regex per iteration
1051 11         20 for my $b (@blocks) {
1052 32         47 my $type = $b->[2];
1053 32 100       75 if ($type == 0) {
    50          
    0          
    0          
1054 15         25 $ret .= $b->[0];
1055             } elsif ($type == 1) {
1056 17         49 my $var = $b->[3];
1057 17         23 my $val;
1058 17 50       30 if (index($var, '.') < 0) {
1059 17         31 $val = $self->{tpl_vars}{$var};
1060             } else {
1061 0         0 $val = $self->array_dive($var, $self->{tpl_vars});
1062             }
1063 17 50       56 if (ref $val eq 'ARRAY') { $ret .= 'ARRAY' }
  0 50       0  
    50          
1064 0         0 elsif (ref $val eq 'HASH') { $ret .= 'HASH' }
1065 17         50 elsif (defined $val) { $ret .= $self->_esc($val) }
1066             } elsif ($type == 2) {
1067 0         0 $self->{char_pos} = $b->[1];
1068 0         0 $ret .= $self->_if_block($b->[0]);
1069             } elsif ($type == -1) {
1070 0         0 next;
1071             } else {
1072 0         0 $ret .= $self->_process_block($b->[0], $b->[1]);
1073             }
1074             }
1075 11         26 $idx++;
1076             }
1077             }
1078              
1079             # Restore only the keys we modified
1080 39         133 for my $i (0 .. $#tpl_keys) {
1081 52 100       124 if ($tpl_exists[$i]) {
1082 23         49 $self->{tpl_vars}{$tpl_keys[$i]} = $tpl_vals[$i];
1083             } else {
1084 29         66 delete $self->{tpl_vars}{$tpl_keys[$i]};
1085             }
1086 52 100       98 if ($ks_exists[$i]) {
1087 23         49 $self->{__S}{$ks_keys[$i]} = $ks_vals[$i];
1088             } else {
1089 29         61 delete $self->{__S}{$ks_keys[$i]};
1090             }
1091             }
1092              
1093 39         226 return $ret;
1094             }
1095              
1096             sub _include_block {
1097 9     9   18 my $self = shift;
1098 9         14 my $str = shift;
1099              
1100 9         20 my $save = $self->{tpl_vars};
1101 9         29 my $inc_tpl = $self->_extract_include_file($str);
1102              
1103 9 50       25 if ($self->{perl_file_dir}) {
1104 9         27 $inc_tpl = $self->{perl_file_dir} . "/$inc_tpl";
1105             }
1106              
1107 9         71 while ($str =~ m/(\w+)=(['"](.+?)['"])/g) {
1108 9         22 my $key = $1;
1109 9         17 my $val = $2;
1110 9 100       41 if ($key eq 'file') { next }
  8         28  
1111 1         3 $val = $self->_convert_vars($val);
1112 1         5 my ($res) = $self->_peval($val);
1113 1 50       7 if (defined $res) {
1114 1         6 $self->assign($key => $res);
1115             } else {
1116 0         0 $self->assign($key => $val);
1117             }
1118             }
1119              
1120 9 50 33     389 if (!-f $inc_tpl || !-r $inc_tpl) {
1121 0         0 $self->{inc_tpl_file} = undef;
1122 0         0 my ($line, $col, $file) = $self->_get_char_location($self->{char_pos}, $self->{tpl_file});
1123 0         0 $self->_error_out("Unable to load include template $inc_tpl in $file on line #$line", 18485);
1124             }
1125              
1126 9         71 local $/;
1127 9 50       444 open my $fh, '<', $inc_tpl or $self->_error_out("Cannot open $inc_tpl: $!", 18485);
1128 9         350 my $content = <$fh>;
1129 9         106 close $fh;
1130              
1131 9         42 my @blocks = $self->_get_blocks($content);
1132 9         41 my $r = $self->_process_blocks(\@blocks);
1133              
1134 9         26 $self->{tpl_vars} = $save;
1135 9         19 $self->{inc_tpl_file} = undef;
1136              
1137 9         91 return $r;
1138             }
1139              
1140             sub _expression_block {
1141 21     21   32 my $self = shift;
1142 21         32 my $str = shift;
1143 21         58 my $inner = shift;
1144              
1145 21 100       88 if ($str !~ /["\d\$\(]/) {
1146 5         21 my ($line, $col, $file) = $self->_get_char_location($self->{char_pos}, $self->{tpl_file});
1147 5         47 $self->_error_out("Unknown block type $str in $file on line #$line", 73467);
1148             }
1149              
1150 16         50 my $after = $self->_convert_vars($inner);
1151 16         46 my ($ret, $err) = $self->_peval($after);
1152              
1153 16         24 my $valid;
1154 16 100 33     76 if (defined $ret && (!ref $ret || ref $ret eq '')) {
      66        
1155 14         20 $valid = 1;
1156             } else {
1157 2         6 $valid = 0;
1158             }
1159              
1160 16 100 100     59 if ($err || !$valid) {
1161 2         41 my ($line, $col, $file) = $self->_get_char_location($self->{char_pos}, $self->{tpl_file});
1162 2         17 $self->_error_out("Unknown tag $str in $file on line #$line", 18933);
1163             }
1164              
1165 14         77 return $ret;
1166             }
1167              
1168             # -------------------------------------------------------------------
1169             # Variable / eval engine
1170             # -------------------------------------------------------------------
1171              
1172             sub _convert_vars {
1173 97     97   123 my $self = shift;
1174 97   50     201 my $str = shift // '';
1175 97 100       230 if (index($str, '$') < 0) { return $str }
  13         40  
1176              
1177             # Check conversion cache — avoids re-running regex substitutions on
1178             # the same expression (e.g. an {if} condition inside a {foreach} loop)
1179 84 100       195 if (exists $self->{_convert_cache}{$str}) {
1180 21         59 return $self->{_convert_cache}{$str};
1181             }
1182              
1183 63         85 my $orig = $str;
1184              
1185             # Step 1: $var.key -> $__S->{sluz_pfx_var}->{key}
1186 63         321 $str =~ s/(\$\w[\w\.]*)/ $self->_dot_to_bracket_cb($1) /ge;
  67         143  
1187              
1188             # Step 2: $__S->{...}["key"] -> $__S->{...}->{key} (PHP bracket syntax)
1189 63 100       174 if (index($str, '[') >= 0) {
1190 3         13 $str =~ s/(\$__S(?:->\{[^}]+\})+)\[(["'])([^\]]+?)\2\]/$1 . '->{' . $3 . '}'/ge;
  1         6  
1191             }
1192              
1193 63         146 $self->{_convert_cache}{$orig} = $str;
1194 63         156 return $str;
1195             }
1196              
1197             sub _dot_to_bracket_cb {
1198 67     67   87 my $self = shift;
1199 67         107 my $match = shift;
1200 67         179 my @parts = split /\./, $match;
1201 67         110 my $first = shift @parts;
1202 67         110 my $var = substr($first, 1);
1203 67         142 my $res = "\$__S->\{$self->{var_prefix}_$var\}";
1204 67         124 for my $p (@parts) {
1205 6 100       23 if ($p =~ /^\d+$/) {
1206 1         5 $res .= "->[$p]";
1207             } else {
1208 5         35 $res .= "->{$p}";
1209             }
1210             }
1211 67         256 return $res;
1212             }
1213              
1214             sub _micro_optimize {
1215 165     165   221 my $self = shift;
1216 165   50     271 my $str = shift // '';
1217 165 100       639 if ($str =~ /^-?\d+(?:\.\d+)?$/) { return $str }
  18         64  
1218              
1219 147 100       324 if (!length $str) { return undef }
  1         2  
1220 146         245 my $first = substr($str, 0, 1);
1221 146         216 my $last = substr($str, -1);
1222              
1223 146 100 66     346 if ($first eq "'" && $last eq "'") {
1224 10         37 my $tmp = substr($str, 1, length($str) - 2);
1225 10 50       69 if (index($tmp, "'") < 0) { return $tmp }
  10         35  
1226             }
1227              
1228 136 100 66     283 if ($first eq '"' && $last eq '"') {
1229 16         39 my $tmp = substr($str, 1, length($str) - 2);
1230 16 100 66     68 if (index($tmp, '$') < 0 && index($tmp, '"') < 0) { return $tmp }
  13         39  
1231             }
1232              
1233 123 100       334 if ($str =~ /^\$__S->\{sluz_pfx_(\w+)\}$/) {
1234 83 100       234 if (exists $self->{tpl_vars}{$1}) { return $self->{tpl_vars}{$1} }
  72         177  
1235             }
1236              
1237 51 100       122 if ($str =~ /^!\$__S->\{sluz_pfx_(\w+)\}$/) {
1238 4 100       16 if (exists $self->{tpl_vars}{$1}) { return !$self->{tpl_vars}{$1} }
  3         9  
1239             }
1240              
1241 48 50 66     140 if ($str =~ /^(\w+)$/ && exists $self->{tpl_vars}{$1}) {
1242 3         7 return $self->{tpl_vars}{$1};
1243             }
1244              
1245 45 0 33     130 if ($str =~ /^!(\w+)$/ && exists $self->{tpl_vars}{$1}) {
1246 0         0 return !$self->{tpl_vars}{$1};
1247             }
1248              
1249 45         119 return undef;
1250             }
1251              
1252             sub _peval {
1253 165     165   249 my $self = shift;
1254 165   50     306 my $str = shift // '';
1255              
1256 165 50       385 if (index($str, '===') >= 0) {
1257 0         0 $str =~ s/===/==/g;
1258             }
1259              
1260 165         323 my $opt = $self->_micro_optimize($str);
1261 165 100       305 if (defined $opt) { return ($opt, 0) }
  117         270  
1262              
1263             # Use the persistent $__S hash (maintained by assign/foreach) instead
1264             # of rebuilding it from tpl_vars on every call
1265 48         75 my $__S = $self->{__S};
1266              
1267             # Check verified sub cache — subs that have succeeded at least once.
1268             # Skip eval/local $SIG overhead (the biggest per-call cost in loops).
1269             # no warnings in the compiled sub suppresses uninitialized-value warnings.
1270 48         115 my $vsub = $self->{_verified_sub_cache}{$str};
1271 48 100       86 if ($vsub) {
1272 11         335 return ($vsub->($__S), 0);
1273             }
1274              
1275             # Check compiled sub cache — avoids re-parsing the same expression
1276 37         85 my $sub = $self->{_sub_cache}{$str};
1277 37 50       73 if (!defined $sub) {
1278             # Compile in main:: first (where user functions live), then Template::Sluz
1279 6     6   54 $sub = eval "package main; no warnings; sub { my \$__S = \$_[0]; return ($str); }";
  6     4   9  
  6     4   509  
  4     3   28  
  4     2   7  
  4     2   352  
  4     2   30  
  4     2   7  
  4     2   304  
  3     2   21  
  3     2   7  
  3     2   211  
  2     2   16  
  2         5  
  2         217  
  2         15  
  2         5  
  2         185  
  2         11  
  2         4  
  2         146  
  2         18  
  2         5  
  2         177  
  2         18  
  2         3  
  2         176  
  2         46  
  2         3  
  2         143  
  2         14  
  2         4  
  2         176  
  2         11  
  2         3  
  2         142  
  2         16  
  2         4  
  2         219  
  37         3619  
1280 37 100       131 if ($@) {
1281 1     1   5 $sub = eval "no warnings; sub { my \$__S = \$_[0]; return ($str); }";
  1         2  
  1         51  
  1         45  
1282             }
1283             # Cache the result (even undef) so we don't recompile failures
1284 37         119 $self->{_sub_cache}{$str} = $sub;
1285             }
1286              
1287 37         49 my $ret;
1288 37 100       86 if ($sub) {
1289 36     0   218 local $SIG{__WARN__} = sub {};
1290 36         58 $ret = eval { $sub->($__S) };
  36         978  
1291 36 100       99 unless ($@) {
1292             # Promote to verified cache — skip eval/SIG on future calls
1293 33         71 $self->{_verified_sub_cache}{$str} = $sub;
1294 33         84 delete $self->{_sub_cache}{$str};
1295 33         223 return ($ret, 0);
1296             }
1297             # Cached sub failed (e.g. function not in main::) — evict and fall through
1298 3         17 delete $self->{_sub_cache}{$str};
1299             }
1300              
1301             {
1302 4     0   7 local $SIG{__WARN__} = sub {};
  4         20  
1303 4     4   19 $ret = eval "no warnings; return ($str);";
  4         6  
  4         220  
  4         283  
1304 4 100       29 if ($@) {
1305 1     1   8 $ret = eval "package main; no warnings; return ($str);";
  1         3  
  1         69  
  1         88  
1306             }
1307             }
1308              
1309 4 100       14 if ($@) {
1310 1         5 return (undef, -1);
1311             }
1312              
1313 3         14 return ($ret, 0);
1314             }
1315              
1316             # -------------------------------------------------------------------
1317             # Error handling
1318             # -------------------------------------------------------------------
1319              
1320             sub _error_out {
1321 11     11   23 my $self = shift;
1322 11         38 my $msg = shift;
1323 11         18 my $err_num = shift;
1324 11         2276 croak "Template::Sluz error #$err_num: $msg";
1325             }
1326              
1327             sub _get_char_location {
1328 10     10   24 my $self = shift;
1329 10         19 my $pos = shift;
1330 10   100     39 my $tpl_file = shift // '';
1331              
1332 10 50       33 if ($self->{inc_tpl_file}) { $tpl_file = $self->{inc_tpl_file} }
  0         0  
1333              
1334 10         34 my $str = $self->_get_tpl_content($tpl_file);
1335 10 50 33     69 if ($pos < 0 || !defined $str) { return (-1, -1, $tpl_file) }
  0         0  
1336              
1337 10         21 my $line = 1;
1338 10         18 my $col = 0;
1339 10         37 for (my $i = 0; $i < length $str; $i++) {
1340 4         6 $col++;
1341 4 50       12 if (substr($str, $i, 1) eq "\n") {
1342 0         0 $line++;
1343 0         0 $col = 0;
1344             }
1345 4 100       13 if ($pos == $i) { return ($line, $col, $tpl_file) }
  1         6  
1346             }
1347              
1348 9 50       23 if ($pos == length $str) { return ($line, $col, $tpl_file) }
  0         0  
1349 9         39 return (-1, -1, $tpl_file);
1350             }
1351              
1352             sub _extract_include_file {
1353 9     9   13 my $self = shift;
1354 9         15 my $str = shift;
1355              
1356 9 100       89 if ($str =~ /\s(file=)(['"].+?['"])/) {
1357 8         27 my $xstr = $self->_convert_vars($2);
1358 8         26 my ($ret) = $self->_peval($xstr);
1359 8         19 $self->{inc_tpl_file} = $ret;
1360 8         21 return $ret;
1361             }
1362              
1363 1 50       10 if ($str =~ /\s(['"].+?['"])/) {
1364 1         4 my $xstr = $self->_convert_vars($1);
1365 1         3 my ($ret) = $self->_peval($xstr);
1366 1         2 $self->{inc_tpl_file} = $ret;
1367 1         3 return $ret;
1368             }
1369              
1370 0         0 my ($line, $col, $file) = $self->_get_char_location($self->{char_pos}, $self->{tpl_file});
1371 0         0 $self->_error_out("Unable to find a file in include block $str in $file on line #$line", 68493);
1372             }
1373              
1374             sub _if_rules_from_tokens {
1375 21     21   23 my $self = shift;
1376 21         45 my $toks = shift;
1377 21         25 my $num = scalar @$toks;
1378 21         23 my $nested = 0;
1379 21         21 my @tmp;
1380              
1381 21         31 my $tif_tag = $self->{_tag_if};
1382 21         23 my $tifc_tag = $self->{_tag_if_close};
1383 21         30 my $tif_prefix = $self->{open_delim} . 'if';
1384              
1385 21         46 for my $i (0 .. $num - 1) {
1386 139         144 my $item = $toks->[$i];
1387 139 100       212 if (index($item, $tif_prefix) == 0) { $nested++ }
  27         31  
1388 139 100       171 if ($item eq $tifc_tag) { $nested-- }
  27         25  
1389              
1390 139         148 my $yes = 0;
1391 139 100       173 if ($nested == 1) {
1392 98   100     117 $yes = $self->is_if_token($item) || 0;
1393 98 100       131 $yes = 0 if $item eq $tifc_tag;
1394             }
1395 139         179 $tmp[$i] = $yes;
1396             }
1397              
1398 21         38 $tmp[$num - 1] = 1;
1399              
1400 21         22 my @conds;
1401 21         42 for my $i (0 .. $num - 1) {
1402 139 100       205 if ($tmp[$i]) {
1403 67         103 my $test = $self->is_if_token($toks->[$i]);
1404 67 100       101 if ($i != $num - 1) { push @conds, $test }
  46         64  
1405             }
1406             }
1407              
1408 21         25 my $str = '';
1409 21         25 my @payloads;
1410 21         23 my $first = 1;
1411 21         38 for my $i (0 .. $num - 1) {
1412 139 100       174 if ($tmp[$i]) {
1413 67 100       82 if (!$first) { push @payloads, $str }
  46         52  
1414 67         77 $first = 0;
1415 67         75 $str = '';
1416             } else {
1417 72         98 $str .= $toks->[$i];
1418             }
1419             }
1420              
1421 21 50       30 if (@conds != @payloads) {
1422 0         0 $self->_error_out("Error parsing if conditions in '$str'", 95320);
1423             }
1424              
1425 21         30 my @ret;
1426 21         93 push @ret, [$conds[$_], $payloads[$_]] for 0 .. $#conds;
1427 21         68 return @ret;
1428             }
1429              
1430             1;
1431              
1432             __END__