File Coverage

blib/lib/Data/ZPath/_Evaluate.pm
Criterion Covered Total %
statement 481 555 86.6
branch 275 382 71.9
condition 129 220 58.6
subroutine 19 21 90.4
pod n/a
total 904 1178 76.7


line stmt bran cond sub pod time code
1 8     8   55 use strict;
  8         17  
  8         330  
2 8     8   42 use warnings;
  8         16  
  8         529  
3              
4             package Data::ZPath::_Evaluate;
5              
6 8     8   65 use Carp qw(croak);
  8         15  
  8         508  
7 8     8   3676 use POSIX qw(ceil floor);
  8         55759  
  8         84  
8 8     8   21126 use Regexp::Util qw(deserialize_regexp);
  8         70972  
  8         107  
9 8     8   3594 use Scalar::Util qw(blessed refaddr);
  8         16  
  8         668  
10              
11 8     8   54 use Data::ZPath::Node;
  8         18  
  8         73095  
12              
13             our $VERSION = '0.001000';
14              
15             sub _pattern_to_regexp {
16 14     14   35 my ( $pat ) = @_;
17              
18 14         37 for my $candidate ( qw{ / | : " ' }, '#' ) {
19 17 100       44 if ( index($pat, $candidate) < 0 ) {
20 14         81 return deserialize_regexp sprintf( 'qr%s%s%s', $candidate, $pat, $candidate );
21             }
22             }
23              
24 0         0 $pat =~ s{\/}{\\\/}g;
25 0         0 return deserialize_regexp sprintf( 'qr/%s/', $pat );
26             }
27              
28             sub _eval_expr {
29 2800     2800   4569 my ( $ast, $ctx ) = @_;
30              
31 2800         4614 my $t = $ast->{t};
32              
33 2800 100       6099 if ( $t eq 'num' ) {
34 254         790 return Data::ZPath::Node->_wrap($ast->{v});
35             }
36 2546 100       4446 if ( $t eq 'str' ) {
37 550         1565 return Data::ZPath::Node->_wrap($ast->{v});
38             }
39 1996 100       3617 if ( $t eq 'path' ) {
40 906         4454 return _eval_path($ast, $ctx);
41             }
42 1090 100       2070 if ( $t eq 'fn' ) {
43 374         969 return _eval_fn($ast, $ctx);
44             }
45 716 100       1418 if ( $t eq 'un' ) {
46 13         55 my @v = _eval_expr($ast->{e}, $ctx);
47 13         47 my $x = _truthy($v[0]);
48              
49 13 100       69 if ( $ast->{op} eq '!' ) {
50 12 100       63 return (Data::ZPath::Node->_wrap($x ? !!0 : !!1));
51             }
52 1 50       7 if ( $ast->{op} eq '~' ) {
53 1         4 my $n = _to_number($v[0]);
54 1 50       5 return unless defined $n;
55 1         6 return Data::ZPath::Node->_wrap((~(int($n))));
56             }
57 0         0 croak "Unknown unary op $ast->{op}";
58             }
59 703 100       1379 if ( $t eq 'bin' ) {
60 696         1623 my @l = _eval_expr($ast->{l}, $ctx);
61 696         2606 my @r = _eval_expr($ast->{r}, $ctx);
62              
63 696         1341 my $lv = $l[0];
64 696         980 my $rv = $r[0];
65 696         1347 my $op = $ast->{op};
66              
67             # Logical ops treat as booleans
68 696 100 100     2576 if ( $op eq '&&' || $op eq '||' ) {
69 22         71 my $lb = _truthy($lv);
70 22         46 my $rb = _truthy($rv);
71 22 100 100     181 return (Data::ZPath::Node->_wrap(
    100 100        
    100          
72             ($op eq '&&') ? ($lb && $rb ? !!1 : !!0) : ($lb || $rb ? !!1 : !!0),
73             undef, undef
74             ));
75             }
76              
77             # Equality (loose-ish, but stable)
78 674 100 100     1667 if ( $op eq '==' || $op eq '!=' ) {
79 628         969 my $eq = 0;
80              
81 628 100 66     1541 if ( @l && @r ) {
82             OUTER:
83 227         418 for my $ln (@l) {
84 228         332 for my $rn (@r) {
85 228 100       569 if ( _equals($ln, $rn) ) {
86 78         150 $eq = 1;
87 78         217 last OUTER;
88             }
89             }
90             }
91             }
92              
93 628 100       1382 $eq = !$eq if $op eq '!=';
94 628 100       1955 return (Data::ZPath::Node->_wrap($eq ? !!1 : !!0));
95             }
96              
97             # Relations (numeric if both numeric, else string)
98 46 100       243 if ( $op =~ /^( >= | <= | > | < )$/x ) {
99 3         7 my $ln = _to_number($lv);
100 3         440 my $rn = _to_number($rv);
101 3         142 my $ok;
102 3 50 33     11 if ( defined $ln && defined $rn ) {
103 3 50       15 $ok = ($op eq '>=' ? $ln >= $rn
    50          
    50          
104             : $op eq '<=' ? $ln <= $rn
105             : $op eq '>' ? $ln > $rn
106             : $ln < $rn);
107             } else {
108 0   0     0 my $ls = _to_string($lv) // '';
109 0   0     0 my $rs = _to_string($rv) // '';
110 0 0       0 $ok = ($op eq '>=' ? $ls ge $rs
    0          
    0          
111             : $op eq '<=' ? $ls le $rs
112             : $op eq '>' ? $ls gt $rs
113             : $ls lt $rs);
114             }
115 3 50       615 return (Data::ZPath::Node->_wrap($ok ? !!1 : !!0));
116             }
117              
118             # Bitwise ops (ints)
119 43 100 100     255 if ( $op eq '&' || $op eq '|' || $op eq '^' ) {
      100        
120 4         12 my $ln = _to_number($lv);
121 4         11 my $rn = _to_number($rv);
122 4 50 33     25000 return () unless defined $ln && defined $rn;
123 4         9 my $li = int($ln);
124 4         25 my $ri = int($rn);
125 4 100       80 my $res = ($op eq '&') ? ($li & $ri) : ($op eq '|') ? ($li | $ri) : ($li ^ $ri);
    100          
126 4         275 return (Data::ZPath::Node->_wrap($res));
127             }
128              
129             # Arithmetic (scalar only)
130 39 50 100     284 if ( $op eq '+' || $op eq '-' || $op eq '*' || $op eq '/' || $op eq '%' ) {
      100        
      100        
      66        
131 39         108 my $ln = _to_number($lv);
132 39         110 my $rn = _to_number($rv);
133              
134 39 100 66     244 if ( $op eq '%' and $ln=~/\./ || $rn=~/\./ ) {
      100        
135 3         21 return Data::ZPath::Node->_wrap(_floaty_modulus($ln, $rn));
136             }
137              
138 36 50 33     157 return () unless defined $ln && defined $rn;
139 36 50       188 my $res =
    50          
    100          
    100          
    100          
    100          
140             $op eq '+' ? ($ln + $rn) :
141             $op eq '-' ? ($ln - $rn) :
142             $op eq '*' ? ($ln * $rn) :
143             $op eq '/' ? ($rn == 0 ? undef : ($ln / $rn)) :
144             ($rn == 0 ? undef : ($ln % $rn));
145 36 50       80 return unless defined $res;
146 36         119 return Data::ZPath::Node->_wrap($res);
147             }
148              
149 0         0 croak "Unknown binary op $op";
150             }
151              
152 7 50       12 if ( $t eq 'ternary' ) {
153 7         15 my @c = _eval_expr($ast->{c}, $ctx);
154 7         17 my $cond = _truthy($c[0]);
155 7 100       19 return $cond ? _eval_expr($ast->{a}, $ctx) : _eval_expr($ast->{b}, $ctx);
156             }
157              
158 0         0 croak "Unknown AST node type: $t";
159             }
160              
161             # Reference implementation of ZPath is in Java, which has a sane
162             # floating point modulus opertator. Try to implement equivalent in Perl.
163             sub _floaty_modulus {
164 3     3   7 my ( $ln, $rn ) = @_;
165 3         10 my $count = POSIX::floor($ln / $rn);
166 3         8 $ln - ( $count * $rn );
167             }
168              
169             sub _eval_path {
170 906     906   2528 my ( $path_ast, $ctx ) = @_;
171              
172 906         1315 my @current = @{$ctx->nodeset};
  906         2277  
173 906         2036 my $parentset = $ctx->parentset;
174              
175 906         1334 for my $seg (@{$path_ast->{s}}) {
  906         2063  
176 1308         4507 my @next;
177              
178 1308 100       7978 if ( $seg->{k} eq 'root' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
179 133         427 @next = ($ctx->root);
180             }
181             elsif ( $seg->{k} eq 'dot' ) {
182 24         58 @next = @current;
183             }
184             elsif ( $seg->{k} eq 'parent' ) {
185 13         33 @next = grep { defined $_ } map { $_->parent } @current;
  13         34  
  13         42  
186 13         33 @next = _dedup_nodes(@next);
187             }
188             elsif ( $seg->{k} eq 'ancestors' ) {
189 2         4 my @anc;
190 2         5 for my $n (@current) {
191 2         7 my $p = $n->parent;
192 2         9 while ( $p ) {
193 3         4 push @anc, $p;
194 3         5 $p = $p->parent;
195             }
196             }
197 2         8 @next = _dedup_nodes(@anc);
198             }
199             elsif ( $seg->{k} eq 'star' ) {
200 93         139 my @kids;
201 93         165 for my $n (@current) {
202 93         286 push @kids, grep { $_->type ne 'attr' } $n->children;
  195         561  
203             }
204 93         267 @next = _dedup_nodes(@kids);
205             }
206             elsif ( $seg->{k} eq 'desc' ) {
207 85         2276 my @acc;
208 85         183 my @stack = @current;
209 85         250 while ( @stack ) {
210 3259         4507 my $n = shift @stack;
211 3259         5045 push @acc, $n;
212 3259         6296 my @kids = grep { $_->type ne 'attr' } $n->children;
  3169         6499  
213 3259         6775 push @stack, @kids;
214             }
215 85         368 @next = _dedup_nodes(@acc);
216             }
217             elsif ( $seg->{k} eq 'index' ) {
218 28         59 my $idx = $seg->{i};
219 28         47 my @kids;
220 28         60 for my $n (@current) {
221 28         80 my @ch = grep { $_->type ne 'attr' } $n->children;
  83         205  
222 28 50       176 push @kids, $ch[$idx] if defined $ch[$idx];
223             }
224 28         124 @next = _dedup_nodes(@kids);
225             }
226             elsif ( $seg->{k} eq 'fnseg' ) {
227 7         16 my @out;
228 7         19 for my $n (@current) {
229 16         76 my $seg_ctx = $ctx->with_nodeset( [$n], \@current );
230 16         103 my @res = _eval_fn({ t => 'fn', n => $seg->{n}, a => $seg->{a} }, $seg_ctx);
231 16         135 push @out, @res;
232             }
233 7         23 @next = @out;
234             }
235             elsif ( $seg->{k} eq 'name' ) {
236 923         1760 my $name = $seg->{n};
237              
238             # XML attribute shorthand: @name or @*
239 923 100       2474 if ( $name =~ /^\@/ ) {
240 158 100       233 if ( $name eq '@*' ) {
241 3         4 my @attrs;
242 3         6 for my $n (@current) { push @attrs, $n->attributes; }
  89         117  
243 3         9 @next = _dedup_nodes(@attrs);
244             } else {
245 155         263 my $attr_name = substr($name, 1);
246 155         194 my @attrs;
247 155         203 for my $n (@current) {
248 160         330 my $raw = $n->raw;
249 160 100 66     673 next unless blessed($raw) && $raw->isa('XML::LibXML::Element');
250 106         424 my $a = $raw->getAttributeNode($attr_name);
251 106 100       369 push @attrs, Data::ZPath::Node->_wrap($a, $n, '@'.$attr_name) if $a;
252             }
253 155         246 @next = _dedup_nodes(@attrs);
254             }
255             } else {
256 765         1345 my @kids;
257 765         1339 for my $n (@current) {
258 3267         12626 my @ch = grep { $_->type ne 'attr' } $n->children;
  4223         10431  
259 3267 50       8741 push @kids, grep { (defined($_->name) && $_->name eq $name) } @ch;
  4223         9752  
260             }
261 765         3345 @next = _dedup_nodes(@kids);
262             }
263              
264 923 100       2587 if ( defined $seg->{i} ) {
265 1         2 my $idx = $seg->{i};
266             # interpret as: among matching name children for each parent, pick #idx
267 1         1 my @picked;
268 1         2 for my $n (@current) {
269 44         258 my @ch = grep { $_->type ne 'attr' } $n->children;
  43         84  
270 44 50       57 my @m = grep { (defined($_->name) && $_->name eq $name) } @ch;
  43         70  
271 44 100       136 push @picked, $m[$idx] if defined $m[$idx];
272             }
273 1         3 @next = _dedup_nodes(@picked);
274             }
275             }
276             else {
277 0         0 croak "Unknown path segment kind: $seg->{k}";
278             }
279              
280             # qualifiers
281 1308 100 66     3467 if ( $seg->{q} && @{$seg->{q}} ) {
  1308         3572  
282             QUALIFIER:
283 95         176 for my $q (@{$seg->{q}}) {
  95         251  
284 96 100 66     786 if (
      66        
285             $q->{t}
286             and $q->{t} eq 'num'
287             and $q->{v} =~ /\A[0-9]+\z/
288             ) {
289 21         41 my $idx = 0 + $q->{v};
290              
291 21 100 66     100 if (
      66        
292             @next
293             and blessed($next[0]->raw)
294             and $next[0]->raw->isa('XML::LibXML::Node')
295             ) {
296 20 50       122 @next = defined $next[$idx] ? ( $next[$idx] ) : ();
297             }
298             else {
299 1         1 my @picked;
300 1         19 for my $node (@next) {
301 1         3 my @ch = grep { $_->type ne 'attr' } $node->children;
  3         6  
302 1 50       17 push @picked, $ch[$idx] if defined $ch[$idx];
303             }
304 1         2 @next = @picked;
305             }
306              
307 21         341 next QUALIFIER;
308             }
309              
310 75         128 my @filtered;
311 75         235 for ( my $i = 0; $i < @next; $i++ ) {
312 668         1127 my $node = $next[$i];
313 668         2219 my $ns_ctx = $ctx->with_nodeset(\@next, \@current);
314 668         1983 my @r = _eval_expr($q, $ns_ctx->with_nodeset([$node], \@next));
315              
316 668         1976 my $ok;
317 668 100 66     2639 if ( $q->{t} and $q->{t} eq 'path' ) {
318 51 100       126 $ok = scalar(@r) ? 1 : 0;
319             }
320             else {
321 617         1425 $ok = _truthy($r[0]);
322             }
323              
324 668 100       3475 push @filtered, $node if $ok;
325             }
326 75         970 @next = @filtered;
327             }
328             }
329              
330 1308         3602 $parentset = \@current;
331 1308         6679 @current = @next;
332             }
333              
334 906         5656 return @current;
335             }
336              
337             sub _eval_fn {
338 396     396   623 my ( $fn_ast, $ctx ) = @_;
339 396         742 my $name = $fn_ast->{n};
340 396         517 my @args = @{$fn_ast->{a}};
  396         781  
341              
342 396         844 my $ns = $ctx->nodeset;
343              
344             # helpers
345             my $eval_arg = sub {
346 280     280   517 my ( $i, $local_ctx ) = @_;
347 280   33     1083 return _eval_expr($args[$i], $local_ctx // $ctx);
348 396         1489 };
349              
350 396 100       842 return Data::ZPath::Node->_wrap(!!0) if $name eq 'false';
351 395 100       821 return Data::ZPath::Node->_wrap(!!1) if $name eq 'true';
352 383 100       616 return Data::ZPath::Node->_wrap(undef) if $name eq 'null';
353              
354 382 100       652 if ( $name eq 'count' ) {
355 68 100       132 if ( @args ) {
356 62         129 my @r = $eval_arg->(0);
357 62         160 return Data::ZPath::Node->_wrap(scalar(@r));
358             }
359 6   33     18 my $scope = $ctx->parentset // $ns;
360 6         39 return Data::ZPath::Node->_wrap(scalar(@$scope));
361             }
362              
363 314 100       561 if ( $name eq 'index' ) {
364 25 100       67 if ( @args ) {
365             # index(expression): for each node matched, its index into its parent
366 4         12 my @r = $eval_arg->(0);
367 4         9 my @out;
368 4         11 for my $n (@r) {
369 3 50       14 if ( defined( my $i = $n->ix ) ) {
    0          
370 3         14 push @out, Data::ZPath::Node->_wrap(0+$i);
371             }
372             elsif ( defined( my $k = $n->key ) ) {
373 0 0       0 push @out, Data::ZPath::Node->_wrap(0+$k) if $k =~ /^[0-9]+$/;
374             }
375             }
376 4         50 return @out;
377             }
378              
379             # index() within qualifier scope: index of THIS node in parentset; otherwise nodeset
380 21         36 my $cur = $ns->[0];
381 21 50       92 return unless $cur;
382              
383 21   66     51 my $scope = $ctx->parentset // $ns;
384 21         54 my $ix = $cur->ix;
385 21 100       70 return Data::ZPath::Node->_wrap($ix) if defined $ix;
386 1         4 my $id = $cur->id;
387 1 50       4 return unless defined $id;
388 1         6 for ( my $i = 0; $i < @$scope; $i++ ) {
389 1         4 my $nid = $scope->[$i]->id;
390 1 50 33     7 if ( defined $nid && $nid eq $id ) {
391 1         6 return Data::ZPath::Node->_wrap($i);
392             }
393             }
394 0         0 return Data::ZPath::Node->_wrap(0);
395             }
396              
397 289 100       524 if ( $name eq 'key' ) {
398 93 100       182 if ( @args ) {
399 90         145 my @r = $eval_arg->(0);
400             return map {
401 90         1342 my $k = $_->key;
  36         80  
402 36 50       94 defined $k ? Data::ZPath::Node->_wrap($k) : ()
403             } @r;
404             }
405 3         8 my $cur = $ns->[0];
406 3 50 33     48 return unless $cur && defined $cur->key;
407 3         18 return Data::ZPath::Node->_wrap($cur->key);
408             }
409              
410 196 100       377 if ( $name eq 'union' ) {
411 7         13 my @all;
412 7         25 for my $i (0 .. $#args) {
413 14         31 push @all, $eval_arg->($i);
414             }
415 7         24 return _dedup_nodes(@all);
416             }
417              
418 189 100       337 if ( $name eq 'intersection' ) {
419 2 50       6 return () unless @args;
420 2         5 my @base = $eval_arg->(0);
421 2   33     6 my %have = map { $_->id // ("p:".refaddr(\$_)) => $_ } @base;
  2         8  
422              
423 2         11 for my $i (1 .. $#args) {
424 3         8 my @r = $eval_arg->($i);
425 3   33     6 my %next = map { $_->id // ("p:".refaddr(\$_)) => 1 } @r;
  8         18  
426 3         13 for my $k (keys %have) {
427 3 100       33 delete $have{$k} unless $next{$k};
428             }
429             }
430 2         21 return values %have;
431             }
432              
433 187 100       381 if ( $name eq 'is-first' ) {
434 3         6 my $cur = $ns->[0];
435 3 50 33     17 return unless $cur && $cur->parent;
436 3         11 return Data::ZPath::Node->_wrap($cur->ix == 0);
437             }
438              
439 184 100       360 if ( $name eq 'is-last' ) {
440 3         16 my @i = _eval_fn({ t=>'fn', n=>'index', a=>[] }, $ctx);
441 3         17 my @c = _eval_fn({ t=>'fn', n=>'count', a=>[] }, $ctx);
442 3 50 33     17 return () unless @i && @c;
443 3 100       12 return (Data::ZPath::Node->_wrap($i[0]->primitive_value == ($c[0]->primitive_value - 1) ? !!1 : !!0));
444             }
445              
446 181 100 100     568 if ( $name eq 'next' || $name eq 'prev' ) {
447 88         131 my $cur = $ns->[0];
448 88 100 66     214 return unless $cur && $cur->parent;
449 86         138 my @siblings = grep { $_->type ne 'attr' } $cur->parent->children;
  190         324  
450 86         105 my $i;
451 86         184 for my $ix ( 0 .. $#siblings ) {
452 138         233 my $sraw = $siblings[$ix]->raw;
453 138         230 my $craw = $cur->raw;
454 138 50 33     667 if ( blessed($sraw) and blessed($craw)
      33        
      33        
455             and $sraw->isa('XML::LibXML::Node')
456             and $craw->isa('XML::LibXML::Node') ) {
457 138 100       160 next unless eval { $sraw->isSameNode($craw) };
  138         383  
458 86         101 $i = $ix;
459 86         121 last;
460             }
461 0 0 0     0 next unless defined $siblings[$ix]->id and defined $cur->id;
462 0 0       0 if ( $siblings[$ix]->id eq $cur->id ) {
463 0         0 $i = $ix;
464 0         0 last;
465             }
466             }
467              
468 86 50       168 return unless defined $i;
469 86 100       178 my $ni = $name eq 'next' ? $i + 1 : $i - 1;
470 86 100 100     407 return if $ni < 0 || $ni > $#siblings;
471 34         140 return $siblings[$ni];
472             }
473              
474 93 100       193 if ( $name eq 'string' ) {
475 7 100       15 if ( @args ) {
476 1         2 my @r = $eval_arg->(0);
477             return map {
478 1         3 my $s = $_->string_value;
  1         3  
479 1 50       5 defined $s ? Data::ZPath::Node->_wrap($s) : ()
480             } @r;
481             }
482 6         9 my $cur = $ns->[0];
483 6 50       12 return () unless $cur;
484 6         13 my $s = $cur->string_value;
485 6 50       20 return defined $s ? (Data::ZPath::Node->_wrap($s)) : ();
486             }
487              
488 86 100       188 if ( $name eq 'number' ) {
489 17 50       40 if ( @args ) {
490 17         39 my @r = $eval_arg->(0);
491             return map {
492 17         33 my $n = $_->number_value;
  17         52  
493 17 50       630 defined $n ? Data::ZPath::Node->_wrap($n) : ()
494             } @r;
495             }
496 0         0 my $cur = $ns->[0];
497 0 0       0 return unless $cur;
498 0         0 my $n = $cur->number_value;
499 0 0       0 return defined $n ? Data::ZPath::Node->_wrap($n) : ();
500             }
501              
502 69 100       151 if ( $name eq 'value' ) {
503 8 50       22 if ( @args ) {
504 8         23 my @r = $eval_arg->(0);
505             return map {
506 8         22 my $v = $_->primitive_value;
  8         26  
507 8         31 Data::ZPath::Node->_wrap($v)
508             } @r;
509             }
510 0         0 my $cur = $ns->[0];
511 0 0       0 return unless $cur;
512 0         0 return Data::ZPath::Node->_wrap($cur->primitive_value);
513             }
514              
515 61 100       120 if ( $name eq 'type' ) {
516 14 100       55 if ( @args ) {
517 8         83 my @r = $eval_arg->(0);
518 8 100       38 return Data::ZPath::Node->_wrap('undefined') unless @r;
519             return map {
520 5         12 Data::ZPath::Node->_wrap($_->type)
  5         16  
521             } @r;
522             }
523 6         9 my $cur = $ns->[0];
524 6 50       19 return Data::ZPath::Node->_wrap($cur ? $cur->type : 'undefined');
525             }
526              
527             # Math helpers: map numeric over input set
528             my $num_input = sub {
529 12     12   17 my ( $expr_idx ) = @_;
530 12 50       31 my @in = @args ? $eval_arg->($expr_idx) : @$ns;
531 12         19 return map { $_->number_value } @in;
  14         33  
532 47         161 };
533              
534 47 100 100     279 if ( $name eq 'ceil' || $name eq 'floor' || $name eq 'round' ) {
      100        
535 4         7 my @in = $num_input->(0);
536 4         6 my @out;
537 4         5 for my $x (@in) {
538 4 50       9 next unless defined $x;
539 4 50       19 my $v = $name eq 'ceil' ? POSIX::ceil($x)
    100          
    100          
540             : $name eq 'floor' ? POSIX::floor($x)
541             : int($x + ($x >= 0 ? 0.5 : -0.5));
542 4         9 push @out, Data::ZPath::Node->_wrap($v);
543             }
544 4         27 return @out;
545             }
546              
547 43 100 100     184 if ( $name eq 'sum' || $name eq 'min' || $name eq 'max' ) {
      100        
548 5         7 my @in;
549 5 50       12 if ( @args ) {
550 5         14 for my $i ( 0 .. $#args ) {
551 8         16 push @in, $num_input->($i);
552             }
553             } else {
554 0         0 @in = $num_input->(0);
555             }
556              
557 5         12 @in = grep { defined } @in;
  10         19  
558 5 50       13 return unless @in;
559              
560 5 100       13 if ( $name eq 'sum' ) {
561 3         7 my $s = 0;
562 3         14 $s += $_ for @in;
563 3         10 return Data::ZPath::Node->_wrap($s);
564             }
565 2 100       5 if ( $name eq 'min' ) {
566 1         2 my $m = $in[0];
567 1   33     5 ( $_ < $m ) and ( $m = $_ ) for @in;
568 1         2 return Data::ZPath::Node->_wrap($m);
569             }
570 1         2 my $m = $in[0];
571 1   66     5 ( $_ > $m ) and ( $m = $_ ) for @in;
572 1         3 return Data::ZPath::Node->_wrap($m);
573             }
574              
575             # String helpers
576             my $str_input = sub {
577 0     0   0 my ( $expr_idx ) = @_;
578 0 0       0 my @in = @args ? $eval_arg->($expr_idx) : @$ns;
579 0         0 return map { $_->string_value } @in;
  0         0  
580 38         118 };
581              
582 38 50       111 if ( $name eq 'escape' ) {
583 0         0 my @in;
584 0 0       0 if ( @args ) {
585 0         0 for my $i (0..$#args) { push @in, $eval_arg->($i); }
  0         0  
586             } else {
587 0         0 @in = @$ns;
588             }
589             return map {
590 0   0     0 my $s = $_->string_value // '';
  0         0  
591 0         0 $s =~ s/&/&/g;
592 0         0 $s =~ s/
593 0         0 $s =~ s/>/>/g;
594 0         0 $s =~ s/"/"/g;
595 0         0 $s =~ s/'/'/g;
596 0         0 Data::ZPath::Node->_wrap($s)
597             } @in;
598             }
599              
600 38 50       95 if ( $name eq 'unescape' ) {
601 0         0 my @in;
602 0 0       0 if ( @args ) {
603 0         0 for my $i (0..$#args) { push @in, $eval_arg->($i); }
  0         0  
604             } else {
605 0         0 @in = @$ns;
606             }
607             return map {
608 0   0     0 my $s = $_->string_value // '';
  0         0  
609 0         0 $s =~ s/</
610 0         0 $s =~ s/>/>/g;
611 0         0 $s =~ s/"/"/g;
612 0         0 $s =~ s/'/'/g;
613 0         0 $s =~ s/&/&/g;
614 0         0 Data::ZPath::Node->_wrap($s)
615             } @in;
616             }
617              
618 38 50       75 if ( $name eq 'literal' ) {
619             # ZTemplate-specific behavior; for Data::ZPath, it's a no-op passthrough
620 0         0 my @in;
621 0 0       0 if ( @args ) {
622 0         0 for my $i (0..$#args) { push @in, $eval_arg->($i); }
  0         0  
623             } else {
624 0         0 @in = @$ns;
625             }
626 0         0 return @in;
627             }
628              
629 38 100       102 if ( $name eq 'format' ) {
630 1 50       5 croak "format(format, expression)" unless @args >= 1;
631 1         4 my @fmt = $eval_arg->(0);
632 1 50 50     8 my $f = $fmt[0] ? ($fmt[0]->string_value // '') : '';
633 1 50       6 my @in = @args > 1 ? $eval_arg->(1) : @$ns;
634             return map {
635 1         3 my $v = $_->primitive_value;
  1         6  
636 1         14 Data::ZPath::Node->_wrap(sprintf($f, $v))
637             } @in;
638             }
639              
640 37 100 66     147 if ( $name eq 'index-of' || $name eq 'last-index-of' ) {
641 2 50       5 croak "$name(search, expression)" unless @args >= 1;
642 2   50     4 my $search = ($eval_arg->(0))[0]->string_value // '';
643 2 50       10 my @in = @args > 1 ? $eval_arg->(1) : @$ns;
644             return map {
645 2   50     3 my $s = $_->string_value // '';
  2         4  
646 2 50       7 my $pos = $name eq 'index-of' ? index($search, $s) : rindex($search, $s);
647 2         4 Data::ZPath::Node->_wrap($pos)
648             } @in;
649             }
650              
651 35 100       72 if ( $name eq 'string-length' ) {
652 2 50       6 my @in = @args ? $eval_arg->(0) : @$ns;
653             return map {
654 2   50     9 my $s = $_->string_value // '';
  2         7  
655 2         6 Data::ZPath::Node->_wrap(length($s))
656             } @in;
657             }
658              
659 33 100 100     151 if ( $name eq 'upper-case' || $name eq 'lower-case' ) {
660 2 50       11 my @in = @args ? $eval_arg->(0) : @$ns;
661             return map {
662 2   50     6 my $s = $_->string_value // '';
  2         6  
663 2 100       8 $s = $name eq 'upper-case' ? uc($s) : lc($s);
664 2         5 Data::ZPath::Node->_wrap($s)
665             } @in;
666             }
667              
668 31 100       60 if ( $name eq 'substring' ) {
669 1 50       3 croak "substring(expression, start, length)" unless @args >= 2;
670 1 50       4 my @in = @args > 2 ? $eval_arg->(0) : @$ns;
671 1   50     4 my $start = ($eval_arg->(1))[0]->number_value // 0;
672 1   50     4 my $len = ($eval_arg->(2))[0]->number_value // 0;
673             return map {
674 1   50     2 my $s = $_->string_value // '';
  1         3  
675 1         5 Data::ZPath::Node->_wrap(substr($s, int($start), int($len)))
676             } @in;
677             }
678              
679 30 100 66     89 if ( $name eq 'match' || $name eq 'matches' ) {
680 1 50       4 croak "match(pattern, expression)" unless @args >= 1;
681 1   50     3 my $pat = ($eval_arg->(0))[0]->string_value // '';
682 1         7 my $re = _pattern_to_regexp( $pat );
683              
684 1 50       11106 my @in = @args > 1 ? $eval_arg->(1) : @$ns;
685             return map {
686 1   50     3 my $s = $_->string_value // '';
  1         5  
687 1 50       14 Data::ZPath::Node->_wrap(($s =~ $re) ? 1 : 0)
688             } @in;
689             }
690              
691 29 100       64 if ( $name eq 'replace' ) {
692 13 50       31 croak "replace(pattern, replace, expression)" unless @args >= 2;
693 13   50     32 my $pat = ($eval_arg->(0))[0]->string_value // '';
694 13   50     99 my $rep = ($eval_arg->(1))[0]->string_value // '';
695 13         71 my $re = _pattern_to_regexp( $pat );
696              
697 13 100       7694 my @in = @args > 2 ? $eval_arg->(2) : @$ns;
698             return map {
699 13   50     33 my $s = $_->string_value // '';
  13         41  
700 13         42 Data::ZPath::Node->_wrap(_string_replace($s, $re, $rep))
701             } @in;
702             }
703              
704 16 100       29 if ( $name eq 'join' ) {
705 3 50       9 croak "join(joiner, expression)" unless @args >= 1;
706 3   50     7 my $joiner = ($eval_arg->(0))[0]->string_value // '';
707 3 50       12 my @in = @args > 1 ? $eval_arg->(1) : @$ns;
708 3   50     5 my @ss = map { $_->string_value // '' } @in;
  9         18  
709 3         12 return Data::ZPath::Node->_wrap(join($joiner, @ss));
710             }
711              
712             # XML functions
713 13 100       25 if ( $name eq 'url' ) {
714 12 50       21 my @in = @args ? $eval_arg->(0) : @$ns;
715             return map {
716 12         17 my $raw = $_->raw;
  12         22  
717 12         15 my $u = '';
718 12 100 66     52 if ( blessed($raw) && $raw->can('namespaceURI') ) {
719 11   100     36 $u = $raw->namespaceURI // '';
720             }
721 12         30 Data::ZPath::Node->_wrap($u)
722             } @in;
723             }
724              
725 1 50       4 if ( $name eq 'local-name' ) {
726 0 0       0 my @in = @args ? $eval_arg->(0) : @$ns;
727             return map {
728 0         0 my $raw = $_->raw;
  0         0  
729 0         0 my $ln = '';
730 0 0 0     0 if ( blessed($raw) && $raw->can('localname') ) {
731 0   0     0 $ln = $raw->localname // ($raw->nodeName // '');
      0        
732             } else {
733 0   0     0 $ln = $_->name // '';
734             }
735 0         0 Data::ZPath::Node->_wrap($ln)
736             } @in;
737             }
738              
739             # CBOR tag() (optional marker), returns empty set if absent
740 1 50       6 if ( $name eq 'tag' ) {
741 1 50       6 my @in = @args ? $eval_arg->(0) : @$ns;
742 1         3 my @out;
743 1         2 for my $n (@in) {
744 1         3 my $raw = $n->raw;
745 1 50 33     5 if ( blessed($raw) and $raw->isa('CBOR::Free::Tagged') ) {
746 1         3 push @out, Data::ZPath::Node->_wrap($raw->[0]);
747             }
748             }
749 1         11 return @out;
750             }
751              
752 0         0 croak "Unknown function '$name'";
753             }
754              
755             sub _string_replace {
756 13     13   74 my ( $string, $pattern, $replacement ) = @_;
757              
758 13         133 my @matches = ( $string =~ /$pattern/p );
759 13         74 unshift @matches, ${^MATCH};
760 13         97 $string =~ s{$pattern}{
761 12         24 my $r = "$replacement";
762 12         56 $r =~ s{ \$ ([0-9]+) }{
763 5 50       32 $1 <= $#matches ? $matches[$1] : ''
764             }xeg;
765 12         38 $r;
766             }eg;
767              
768 13         82 return $string;
769             }
770              
771             sub _dedup_nodes {
772 1152     1152   1795 my %seen;
773             return grep {
774 1152         2272 my $raw = $_->raw;
  4151         7564  
775 4151         7712 my $key = $_->id;
776              
777 4151 100 100     10785 if ( blessed($raw) and $raw->isa('XML::LibXML::Node') ) {
778 1984   50     8877 $key = join ':', 'xmlpath', $raw->nodeType, ($raw->nodePath // q{});
779             }
780              
781 4151         15461 not $seen{$key}++;
782             } @_;
783             }
784              
785             sub _truthy {
786 681     681   1147 my ( $n ) = @_;
787 681 100       1355 return !!0 unless $n;
788 673         1600 my $pv = $n->primitive_value;
789 673         1361 return !!$pv;
790             }
791              
792             sub _to_number {
793 93     93   166 my ( $n ) = @_;
794 93 50       206 return undef unless $n;
795 93         246 return $n->number_value;
796             }
797              
798             sub _to_string {
799 0     0   0 my ( $n ) = @_;
800 0 0       0 return undef unless $n;
801 0         0 return $n->string_value;
802             }
803              
804              
805             sub _equals {
806 228     228   362 my ( $a, $b ) = @_;
807 228 50 33     809 return !!0 unless $a && $b;
808              
809 228         559 my $a_type = $a->type;
810 228         549 my $b_type = $b->type;
811              
812 228 50       447 return $a_type eq 'null' if $b_type eq 'null';
813 228 50       412 return $b_type eq 'null' if $a_type eq 'null';
814              
815 228 50 33     543 if ( $a_type eq 'boolean' and $b_type eq 'boolean' ) {
816 0         0 my $av = !!$a->primative_value;
817 0         0 my $bv = !!$b->primative_value;
818              
819 0         0 return $av == $bv;
820             }
821              
822 228 100 66     549 if ( $a_type eq 'number' and $b_type eq 'number' ) {
823 67         190 my $av = $a->number_value;
824 67         175 my $bv = $b->number_value;
825              
826             # Floating point comparison
827 67 100 66     280 if ( $av =~ /\./ or $bv =~ /\./ ) {
828 1         6 return abs($av-$bv) < $Data::ZPath::Epsilon;
829             }
830              
831 66         229 return $av == $bv;
832             }
833              
834 161         450 my @string_like = qw( string text attr comment element );
835 161 100 100     311 if ( grep { $a_type eq $_ } @string_like
  805         1402  
836 790         1405 and grep { $b_type eq $_ } @string_like ) {
837 140         375 my $av = $a->string_value;
838 140         298 my $bv = $b->string_value;
839 140         615 return "$av" eq "$bv";
840             }
841              
842 21 100       56 return unless $a->id;
843 3 100       10 return unless $b->id;
844 2         9 return $a->id eq $b->id;
845             }
846              
847              
848             1;