File Coverage

blib/lib/Template/LiquidX/Tidy/impl.pm
Criterion Covered Total %
statement 169 229 73.8
branch 58 110 52.7
condition 46 125 36.8
subroutine 12 12 100.0
pod n/a
total 285 476 59.8


line stmt bran cond sub pod time code
1             package Template::LiquidX::Tidy::impl;
2              
3 3     3   20 use strict;
  3         6  
  3         86  
4 3     3   14 use warnings;
  3         18  
  3         106  
5 3     3   19 use experimental 'signatures';
  3         6  
  3         16  
6              
7             our $VERSION = '0.01';
8              
9 3     3   382 use Exporter 'import';
  3         6  
  3         172  
10             our @EXPORT_OK = qw(_tidy_list _tidy_make_string);
11 3     3   20 use vars '%defaults';
  3         5  
  3         802  
12             *defaults = \%Template::LiquidX::Tidy::defaults;
13              
14             # parser regex
15             our $PI_START = qr/\{%-?/;
16             our $PI_END = qr/-?%\}/;
17             our $LV_START = qr/\{\{/;
18              
19             # pseudo html indenter
20             # $html - fragment of html to chunk into lines
21             # $args - indenter configuration
22             # $level - current level
23             # $clevel - current level for debugging
24             # $stack - additional storage for
25             # open_tags /
26             # closed_tags /
27             # level at return time
28             # returns a list of [ level, fragment ] pairs and modifies $stack
29 12     12   15 sub _tidy_html ($html, $args, $level, $clevel, $stack) {
  12         29  
  12         19  
  12         18  
  12         16  
  12         48  
  12         23  
30             #print ((" " x $clevel ). "_tidy_html <<$html>> <<$level>>\n");
31 12         30 my $nl = $html =~ /\n/;
32 12         17 my @return;
33 12         19 my $last_line_start = 0;
34 12         23 my $start_level = $level;
35 12         19 my $level_change = '';
36 12         74 while ($html =~ m{(?: (? )
37             | (? \n )
38             | (? />)
39             | <(? /)? (? \w+ )
40             ) }gsx) {
41             #use Data::Dumper; warn Dumper \%+;
42 3 50   3   1346 if ($+{cdata}) {
  3 100       1219  
  3         8825  
  9         73  
43             # ignore
44             }
45             elsif (length $+{nl}) {
46 3         14 1 while $level_change =~ s/\(\)//;
47             push @return, [ $start_level - ($level_change =~ y/)//), (substr $html, $last_line_start, (pos $html) - $last_line_start),
48             +{ html => 1,
49             ($stack->{open_tags} ? (open => [$stack->{open_tags}->@*]) : ()),
50 3 50       50 ($stack->{closed_tags} ? (closed => [$stack->{closed_tags}->@*]) : ()), } ];
    50          
51 3         11 $last_line_start = pos($html);
52 3         6 $start_level = $level;
53 3         10 $level_change = '';
54             }
55             else {
56 6 50       49 my $tag = $+{tag} ? lc $+{tag} : '';
57 6 50       98 if ($tag =~ /^(?| img | br | link | meta )$/ix) {
    50          
    100          
58 0         0 push $stack->{waiting}->@*, $tag;
59             }
60             elsif ($+{close2}) {
61 0 0 0     0 if ($stack->{waiting} && $stack->{waiting}->@*) {
62 0         0 pop $stack->{waiting}->@*;
63             } else {
64 0         0 pop $stack->{open_tags}->@*;
65 0         0 $level_change .= ')';
66 0         0 $level--;
67             #print ((" " x $clevel ). " close2 <<$level_change>> <<$start_level>>\n");
68             }
69             }
70             elsif ($+{close}) {
71 3 50 33     22 if ($stack->{waiting} && $stack->{waiting}->@* && $stack->{waiting}[-1] eq $tag) {
      33        
72 0         0 pop $stack->{waiting}->@*;
73             }
74             else {
75 3 50 33     25 if ($stack->{open_tags} && $stack->{open_tags}[-1] eq $tag) {
76 3         9 pop $stack->{open_tags}->@*;
77             } else {
78 0         0 push $stack->{closed_tags}->@*, $tag;
79             }
80 3         7 $level_change .= ')';
81 3         4 $level--;
82 3         21 $stack->{waiting}->@* = ();
83             #print ((" " x $clevel ). " close<<$tag>> <<$level_change>> <<$start_level>>\n");
84             }
85             }
86             else {
87 3         14 push $stack->{open_tags}->@*, $tag;
88 3         8 $level_change .= '(';
89 3         4 $level++;
90 3         27 $stack->{waiting}->@* = ();
91             #print ((" " x $clevel ). " open<<$tag>> <<$level_change>> <<$start_level>>\n");
92             }
93             }
94             }
95 12 100       32 if ($last_line_start < length $html) {
96 9         22 1 while $level_change =~ s/\(\)//;
97             push @return, [ $start_level - ($level_change =~ y/)//), (substr $html, $last_line_start),
98             +{ html => 1,
99             ($stack->{open_tags} ? (open => [$stack->{open_tags}->@*]) : ()),
100 9 50       69 ($stack->{closed_tags} ? (closed => [$stack->{closed_tags}->@*]) : ()), } ];
    50          
101             }
102 12         27 $stack->{level} = $level;
103             @return
104 12         41 }
105              
106             # liquid indenter part
107             # $obj - an object, subclass of Template::Liquid::Document
108             # $args - indenter configuration
109             # $level - current level
110             # $clevel - current level for debugging
111             # $stack - additional storage for the html indenter in particular
112             # returns a list of [ level, fragment ] pairs and modifies $stack
113 42     42   58 sub _tidy_list ($obj, $args, $level, $clevel, $stack = {}) {
  42         55  
  42         60  
  42         55  
  42         49  
  42         59  
  42         57  
114 42         55 my @return;
115             push @return, [ $level, $obj->{markup}, +{ markup => $obj } ]
116 42 100       168 if defined $obj->{markup};
117              
118 42         73 my $nlevel = $level + 1;
119 42         82 for my $node ($obj->{nodelist}->@*) {
120 36         49 my @result;
121 36 100       69 if (ref $node) {
122 24         65 @result = _tidy_list($node, $args, $nlevel, $clevel + 1, $stack)
123             } else {
124 12 50 33     54 if ($args->{html} // $defaults{html}) {
125 12         32 @result = _tidy_html($node, $args, $nlevel, $clevel + 1, $stack);
126 12 50       35 if ($stack->{level}) {
127 12         27 $nlevel = delete $stack->{level};
128             }
129             } else {
130 0         0 @result = [ $nlevel, $node, +{ text => 1 } ]
131             }
132             }
133 36         74 push @return, @result;
134             }
135              
136 42         75 for my $block ($obj->{blocks}->@*) {
137 15         24 my @result;
138 15 50       30 if (ref $block) {
139 15         32 @result = _tidy_list($block, $args, $level, $clevel + 1, $stack)
140             } else {
141 0 0 0     0 if ($args->{html} // $defaults{html}) {
142 0         0 @result = _tidy_html($block, $args, $level, $clevel + 1, $stack)
143             } else {
144 0         0 @result = [ $level, $block, +{ text => 1 } ]
145             }
146             }
147 15         30 push @return, @result;
148             }
149              
150             push @return, [ $level, $obj->{markup_2}, +{ markup_2 => $obj } ]
151 42 100       97 if defined $obj->{markup_2};
152              
153             @return
154 42         108 }
155              
156             # check if the tag should always be linebroken
157             # $args->{force_nl} - config option to enable linebreaks
158             # $args->{force_nl_tags} - whitespace separated string with tags that should be linebroken
159 12     12   20 sub _force_nl_tags ($args, $content) {
  12         16  
  12         18  
  12         17  
160 12   66     44 my $force_nl = $args->{force_nl} // $defaults{force_nl};
161 12   33     40 my $force_nl_tags = $args->{force_nl_tags} // $defaults{force_nl_tags};
162 12 100       36 return unless $force_nl;
163 8         49 my @tags = split ' ', $force_nl_tags;
164 8 50       29 return 1 unless @tags;
165 8         20 my $re = join '|', map { "\Q$_" } @tags;
  104         190  
166 8         231 return $content =~ /$PI_START\s*($re)/;
167             }
168              
169             # there is only whitespace before $list[$i] and a linebreak
170 19     19   28 sub _ws_only_before ($i, @list) {
  19         30  
  19         44  
  19         28  
171 19         39 while ($i > 0) {
172 19         25 $i--;
173             # found newline, exit search
174 19 50       46 if ($list[$i]->[1] =~ /\n\z/) {
175 0         0 return 1;
176             }
177             # found non whitespace
178 19 50       57 if ($list[$i]->[1] =~ /\S/) {
179 19         63 return;
180             }
181             }
182 0         0 return 1;
183             }
184              
185             # there is only whitespace after $list[$i] and a processing instruction
186 27     27   38 sub _ws_only_after_beforepi ($i, @list) {
  27         53  
  27         56  
  27         43  
187 27         65 while ($i < $#list) {
188 24         35 $i++;
189             # found processing instruction, exit search
190 24 100       79 if ($list[$i]->[1] =~ /\{%-?/) {
191 21         79 return 1;
192             }
193             # found non whitespace
194 3 50       23 if ($list[$i]->[1] =~ /\S/) {
195 3         14 return;
196             }
197             }
198 3         11 return;
199             }
200              
201             # liquid indenter main routine
202             # $obj - the root element object, subclass of Template::Liquid::Document
203             # $args - indenter configuration
204             # @list - a list of [ level, string fragment ] pairs as produced by _tidy_html / _tidy_list
205             # returns the indented document string
206 3     3   6 sub _tidy_make_string ($obj, $args, @list) {
  3         6  
  3         5  
  3         7  
  3         4  
207 3         8 my $level_correct = -1;
208             #my $level_correct = 0;
209             #use Data::Dumper;
210             #print Dumper \@list;
211              
212 3         6 my $result = '';
213              
214             # config
215 3   33     15 my $indent = $args->{indent} // $defaults{indent};
216 3   33     15 my $short_if = $args->{short_if} // $defaults{short_if} // 0;
      50        
217 3   33     14 my $indent_html = $args->{html} // $defaults{html};
218              
219             # state
220 3         5 my $next_nl = 0; # next PI needs to have a newline because we just deleted one
221              
222 3         12 for my $i (0..$#list) {
223             # poke = previous line
224 51 100       130 my $poke_content = $i > 0 ? $list[$i - 1]->[1] : '';
225              
226 51         79 my $it = $list[$i];
227 51         82 my $content = $it->[1];
228              
229             # peek = next line
230 51 100       112 my $peek_content = $i < $#list ? $list[$i + 1]->[1] : '';
231              
232 51         90 my $level = $it->[0] + $level_correct;
233 51 100       94 my $peek_level = $i < $#list ? $list[$i + 1]->[0] + $level_correct : $level;
234              
235 51 100       233 if ($content =~ /$PI_START/) {
236             # $result .= "\e[34mPI;$level\e[0m";
237 24 100 100     135 if (($content !~ /\n/ && !_force_nl_tags($args, $content))
      66        
238             || $content =~ /$PI_START\s*post_url\s/) {
239             # $result .= "\e[35mA\e[0m";
240             # we stripped a newline, put it back
241 4 50       8 if ($next_nl) {
242 0         0 $result .= "\n";
243 0         0 $next_nl = 0;
244             }
245              
246             # there was only whitespace before this tag (which we deleted)
247             # -> add indentation
248 4 50       9 if (_ws_only_before($i, @list)) {
249 0         0 my $m1 = $content;
250 0 0       0 $m1 =~ s/^\s*+/' ' x ($indent * $level)/e
  0         0  
251             if $indent_html;
252             #$m1 =~ s/\n\K/' ' x ($indent * ($level + 1))/ge;
253              
254 0         0 $result .= $m1;
255 0         0 next;
256             }
257              
258 4         9 $result .= $content;
259 4         9 next;
260             }
261              
262 20 100 33     110 if ($short_if > 0 && !$next_nl && $content !~ /\n/) {
      66        
263 8 0 66     89 if ($content =~ /$PI_START\s*(if|unless)\s/ && (length $peek_content) <= $short_if
      33        
      33        
264             && ($i + 2 <= $#list) && $list[$i + 2]->[1] =~ /$PI_START/) {
265 0         0 $result .= $content;
266 0         0 next;
267             }
268 8 0 33     68 if ($content =~ /$PI_START\s*end(if|unless)\s/ && (length $poke_content) <= $short_if
      33        
      0        
269             && ($i - 2 >= 0) && $list[$i - 2]->[1] =~ /$PI_START\s*(\Q$1\E\s|elsif\s|else)/) {
270 0         0 $result .= $content;
271 0         0 next;
272             }
273             }
274              
275 20         40 my $m1 = $content;
276             # normalise
277 20         209 $m1 =~ s/\s*($PI_END)/ $1/;
278 20         131 $m1 =~ s/($PI_START)\s*/$1 /;
279             # $result .= "[\e[33m$m1\e[0m]";
280             # $result .= "{\e[32m$poke_content\e[0m}";
281 20         39 my $line = 0;
282              
283             # previous line not empty
284             # -> we assume the {% goes to previous line
285 20 50 33     135 if ( ($poke_content =~ /^(.*)\z/m && length $1)
    0 0        
      33        
      0        
286             || ($indent_html && $poke_content =~ />\s*\n\z/) ) {
287 20         123 $m1 =~ s/$PI_START\K\s*/\n/;
288             }
289             # there was only whitespace before this tag (which we deleted)
290             # -> add indentation
291             elsif (_ws_only_before($i, @list) || $next_nl) {
292             # $result .= "[\e[33mwsonly before $i; next_nl:$next_nl\e[0m]";
293             # if ($indent_html && !$next_nl) {
294             # # we can indent the whole tag
295             # $m1 =~ s/^\s*+/' ' x ($indent * $level)/e;
296             # $line++;
297             # } else {
298 0 0       0 my $local_indent = $poke_content =~ /^(.*)\z/m ? length $1 : 0;
299 0         0 my $required_indent = $indent * $level;
300 0         0 $required_indent -= $local_indent;
301 0         0 $required_indent -= length '{% ';
302 0 0 0     0 if ($required_indent >= 0 && !$next_nl) {
303 0         0 $m1 =~ s/$PI_START\K\s*/' ' x ($required_indent + 1)/e;
  0         0  
304             } else {
305             # force newline after {%
306 0         0 $m1 =~ s/$PI_START\K\s*/\n/;
307             }
308             # }
309             }
310 20         39 $next_nl = 0;
311 20 50       64 $m1 =~ s/\n\K(?!["'])[ \t]*/' ' x ($indent * ($level + ($line++ ? 1 : 0)))/ge;
  20         83  
312             # $result .= "[\e[33m$m1\e[0m]";
313              
314             # next line is no processing instruction
315             # -> the %} should go to the next line, indented
316 20 50 66     114 if ($peek_content !~ /$PI_START/ && $peek_content !~ /^\n/) {
317 17         109 $m1 =~ s/ ($PI_END)/"\n" . (' ' x ($indent * $peek_level)) . $1/e;
  17         75  
318             }
319              
320             # clean "blank" lines
321 20         59 $m1 =~ s/^[ \t]+(?=\n)//gm;
322              
323             # special case for the last command in file:
324             # the %} goes on its own line
325 20 50 33     89 if ($i == $#list || ($peek_content eq "\n"
      33        
326 0         0 && !grep { $_->[1] ne "\n" } @list[ ($i + 1) .. $#list ]) ) {
327 0         0 $m1 =~ s/ ($PI_END)/\n$1/;
328             }
329              
330             # $result .= "\e[35mB\e[0m";
331 20         40 $result .= $m1;
332 20         54 next;
333             }
334              
335             # indent content that starts with {{ (liquid variables)
336 27 50 66     185 if ($content =~ /^\s*$LV_START/ && (_ws_only_before($i, @list)) ) {
337 0         0 my $m1 = $content;
338 0 0       0 if ($indent_html) {
339 0         0 $m1 =~ s/^\s*+/' ' x ($indent * $level)/e;
  0         0  
340             }
341 0         0 $m1 =~ s/\n\K/' ' x ($indent * ($level + 1))/ge;
  0         0  
342              
343 0         0 $result .= $m1;
344 0         0 next;
345             }
346              
347             # indent content that start with < (html tag)
348 27 50 66     128 if ($indent_html && $content =~ /^\s*<[^!]/ && ($poke_content =~ /\n\z/) ) {
      66        
349 0         0 my $m1 = $content;
350 0         0 $m1 =~ s/^\s*+/' ' x ($indent * $level)/e;
  0         0  
351              
352             # next line is a processing instruction
353             # -> delete the last newline if the line ends with > (html tag end)
354 0 0       0 if (_ws_only_after_beforepi($i, @list)) {
355 0         0 $next_nl = $m1 =~ s/>\K\s*\n\z//;
356             }
357              
358 0         0 $result .= $m1;
359 0         0 next;
360             }
361              
362             # next line is a processing instruction
363             # -> delete the last newline if the line ends with > (html tag end)
364 27 100 66     109 if ($indent_html && _ws_only_after_beforepi($i, @list)) {
365 21         39 my $m1 = $content;
366 21 50       52 if ($m1 =~ s/(?\K\s*\n\z//) {
367 0         0 $result .= $m1;
368 0         0 $next_nl = 1;
369 0         0 next;
370             }
371              
372             # ignore plain newline if it follows a processing instruction
373 21 0 33     50 if ($m1 eq "\n" && $poke_content =~ /$PI_START/ && $poke_content !~ /$PI_START\s*capture/) {
      33        
374 0         0 $next_nl = 1;
375 0         0 next;
376             }
377             }
378              
379             # ignore white space in front of liquid variables and liquid processing instructions
380 27 0 33     106 if ($indent_html
      0        
      33        
      0        
381             && $content =~ /^[ \t]+$/
382             && ($peek_content =~ /$LV_START/ || $peek_content =~ /$PI_START/)
383             && $poke_content =~ /\n\z/) {
384              
385             # skip this indentation, will be corrected when the next token is processed
386 0         0 next;
387             }
388              
389             # indent html text
390 27 50 66     177 if ($indent_html
      33        
391             && !($it->[2] && $it->[2]{open} && $it->[2]{open}->@* && $it->[2]{open}[-1] eq 'script')) {
392 27         46 my $m1 = $content;
393 27 50       112 if ($poke_content =~ /\n\z/) {
394 0         0 $m1 =~ s/^[ \t]++/' ' x ($indent * $level)/e;
  0         0  
395             }
396             # clean "blank" lines
397 27         63 $m1 =~ s/^[ \t]+(?=\n)//gm;
398              
399 27         53 $result .= $m1;
400 27         64 next;
401             }
402              
403             # fallback: don't touch the content, add as is
404 0         0 $result .= $content;
405 0         0 next;
406             }
407             $result
408 3         36 }
409              
410             1;
411              
412             =head1 NAME
413              
414             Template::LiquidX::Tidy::impl - The implementation of Template::LiquidX::Tidy
415              
416             =head1 SYNOPSIS
417              
418             For internal usage.
419              
420             =head1 METHODS
421              
422             =head2 _tidy_list($obj, $args, $level, $clevel, $stack = {})
423              
424             The Liquid indenter part
425              
426             =over 4
427              
428             =item B<$obj>
429              
430             an object, subclass of Template::Liquid::Document
431              
432             =item B<$args>
433              
434             The indenter configuration
435              
436             =back
437              
438             returns a list of [ level, fragment ] pairs and modifies C<$stack>
439              
440             =head2 _tidy_make_string($obj, $args, @list)
441              
442             Liquid indenter main routine. This does the grunt work of joining
443             together all the tuples returned by the calls to _tidy_list
444              
445             =over 4
446              
447             =item B<$obj>
448              
449             an object, subclass of Template::Liquid::Document
450              
451             =item B<$args>
452              
453             The indenter configuration
454              
455             =back
456              
457             returns the indented document string
458              
459             =cut
460