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