1 package HTML::Zoom::FilterBuilder;
4 use base qw(HTML::Zoom::SubObject);
5 use HTML::Zoom::CodeStream;
7 sub _stream_from_code {
8 shift->_zconfig->stream_utils->stream_from_code(@_)
11 sub _stream_from_array {
12 shift->_zconfig->stream_utils->stream_from_array(@_)
15 sub _stream_from_proto {
16 shift->_zconfig->stream_utils->stream_from_proto(@_)
20 shift->_zconfig->stream_utils->stream_concat(@_)
23 sub _flatten_stream_of_streams {
24 shift->_zconfig->stream_utils->flatten_stream_of_streams(@_)
27 sub set_attr { shift->set_attribute(@_); }
31 my $attr = $self->_parse_attribute_args(@_);
33 my $a = (my $evt = $_[0])->{attrs};
34 my @kadd = grep {!exists $a->{$_}} keys %$attr;
35 +{ %$evt, raw => undef, raw_attrs => undef,
36 attrs => { %$a, %$attr },
37 @kadd ? (attr_names => [ @{$evt->{attr_names}}, @kadd ]) : ()
42 sub _parse_attribute_args {
45 my $opts = ref($_[0]) eq 'HASH' ? $_[0] : {$_[0] => $_[1]};
46 for (values %{$opts}) { $self->_zconfig->parser->html_escape($_); }
51 die "renamed to add_to_attribute. killing this entirely for 1.0";
54 sub add_class { shift->add_to_attribute('class',@_) }
56 sub remove_class { shift->remove_from_attribute('class',@_) }
58 sub set_class { shift->set_attribute('class',@_) }
60 sub set_id { shift->set_attribute('id',@_) }
62 sub add_to_attribute {
64 my $attr = $self->_parse_attribute_args(@_);
66 my $a = (my $evt = $_[0])->{attrs};
67 my @kadd = grep {!exists $a->{$_}} keys %$attr;
68 +{ %$evt, raw => undef, raw_attrs => undef,
71 map {$_ => join(' ', (exists $a->{$_} ? $a->{$_} : ()), $attr->{$_}) }
74 @kadd ? (attr_names => [ @{$evt->{attr_names}}, @kadd ]) : ()
79 sub remove_from_attribute {
81 my $attr = $self->_parse_attribute_args(@_);
83 my $a = (my $evt = $_[0])->{attrs};
84 +{ %$evt, raw => undef, raw_attrs => undef,
87 #TODO needs to support multiple removes
88 map { my $tar = $_; $_ => join ' ',
89 map {$attr->{$tar} ne $_} split ' ', $a->{$_} }
90 grep {exists $a->{$_}} keys %$attr
96 sub remove_attribute {
97 my ($self, $args) = @_;
98 my $name = (ref($args) eq 'HASH') ? $args->{name} : $args;
100 my $a = (my $evt = $_[0])->{attrs};
101 return $evt unless exists $a->{$name};
102 $a = { %$a }; delete $a->{$name};
103 +{ %$evt, raw => undef, raw_attrs => undef,
105 attr_names => [ grep $_ ne $name, @{$evt->{attr_names}} ]
110 sub transform_attribute {
112 my ( $name, $code ) = @_ > 1 ? @_ : @{$_[0]}{qw(name code)};
116 my %a = %{ $evt->{attrs} };
117 my @names = @{ $evt->{attr_names} };
119 my $existed_before = exists $a{$name};
120 my $v = $code->( $a{$name} );
121 my $deleted = $existed_before && ! defined $v;
122 my $added = ! $existed_before && defined $v;
129 @names = grep $_ ne $name, @names;
133 +{ %$evt, raw => undef, raw_attrs => undef,
136 ? (attr_names => \@names )
143 my ($self, $options) = @_;
144 my ($into, $passthrough, $content, $filter, $flush_before) =
145 @{$options}{qw(into passthrough content filter flush_before)};
147 my ($evt, $stream) = @_;
148 # We wipe the contents of @$into here so that other actions depending
149 # on this (such as a repeater) can be invoked multiple times easily.
150 # I -suspect- it's better for that state reset to be managed here; if it
151 # ever becomes painful the decision should be revisited
153 @$into = $content ? () : ($evt);
155 if ($evt->{is_in_place_close}) {
156 return $evt if $passthrough || $content;
159 my $name = $evt->{name};
161 my $_next = $content ? 'peek' : 'next';
164 $stream = do { local $_ = $stream; $filter->($stream) };
167 local $_ = $self->_stream_concat(
168 $self->_stream_from_array($evt),
173 $evt = $stream->next;
176 my $collector = $self->_stream_from_code(sub {
177 return unless $stream;
178 while (my ($evt) = $stream->$_next) {
179 $depth++ if ($evt->{type} eq 'OPEN');
180 $depth-- if ($evt->{type} eq 'CLOSE');
184 push(@$into, $evt) if $into;
185 return $evt if $passthrough;
188 push(@$into, $evt) if $into;
189 $stream->next if $content;
190 return $evt if $passthrough;
192 die "Never saw closing </${name}> before end of source";
195 if ($passthrough||$content) {
196 $evt = { %$evt, flush => 1 };
198 $evt = { type => 'EMPTY', flush => 1 };
201 return ($passthrough||$content||$flush_before)
202 ? [ $evt, $collector ]
207 sub collect_content {
208 my ($self, $options) = @_;
209 $self->collect({ %{$options||{}}, content => 1 })
213 my ($self, $events) = @_;
214 my $coll_proto = $self->collect({ passthrough => 1 });
216 my $emit = $self->_stream_from_proto($events);
217 my $coll = &$coll_proto;
219 if(ref $coll eq 'ARRAY') {
220 my $firstbit = $self->_stream_from_proto([$coll->[0]]);
221 return $self->_stream_concat($emit, $firstbit, $coll->[1]);
222 } elsif(ref $coll eq 'HASH') {
223 return [$emit, $coll];
225 return $self->_stream_concat($emit, $coll);
227 } else { return $emit }
232 my ($self, $events) = @_;
233 my $coll_proto = $self->collect({ passthrough => 1 });
236 my $emit = $self->_stream_from_proto($events);
237 my $coll = &$coll_proto;
238 return ref($coll) eq 'HASH' # single event, no collect
240 : [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
244 sub prepend_content {
245 my ($self, $events) = @_;
246 my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
249 my $emit = $self->_stream_from_proto($events);
250 if ($evt->{is_in_place_close}) {
251 $evt = { %$evt }; delete @{$evt}{qw(raw is_in_place_close)};
252 return [ $evt, $self->_stream_from_array(
253 $emit->next, { type => 'CLOSE', name => $evt->{name} }
256 my $coll = &$coll_proto;
257 return [ $coll->[0], $self->_stream_concat($emit, $coll->[1]) ];
262 my ($self, $events) = @_;
263 my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
266 my $emit = $self->_stream_from_proto($events);
267 if ($evt->{is_in_place_close}) {
268 $evt = { %$evt }; delete @{$evt}{qw(raw is_in_place_close)};
269 return [ $evt, $self->_stream_from_array(
270 $emit->next, { type => 'CLOSE', name => $evt->{name} }
273 my $coll = &$coll_proto;
274 return [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
279 my ($self, $replace_with, $options) = @_;
280 my $coll_proto = $self->collect($options);
282 my ($evt, $stream) = @_;
283 my $emit = $self->_stream_from_proto($replace_with);
284 my $coll = &$coll_proto;
285 # if we're replacing the contents of an in place close
286 # then we need to handle that here
287 if ($options->{content}
288 && ref($coll) eq 'HASH'
289 && $coll->{is_in_place_close}
291 my $close = $stream->next;
292 # shallow copy and nuke in place and raw (to force smart print)
293 $_ = { %$_ }, delete @{$_}{qw(is_in_place_close raw)} for ($coll, $close);
294 $emit = $self->_stream_concat(
296 $self->_stream_from_array($close),
299 # For a straightforward replace operation we can, in fact, do the emit
300 # -before- the collect, and my first cut did so. However in order to
301 # use the captured content in generating the new content, we need
302 # the collect stage to happen first - and it seems highly unlikely
303 # that in normal operation the collect phase will take long enough
304 # for the difference to be noticeable
307 ? (ref $coll eq 'ARRAY' # [ event, stream ]
308 ? [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ]
309 : (ref $coll eq 'HASH' # event or stream?
311 : $self->_stream_concat($coll, $emit))
318 sub replace_content {
319 my ($self, $replace_with, $options) = @_;
320 $self->replace($replace_with, { %{$options||{}}, content => 1 })
324 my ($self, $repeat_for, $options) = @_;
325 $options->{into} = \my @into;
327 my $repeat_between = delete $options->{repeat_between};
328 if ($repeat_between) {
329 $options->{filter} = sub {
330 $_->select($repeat_between)->collect({ into => \@between })
334 my $s = $self->_stream_from_proto($repeat_for);
335 # We have to test $repeat_between not @between here because
336 # at the point we're constructing our return stream @between
337 # hasn't been populated yet - but we can test @between in the
338 # map routine because it has been by then and that saves us doing
339 # the extra stream construction if we don't need it.
340 $self->_flatten_stream_of_streams(do {
341 if ($repeat_between) {
343 local $_ = $self->_stream_from_array(@into);
344 (@between && $s->peek)
345 ? $self->_stream_concat(
346 $_[0]->($_), $self->_stream_from_array(@between)
352 local $_ = $self->_stream_from_array(@into);
358 $self->replace($repeater, $options);
362 my ($self, $repeat_for, $options) = @_;
363 $self->repeat($repeat_for, { %{$options||{}}, content => 1 })
367 my ($self, $to) = @_;
370 push @$to, $evt->{'attrs'}->{'name'};
380 $_->select('input')->validation_rules($to)
381 ->select('select')->validation_rules($to);
388 my ($self,$val) = @_;
392 $_->select('input')->val($val)
393 #->select('select')->val($val)
400 sub validation_rules {
401 my ($self, $to) = @_;
404 $to->{$evt->{'attrs'}->{'name'}}
405 = [split ' ', $evt->{'attrs'}->{'data-validate'}||""];
411 #if val is a hashref automatically match to name, otherwise fill as is.
412 my ($self, $val) = @_;
415 my $attrs = $evt->{'attrs'};
416 my $nm = $attrs->{'name'};
417 my $tar = defined $val && ref $val eq 'HASH' ? $val->{$nm} : $val;
419 if($evt->{'name'} eq 'select') {
420 #if we are select do something more complicated
421 warn "Can't do selects yet";
423 $evt->{'raw'} = undef;
424 $evt->{'raw_attrs'} = undef;
425 push @{$evt->{'attr_names'}}, 'value' unless exists $attrs->{'value'};
426 $attrs->{'value'} = $tar;
427 #check if we are a checkbox
428 if($attrs->{'type'} eq 'checkbox') {
430 push @{$evt->{'attr_names'}}, 'selected' unless exists $attrs->{'selected'};
431 $attrs->{'selected'} = $tar ? 'selected' : '';
433 delete $attrs->{'selected'};
434 $evt->{'attr_names'} = [ grep $_ ne 'selected', @{$evt->{'attr_names'}} ];
448 HTML::Zoom::FilterBuilder - Add Filters to a Stream
452 Create an L<HTML::Zoom> instance:
455 my $root = HTML::Zoom
459 <title>Default Title</title>
461 <body bad_attr='junk'>
467 Create a new attribute on the C<body> tag:
471 ->set_attribute(class=>'main');
473 Add a extra value to an existing attribute:
477 ->add_to_attribute(class=>'one-column');
479 Set the content of the C<title> tag:
483 ->replace_content('Hello World');
485 Set content from another L<HTML::Zoom> instance:
487 my $body = HTML::Zoom
491 <p id="p2">Is the Time</p>
497 ->replace_content($body);
499 Set an attribute on multiple matches:
503 ->set_attribute(class=>'para');
509 ->remove_attribute('bad_attr');
515 my $output = $root->to_html;
522 <title>Hello World</title>
524 <body class="main one-column"><div id="stuff">
525 <p class="para">Well Now</p>
526 <p id="p2" class="para">Is the Time</p>
534 is($output, $expect, 'Synopsis code works ok');
540 Given a L<HTML::Zoom> stream, provide methods to apply filters which
541 alter the content of that stream.
545 This class defines the following public API
549 Sets an attribute of a given name to a given value for all matching selections.
553 ->set_attribute(class=>'paragraph')
555 ->set_attribute({class=>'paragraph', name=>'divider'});
557 Overrides existing values, if such exist. When multiple L</set_attribute>
558 calls are made against the same or overlapping selection sets, the final
561 =head2 add_to_attribute
563 Adds a value to an existing attribute, or creates one if the attribute does not
564 yet exist. You may call this method with either an Array or HashRef of Args.
568 ->set_attribute({class => 'paragraph', name => 'test'})
570 ->add_to_attribute(class=>'divider');
572 Attributes with more than one value will have a dividing space.
574 =head2 remove_attribute
576 Removes an attribute and all its values.
580 ->set_attribute(class=>'paragraph')
582 ->remove_attribute('class');
584 =head2 remove_from_attribute
586 Removes a value from existing attribute
590 ->set_attribute(class=>'paragraph lead')
592 ->remove_from_attribute('class' => 'lead');
594 Removes attributes from the original stream or events already added.
598 Add to a class attribute
602 Remove from a class attribute
604 =head2 transform_attribute
606 Transforms (or creates or deletes) an attribute by running the passed
607 coderef on it. If the coderef returns nothing, the attribute is
612 ->transform_attribute( href => sub {
613 ( my $a = shift ) =~ s/localhost/example.com/;
620 Collects and extracts results of L<HTML::Zoom/select>. It takes the following
621 optional common options as hash reference.
625 =item into [ARRAY REFERENCE]
627 Where to save collected events (selected elements).
629 $z1->select('#main-content')
630 ->collect({ into => \@body })
632 $z2->select('#main-content')
638 Run filter on collected elements (locally setting $_ to stream, and passing
639 stream as an argument to given code reference). Filtered stream would be
644 filter => sub { $_->select('.inner')->replace_content('bar!') },
648 It can be used to further filter selection. For example
652 filter => sub { $_->select('td') },
656 is equivalent to (not implemented yet) descendant selector combination, i.e.
660 =item passthrough [BOOLEAN]
662 Extract copy of elements; the stream is unchanged (it does not remove collected
663 elements). For example without 'passthrough'
665 HTML::Zoom->from_html('<foo><bar /></foo>')
667 ->collect({ content => 1 })
670 returns '<foo></foo>', while with C<passthrough> option
672 HTML::Zoom->from_html('<foo><bar /></foo>')
674 ->collect({ content => 1, passthough => 1 })
677 returns '<foo><bar /></foo>'.
679 =item content [BOOLEAN]
681 Collect content of the element, and not the element itself.
685 HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
690 would return '<p>foo</p>', while
692 HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
694 ->collect({ content => 1 })
697 would return '<h1></h1><p>foo</p>'.
699 See also L</collect_content>.
701 =item flush_before [BOOLEAN]
703 Generate C<flush> event before collecting, to ensure that the HTML generated up
704 to selected element being collected is flushed throught to the browser. Usually
705 used in L</repeat> or L</repeat_content>.
709 =head2 collect_content
711 Collects contents of L<HTML::Zoom/select> result.
713 HTML::Zoom->from_file($foo)
714 ->select('#main-content')
715 ->collect_content({ into => \@foo_body })
718 ->replace_content(\@foo_body)
721 Equivalent to running L</collect> with C<content> option set.
725 Given a L<HTML::Zoom/select> result, add given content (which might be string,
726 array or another L<HTML::Zoom> object) before it.
729 ->select('input[name="foo"]')
730 ->add_before(\ '<span class="warning">required field</span>');
734 Like L</add_before>, only after L<HTML::Zoom/select> result.
740 You can add zoom events directly
744 ->add_after([ { type => 'TEXT', raw => 'O HAI' } ]);
746 =head2 prepend_content
748 Similar to add_before, but adds the content to the match.
751 ->from_html(q[<p>World</p>])
753 ->prepend_content("Hello ")
756 ## <p>Hello World</p>
758 Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
760 =head2 append_content
762 Similar to add_after, but adds the content to the match.
765 ->from_html(q[<p>Hello </p>])
767 ->prepend_content("World")
770 ## <p>Hello World</p>
772 Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
776 Given a L<HTML::Zoom/select> result, replace it with a string, array or another
777 L<HTML::Zoom> object. It takes the same optional common options as L</collect>
778 (via hash reference).
780 =head2 replace_content
782 Given a L<HTML::Zoom/select> result, replace the content with a string, array
783 or another L<HTML::Zoom> object.
786 ->select('title, #greeting')
787 ->replace_content('Hello world!');
791 For a given selection, repeat over transformations, typically for the purposes
792 of populating lists. Takes either an array of anonymous subroutines or a zoom-
793 able object consisting of transformation.
795 Example of array reference style (when it doesn't matter that all iterations are
798 $zoom->select('table')->repeat([
802 $_->select('td')->replace_content($e);
807 Subroutines would be run with $_ localized to result of L<HTML::Zoom/select> (of
808 collected elements), and with said result passed as parameter to subroutine.
810 You might want to use CodeStream when you don't have all elements upfront
812 $zoom->select('.contents')->repeat(sub {
813 HTML::Zoom::CodeStream->new({
815 while (my $line = $fh->getline) {
817 $_->select('.lno')->replace_content($fh->input_line_number)
818 ->select('.line')->replace_content($line)
826 In addition to common options as in L</collect>, it also supports:
830 =item repeat_between [SELECTOR]
832 Selects object to be repeated between items. In the case of array this object
833 is put between elements, in case of iterator it is put between results of
834 subsequent iterations, in the case of streamable it is put between events
837 See documentation for L</repeat_content>
841 =head2 repeat_content
843 Given a L<HTML::Zoom/select> result, run provided iterator passing content of
844 this result to this iterator. Accepts the same options as L</repeat>.
846 Equivalent to using C<contents> option with L</repeat>.
853 $_->select('.name')->replace_content('Matt')
854 ->select('.age')->replace_content('26')
857 $_->select('.name')->replace_content('Mark')
858 ->select('.age')->replace_content('0x29')
861 $_->select('.name')->replace_content('Epitaph')
862 ->select('.age')->replace_content('<redacted>')
865 { repeat_between => '.between' }
875 See L<HTML::Zoom> for authors.
879 See L<HTML::Zoom> for the license.