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