patches from rt
[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;
f0ddc273 46
ac652399 47 die "Long form arg (name => 'class', value => 'x') is no longer supported"
f0ddc273 48 if(@_ == 1 && $_[0]->{'name'} && $_[0]->{'value'});
1c4455ae 49 my ($name, $value) = @_ > 1 ? @_ : @{$_[0]}{qw(name value)};
50 return ($name, $self->_zconfig->parser->html_escape($value));
51}
52
456a815d 53sub add_attribute {
2daa653a 54 die "renamed to add_to_attribute. killing this entirely for 1.0";
55}
56
f0ddc273 57sub add_class { shift->add_to_attribute('class',@_) }
58
59sub remove_class { shift->remove_attribute('class',@_) }
60
61sub set_class { shift->set_attribute('class',@_) }
62
63sub set_id { shift->set_attribute('id',@_) }
64
2daa653a 65sub add_to_attribute {
1c4455ae 66 my $self = shift;
67 my ($name, $value) = $self->_parse_attribute_args(@_);
456a815d 68 sub {
8f962884 69 my $a = (my $evt = $_[0])->{attrs};
456a815d 70 my $e = exists $a->{$name};
71 +{ %$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 : (attr_names => [ @{$evt->{attr_names}}, $name ]))
79 }
80 };
81}
82
83sub remove_attribute {
84 my ($self, $args) = @_;
1c4455ae 85 my $name = (ref($args) eq 'HASH') ? $args->{name} : $args;
456a815d 86 sub {
8f962884 87 my $a = (my $evt = $_[0])->{attrs};
456a815d 88 return $evt unless exists $a->{$name};
89 $a = { %$a }; delete $a->{$name};
90 +{ %$evt, raw => undef, raw_attrs => undef,
91 attrs => $a,
92 attr_names => [ grep $_ ne $name, @{$evt->{attr_names}} ]
93 }
94 };
95}
96
5cac799e 97sub transform_attribute {
98 my $self = shift;
99 my ( $name, $code ) = @_ > 1 ? @_ : @{$_[0]}{qw(name code)};
100
101 sub {
102 my $evt = $_[0];
103 my %a = %{ $evt->{attrs} };
104 my @names = @{ $evt->{attr_names} };
105
106 my $existed_before = exists $a{$name};
107 my $v = $code->( $a{$name} );
108 my $deleted = $existed_before && ! defined $v;
109 my $added = ! $existed_before && defined $v;
110 if( $added ) {
111 push @names, $name;
112 $a{$name} = $v;
113 }
114 elsif( $deleted ) {
115 delete $a{$name};
116 @names = grep $_ ne $name, @names;
117 } else {
118 $a{$name} = $v;
119 }
120 +{ %$evt, raw => undef, raw_attrs => undef,
121 attrs => \%a,
122 ( $deleted || $added
123 ? (attr_names => \@names )
124 : () )
125 }
126 };
127}
128
76cecb10 129sub collect {
130 my ($self, $options) = @_;
1c4455ae 131 my ($into, $passthrough, $content, $filter, $flush_before) =
132 @{$options}{qw(into passthrough content filter flush_before)};
76cecb10 133 sub {
134 my ($evt, $stream) = @_;
b4d044eb 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 if ($into) {
865bb5d2 140 @$into = $content ? () : ($evt);
b4d044eb 141 }
76cecb10 142 if ($evt->{is_in_place_close}) {
865bb5d2 143 return $evt if $passthrough || $content;
76cecb10 144 return;
145 }
146 my $name = $evt->{name};
147 my $depth = 1;
865bb5d2 148 my $_next = $content ? 'peek' : 'next';
2abde91e 149 if ($filter) {
150 if ($content) {
151 $stream = do { local $_ = $stream; $filter->($stream) };
152 } else {
153 $stream = do {
154 local $_ = $self->_stream_concat(
155 $self->_stream_from_array($evt),
156 $stream,
157 );
158 $filter->($_);
159 };
160 $evt = $stream->next;
161 }
162 }
76cecb10 163 my $collector = $self->_stream_from_code(sub {
164 return unless $stream;
165 while (my ($evt) = $stream->$_next) {
166 $depth++ if ($evt->{type} eq 'OPEN');
167 $depth-- if ($evt->{type} eq 'CLOSE');
168 unless ($depth) {
169 undef $stream;
865bb5d2 170 return if $content;
76cecb10 171 push(@$into, $evt) if $into;
172 return $evt if $passthrough;
173 return;
174 }
175 push(@$into, $evt) if $into;
865bb5d2 176 $stream->next if $content;
76cecb10 177 return $evt if $passthrough;
178 }
179 die "Never saw closing </${name}> before end of source";
180 });
1c4455ae 181 if ($flush_before) {
182 if ($passthrough||$content) {
183 $evt = { %$evt, flush => 1 };
184 } else {
185 $evt = { type => 'EMPTY', flush => 1 };
186 }
187 }
188 return ($passthrough||$content||$flush_before)
189 ? [ $evt, $collector ]
190 : $collector;
76cecb10 191 };
192}
193
865bb5d2 194sub collect_content {
195 my ($self, $options) = @_;
196 $self->collect({ %{$options||{}}, content => 1 })
197}
198
456a815d 199sub add_before {
200 my ($self, $events) = @_;
94a3ddd9 201 my $coll_proto = $self->collect({ passthrough => 1 });
202 sub {
203 my $emit = $self->_stream_from_proto($events);
204 my $coll = &$coll_proto;
205 if($coll) {
206 if(ref $coll eq 'ARRAY') {
207 my $firstbit = $self->_stream_from_proto([$coll->[0]]);
208 return $self->_stream_concat($emit, $firstbit, $coll->[1]);
209 } elsif(ref $coll eq 'HASH') {
210 return [$emit, $coll];
211 } else {
212 return $self->_stream_concat($emit, $coll);
213 }
214 } else { return $emit }
215 }
456a815d 216}
217
218sub add_after {
219 my ($self, $events) = @_;
b616863d 220 my $coll_proto = $self->collect({ passthrough => 1 });
456a815d 221 sub {
8f962884 222 my ($evt) = @_;
94a3ddd9 223 my $emit = $self->_stream_from_proto($events);
b616863d 224 my $coll = &$coll_proto;
995bc8be 225 return ref($coll) eq 'HASH' # single event, no collect
226 ? [ $coll, $emit ]
227 : [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
456a815d 228 };
8f962884 229}
456a815d 230
865bb5d2 231sub prepend_content {
456a815d 232 my ($self, $events) = @_;
94a3ddd9 233 my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
456a815d 234 sub {
8f962884 235 my ($evt) = @_;
94a3ddd9 236 my $emit = $self->_stream_from_proto($events);
456a815d 237 if ($evt->{is_in_place_close}) {
238 $evt = { %$evt }; delete @{$evt}{qw(raw is_in_place_close)};
239 return [ $evt, $self->_stream_from_array(
94a3ddd9 240 $emit->next, { type => 'CLOSE', name => $evt->{name} }
456a815d 241 ) ];
242 }
94a3ddd9 243 my $coll = &$coll_proto;
244 return [ $coll->[0], $self->_stream_concat($emit, $coll->[1]) ];
456a815d 245 };
246}
247
865bb5d2 248sub append_content {
8f962884 249 my ($self, $events) = @_;
865bb5d2 250 my $coll_proto = $self->collect({ passthrough => 1, content => 1 });
8f962884 251 sub {
252 my ($evt) = @_;
94a3ddd9 253 my $emit = $self->_stream_from_proto($events);
8f962884 254 if ($evt->{is_in_place_close}) {
255 $evt = { %$evt }; delete @{$evt}{qw(raw is_in_place_close)};
256 return [ $evt, $self->_stream_from_array(
94a3ddd9 257 $emit->next, { type => 'CLOSE', name => $evt->{name} }
8f962884 258 ) ];
259 }
b616863d 260 my $coll = &$coll_proto;
8f962884 261 return [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ];
262 };
263}
264
456a815d 265sub replace {
3cdbc13f 266 my ($self, $replace_with, $options) = @_;
b616863d 267 my $coll_proto = $self->collect($options);
456a815d 268 sub {
269 my ($evt, $stream) = @_;
3cdbc13f 270 my $emit = $self->_stream_from_proto($replace_with);
b616863d 271 my $coll = &$coll_proto;
a88c1c57 272 # if we're replacing the contents of an in place close
273 # then we need to handle that here
274 if ($options->{content}
275 && ref($coll) eq 'HASH'
ec687101 276 && $coll->{is_in_place_close}
a88c1c57 277 ) {
a88c1c57 278 my $close = $stream->next;
ec687101 279 # shallow copy and nuke in place and raw (to force smart print)
280 $_ = { %$_ }, delete @{$_}{qw(is_in_place_close raw)} for ($coll, $close);
a88c1c57 281 $emit = $self->_stream_concat(
282 $emit,
283 $self->_stream_from_array($close),
284 );
285 }
451b3b30 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
11cc25dd 292 return
293 ($coll
a88c1c57 294 ? (ref $coll eq 'ARRAY' # [ event, stream ]
451b3b30 295 ? [ $coll->[0], $self->_stream_concat($coll->[1], $emit) ]
a88c1c57 296 : (ref $coll eq 'HASH' # event or stream?
297 ? [ $coll, $emit ]
298 : $self->_stream_concat($coll, $emit))
11cc25dd 299 )
300 : $emit
301 );
456a815d 302 };
303}
304
865bb5d2 305sub replace_content {
306 my ($self, $replace_with, $options) = @_;
307 $self->replace($replace_with, { %{$options||{}}, content => 1 })
308}
309
3cdbc13f 310sub repeat {
311 my ($self, $repeat_for, $options) = @_;
312 $options->{into} = \my @into;
f8ed299b 313 my @between;
314 my $repeat_between = delete $options->{repeat_between};
315 if ($repeat_between) {
f8ed299b 316 $options->{filter} = sub {
d80786d0 317 $_->select($repeat_between)->collect({ into => \@between })
94a3ddd9 318 }
f8ed299b 319 }
3cdbc13f 320 my $repeater = sub {
f8ed299b 321 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.
6d0f20a6 327 $self->_flatten_stream_of_streams(do {
328 if ($repeat_between) {
329 $s->map(sub {
330 local $_ = $self->_stream_from_array(@into);
331 (@between && $s->peek)
332 ? $self->_stream_concat(
333 $_[0]->($_), $self->_stream_from_array(@between)
334 )
335 : $_[0]->($_)
336 })
337 } else {
338 $s->map(sub {
339 local $_ = $self->_stream_from_array(@into);
340 $_[0]->($_)
f8ed299b 341 })
6d0f20a6 342 }
343 })
3cdbc13f 344 };
345 $self->replace($repeater, $options);
346}
347
865bb5d2 348sub repeat_content {
349 my ($self, $repeat_for, $options) = @_;
350 $self->repeat($repeat_for, { %{$options||{}}, content => 1 })
351}
352
456a815d 3531;
556c8616 354
355=head1 NAME
356
357HTML::Zoom::FilterBuilder - Add Filters to a Stream
358
244252e7 359=head1 SYNOPSIS
360
a42917f6 361Create an L<HTML::Zoom> instance:
362
0d8f057e 363 use HTML::Zoom;
364 my $root = HTML::Zoom
365 ->from_html(<<MAIN);
366 <html>
367 <head>
368 <title>Default Title</title>
369 </head>
a42917f6 370 <body bad_attr='junk'>
0d8f057e 371 Default Content
372 </body>
373 </html>
374 MAIN
375
a42917f6 376Create a new attribute on the C<body> tag:
377
378 $root = $root
379 ->select('body')
380 ->set_attribute(class=>'main');
381
382Add a extra value to an existing attribute:
383
384 $root = $root
385 ->select('body')
386 ->add_to_attribute(class=>'one-column');
387
388Set the content of the C<title> tag:
389
390 $root = $root
391 ->select('title')
392 ->replace_content('Hello World');
393
394Set content from another L<HTML::Zoom> instance:
395
0d8f057e 396 my $body = HTML::Zoom
397 ->from_html(<<BODY);
398 <div id="stuff">
2daa653a 399 <p>Well Now</p>
f8ad684d 400 <p id="p2">Is the Time</p>
0d8f057e 401 </div>
402 BODY
403
a42917f6 404 $root = $root
f8ad684d 405 ->select('body')
a42917f6 406 ->replace_content($body);
407
408Set an attribute on multiple matches:
409
410 $root = $root
f8ad684d 411 ->select('p')
a42917f6 412 ->set_attribute(class=>'para');
413
414Remove an attribute:
415
416 $root = $root
417 ->select('body')
418 ->remove_attribute('bad_attr');
0d8f057e 419
420will produce:
421
422=begin testinfo
423
a42917f6 424 my $output = $root->to_html;
0d8f057e 425 my $expect = <<HTML;
426
427=end testinfo
428
429 <html>
430 <head>
431 <title>Hello World</title>
432 </head>
434a11c8 433 <body class="main one-column"><div id="stuff">
adb30a8a 434 <p class="para">Well Now</p>
a42917f6 435 <p id="p2" class="para">Is the Time</p>
0d8f057e 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
244252e7 446
556c8616 447=head1 DESCRIPTION
448
449Given a L<HTML::Zoom> stream, provide methods to apply filters which
450alter the content of that stream.
451
f6644c71 452=head1 METHODS
453
454This class defines the following public API
455
e225a4bd 456=head2 set_attribute
f6644c71 457
f8ad684d 458Sets 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')
94a3ddd9 464 ->set_attribute({name=>'class', value=>'divider'});
434a11c8 465
f8ad684d 466
467Overrides existing values, if such exist. When multiple L</set_attribute>
468calls are made against the same or overlapping selection sets, the final
469call wins.
f6644c71 470
e225a4bd 471=head2 add_to_attribute
f6644c71 472
434a11c8 473Adds a value to an existing attribute, or creates one if the attribute does not
94a3ddd9 474yet exist. You may call this method with either an Array or HashRef of Args.
475
476Here'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
484And the exact same effect using the 'short form' Array:
f6644c71 485
434a11c8 486 $html_zoom
487 ->select('p')
488 ->set_attribute(class=>'paragraph')
489 ->then
94a3ddd9 490 ->add_to_attribute(class=>'divider');
f6644c71 491
434a11c8 492Attributes with more than one value will have a dividing space.
493
e225a4bd 494=head2 remove_attribute
434a11c8 495
496Removes 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
504Removes attributes from the original stream or events already added.
f6644c71 505
5cac799e 506=head2 transform_attribute
507
508Transforms (or creates or deletes) an attribute by running the passed
509coderef on it. If the coderef returns nothing, the attribute is
510removed.
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
f6644c71 520=head2 collect
521
ac3acd87 522Collects and extracts results of L<HTML::Zoom/select>. It takes the following
523optional common options as hash reference.
524
525=over
526
527=item into [ARRAY REFERENCE]
528
529Where 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
540Run filter on collected elements (locally setting $_ to stream, and passing
541stream as an argument to given code reference). Filtered stream would be
542returned.
543
544 $z->select('.outer')
545 ->collect({
546 filter => sub { $_->select('.inner')->replace_content('bar!') },
547 passthrough => 1,
548 })
549
550It 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
558is equivalent to (not implemented yet) descendant selector combination, i.e.
559
560 $z->select('tr td')
561
562=item passthrough [BOOLEAN]
563
564Extract copy of elements; the stream is unchanged (it does not remove collected
565elements). For example without 'passthrough'
566
567 HTML::Zoom->from_html('<foo><bar /></foo>')
568 ->select('foo')
569 ->collect({ content => 1 })
570 ->to_html
571
572returns '<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
579returns '<foo><bar /></foo>'.
580
581=item content [BOOLEAN]
582
583Collect content of the element, and not the element itself.
584
585For example
586
587 HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
588 ->select('h1')
589 ->collect
590 ->to_html
591
592would 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
599would return '<h1></h1><p>foo</p>'.
600
601See also L</collect_content>.
602
603=item flush_before [BOOLEAN]
604
605Generate C<flush> event before collecting, to ensure that the HTML generated up
606to selected element being collected is flushed throught to the browser. Usually
607used in L</repeat> or L</repeat_content>.
608
609=back
f6644c71 610
611=head2 collect_content
612
ac3acd87 613Collects 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
623Equivalent to running L</collect> with C<content> option set.
f6644c71 624
625=head2 add_before
626
ac3acd87 627Given a L<HTML::Zoom/select> result, add given content (which might be string,
628array 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>');
f6644c71 633
634=head2 add_after
635
ac3acd87 636Like L</add_before>, only after L<HTML::Zoom/select> result.
637
638 $html_zoom
639 ->select('p')
640 ->add_after("\n\n");
641
642You can add zoom events directly
643
644 $html_zoom
645 ->select('p')
646 ->add_after([ { type => 'TEXT', raw => 'O HAI' } ]);
f6644c71 647
648=head2 prepend_content
649
94a3ddd9 650Similar 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
660Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
f6644c71 661
662=head2 append_content
663
94a3ddd9 664Similar 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
674Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
f6644c71 675
676=head2 replace
677
ac3acd87 678Given a L<HTML::Zoom/select> result, replace it with a string, array or another
679L<HTML::Zoom> object. It takes the same optional common options as L</collect>
680(via hash reference).
f6644c71 681
682=head2 replace_content
683
244252e7 684Given a L<HTML::Zoom/select> result, replace the content with a string, array
685or another L<HTML::Zoom> object.
f6644c71 686
ac3acd87 687 $html_zoom
688 ->select('title, #greeting')
689 ->replace_content('Hello world!');
690
f6644c71 691=head2 repeat
692
94a3ddd9 693For a given selection, repeat over transformations, typically for the purposes
694of populating lists. Takes either an array of anonymous subroutines or a zoom-
695able object consisting of transformation.
ac3acd87 696
94a3ddd9 697Example of array reference style (when it doesn't matter that all iterations are
698pre-generated)
ac3acd87 699
700 $zoom->select('table')->repeat([
701 map {
702 my $elem = $_;
703 sub {
704 $_->select('td')->replace_content($e);
705 }
706 } @list
707 ]);
94a3ddd9 708
709Subroutines would be run with $_ localized to result of L<HTML::Zoom/select> (of
710collected elements), and with said result passed as parameter to subroutine.
711
712You 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 });
ac3acd87 727
94a3ddd9 728In addition to common options as in L</collect>, it also supports:
ac3acd87 729
730=over
731
732=item repeat_between [SELECTOR]
733
734Selects object to be repeated between items. In the case of array this object
735is put between elements, in case of iterator it is put between results of
736subsequent iterations, in the case of streamable it is put between events
737(->to_stream->next).
738
739See documentation for L</repeat_content>
740
741=back
f6644c71 742
743=head2 repeat_content
744
ac3acd87 745Given a L<HTML::Zoom/select> result, run provided iterator passing content of
746this result to this iterator. Accepts the same options as L</repeat>.
747
748Equivalent 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
f6644c71 770
556c8616 771=head1 ALSO SEE
772
773L<HTML::Zoom>
774
775=head1 AUTHORS
776
777See L<HTML::Zoom> for authors.
778
779=head1 LICENSE
780
781See L<HTML::Zoom> for the license.
782
783=cut
784