File Coverage

lib/Jinja2/TT2/Filters.pm
Criterion Covered Total %
statement 19 28 67.8
branch 5 8 62.5
condition 3 8 37.5
subroutine 5 8 62.5
pod 4 5 80.0
total 36 57 63.1


line stmt bran cond sub pod time code
1             package Jinja2::TT2::Filters;
2              
3 1     1   9 use strict;
  1         1  
  1         45  
4 1     1   6 use warnings;
  1         2  
  1         51  
5 1     1   13 use v5.20;
  1         4  
6              
7             # Jinja2 filter to TT2 mapping
8             # Types:
9             # vmethod - TT2 virtual method (e.g., list.join)
10             # filter - TT2 filter (e.g., | html)
11             # custom - Custom code transformation
12             # none - No direct equivalent (passthrough or comment)
13              
14             my %FILTER_MAP = (
15             # String filters
16             upper => {
17             type => 'vmethod',
18             name => 'upper',
19             },
20             lower => {
21             type => 'vmethod',
22             name => 'lower',
23             },
24             capitalize => {
25             type => 'vmethod',
26             name => 'ucfirst', # Close approximation
27             },
28             title => {
29             type => 'filter',
30             name => 'title', # TT2 doesn't have this; needs plugin
31             },
32             trim => {
33             type => 'vmethod',
34             name => 'trim',
35             },
36             striptags => {
37             type => 'filter',
38             name => 'html_strip', # Needs Template::Plugin::HTML
39             },
40             escape => {
41             type => 'filter',
42             name => 'html_entity',
43             },
44             e => {
45             type => 'filter',
46             name => 'html_entity',
47             },
48             safe => {
49             type => 'none', # TT2 doesn't auto-escape
50             name => '',
51             },
52             forceescape => {
53             type => 'filter',
54             name => 'html_entity',
55             },
56              
57             # Numeric filters
58             abs => {
59             type => 'custom',
60             code => sub {
61             my ($base) = @_;
62             return "($base >= 0 ? $base : -$base)";
63             },
64             },
65             int => {
66             type => 'vmethod',
67             name => 'int',
68             },
69             float => {
70             type => 'none', # TT2 numbers are already floats
71             name => '',
72             },
73             round => {
74             type => 'custom',
75             code => sub {
76             my ($base, @args) = @_;
77             my $precision = $args[0] // 0;
78             return "format($base, '%." . $precision . "f')";
79             },
80             },
81             filesizeformat => {
82             type => 'none', # Needs custom implementation
83             name => 'filesizeformat',
84             },
85              
86             # List filters
87             first => {
88             type => 'vmethod',
89             name => 'first',
90             },
91             last => {
92             type => 'vmethod',
93             name => 'last',
94             },
95             length => {
96             type => 'vmethod',
97             name => 'size',
98             },
99             count => {
100             type => 'vmethod',
101             name => 'size',
102             },
103             reverse => {
104             type => 'vmethod',
105             name => 'reverse',
106             },
107             sort => {
108             type => 'vmethod',
109             name => 'sort',
110             },
111             join => {
112             type => 'vmethod',
113             name => 'join',
114             },
115             sum => {
116             type => 'custom',
117             code => sub {
118             my ($base) = @_;
119             # TT2 needs a loop for sum
120             return "$base.join('+')"; # Simplified; needs proper handling
121             },
122             },
123             min => {
124             type => 'custom',
125             code => sub {
126             my ($base) = @_;
127             return "$base.sort.first";
128             },
129             },
130             max => {
131             type => 'custom',
132             code => sub {
133             my ($base) = @_;
134             return "$base.sort.last";
135             },
136             },
137             random => {
138             type => 'custom',
139             code => sub {
140             my ($base) = @_;
141             return "$base.pick"; # TT2 List plugin
142             },
143             },
144             unique => {
145             type => 'vmethod',
146             name => 'unique',
147             },
148             list => {
149             type => 'none', # Already a list in TT2
150             name => '',
151             },
152             batch => {
153             type => 'vmethod',
154             name => 'batch',
155             },
156             slice => {
157             type => 'vmethod',
158             name => 'slice',
159             },
160              
161             # Dict filters
162             dictsort => {
163             type => 'vmethod',
164             name => 'sort',
165             },
166             items => {
167             type => 'vmethod',
168             name => 'pairs',
169             },
170              
171             # String manipulation
172             replace => {
173             type => 'vmethod',
174             name => 'replace',
175             },
176             truncate => {
177             type => 'filter',
178             name => 'truncate',
179             },
180             wordwrap => {
181             type => 'filter',
182             name => 'wrap',
183             },
184             wordcount => {
185             type => 'custom',
186             code => sub {
187             my ($base) = @_;
188             return "$base.split.size";
189             },
190             },
191             center => {
192             type => 'filter',
193             name => 'center',
194             },
195             indent => {
196             type => 'filter',
197             name => 'indent',
198             },
199             format => {
200             type => 'filter',
201             name => 'format',
202             },
203              
204             # URL filters
205             urlencode => {
206             type => 'filter',
207             name => 'uri',
208             },
209             urlize => {
210             type => 'none', # Needs custom plugin
211             name => 'urlize',
212             },
213              
214             # JSON
215             tojson => {
216             type => 'filter',
217             name => 'json', # Needs Template::Plugin::JSON
218             },
219              
220             # Misc
221             default => {
222             type => 'custom',
223             code => sub {
224             my ($base, @args) = @_;
225             my $default = $args[0] // "''";
226             return "($base || $default)";
227             },
228             },
229             d => { # Alias for default
230             type => 'custom',
231             code => sub {
232             my ($base, @args) = @_;
233             my $default = $args[0] // "''";
234             return "($base || $default)";
235             },
236             },
237             string => {
238             type => 'none', # TT2 auto-stringifies
239             name => '',
240             },
241             pprint => {
242             type => 'filter',
243             name => 'dumper', # Template::Plugin::Dumper
244             },
245              
246             # Selection filters
247             select => {
248             type => 'vmethod',
249             name => 'grep', # Approximate
250             },
251             reject => {
252             type => 'custom',
253             code => sub {
254             my ($base, @args) = @_;
255             return "$base.reject(@args)"; # Needs custom vmethod
256             },
257             },
258             selectattr => {
259             type => 'none',
260             name => 'selectattr',
261             },
262             rejectattr => {
263             type => 'none',
264             name => 'rejectattr',
265             },
266             groupby => {
267             type => 'none',
268             name => 'groupby',
269             },
270             map => {
271             type => 'none', # Complex in TT2
272             name => 'map',
273             },
274             attr => {
275             type => 'custom',
276             code => sub {
277             my ($base, @args) = @_;
278             my $attr = $args[0] // '';
279             $attr =~ s/^['"]|['"]$//g;
280             return "$base.$attr";
281             },
282             },
283              
284             # XML
285             xmlattr => {
286             type => 'none',
287             name => 'xmlattr',
288             },
289             );
290              
291             sub new {
292 1     1 0 4 my ($class, %opts) = @_;
293             return bless {
294             custom_filters => $opts{custom_filters} // {},
295 1   50     21 }, $class;
296             }
297              
298             sub map_filter {
299 10     10 1 22 my ($self, $name, $args) = @_;
300              
301             # Check custom filters first
302 10 50       38 if (exists $self->{custom_filters}{$name}) {
303 0         0 return $self->{custom_filters}{$name};
304             }
305              
306             # Check built-in mapping
307 10 50       33 if (exists $FILTER_MAP{$name}) {
308 10         22 my $mapping = $FILTER_MAP{$name};
309              
310 10 50       26 if ($mapping->{type} eq 'custom') {
311             return {
312             type => 'custom',
313             code => $mapping->{code},
314 0         0 };
315             }
316              
317             my $result = {
318             type => $mapping->{type},
319             name => $mapping->{name},
320 10         40 };
321              
322             # Pass through arguments for vmethods/filters that need them
323 10 100 66     54 if ($args && @$args) {
324 1         4 $result->{args} = join(', ', @$args);
325             }
326              
327 10         28 return $result;
328             }
329              
330             # Unknown filter - pass through as-is
331             return {
332 0           type => 'filter',
333             name => $name,
334             };
335             }
336              
337             sub register_filter {
338 0     0 1   my ($self, $name, $mapping) = @_;
339 0           $self->{custom_filters}{$name} = $mapping;
340             }
341              
342             # Get list of all known filters
343             sub list_filters {
344 0     0 1   my ($self) = @_;
345 0           return sort keys %FILTER_MAP;
346             }
347              
348             # Check if a filter has a TT2 equivalent
349             sub has_equivalent {
350 0     0 1   my ($self, $name) = @_;
351 0   0       return exists $FILTER_MAP{$name} && $FILTER_MAP{$name}{type} ne 'none';
352             }
353              
354             1;
355              
356             __END__