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