add peek method to streams, implement 'inside' for collect
[catagits/HTML-Zoom.git] / lib / HTML / Zoom / FilterStream.pm
1 package HTML::Zoom::FilterStream;
2
3 use strict;
4 use warnings FATAL => 'all';
5
6 sub new {
7   my ($class, $args) = @_;
8   bless(
9     {
10       _stream => $args->{stream},
11       _match => $args->{match},
12       _filter => $args->{filter},
13     },
14     $class
15   );
16 }
17
18 sub peek {
19   my ($self) = @_;
20   if (exists $self->{_peeked}) {
21     return ($self->{_peeked});
22   }
23   if (my ($peeked) = $self->next) {
24     return ($self->{_peeked} = $peeked);
25   }
26   return;
27 }
28
29 sub next {
30   my ($self) = @_;
31
32   # peeked entry so return that
33
34   if (exists $self->{_peeked}) {
35     return (delete $self->{_peeked});
36   }
37
38   # if our main stream is already gone then we can short-circuit
39   # straight out - there's no way for an alternate stream to be there
40
41   return unless $self->{_stream};
42
43   # if we have an alternate stream (provided by a filter call resulting
44   # from a match on the main stream) then we want to read from that until
45   # it's gone - we're still effectively "in the match" but this is the
46   # point at which that fact is abstracted away from downstream consumers
47
48   if (my $alt = $self->{_alt_stream}) {
49
50     if (my ($evt) = $alt->next) {
51       return $evt;
52     }
53
54     # once the alternate stream is exhausted we can throw it away so future
55     # requests fall straight through to the main stream
56
57     delete $self->{_alt_stream};
58   }
59
60   # if there's no alternate stream currently, process the main stream
61
62   while (my ($evt) = $self->{_stream}->next) {
63
64     # don't match this event? return it immediately
65
66     return $evt unless $evt->{type} eq 'OPEN' and $self->{_match}->($evt);
67
68     # run our filter routine against the current event
69
70     my ($res) = $self->{_filter}->($evt, $self->{_stream});
71
72     # if the result is just an event, we can return that now
73
74     return $res if ref($res) eq 'HASH';
75
76     # if no result at all, jump back to the top of the loop to get the
77     # next event and try again - the filter has eaten this one
78
79     next unless defined $res;
80
81     # ARRAY means a pair of [ $evt, $new_stream ]
82
83     if (ref($res) eq 'ARRAY') {
84       $self->{_alt_stream} = $res->[1];
85       return $res->[0];
86     }
87
88     # the filter returned a stream - if it contains something return the
89     # first entry and stash it as the new alternate stream
90
91     if (my ($new_evt) = $res->next) {
92       $self->{_alt_stream} = $res;
93       return $new_evt;
94     }
95
96     # we got a new alternate stream but it turned out to be empty
97     # - this will happens for e.g. with an in place close (<foo />) that's
98     # being removed. In that case, we fall off to loop back round and try
99     # the next event from our main stream
100   }
101
102   # main stream exhausted so throw it away so we hit the short circuit
103   # at the top and return nothing to indicate to our caller we're done
104
105   delete $self->{_stream};
106   return;
107 }
108
109 1;