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 #die "Long form arg (name => 'class', value => 'x') is no longer supported"
46 #if(@_ == 1 && $_[0]->{'name'} && $_[0]->{'value'});
48 my $opts = ref($_[0]) eq 'HASH' ? $_[0] : {$_[0] => $_[1]};
49 for (values %{$opts}) { $self->_zconfig->parser->html_escape($_); }
54 die "renamed to add_to_attribute. killing this entirely for 1.0";
57 sub add_class { shift->add_to_attribute('class',@_) }
59 sub remove_class { shift->remove_from_attribute('class',@_) }
61 sub set_class { shift->set_attribute('class',@_) }
63 sub set_id { shift->set_attribute('id',@_) }
65 sub add_to_attribute {
67 my $attr = $self->_parse_attribute_args(@_);
69 my $a = (my $evt = $_[0])->{attrs};
70 my @kadd = grep {!exists $a->{$_}} keys %$attr;
71 +{ %$evt, raw => undef, raw_attrs => undef,
74 map {$_ => join(' ', (exists $a->{$_} ? $a->{$_} : ()), $attr->{$_}) }
77 @kadd ? (attr_names => [ @{$evt->{attr_names}}, @kadd ]) : ()
82 sub remove_from_attribute {
84 my $attr = $self->_parse_attribute_args(@_);
86 my $a = (my $evt = $_[0])->{attrs};
87 +{ %$evt, raw => undef, raw_attrs => undef,
90 #TODO needs to support multiple removes
91 map { my $tar = $_; $_ => join ' ',
92 map {$attr->{$tar} ne $_} split ' ', $a->{$_} }
93 grep {exists $a->{$_}} keys %$attr
99 sub remove_attribute {
100 my ($self, $args) = @_;
101 my $name = (ref($args) eq 'HASH') ? $args->{name} : $args;
103 my $a = (my $evt = $_[0])->{attrs};
104 return $evt unless exists $a->{$name};
105 $a = { %$a }; delete $a->{$name};
106 +{ %$evt, raw => undef, raw_attrs => undef,
108 attr_names => [ grep $_ ne $name, @{$evt->{attr_names}} ]
113 sub transform_attribute {
115 my ( $name, $code ) = @_ > 1 ? @_ : @{$_[0]}{qw(name code)};
119 my %a = %{ $evt->{attrs} };
120 my @names = @{ $evt->{attr_names} };
122 my $existed_before = exists $a{$name};
123 my $v = $code->( $a{$name} );
124 my $deleted = $existed_before && ! defined $v;
125 my $added = ! $existed_before && defined $v;
132 @names = grep $_ ne $name, @names;
136 +{ %$evt, raw => undef, raw_attrs => undef,
139 ? (attr_names => \@names )
146 my ($self, $options) = @_;
147 my ($into, $passthrough, $content, $filter, $flush_before) =
148 @{$options}{qw(into passthrough content filter flush_before)};
150 my ($evt, $stream) = @_;
151 # We wipe the contents of @$into here so that other actions depending
152 # on this (such as a repeater) can be invoked multiple times easily.
153 # I -suspect- it's better for that state reset to be managed here; if it
154 # ever becomes painful the decision should be revisited
156 @$into = $content ? () : ($evt);
158 if ($evt->{is_in_place_close}) {
159 return $evt if $passthrough || $content;
162 my $name = $evt->{name};
164 my $_next = $content ? 'peek' : 'next';
167 $stream = do { local $_ = $stream; $filter->($stream) };
170 local $_ = $self->_stream_concat(
171 $self->_stream_from_array($evt),
176 $evt = $stream->next;
179 my $collector = $self->_stream_from_code(sub {
180 return unless $stream;
181 while (my ($evt) = $stream->$_next) {
182 $depth++ if ($evt->{type} eq 'OPEN');
183 $depth-- if ($evt->{type} eq 'CLOSE');
187 push(@$into, $evt) if $into;
188 return $evt if $passthrough;
191 push(@$into, $evt) if $into;
192 $stream->next if $content;
193 return $evt if $passthrough;
195 die "Never saw closing </${name}> before end of source";
198 if ($passthrough||$content) {
199 $evt = { %$evt, flush => 1 };
201 $evt = { type => 'EMPTY', flush => 1 };
204 return ($passthrough||$content||$flush_before)
205 ? [ $evt, $collector ]
210 sub collect_content {
211 my ($self, $options) = @_;
212 $self->collect({ %{$options||{}}, content => 1 })
216 my ($self, $events) = @_;
217 my $coll_proto = $self->collect({ passthrough => 1 });
219 my $emit = $self->_stream_from_proto($events);
220 my $coll = &$coll_proto;
222 if(ref $coll eq 'ARRAY') {
223 my $firstbit = $self->_stream_from_proto([$coll->[0]]);
224 return $self->_stream_concat($emit, $firstbit, $coll->[1]);
225 } elsif(ref $coll eq 'HASH') {
226 return [$emit, $coll];
228 return $self->_stream_concat($emit, $coll);
230 } else { return $emit }
235 my ($self, $events) = @_;
236 my $coll_proto = $self->collect({ passthrough => 1 });
239 my $emit = $self->_stream_from_proto($events);
240 my $coll = &$coll_proto;
241 return ref($coll) eq 'HASH' # single event, no collect
243 : [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
247 sub prepend_content {
248 my ($self, $events) = @_;
249 my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
252 my $emit = $self->_stream_from_proto($events);
253 if ($evt->{is_in_place_close}) {
254 $evt = { %$evt }; delete @{$evt}{qw(raw is_in_place_close)};
255 return [ $evt, $self->_stream_from_array(
256 $emit->next, { type => 'CLOSE', name => $evt->{name} }
259 my $coll = &$coll_proto;
260 return [ $coll->[0], $self->_stream_concat($emit, $coll->[1]) ];
265 my ($self, $events) = @_;
266 my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
269 my $emit = $self->_stream_from_proto($events);
270 if ($evt->{is_in_place_close}) {
271 $evt = { %$evt }; delete @{$evt}{qw(raw is_in_place_close)};
272 return [ $evt, $self->_stream_from_array(
273 $emit->next, { type => 'CLOSE', name => $evt->{name} }
276 my $coll = &$coll_proto;
277 return [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
282 my ($self, $replace_with, $options) = @_;
283 my $coll_proto = $self->collect($options);
285 my ($evt, $stream) = @_;
286 my $emit = $self->_stream_from_proto($replace_with);
287 my $coll = &$coll_proto;
288 # if we're replacing the contents of an in place close
289 # then we need to handle that here
290 if ($options->{content}
291 && ref($coll) eq 'HASH'
292 && $coll->{is_in_place_close}
294 my $close = $stream->next;
295 # shallow copy and nuke in place and raw (to force smart print)
296 $_ = { %$_ }, delete @{$_}{qw(is_in_place_close raw)} for ($coll, $close);
297 $emit = $self->_stream_concat(
299 $self->_stream_from_array($close),
302 # For a straightforward replace operation we can, in fact, do the emit
303 # -before- the collect, and my first cut did so. However in order to
304 # use the captured content in generating the new content, we need
305 # the collect stage to happen first - and it seems highly unlikely
306 # that in normal operation the collect phase will take long enough
307 # for the difference to be noticeable
310 ? (ref $coll eq 'ARRAY' # [ event, stream ]
311 ? [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ]
312 : (ref $coll eq 'HASH' # event or stream?
314 : $self->_stream_concat($coll, $emit))
321 sub replace_content {
322 my ($self, $replace_with, $options) = @_;
323 $self->replace($replace_with, { %{$options||{}}, content => 1 })
327 my ($self, $repeat_for, $options) = @_;
328 $options->{into} = \my @into;
330 my $repeat_between = delete $options->{repeat_between};
331 if ($repeat_between) {
332 $options->{filter} = sub {
333 $_->select($repeat_between)->collect({ into => \@between })
337 my $s = $self->_stream_from_proto($repeat_for);
338 # We have to test $repeat_between not @between here because
339 # at the point we're constructing our return stream @between
340 # hasn't been populated yet - but we can test @between in the
341 # map routine because it has been by then and that saves us doing
342 # the extra stream construction if we don't need it.
343 $self->_flatten_stream_of_streams(do {
344 if ($repeat_between) {
346 local $_ = $self->_stream_from_array(@into);
347 (@between && $s->peek)
348 ? $self->_stream_concat(
349 $_[0]->($_), $self->_stream_from_array(@between)
355 local $_ = $self->_stream_from_array(@into);
361 $self->replace($repeater, $options);
365 my ($self, $repeat_for, $options) = @_;
366 $self->repeat($repeat_for, { %{$options||{}}, content => 1 })
370 my ($self, $to) = @_;
373 push @$to, $evt->{'attrs'}->{'name'};
383 $_->select('input')->validation_rules($to)
384 ->select('select')->validation_rules($to);
391 my ($self,$val) = @_;
395 $_->select('input')->val($val)
396 #->select('select')->val($val)
403 sub validation_rules {
404 my ($self, $to) = @_;
407 $to->{$evt->{'attrs'}->{'name'}}
408 = [split ' ', $evt->{'attrs'}->{'data-validate'}||""];
414 #if val is a hashref automatically match to name, otherwise fill as is.
415 my ($self, $val) = @_;
418 my $attrs = $evt->{'attrs'};
419 my $nm = $attrs->{'name'};
420 my $tar = defined $val && ref $val eq 'HASH' ? $val->{$nm} : $val;
422 if($evt->{'name'} eq 'select') {
423 #if we are select do something more complicated
424 warn "Can't do selects yet";
426 $evt->{'raw'} = undef;
427 $evt->{'raw_attrs'} = undef;
428 push @{$evt->{'attr_names'}}, 'value' unless exists $attrs->{'value'};
429 $attrs->{'value'} = $tar;
430 #check if we are a checkbox
431 if(exists $attrs->{'type'} && $attrs->{'type'} eq 'checkbox') {
433 push @{$evt->{'attr_names'}}, 'selected' unless exists $attrs->{'selected'};
434 $attrs->{'selected'} = $tar ? 'selected' : '';
436 delete $attrs->{'selected'};
437 $evt->{'attr_names'} = [ grep $_ ne 'selected', @{$evt->{'attr_names'}} ];
451 HTML::Zoom::FilterBuilder - Add Filters to a Stream
455 Create an L<HTML::Zoom> instance:
458 my $root = HTML::Zoom
462 <title>Default Title</title>
464 <body bad_attr='junk'>
470 Create a new attribute on the C<body> tag:
474 ->set_attribute(class=>'main');
476 Add a extra value to an existing attribute:
480 ->add_to_attribute(class=>'one-column');
482 Set the content of the C<title> tag:
486 ->replace_content('Hello World');
488 Set content from another L<HTML::Zoom> instance:
490 my $body = HTML::Zoom
494 <p id="p2">Is the Time</p>
500 ->replace_content($body);
502 Set an attribute on multiple matches:
506 ->set_attribute(class=>'para');
512 ->remove_attribute('bad_attr');
518 my $output = $root->to_html;
525 <title>Hello World</title>
527 <body class="main one-column"><div id="stuff">
528 <p class="para">Well Now</p>
529 <p id="p2" class="para">Is the Time</p>
537 is($output, $expect, 'Synopsis code works ok');
543 Given a L<HTML::Zoom> stream, provide methods to apply filters which
544 alter the content of that stream.
548 This class defines the following public API
552 Sets an attribute of a given name to a given value for all matching selections.
556 ->set_attribute(class=>'paragraph')
558 ->set_attribute({class=>'paragraph', name=>'divider'});
560 Overrides existing values, if such exist. When multiple L</set_attribute>
561 calls are made against the same or overlapping selection sets, the final
564 =head2 add_to_attribute
566 Adds a value to an existing attribute, or creates one if the attribute does not
567 yet exist. You may call this method with either an Array or HashRef of Args.
571 ->set_attribute({class => 'paragraph', name => 'test'})
573 ->add_to_attribute(class=>'divider');
575 Attributes with more than one value will have a dividing space.
577 =head2 remove_attribute
579 Removes an attribute and all its values.
583 ->set_attribute(class=>'paragraph')
585 ->remove_attribute('class');
587 =head2 remove_from_attribute
589 Removes a value from existing attribute
593 ->set_attribute(class=>'paragraph lead')
595 ->remove_from_attribute('class' => 'lead');
597 Removes attributes from the original stream or events already added.
601 Add to a class attribute
605 Remove from a class attribute
607 =head2 transform_attribute
609 Transforms (or creates or deletes) an attribute by running the passed
610 coderef on it. If the coderef returns nothing, the attribute is
615 ->transform_attribute( href => sub {
616 ( my $a = shift ) =~ s/localhost/example.com/;
623 Collects and extracts results of L<HTML::Zoom/select>. It takes the following
624 optional common options as hash reference.
628 =item into [ARRAY REFERENCE]
630 Where to save collected events (selected elements).
632 $z1->select('#main-content')
633 ->collect({ into => \@body })
635 $z2->select('#main-content')
641 Run filter on collected elements (locally setting $_ to stream, and passing
642 stream as an argument to given code reference). Filtered stream would be
647 filter => sub { $_->select('.inner')->replace_content('bar!') },
651 It can be used to further filter selection. For example
655 filter => sub { $_->select('td') },
659 is equivalent to (not implemented yet) descendant selector combination, i.e.
663 =item passthrough [BOOLEAN]
665 Extract copy of elements; the stream is unchanged (it does not remove collected
666 elements). For example without 'passthrough'
668 HTML::Zoom->from_html('<foo><bar /></foo>')
670 ->collect({ content => 1 })
673 returns '<foo></foo>', while with C<passthrough> option
675 HTML::Zoom->from_html('<foo><bar /></foo>')
677 ->collect({ content => 1, passthough => 1 })
680 returns '<foo><bar /></foo>'.
682 =item content [BOOLEAN]
684 Collect content of the element, and not the element itself.
688 HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
693 would return '<p>foo</p>', while
695 HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
697 ->collect({ content => 1 })
700 would return '<h1></h1><p>foo</p>'.
702 See also L</collect_content>.
704 =item flush_before [BOOLEAN]
706 Generate C<flush> event before collecting, to ensure that the HTML generated up
707 to selected element being collected is flushed throught to the browser. Usually
708 used in L</repeat> or L</repeat_content>.
712 =head2 collect_content
714 Collects contents of L<HTML::Zoom/select> result.
716 HTML::Zoom->from_file($foo)
717 ->select('#main-content')
718 ->collect_content({ into => \@foo_body })
721 ->replace_content(\@foo_body)
724 Equivalent to running L</collect> with C<content> option set.
728 Given a L<HTML::Zoom/select> result, add given content (which might be string,
729 array or another L<HTML::Zoom> object) before it.
732 ->select('input[name="foo"]')
733 ->add_before(\ '<span class="warning">required field</span>');
737 Like L</add_before>, only after L<HTML::Zoom/select> result.
743 You can add zoom events directly
747 ->add_after([ { type => 'TEXT', raw => 'O HAI' } ]);
749 =head2 prepend_content
751 Similar to add_before, but adds the content to the match.
754 ->from_html(q[<p>World</p>])
756 ->prepend_content("Hello ")
759 ## <p>Hello World</p>
761 Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
763 =head2 append_content
765 Similar to add_after, but adds the content to the match.
768 ->from_html(q[<p>Hello </p>])
770 ->prepend_content("World")
773 ## <p>Hello World</p>
775 Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
779 Given a L<HTML::Zoom/select> result, replace it with a string, array or another
780 L<HTML::Zoom> object. It takes the same optional common options as L</collect>
781 (via hash reference).
783 =head2 replace_content
785 Given a L<HTML::Zoom/select> result, replace the content with a string, array
786 or another L<HTML::Zoom> object.
789 ->select('title, #greeting')
790 ->replace_content('Hello world!');
794 For a given selection, repeat over transformations, typically for the purposes
795 of populating lists. Takes either an array of anonymous subroutines or a zoom-
796 able object consisting of transformation.
798 Example of array reference style (when it doesn't matter that all iterations are
801 $zoom->select('table')->repeat([
805 $_->select('td')->replace_content($e);
810 Subroutines would be run with $_ localized to result of L<HTML::Zoom/select> (of
811 collected elements), and with said result passed as parameter to subroutine.
813 You might want to use CodeStream when you don't have all elements upfront
815 $zoom->select('.contents')->repeat(sub {
816 HTML::Zoom::CodeStream->new({
818 while (my $line = $fh->getline) {
820 $_->select('.lno')->replace_content($fh->input_line_number)
821 ->select('.line')->replace_content($line)
829 In addition to common options as in L</collect>, it also supports:
833 =item repeat_between [SELECTOR]
835 Selects object to be repeated between items. In the case of array this object
836 is put between elements, in case of iterator it is put between results of
837 subsequent iterations, in the case of streamable it is put between events
840 See documentation for L</repeat_content>
844 =head2 repeat_content
846 Given a L<HTML::Zoom/select> result, run provided iterator passing content of
847 this result to this iterator. Accepts the same options as L</repeat>.
849 Equivalent to using C<contents> option with L</repeat>.
856 $_->select('.name')->replace_content('Matt')
857 ->select('.age')->replace_content('26')
860 $_->select('.name')->replace_content('Mark')
861 ->select('.age')->replace_content('0x29')
864 $_->select('.name')->replace_content('Epitaph')
865 ->select('.age')->replace_content('<redacted>')
868 { repeat_between => '.between' }
878 See L<HTML::Zoom> for authors.
882 See L<HTML::Zoom> for the license.