File Coverage

blib/lib/HTML/Zoom/FilterBuilder.pm
Criterion Covered Total %
statement 172 192 89.5
branch 79 94 84.0
condition 24 33 72.7
subroutine 35 41 85.3
pod 14 20 70.0
total 324 380 85.2


line stmt bran cond sub pod time code
1             package HTML::Zoom::FilterBuilder;
2              
3 13     13   1091 use strictures 1;
  13         95  
  13         367  
4 13     13   1099 use base qw(HTML::Zoom::SubObject);
  13         228  
  13         1394  
5 13     13   4597 use HTML::Zoom::CodeStream;
  13         28  
  13         48659  
6              
7             sub _stream_from_code {
8 129     129   697 shift->_zconfig->stream_utils->stream_from_code(@_)
9             }
10              
11             sub _stream_from_array {
12 51     51   163 shift->_zconfig->stream_utils->stream_from_array(@_)
13             }
14              
15             sub _stream_from_proto {
16 149     149   505 shift->_zconfig->stream_utils->stream_from_proto(@_)
17             }
18              
19             sub _stream_concat {
20 130     130   380 shift->_zconfig->stream_utils->stream_concat(@_)
21             }
22              
23             sub _flatten_stream_of_streams {
24 18     18   171 shift->_zconfig->stream_utils->flatten_stream_of_streams(@_)
25             }
26              
27 0     0 0 0 sub set_attr { shift->set_attribute(@_); }
28              
29             sub set_attribute {
30 5     5 1 25 my $self = shift;
31 5         21 my ($name, $value) = $self->_parse_attribute_args(@_);
32             sub {
33 6     6   17 my $a = (my $evt = $_[0])->{attrs};
34 6         16 my $e = exists $a->{$name};
35 4         39 +{ %$evt, raw => undef, raw_attrs => undef,
36             attrs => { %$a, $name => $value },
37             ($e # add to name list if not present
38             ? ()
39 6 100       66 : (attr_names => [ @{$evt->{attr_names}}, $name ]))
40             }
41 5         55 };
42             }
43              
44             sub _parse_attribute_args {
45 18     18   30 my $self = shift;
46              
47 18 0 33     74 die "Long form arg (name => 'class', value => 'x') is no longer supported"
      33        
48             if(@_ == 1 && $_[0]->{'name'} && $_[0]->{'value'});
49 18 50       58 my ($name, $value) = @_ > 1 ? @_ : @{$_[0]}{qw(name value)};
  0         0  
50 18         68 return ($name, $self->_zconfig->parser->html_escape($value));
51             }
52              
53             sub add_attribute {
54 0     0 0 0 die "renamed to add_to_attribute. killing this entirely for 1.0";
55             }
56              
57 0     0 0 0 sub add_class { shift->add_to_attribute('class',@_) }
58              
59 0     0 0 0 sub remove_class { shift->remove_attribute('class',@_) }
60              
61 0     0 0 0 sub set_class { shift->set_attribute('class',@_) }
62              
63 0     0 0 0 sub set_id { shift->set_attribute('id',@_) }
64              
65             sub add_to_attribute {
66 13     13 1 32 my $self = shift;
67 13         49 my ($name, $value) = $self->_parse_attribute_args(@_);
68             sub {
69 18     18   46 my $a = (my $evt = $_[0])->{attrs};
70 18         37 my $e = exists $a->{$name};
71 10         83 +{ %$evt, raw => undef, raw_attrs => undef,
72             attrs => {
73             %$a,
74             $name => join(' ', ($e ? $a->{$name} : ()), $value)
75             },
76             ($e # add to name list if not present
77             ? ()
78 18 100       200 : (attr_names => [ @{$evt->{attr_names}}, $name ]))
    100          
79             }
80 13         97 };
81             }
82              
83             sub remove_attribute {
84 3     3 1 21 my ($self, $args) = @_;
85 3 100       14 my $name = (ref($args) eq 'HASH') ? $args->{name} : $args;
86             sub {
87 3     3   9 my $a = (my $evt = $_[0])->{attrs};
88 3 100       13 return $evt unless exists $a->{$name};
89 2         10 $a = { %$a }; delete $a->{$name};
  2         14  
90 2         23 +{ %$evt, raw => undef, raw_attrs => undef,
91             attrs => $a,
92 2         9 attr_names => [ grep $_ ne $name, @{$evt->{attr_names}} ]
93             }
94 3         26 };
95             }
96              
97             sub transform_attribute {
98 4     4 1 47 my $self = shift;
99 4 50       12 my ( $name, $code ) = @_ > 1 ? @_ : @{$_[0]}{qw(name code)};
  4         12  
100              
101             sub {
102 4     4   9 my $evt = $_[0];
103 4         6 my %a = %{ $evt->{attrs} };
  4         16  
104 4         5 my @names = @{ $evt->{attr_names} };
  4         13  
105              
106 4         8 my $existed_before = exists $a{$name};
107 4         16 my $v = $code->( $a{$name} );
108 4   100     31 my $deleted = $existed_before && ! defined $v;
109 4   100     15 my $added = ! $existed_before && defined $v;
110 4 100       11 if( $added ) {
    100          
111 1         3 push @names, $name;
112 1         2 $a{$name} = $v;
113             }
114             elsif( $deleted ) {
115 1         3 delete $a{$name};
116 1         4 @names = grep $_ ne $name, @names;
117             } else {
118 2         4 $a{$name} = $v;
119             }
120 4 100 100     53 +{ %$evt, raw => undef, raw_attrs => undef,
121             attrs => \%a,
122             ( $deleted || $added
123             ? (attr_names => \@names )
124             : () )
125             }
126 4         41 };
127             }
128              
129             sub collect {
130 135     135 1 250 my ($self, $options) = @_;
131 135         392 my ($into, $passthrough, $content, $filter, $flush_before) =
132 135         206 @{$options}{qw(into passthrough content filter flush_before)};
133             sub {
134 135     135   220 my ($evt, $stream) = @_;
135             # We wipe the contents of @$into here so that other actions depending
136             # on this (such as a repeater) can be invoked multiple times easily.
137             # I -suspect- it's better for that state reset to be managed here; if it
138             # ever becomes painful the decision should be revisited
139 135 100       444 if ($into) {
140 25 100       93 @$into = $content ? () : ($evt);
141             }
142 135 100       404 if ($evt->{is_in_place_close}) {
143 6 100 66     50 return $evt if $passthrough || $content;
144 3         11 return;
145             }
146 129         249 my $name = $evt->{name};
147 129         170 my $depth = 1;
148 129 100       418 my $_next = $content ? 'peek' : 'next';
149 129 100       264 if ($filter) {
150 4 50       13 if ($content) {
151 4         7 $stream = do { local $_ = $stream; $filter->($stream) };
  4         8  
  4         14  
152             } else {
153 0         0 $stream = do {
154 0         0 local $_ = $self->_stream_concat(
155             $self->_stream_from_array($evt),
156             $stream,
157             );
158 0         0 $filter->($_);
159             };
160 0         0 $evt = $stream->next;
161             }
162             }
163             my $collector = $self->_stream_from_code(sub {
164 199 100       469 return unless $stream;
165 193         1020 while (my ($evt) = $stream->$_next) {
166 436 100       1330 $depth++ if ($evt->{type} eq 'OPEN');
167 436 100       996 $depth-- if ($evt->{type} eq 'CLOSE');
168 436 100       930 unless ($depth) {
169 129         303 undef $stream;
170 129 100       740 return if $content;
171 14 100       39 push(@$into, $evt) if $into;
172 14 100       52 return $evt if $passthrough;
173 8         41 return;
174             }
175 307 100       863 push(@$into, $evt) if $into;
176 307 100       1050 $stream->next if $content;
177 307 100       1407 return $evt if $passthrough;
178             }
179 0         0 die "Never saw closing </${name}> before end of source";
180 129         789 });
181 129 100       443 if ($flush_before) {
182 1 50 33     7 if ($passthrough||$content) {
183 0         0 $evt = { %$evt, flush => 1 };
184             } else {
185 1         5 $evt = { type => 'EMPTY', flush => 1 };
186             }
187             }
188 129 100 100     1196 return ($passthrough||$content||$flush_before)
189             ? [ $evt, $collector ]
190             : $collector;
191 135         894 };
192             }
193              
194             sub collect_content {
195 3     3 1 8 my ($self, $options) = @_;
196 3 50       6 $self->collect({ %{$options||{}}, content => 1 })
  3         25  
197             }
198              
199             sub add_before {
200 3     3 1 15 my ($self, $events) = @_;
201 3         19 my $coll_proto = $self->collect({ passthrough => 1 });
202             sub {
203 3     3   15 my $emit = $self->_stream_from_proto($events);
204 3         13 my $coll = &$coll_proto;
205 3 50       9 if($coll) {
206 3 50       20 if(ref $coll eq 'ARRAY') {
    0          
207 3         12 my $firstbit = $self->_stream_from_proto([$coll->[0]]);
208 3         19 return $self->_stream_concat($emit, $firstbit, $coll->[1]);
209             } elsif(ref $coll eq 'HASH') {
210 0         0 return [$emit, $coll];
211             } else {
212 0         0 return $self->_stream_concat($emit, $coll);
213             }
214 0         0 } else { return $emit }
215             }
216 3         30 }
217              
218             sub add_after {
219 3     3 1 14 my ($self, $events) = @_;
220 3         16 my $coll_proto = $self->collect({ passthrough => 1 });
221             sub {
222 3     3   6 my ($evt) = @_;
223 3         12 my $emit = $self->_stream_from_proto($events);
224 3         13 my $coll = &$coll_proto;
225 3 50       20 return ref($coll) eq 'HASH' # single event, no collect
226             ? [ $coll, $emit ]
227             : [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
228 3         32 };
229             }
230              
231             sub prepend_content {
232 6     6 1 23 my ($self, $events) = @_;
233 6         34 my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
234             sub {
235 6     6   14 my ($evt) = @_;
236 6         23 my $emit = $self->_stream_from_proto($events);
237 6 100       30 if ($evt->{is_in_place_close}) {
238 1         7 $evt = { %$evt }; delete @{$evt}{qw(raw is_in_place_close)};
  1         2  
  1         115  
239 1         5 return [ $evt, $self->_stream_from_array(
240             $emit->next, { type => 'CLOSE', name => $evt->{name} }
241             ) ];
242             }
243 5         15 my $coll = &$coll_proto;
244 5         20 return [ $coll->[0], $self->_stream_concat($emit, $coll->[1]) ];
245 6         50 };
246             }
247              
248             sub append_content {
249 3     3 1 25 my ($self, $events) = @_;
250 3         18 my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
251             sub {
252 3     3   8 my ($evt) = @_;
253 3         11 my $emit = $self->_stream_from_proto($events);
254 3 50       21 if ($evt->{is_in_place_close}) {
255 0         0 $evt = { %$evt }; delete @{$evt}{qw(raw is_in_place_close)};
  0         0  
  0         0  
256 0         0 return [ $evt, $self->_stream_from_array(
257             $emit->next, { type => 'CLOSE', name => $evt->{name} }
258             ) ];
259             }
260 3         10 my $coll = &$coll_proto;
261 3         13 return [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
262 3         29 };
263             }
264              
265             sub replace {
266 112     112 1 331 my ($self, $replace_with, $options) = @_;
267 112         280 my $coll_proto = $self->collect($options);
268             sub {
269 113     113   191 my ($evt, $stream) = @_;
270 113         292 my $emit = $self->_stream_from_proto($replace_with);
271 113         477 my $coll = &$coll_proto;
272             # if we're replacing the contents of an in place close
273             # then we need to handle that here
274 113 100 100     707 if ($options->{content}
      66        
275             && ref($coll) eq 'HASH'
276             && $coll->{is_in_place_close}
277             ) {
278 3         12 my $close = $stream->next;
279             # shallow copy and nuke in place and raw (to force smart print)
280 3         27 $_ = { %$_ }, delete @{$_}{qw(is_in_place_close raw)} for ($coll, $close);
  6         35  
281 3         9 $emit = $self->_stream_concat(
282             $emit,
283             $self->_stream_from_array($close),
284             );
285             }
286             # For a straightforward replace operation we can, in fact, do the emit
287             # -before- the collect, and my first cut did so. However in order to
288             # use the captured content in generating the new content, we need
289             # the collect stage to happen first - and it seems highly unlikely
290             # that in normal operation the collect phase will take long enough
291             # for the difference to be noticeable
292             return
293 113 100       637 ($coll
    100          
    50          
294             ? (ref $coll eq 'ARRAY' # [ event, stream ]
295             ? [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ]
296             : (ref $coll eq 'HASH' # event or stream?
297             ? [ $coll, $emit ]
298             : $self->_stream_concat($coll, $emit))
299             )
300             : $emit
301             );
302 112         1103 };
303             }
304              
305             sub replace_content {
306 74     74 1 143 my ($self, $replace_with, $options) = @_;
307 74 50       115 $self->replace($replace_with, { %{$options||{}}, content => 1 })
  74         742  
308             }
309              
310             sub repeat {
311 18     18 1 56 my ($self, $repeat_for, $options) = @_;
312 18         62 $options->{into} = \my @into;
313 18         29 my @between;
314 18         47 my $repeat_between = delete $options->{repeat_between};
315 18 100       50 if ($repeat_between) {
316             $options->{filter} = sub {
317 2     2   23 $_->select($repeat_between)->collect({ into => \@between })
318             }
319 2         11 }
320             my $repeater = sub {
321 18     18   63 my $s = $self->_stream_from_proto($repeat_for);
322             # We have to test $repeat_between not @between here because
323             # at the point we're constructing our return stream @between
324             # hasn't been populated yet - but we can test @between in the
325             # map routine because it has been by then and that saves us doing
326             # the extra stream construction if we don't need it.
327 18         36 $self->_flatten_stream_of_streams(do {
328 18 100       47 if ($repeat_between) {
329             $s->map(sub {
330 5         19 local $_ = $self->_stream_from_array(@into);
331 5 100 66     38 (@between && $s->peek)
332             ? $self->_stream_concat(
333             $_[0]->($_), $self->_stream_from_array(@between)
334             )
335             : $_[0]->($_)
336             })
337 2         22 } else {
338             $s->map(sub {
339 39         120 local $_ = $self->_stream_from_array(@into);
340 39         277 $_[0]->($_)
341             })
342 16         162 }
343             })
344 18         120 };
345 18         63 $self->replace($repeater, $options);
346             }
347              
348             sub repeat_content {
349 15     15 1 75 my ($self, $repeat_for, $options) = @_;
350 15 100       32 $self->repeat($repeat_for, { %{$options||{}}, content => 1 })
  15         130  
351             }
352              
353             1;
354              
355             =head1 NAME
356              
357             HTML::Zoom::FilterBuilder - Add Filters to a Stream
358              
359             =head1 SYNOPSIS
360              
361             Create an L<HTML::Zoom> instance:
362              
363             use HTML::Zoom;
364             my $root = HTML::Zoom
365             ->from_html(<<MAIN);
366             <html>
367             <head>
368             <title>Default Title</title>
369             </head>
370             <body bad_attr='junk'>
371             Default Content
372             </body>
373             </html>
374             MAIN
375              
376             Create a new attribute on the C<body> tag:
377              
378             $root = $root
379             ->select('body')
380             ->set_attribute(class=>'main');
381              
382             Add a extra value to an existing attribute:
383              
384             $root = $root
385             ->select('body')
386             ->add_to_attribute(class=>'one-column');
387              
388             Set the content of the C<title> tag:
389              
390             $root = $root
391             ->select('title')
392             ->replace_content('Hello World');
393              
394             Set content from another L<HTML::Zoom> instance:
395              
396             my $body = HTML::Zoom
397             ->from_html(<<BODY);
398             <div id="stuff">
399             <p>Well Now</p>
400             <p id="p2">Is the Time</p>
401             </div>
402             BODY
403              
404             $root = $root
405             ->select('body')
406             ->replace_content($body);
407              
408             Set an attribute on multiple matches:
409              
410             $root = $root
411             ->select('p')
412             ->set_attribute(class=>'para');
413              
414             Remove an attribute:
415              
416             $root = $root
417             ->select('body')
418             ->remove_attribute('bad_attr');
419              
420             will produce:
421              
422             =begin testinfo
423              
424             my $output = $root->to_html;
425             my $expect = <<HTML;
426              
427             =end testinfo
428              
429             <html>
430             <head>
431             <title>Hello World</title>
432             </head>
433             <body class="main one-column"><div id="stuff">
434             <p class="para">Well Now</p>
435             <p id="p2" class="para">Is the Time</p>
436             </div>
437             </body>
438             </html>
439              
440             =begin testinfo
441              
442             HTML
443             is($output, $expect, 'Synopsis code works ok');
444              
445             =end testinfo
446              
447             =head1 DESCRIPTION
448              
449             Given a L<HTML::Zoom> stream, provide methods to apply filters which
450             alter the content of that stream.
451              
452             =head1 METHODS
453              
454             This class defines the following public API
455              
456             =head2 set_attribute
457              
458             Sets an attribute of a given name to a given value for all matching selections.
459              
460             $html_zoom
461             ->select('p')
462             ->set_attribute(class=>'paragraph')
463             ->select('div')
464             ->set_attribute({name=>'class', value=>'divider'});
465              
466              
467             Overrides existing values, if such exist. When multiple L</set_attribute>
468             calls are made against the same or overlapping selection sets, the final
469             call wins.
470              
471             =head2 add_to_attribute
472              
473             Adds a value to an existing attribute, or creates one if the attribute does not
474             yet exist. You may call this method with either an Array or HashRef of Args.
475              
476             Here's the 'long form' HashRef:
477              
478             $html_zoom
479             ->select('p')
480             ->set_attribute(class=>'paragraph')
481             ->then
482             ->add_to_attribute({name=>'class', value=>'divider'});
483              
484             And the exact same effect using the 'short form' Array:
485              
486             $html_zoom
487             ->select('p')
488             ->set_attribute(class=>'paragraph')
489             ->then
490             ->add_to_attribute(class=>'divider');
491              
492             Attributes with more than one value will have a dividing space.
493              
494             =head2 remove_attribute
495              
496             Removes an attribute and all its values.
497              
498             $html_zoom
499             ->select('p')
500             ->set_attribute(class=>'paragraph')
501             ->then
502             ->remove_attribute('class');
503              
504             Removes attributes from the original stream or events already added.
505              
506             =head2 transform_attribute
507              
508             Transforms (or creates or deletes) an attribute by running the passed
509             coderef on it. If the coderef returns nothing, the attribute is
510             removed.
511              
512             $html_zoom
513             ->select('a')
514             ->transform_attribute( href => sub {
515             ( my $a = shift ) =~ s/localhost/example.com/;
516             return $a;
517             },
518             );
519              
520             =head2 collect
521              
522             Collects and extracts results of L<HTML::Zoom/select>. It takes the following
523             optional common options as hash reference.
524              
525             =over
526              
527             =item into [ARRAY REFERENCE]
528              
529             Where to save collected events (selected elements).
530              
531             $z1->select('#main-content')
532             ->collect({ into => \@body })
533             ->run;
534             $z2->select('#main-content')
535             ->replace(\@body)
536             ->memoize;
537              
538             =item filter [CODE]
539              
540             Run filter on collected elements (locally setting $_ to stream, and passing
541             stream as an argument to given code reference). Filtered stream would be
542             returned.
543              
544             $z->select('.outer')
545             ->collect({
546             filter => sub { $_->select('.inner')->replace_content('bar!') },
547             passthrough => 1,
548             })
549              
550             It can be used to further filter selection. For example
551              
552             $z->select('tr')
553             ->collect({
554             filter => sub { $_->select('td') },
555             passthrough => 1,
556             })
557              
558             is equivalent to (not implemented yet) descendant selector combination, i.e.
559              
560             $z->select('tr td')
561              
562             =item passthrough [BOOLEAN]
563              
564             Extract copy of elements; the stream is unchanged (it does not remove collected
565             elements). For example without 'passthrough'
566              
567             HTML::Zoom->from_html('<foo><bar /></foo>')
568             ->select('foo')
569             ->collect({ content => 1 })
570             ->to_html
571              
572             returns '<foo></foo>', while with C<passthrough> option
573              
574             HTML::Zoom->from_html('<foo><bar /></foo>')
575             ->select('foo')
576             ->collect({ content => 1, passthough => 1 })
577             ->to_html
578              
579             returns '<foo><bar /></foo>'.
580              
581             =item content [BOOLEAN]
582              
583             Collect content of the element, and not the element itself.
584              
585             For example
586              
587             HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
588             ->select('h1')
589             ->collect
590             ->to_html
591              
592             would return '<p>foo</p>', while
593              
594             HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
595             ->select('h1')
596             ->collect({ content => 1 })
597             ->to_html
598              
599             would return '<h1></h1><p>foo</p>'.
600              
601             See also L</collect_content>.
602              
603             =item flush_before [BOOLEAN]
604              
605             Generate C<flush> event before collecting, to ensure that the HTML generated up
606             to selected element being collected is flushed throught to the browser. Usually
607             used in L</repeat> or L</repeat_content>.
608              
609             =back
610              
611             =head2 collect_content
612              
613             Collects contents of L<HTML::Zoom/select> result.
614              
615             HTML::Zoom->from_file($foo)
616             ->select('#main-content')
617             ->collect_content({ into => \@foo_body })
618             ->run;
619             $z->select('#foo')
620             ->replace_content(\@foo_body)
621             ->memoize;
622              
623             Equivalent to running L</collect> with C<content> option set.
624              
625             =head2 add_before
626              
627             Given a L<HTML::Zoom/select> result, add given content (which might be string,
628             array or another L<HTML::Zoom> object) before it.
629              
630             $html_zoom
631             ->select('input[name="foo"]')
632             ->add_before(\ '<span class="warning">required field</span>');
633              
634             =head2 add_after
635              
636             Like L</add_before>, only after L<HTML::Zoom/select> result.
637              
638             $html_zoom
639             ->select('p')
640             ->add_after("\n\n");
641              
642             You can add zoom events directly
643              
644             $html_zoom
645             ->select('p')
646             ->add_after([ { type => 'TEXT', raw => 'O HAI' } ]);
647              
648             =head2 prepend_content
649              
650             Similar to add_before, but adds the content to the match.
651              
652             HTML::Zoom
653             ->from_html(q[<p>World</p>])
654             ->select('p')
655             ->prepend_content("Hello ")
656             ->to_html
657            
658             ## <p>Hello World</p>
659            
660             Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
661              
662             =head2 append_content
663              
664             Similar to add_after, but adds the content to the match.
665              
666             HTML::Zoom
667             ->from_html(q[<p>Hello </p>])
668             ->select('p')
669             ->prepend_content("World")
670             ->to_html
671            
672             ## <p>Hello World</p>
673              
674             Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
675              
676             =head2 replace
677              
678             Given a L<HTML::Zoom/select> result, replace it with a string, array or another
679             L<HTML::Zoom> object. It takes the same optional common options as L</collect>
680             (via hash reference).
681              
682             =head2 replace_content
683              
684             Given a L<HTML::Zoom/select> result, replace the content with a string, array
685             or another L<HTML::Zoom> object.
686              
687             $html_zoom
688             ->select('title, #greeting')
689             ->replace_content('Hello world!');
690              
691             =head2 repeat
692              
693             For a given selection, repeat over transformations, typically for the purposes
694             of populating lists. Takes either an array of anonymous subroutines or a zoom-
695             able object consisting of transformation.
696              
697             Example of array reference style (when it doesn't matter that all iterations are
698             pre-generated)
699              
700             $zoom->select('table')->repeat([
701             map {
702             my $elem = $_;
703             sub {
704             $_->select('td')->replace_content($e);
705             }
706             } @list
707             ]);
708            
709             Subroutines would be run with $_ localized to result of L<HTML::Zoom/select> (of
710             collected elements), and with said result passed as parameter to subroutine.
711              
712             You might want to use CodeStream when you don't have all elements upfront
713              
714             $zoom->select('.contents')->repeat(sub {
715             HTML::Zoom::CodeStream->new({
716             code => sub {
717             while (my $line = $fh->getline) {
718             return sub {
719             $_->select('.lno')->replace_content($fh->input_line_number)
720             ->select('.line')->replace_content($line)
721             }
722             }
723             return
724             },
725             })
726             });
727              
728             In addition to common options as in L</collect>, it also supports:
729              
730             =over
731              
732             =item repeat_between [SELECTOR]
733              
734             Selects object to be repeated between items. In the case of array this object
735             is put between elements, in case of iterator it is put between results of
736             subsequent iterations, in the case of streamable it is put between events
737             (->to_stream->next).
738              
739             See documentation for L</repeat_content>
740              
741             =back
742              
743             =head2 repeat_content
744              
745             Given a L<HTML::Zoom/select> result, run provided iterator passing content of
746             this result to this iterator. Accepts the same options as L</repeat>.
747              
748             Equivalent to using C<contents> option with L</repeat>.
749              
750             $html_zoom
751             ->select('#list')
752             ->repeat_content(
753             [
754             sub {
755             $_->select('.name')->replace_content('Matt')
756             ->select('.age')->replace_content('26')
757             },
758             sub {
759             $_->select('.name')->replace_content('Mark')
760             ->select('.age')->replace_content('0x29')
761             },
762             sub {
763             $_->select('.name')->replace_content('Epitaph')
764             ->select('.age')->replace_content('<redacted>')
765             },
766             ],
767             { repeat_between => '.between' }
768             );
769              
770              
771             =head1 ALSO SEE
772              
773             L<HTML::Zoom>
774              
775             =head1 AUTHORS
776              
777             See L<HTML::Zoom> for authors.
778              
779             =head1 LICENSE
780              
781             See L<HTML::Zoom> for the license.
782              
783             =cut
784