remove die as it stops actual use cases
[catagits/HTML-Zoom.git] / lib / HTML / Zoom / FilterBuilder.pm
1 package HTML::Zoom::FilterBuilder;
2
3 use strictures 1;
4 use base qw(HTML::Zoom::SubObject);
5 use HTML::Zoom::CodeStream;
6
7 sub _stream_from_code {
8   shift->_zconfig->stream_utils->stream_from_code(@_)
9 }
10
11 sub _stream_from_array {
12   shift->_zconfig->stream_utils->stream_from_array(@_)
13 }
14
15 sub _stream_from_proto {
16   shift->_zconfig->stream_utils->stream_from_proto(@_)
17 }
18
19 sub _stream_concat {
20   shift->_zconfig->stream_utils->stream_concat(@_)
21 }
22
23 sub _flatten_stream_of_streams {
24   shift->_zconfig->stream_utils->flatten_stream_of_streams(@_)
25 }
26
27 sub set_attr { shift->set_attribute(@_); }
28
29 sub set_attribute {
30   my $self = shift;
31   my $attr = $self->_parse_attribute_args(@_);
32   sub {
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 ]) : ()
38      }
39    };
40 }
41
42 sub _parse_attribute_args {
43   my $self = shift;
44
45   #die "Long form arg (name => 'class', value => 'x') is no longer supported"
46     #if(@_ == 1 && $_[0]->{'name'} && $_[0]->{'value'});
47
48   my $opts = ref($_[0]) eq 'HASH' ? $_[0] : {$_[0] => $_[1]};
49   for (values %{$opts}) { $self->_zconfig->parser->html_escape($_); }
50   return $opts;
51 }
52
53 sub add_attribute {
54     die "renamed to add_to_attribute. killing this entirely for 1.0";
55 }
56
57 sub add_class { shift->add_to_attribute('class',@_) }
58
59 sub remove_class { shift->remove_from_attribute('class',@_) }
60
61 sub set_class { shift->set_attribute('class',@_) }
62
63 sub set_id { shift->set_attribute('id',@_) }
64
65 sub add_to_attribute {
66   my $self = shift;
67   my $attr = $self->_parse_attribute_args(@_);
68   sub {
69     my $a = (my $evt = $_[0])->{attrs};
70     my @kadd = grep {!exists $a->{$_}} keys %$attr;
71     +{ %$evt, raw => undef, raw_attrs => undef,
72        attrs => {
73          %$a,
74          map {$_ => join(' ', (exists $a->{$_} ? $a->{$_} : ()), $attr->{$_}) } 
75           keys %$attr
76       },
77       @kadd ? (attr_names => [ @{$evt->{attr_names}}, @kadd ]) : ()
78     }
79   };
80 }
81
82 sub remove_from_attribute {
83   my $self = shift;
84   my $attr = $self->_parse_attribute_args(@_);
85   sub {
86     my $a = (my $evt = $_[0])->{attrs};
87     +{ %$evt, raw => undef, raw_attrs => undef,
88        attrs => {
89          %$a,
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
94       },
95     }
96   };
97 }
98
99 sub remove_attribute {
100   my ($self, $args) = @_;
101   my $name = (ref($args) eq 'HASH') ? $args->{name} : $args;
102   sub {
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,
107        attrs => $a,
108        attr_names => [ grep $_ ne $name, @{$evt->{attr_names}} ]
109     }
110   };
111 }
112
113 sub transform_attribute {
114   my $self = shift;
115   my ( $name, $code ) = @_ > 1 ? @_ : @{$_[0]}{qw(name code)};
116
117   sub {
118     my $evt = $_[0];
119     my %a = %{ $evt->{attrs} };
120     my @names = @{ $evt->{attr_names} };
121
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;
126     if( $added ) {
127         push @names, $name;
128         $a{$name} = $v;
129     }
130     elsif( $deleted ) {
131         delete $a{$name};
132         @names = grep $_ ne $name, @names;
133     } else {
134         $a{$name} = $v;
135     }
136     +{ %$evt, raw => undef, raw_attrs => undef,
137        attrs => \%a,
138       ( $deleted || $added
139         ? (attr_names => \@names )
140         : () )
141      }
142    };
143 }
144
145 sub collect {
146   my ($self, $options) = @_;
147   my ($into, $passthrough, $content, $filter, $flush_before) =
148     @{$options}{qw(into passthrough content filter flush_before)};
149   sub {
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
155     if ($into) {
156       @$into = $content ? () : ($evt);
157     }
158     if ($evt->{is_in_place_close}) {
159       return $evt if $passthrough || $content;
160       return;
161     }
162     my $name = $evt->{name};
163     my $depth = 1;
164     my $_next = $content ? 'peek' : 'next';
165     if ($filter) {
166       if ($content) {
167         $stream = do { local $_ = $stream; $filter->($stream) };
168       } else {
169         $stream = do {
170           local $_ = $self->_stream_concat(
171                        $self->_stream_from_array($evt),
172                        $stream,
173                      );
174           $filter->($_);
175         };
176         $evt = $stream->next;
177       }
178     }
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');
184         unless ($depth) {
185           undef $stream;
186           return if $content;
187           push(@$into, $evt) if $into;
188           return $evt if $passthrough;
189           return;
190         }
191         push(@$into, $evt) if $into;
192         $stream->next if $content;
193         return $evt if $passthrough;
194       }
195       die "Never saw closing </${name}> before end of source";
196     });
197     if ($flush_before) {
198       if ($passthrough||$content) {
199         $evt = { %$evt, flush => 1 };
200       } else {
201         $evt = { type => 'EMPTY', flush => 1 };
202       }
203     }
204     return ($passthrough||$content||$flush_before)
205              ? [ $evt, $collector ]
206              : $collector;
207   };
208 }
209
210 sub collect_content {
211   my ($self, $options) = @_;
212   $self->collect({ %{$options||{}}, content => 1 })
213 }
214
215 sub add_before {
216   my ($self, $events) = @_;
217   my $coll_proto = $self->collect({ passthrough => 1 });
218   sub {
219     my $emit = $self->_stream_from_proto($events);
220     my $coll = &$coll_proto;
221     if($coll) {
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];
227       } else {
228         return $self->_stream_concat($emit, $coll);
229       }
230     } else { return $emit }
231   }
232 }
233
234 sub add_after {
235   my ($self, $events) = @_;
236   my $coll_proto = $self->collect({ passthrough => 1 });
237   sub {
238     my ($evt) = @_;
239     my $emit = $self->_stream_from_proto($events);
240     my $coll = &$coll_proto;
241     return ref($coll) eq 'HASH' # single event, no collect
242       ? [ $coll, $emit ]
243       : [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
244   };
245 }
246
247 sub prepend_content {
248   my ($self, $events) = @_;
249   my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
250   sub {
251     my ($evt) = @_;
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} }
257       ) ];
258     }
259     my $coll = &$coll_proto;
260     return [ $coll->[0], $self->_stream_concat($emit, $coll->[1]) ];
261   };
262 }
263
264 sub append_content {
265   my ($self, $events) = @_;
266   my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
267   sub {
268     my ($evt) = @_;
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} }
274       ) ];
275     }
276     my $coll = &$coll_proto;
277     return [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
278   };
279 }
280
281 sub replace {
282   my ($self, $replace_with, $options) = @_;
283   my $coll_proto = $self->collect($options);
284   sub {
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}
293       ) {
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(
298                 $emit,
299                 $self->_stream_from_array($close),
300               );
301     }
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
308     return
309       ($coll
310         ? (ref $coll eq 'ARRAY' # [ event, stream ]
311             ? [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ]
312             : (ref $coll eq 'HASH' # event or stream?
313                  ? [ $coll, $emit ]
314                  : $self->_stream_concat($coll, $emit))
315           )
316         : $emit
317       );
318   };
319 }
320
321 sub replace_content {
322   my ($self, $replace_with, $options) = @_;
323   $self->replace($replace_with, { %{$options||{}}, content => 1 })
324 }
325
326 sub repeat {
327   my ($self, $repeat_for, $options) = @_;
328   $options->{into} = \my @into;
329   my @between;
330   my $repeat_between = delete $options->{repeat_between};
331   if ($repeat_between) {
332     $options->{filter} = sub {
333       $_->select($repeat_between)->collect({ into => \@between })
334     }
335   }
336   my $repeater = sub {
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) {
345         $s->map(sub {
346               local $_ = $self->_stream_from_array(@into);
347               (@between && $s->peek)
348                 ? $self->_stream_concat(
349                     $_[0]->($_), $self->_stream_from_array(@between)
350                   )
351                 : $_[0]->($_)
352             })
353       } else {
354         $s->map(sub {
355               local $_ = $self->_stream_from_array(@into);
356               $_[0]->($_)
357           })
358       }
359     })
360   };
361   $self->replace($repeater, $options);
362 }
363
364 sub repeat_content {
365   my ($self, $repeat_for, $options) = @_;
366   $self->repeat($repeat_for, { %{$options||{}}, content => 1 })
367 }
368
369 sub extract_names {
370   my ($self, $to) = @_;
371   sub {
372     my ($evt) = @_;
373     push @$to, $evt->{'attrs'}->{'name'};
374     $evt;
375   }
376 };
377
378 sub validate_form {
379   my ($self,$to) = @_;
380   $self->collect({ 
381     filter => sub {
382       return 
383         $_->select('input')->validation_rules($to)
384         ->select('select')->validation_rules($to);
385     },
386     passthrough => 1,
387   });
388 }
389
390 sub fill_form {
391   my ($self,$val) = @_;
392   $self->collect({ 
393     filter => sub {
394       return 
395         $_->select('input')->val($val)
396         #->select('select')->val($val)
397         ;
398     },
399     passthrough => 1,
400   });
401 }
402
403 sub validation_rules {
404   my ($self, $to) = @_;
405   sub {
406     my ($evt) = @_;
407     $to->{$evt->{'attrs'}->{'name'}} 
408       = [split ' ', $evt->{'attrs'}->{'data-validate'}||""];
409     $evt;
410   }
411 }
412
413 sub val {
414   #if val is a hashref automatically match to name, otherwise fill as is.
415   my ($self, $val) = @_;
416   sub {
417     my ($evt) = @_;
418     my $attrs = $evt->{'attrs'};
419     my $nm = $attrs->{'name'};
420     my $tar = defined $val && ref $val eq 'HASH' ? $val->{$nm} : $val;
421     if(defined $tar) {
422       if($evt->{'name'} eq 'select') {
423         #if we are select do something more complicated
424         warn "Can't do selects yet";
425       } else {
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') {
432           if($tar) {
433             push @{$evt->{'attr_names'}}, 'selected' unless exists $attrs->{'selected'};
434             $attrs->{'selected'} = $tar ? 'selected' : '';
435           } else {
436             delete $attrs->{'selected'};
437             $evt->{'attr_names'} = [ grep $_ ne 'selected', @{$evt->{'attr_names'}} ];
438           }
439         }
440       }
441     }
442     $evt;
443   }
444 }
445
446
447 1;
448
449 =head1 NAME
450
451 HTML::Zoom::FilterBuilder - Add Filters to a Stream
452
453 =head1 SYNOPSIS
454
455 Create an L<HTML::Zoom> instance:
456
457   use HTML::Zoom;
458   my $root = HTML::Zoom
459       ->from_html(<<MAIN);
460   <html>
461     <head>
462       <title>Default Title</title>
463     </head>
464     <body bad_attr='junk'>
465       Default Content
466     </body>
467   </html>
468   MAIN
469
470 Create a new attribute on the  C<body> tag:
471
472   $root = $root
473     ->select('body')
474     ->set_attribute(class=>'main');
475
476 Add a extra value to an existing attribute:
477
478   $root = $root
479     ->select('body')
480     ->add_to_attribute(class=>'one-column');
481
482 Set the content of the C<title> tag:
483
484   $root = $root
485     ->select('title')
486     ->replace_content('Hello World');
487
488 Set content from another L<HTML::Zoom> instance:
489
490   my $body = HTML::Zoom
491       ->from_html(<<BODY);
492   <div id="stuff">
493       <p>Well Now</p>
494       <p id="p2">Is the Time</p>
495   </div>
496   BODY
497
498   $root = $root
499     ->select('body')
500     ->replace_content($body);
501
502 Set an attribute on multiple matches:
503
504   $root = $root
505     ->select('p')
506     ->set_attribute(class=>'para');
507
508 Remove an attribute:
509
510   $root = $root
511     ->select('body')
512     ->remove_attribute('bad_attr');
513
514 will produce:
515
516 =begin testinfo
517
518   my $output = $root->to_html;
519   my $expect = <<HTML;
520
521 =end testinfo
522
523   <html>
524     <head>
525       <title>Hello World</title>
526     </head>
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>
530   </div>
531   </body>
532   </html>
533
534 =begin testinfo
535
536   HTML
537   is($output, $expect, 'Synopsis code works ok');
538
539 =end testinfo
540
541 =head1 DESCRIPTION
542
543 Given a L<HTML::Zoom> stream, provide methods to apply filters which
544 alter the content of that stream.
545
546 =head1 METHODS
547
548 This class defines the following public API
549
550 =head2 set_attribute
551
552 Sets an attribute of a given name to a given value for all matching selections.
553
554     $html_zoom
555       ->select('p')
556       ->set_attribute(class=>'paragraph')
557       ->select('div')
558       ->set_attribute({class=>'paragraph', name=>'divider'});
559
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
562 call wins.
563
564 =head2 add_to_attribute
565
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.
568
569     $html_zoom
570       ->select('p')
571       ->set_attribute({class => 'paragraph', name => 'test'})
572       ->then
573       ->add_to_attribute(class=>'divider');
574
575 Attributes with more than one value will have a dividing space.
576
577 =head2 remove_attribute
578
579 Removes an attribute and all its values.
580
581     $html_zoom
582       ->select('p')
583       ->set_attribute(class=>'paragraph')
584       ->then
585       ->remove_attribute('class');
586
587 =head2 remove_from_attribute
588
589 Removes a value from existing attribute
590
591     $html_zoom
592       ->select('p')
593       ->set_attribute(class=>'paragraph lead')
594       ->then
595       ->remove_from_attribute('class' => 'lead');
596
597 Removes attributes from the original stream or events already added.
598
599 =head2 add_class
600
601 Add to a class attribute
602
603 =head2 remove_class
604
605 Remove from a class attribute
606
607 =head2 transform_attribute
608
609 Transforms (or creates or deletes) an attribute by running the passed
610 coderef on it.  If the coderef returns nothing, the attribute is
611 removed.
612
613     $html_zoom
614       ->select('a')
615       ->transform_attribute( href => sub {
616             ( my $a = shift ) =~ s/localhost/example.com/;
617             return $a;
618           },
619         );
620
621 =head2 collect
622
623 Collects and extracts results of L<HTML::Zoom/select>.  It takes the following
624 optional common options as hash reference.
625
626 =over
627
628 =item into [ARRAY REFERENCE]
629
630 Where to save collected events (selected elements).
631
632     $z1->select('#main-content')
633        ->collect({ into => \@body })
634        ->run;
635     $z2->select('#main-content')
636        ->replace(\@body)
637        ->memoize;
638
639 =item filter [CODE]
640
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
643 returned.
644
645     $z->select('.outer')
646       ->collect({
647         filter => sub { $_->select('.inner')->replace_content('bar!') },
648         passthrough => 1,
649       })
650
651 It can be used to further filter selection.  For example
652
653     $z->select('tr')
654       ->collect({
655         filter => sub { $_->select('td') },
656         passthrough => 1,
657       })
658
659 is equivalent to (not implemented yet) descendant selector combination, i.e.
660
661     $z->select('tr td')
662
663 =item passthrough [BOOLEAN]
664
665 Extract copy of elements; the stream is unchanged (it does not remove collected
666 elements).  For example without 'passthrough'
667
668     HTML::Zoom->from_html('<foo><bar /></foo>')
669       ->select('foo')
670       ->collect({ content => 1 })
671       ->to_html
672
673 returns '<foo></foo>', while with C<passthrough> option
674
675     HTML::Zoom->from_html('<foo><bar /></foo>')
676       ->select('foo')
677       ->collect({ content => 1, passthough => 1 })
678       ->to_html
679
680 returns '<foo><bar /></foo>'.
681
682 =item content [BOOLEAN]
683
684 Collect content of the element, and not the element itself.
685
686 For example
687
688     HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
689       ->select('h1')
690       ->collect
691       ->to_html
692
693 would return '<p>foo</p>', while
694
695     HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
696       ->select('h1')
697       ->collect({ content => 1 })
698       ->to_html
699
700 would return '<h1></h1><p>foo</p>'.
701
702 See also L</collect_content>.
703
704 =item flush_before [BOOLEAN]
705
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>.
709
710 =back
711
712 =head2 collect_content
713
714 Collects contents of L<HTML::Zoom/select> result.
715
716     HTML::Zoom->from_file($foo)
717               ->select('#main-content')
718               ->collect_content({ into => \@foo_body })
719               ->run;
720     $z->select('#foo')
721       ->replace_content(\@foo_body)
722       ->memoize;
723
724 Equivalent to running L</collect> with C<content> option set.
725
726 =head2 add_before
727
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.
730
731     $html_zoom
732         ->select('input[name="foo"]')
733         ->add_before(\ '<span class="warning">required field</span>');
734
735 =head2 add_after
736
737 Like L</add_before>, only after L<HTML::Zoom/select> result.
738
739     $html_zoom
740         ->select('p')
741         ->add_after("\n\n");
742
743 You can add zoom events directly
744
745     $html_zoom
746         ->select('p')
747         ->add_after([ { type => 'TEXT', raw => 'O HAI' } ]);
748
749 =head2 prepend_content
750
751 Similar to add_before, but adds the content to the match.
752
753   HTML::Zoom
754     ->from_html(q[<p>World</p>])
755     ->select('p')
756     ->prepend_content("Hello ")
757     ->to_html
758     
759   ## <p>Hello World</p>
760   
761 Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
762
763 =head2 append_content
764
765 Similar to add_after, but adds the content to the match.
766
767   HTML::Zoom
768     ->from_html(q[<p>Hello </p>])
769     ->select('p')
770     ->prepend_content("World")
771     ->to_html
772     
773   ## <p>Hello World</p>
774
775 Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
776
777 =head2 replace
778
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).
782
783 =head2 replace_content
784
785 Given a L<HTML::Zoom/select> result, replace the content with a string, array
786 or another L<HTML::Zoom> object.
787
788     $html_zoom
789       ->select('title, #greeting')
790       ->replace_content('Hello world!');
791
792 =head2 repeat
793
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.
797
798 Example of array reference style (when it doesn't matter that all iterations are
799 pre-generated)
800
801     $zoom->select('table')->repeat([
802       map {
803         my $elem = $_;
804         sub {
805           $_->select('td')->replace_content($e);
806         }
807       } @list
808     ]);
809     
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.
812
813 You might want to use CodeStream when you don't have all elements upfront
814
815     $zoom->select('.contents')->repeat(sub {
816       HTML::Zoom::CodeStream->new({
817         code => sub {
818           while (my $line = $fh->getline) {
819             return sub {
820               $_->select('.lno')->replace_content($fh->input_line_number)
821                 ->select('.line')->replace_content($line)
822             }
823           }
824           return
825         },
826       })
827     });
828
829 In addition to common options as in L</collect>, it also supports:
830
831 =over
832
833 =item repeat_between [SELECTOR]
834
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
838 (->to_stream->next).
839
840 See documentation for L</repeat_content>
841
842 =back
843
844 =head2 repeat_content
845
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>.
848
849 Equivalent to using C<contents> option with L</repeat>.
850
851     $html_zoom
852        ->select('#list')
853        ->repeat_content(
854           [
855              sub {
856                 $_->select('.name')->replace_content('Matt')
857                   ->select('.age')->replace_content('26')
858              },
859              sub {
860                 $_->select('.name')->replace_content('Mark')
861                   ->select('.age')->replace_content('0x29')
862              },
863              sub {
864                 $_->select('.name')->replace_content('Epitaph')
865                   ->select('.age')->replace_content('<redacted>')
866              },
867           ],
868           { repeat_between => '.between' }
869        );
870
871
872 =head1 ALSO SEE
873
874 L<HTML::Zoom>
875
876 =head1 AUTHORS
877
878 See L<HTML::Zoom> for authors.
879
880 =head1 LICENSE
881
882 See L<HTML::Zoom> for the license.
883
884 =cut
885