1# $Id: Step.pm,v 1.35 2001/04/01 16:56:40 matt Exp $
2
3package XML::XPath::Step;
4use XML::XPath::Parser;
5use XML::XPath::Node;
6use strict;
7
8# the beginnings of using XS for this file...
9# require DynaLoader;
10# use vars qw/$VERSION @ISA/;
11# $VERSION = '1.0';
12# @ISA = qw(DynaLoader);
13#
14# bootstrap XML::XPath::Step $VERSION;
15
16sub test_qname () { 0; } # Full name
17sub test_ncwild () { 1; } # NCName:*
18sub test_any () { 2; } # *
19
20sub test_attr_qname () { 3; } # @ns:attrib
21sub test_attr_ncwild () { 4; } # @nc:*
22sub test_attr_any () { 5; } # @*
23
24sub test_nt_comment () { 6; } # comment()
25sub test_nt_text () { 7; } # text()
26sub test_nt_pi () { 8; } # processing-instruction()
27sub test_nt_node () { 9; } # node()
28
29sub new {
30    my $class = shift;
31    my ($pp, $axis, $test, $literal) = @_;
32    my $axis_method = "axis_$axis";
33    $axis_method =~ tr/-/_/;
34    my $self = {
35        pp => $pp, # the XML::XPath::Parser class
36        axis => $axis,
37        axis_method => $axis_method,
38        test => $test,
39        literal => $literal,
40        predicates => [],
41        };
42    bless $self, $class;
43}
44
45sub as_string {
46    my $self = shift;
47    my $string = $self->{axis} . "::";
48
49    my $test = $self->{test};
50
51    if ($test == test_nt_pi) {
52        $string .= 'processing-instruction(';
53        if ($self->{literal}->value) {
54            $string .= $self->{literal}->as_string;
55        }
56        $string .= ")";
57    }
58    elsif ($test == test_nt_comment) {
59        $string .= 'comment()';
60    }
61    elsif ($test == test_nt_text) {
62        $string .= 'text()';
63    }
64    elsif ($test == test_nt_node) {
65        $string .= 'node()';
66    }
67    elsif ($test == test_ncwild || $test == test_attr_ncwild) {
68        $string .= $self->{literal} . ':*';
69    }
70    else {
71        $string .= $self->{literal};
72    }
73
74    foreach (@{$self->{predicates}}) {
75        next unless defined $_;
76        $string .= "[" . $_->as_string . "]";
77    }
78    return $string;
79}
80
81sub as_xml {
82    my $self = shift;
83    my $string = "<Step>\n";
84    $string .= "<Axis>" . $self->{axis} . "</Axis>\n";
85    my $test = $self->{test};
86
87    $string .= "<Test>";
88
89    if ($test == test_nt_pi) {
90        $string .= '<processing-instruction';
91        if ($self->{literal}->value) {
92            $string .= '>';
93            $string .= $self->{literal}->as_string;
94            $string .= '</processing-instruction>';
95        }
96        else {
97            $string .= '/>';
98        }
99    }
100    elsif ($test == test_nt_comment) {
101        $string .= '<comment/>';
102    }
103    elsif ($test == test_nt_text) {
104        $string .= '<text/>';
105    }
106    elsif ($test == test_nt_node) {
107        $string .= '<node/>';
108    }
109    elsif ($test == test_ncwild || $test == test_attr_ncwild) {
110        $string .= '<namespace-prefix>' . $self->{literal} . '</namespace-prefix>';
111    }
112    else {
113        $string .= '<nametest>' . $self->{literal} . '</nametest>';
114    }
115
116    $string .= "</Test>\n";
117
118    foreach (@{$self->{predicates}}) {
119        next unless defined $_;
120        $string .= "<Predicate>\n" . $_->as_xml() . "</Predicate>\n";
121    }
122
123    $string .= "</Step>\n";
124
125    return $string;
126}
127
128sub evaluate {
129    my $self = shift;
130    my $from = shift; # context nodeset
131
132#    warn "Step::evaluate called with ", $from->size, " length nodeset\n";
133
134    $self->{pp}->set_context_set($from);
135
136    my $initial_nodeset = XML::XPath::NodeSet->new();
137
138    # See spec section 2.1, paragraphs 3,4,5:
139    # The node-set selected by the location step is the node-set
140    # that results from generating an initial node set from the
141    # axis and node-test, and then filtering that node-set by
142    # each of the predicates in turn.
143
144    # Make each node in the nodeset be the context node, one by one
145    for(my $i = 1; $i <= $from->size; $i++) {
146        $self->{pp}->set_context_pos($i);
147        $initial_nodeset->append($self->evaluate_node($from->get_node($i)));
148    }
149
150#    warn "Step::evaluate initial nodeset size: ", $initial_nodeset->size, "\n";
151
152    $self->{pp}->set_context_set(undef);
153
154    $initial_nodeset->sort;
155
156    return $initial_nodeset;
157}
158
159# Evaluate the step against a particular node
160sub evaluate_node {
161    my $self = shift;
162    my $context = shift;
163
164#    warn "Evaluate node: $self->{axis}\n";
165
166#    warn "Node: ", $context->[node_name], "\n";
167
168    my $method = $self->{axis_method};
169
170    my $results = XML::XPath::NodeSet->new();
171    no strict 'refs';
172    eval {
173        $method->($self, $context, $results);
174    };
175    if ($@) {
176        die "axis $method not implemented [$@]\n";
177    }
178
179#    warn("results: ", join('><', map {$_->string_value} @$results), "\n");
180    # filter initial nodeset by each predicate
181    foreach my $predicate (@{$self->{predicates}}) {
182        $results = $self->filter_by_predicate($results, $predicate);
183    }
184
185    return $results;
186}
187
188sub axis_ancestor {
189    my $self = shift;
190    my ($context, $results) = @_;
191
192    my $parent = $context->getParentNode;
193
194    START:
195    return $results unless $parent;
196    if (node_test($self, $parent)) {
197        $results->push($parent);
198    }
199    $parent = $parent->getParentNode;
200    goto START;
201}
202
203sub axis_ancestor_or_self {
204    my $self = shift;
205    my ($context, $results) = @_;
206
207    START:
208    return $results unless $context;
209    if (node_test($self, $context)) {
210        $results->push($context);
211    }
212    $context = $context->getParentNode;
213    goto START;
214}
215
216sub axis_attribute {
217    my $self = shift;
218    my ($context, $results) = @_;
219
220    foreach my $attrib (@{$context->getAttributes}) {
221        if ($self->test_attribute($attrib)) {
222            $results->push($attrib);
223        }
224    }
225}
226
227sub axis_child {
228    my $self = shift;
229    my ($context, $results) = @_;
230
231    foreach my $node (@{$context->getChildNodes}) {
232        if (node_test($self, $node)) {
233            $results->push($node);
234        }
235    }
236}
237
238sub axis_descendant {
239    my $self = shift;
240    my ($context, $results) = @_;
241
242    my @stack = $context->getChildNodes;
243
244    while (@stack) {
245        my $node = pop @stack;
246        if (node_test($self, $node)) {
247            $results->unshift($node);
248        }
249        push @stack, $node->getChildNodes;
250    }
251}
252
253sub axis_descendant_or_self {
254    my $self = shift;
255    my ($context, $results) = @_;
256
257    my @stack = ($context);
258
259    while (@stack) {
260        my $node = pop @stack;
261        if (node_test($self, $node)) {
262            $results->unshift($node);
263        }
264        push @stack, $node->getChildNodes;
265    }
266}
267
268sub axis_following {
269    my $self = shift;
270    my ($context, $results) = @_;
271
272    START:
273
274    my $parent = $context->getParentNode;
275    return $results unless $parent;
276
277    while ($context = $context->getNextSibling) {
278        axis_descendant_or_self($self, $context, $results);
279    }
280
281    $context = $parent;
282    goto START;
283}
284
285sub axis_following_sibling {
286    my $self = shift;
287    my ($context, $results) = @_;
288
289    while ($context = $context->getNextSibling) {
290        if (node_test($self, $context)) {
291            $results->push($context);
292        }
293    }
294}
295
296sub axis_namespace {
297    my $self = shift;
298    my ($context, $results) = @_;
299
300    return $results unless $context->isElementNode;
301    foreach my $ns (@{$context->getNamespaces}) {
302        if ($self->test_namespace($ns)) {
303            $results->push($ns);
304        }
305    }
306}
307
308sub axis_parent {
309    my $self = shift;
310    my ($context, $results) = @_;
311
312    my $parent = $context->getParentNode;
313    return $results unless $parent;
314    if (node_test($self, $parent)) {
315        $results->push($parent);
316    }
317}
318
319sub axis_preceding {
320    my $self = shift;
321    my ($context, $results) = @_;
322
323    # all preceding nodes in document order, except ancestors
324
325    START:
326
327    my $parent = $context->getParentNode;
328    return $results unless $parent;
329
330    while ($context = $context->getPreviousSibling) {
331        axis_descendant_or_self($self, $context, $results);
332    }
333
334    $context = $parent;
335    goto START;
336}
337
338sub axis_preceding_sibling {
339    my $self = shift;
340    my ($context, $results) = @_;
341
342    while ($context = $context->getPreviousSibling) {
343        if (node_test($self, $context)) {
344            $results->push($context);
345        }
346    }
347}
348
349sub axis_self {
350    my $self = shift;
351    my ($context, $results) = @_;
352
353    if (node_test($self, $context)) {
354        $results->push($context);
355    }
356}
357
358sub node_test {
359    my $self = shift;
360    my $node = shift;
361
362    # if node passes test, return true
363
364    my $test = $self->{test};
365
366    return 1 if $test == test_nt_node;
367
368    if ($test == test_any) {
369        return 1 if $node->isElementNode && defined $node->getName;
370    }
371
372    local $^W;
373
374    if ($test == test_ncwild) {
375        return unless $node->isElementNode;
376        my $match_ns = $self->{pp}->get_namespace($self->{literal}, $node);
377        if (my $node_nsnode = $node->getNamespace()) {
378            return 1 if $match_ns eq $node_nsnode->getValue;
379        }
380    }
381    elsif ($test == test_qname) {
382        return unless $node->isElementNode;
383        if ($self->{literal} =~ /:/) {
384            my ($prefix, $name) = split(':', $self->{literal}, 2);
385            my $match_ns = $self->{pp}->get_namespace($prefix, $node);
386            if (my $node_nsnode = $node->getNamespace()) {
387#                warn "match: '$self->{literal}' match NS: '$match_ns' got NS: '", $node_nsnode->getValue, "'\n";
388                return 1 if ($match_ns eq $node_nsnode->getValue) &&
389                        ($name eq $node->getLocalName);
390            }
391        }
392        else {
393#            warn "Node test: ", $node->getName, "\n";
394            return 1 if $node->getName eq $self->{literal};
395        }
396    }
397    elsif ($test == test_nt_text) {
398        return 1 if $node->isTextNode;
399    }
400    elsif ($test == test_nt_comment) {
401        return 1 if $node->isCommentNode;
402    }
403#     elsif ($test == test_nt_pi && !$self->{literal}) {
404#         warn "Unreachable code???";
405#         return 1 if $node->isPINode;
406#     }
407    elsif ($test == test_nt_pi) {
408        return unless $node->isPINode;
409        if (my $val = $self->{literal}->value) {
410            return 1 if $node->getTarget eq $val;
411        }
412        else {
413            return 1;
414        }
415    }
416
417    return; # fallthrough returns false
418}
419
420sub test_attribute {
421    my $self = shift;
422    my $node = shift;
423
424#    warn "test_attrib: '$self->{test}' against: ", $node->getName, "\n";
425#    warn "node type: $node->[node_type]\n";
426
427    my $test = $self->{test};
428
429    return 1 if ($test == test_attr_any) || ($test == test_nt_node);
430
431    if ($test == test_attr_ncwild) {
432        my $match_ns = $self->{pp}->get_namespace($self->{literal}, $node);
433        if (my $node_nsnode = $node->getNamespace()) {
434            return 1 if $match_ns eq $node_nsnode->getValue;
435        }
436    }
437    elsif ($test == test_attr_qname) {
438        if ($self->{literal} =~ /:/) {
439            my ($prefix, $name) = split(':', $self->{literal}, 2);
440            my $match_ns = $self->{pp}->get_namespace($prefix, $node);
441            if (my $node_nsnode = $node->getNamespace()) {
442                return 1 if ($match_ns eq $node_nsnode->getValue) &&
443                        ($name eq $node->getLocalName);
444            }
445        }
446        else {
447            return 1 if $node->getName eq $self->{literal};
448        }
449    }
450
451    return; # fallthrough returns false
452}
453
454sub test_namespace {
455    my $self = shift;
456    my $node = shift;
457
458    # Not sure if this is correct. The spec seems very unclear on what
459    # constitutes a namespace test... bah!
460
461    my $test = $self->{test};
462
463    return 1 if $test == test_any; # True for all nodes of principal type
464
465    if ($test == test_any) {
466        return 1;
467    }
468    elsif ($self->{literal} eq $node->getExpanded) {
469        return 1;
470    }
471
472    return;
473}
474
475sub filter_by_predicate {
476    my $self = shift;
477    my ($nodeset, $predicate) = @_;
478
479    # See spec section 2.4, paragraphs 2 & 3:
480    # For each node in the node-set to be filtered, the predicate Expr
481    # is evaluated with that node as the context node, with the number
482    # of nodes in the node set as the context size, and with the
483    # proximity position of the node in the node set with respect to
484    # the axis as the context position.
485
486    if (!ref($nodeset)) { # use ref because nodeset has a bool context
487        die "No nodeset!!!";
488    }
489
490#    warn "Filter by predicate: $predicate\n";
491
492    my $newset = XML::XPath::NodeSet->new();
493
494    for(my $i = 1; $i <= $nodeset->size; $i++) {
495        # set context set each time 'cos a loc-path in the expr could change it
496        $self->{pp}->set_context_set($nodeset);
497        $self->{pp}->set_context_pos($i);
498        my $result = $predicate->evaluate($nodeset->get_node($i));
499        if ($result->isa('XML::XPath::Boolean')) {
500            if ($result->value) {
501                $newset->push($nodeset->get_node($i));
502            }
503        }
504        elsif ($result->isa('XML::XPath::Number')) {
505            if ($result->value == $i) {
506                $newset->push($nodeset->get_node($i));
507            }
508        }
509        else {
510            if ($result->to_boolean->value) {
511                $newset->push($nodeset->get_node($i));
512            }
513        }
514    }
515
516    return $newset;
517}
518
5191;
520