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