die instead
[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')
94a3ddd9 466 ->set_attribute({name=>'class', value=>'divider'});
434a11c8 467
f8ad684d 468
469Overrides existing values, if such exist. When multiple L</set_attribute>
470calls are made against the same or overlapping selection sets, the final
471call wins.
f6644c71 472
e225a4bd 473=head2 add_to_attribute
f6644c71 474
434a11c8 475Adds a value to an existing attribute, or creates one if the attribute does not
94a3ddd9 476yet exist. You may call this method with either an Array or HashRef of Args.
477
478Here's the 'long form' HashRef:
479
480 $html_zoom
481 ->select('p')
482 ->set_attribute(class=>'paragraph')
483 ->then
484 ->add_to_attribute({name=>'class', value=>'divider'});
485
486And the exact same effect using the 'short form' Array:
f6644c71 487
434a11c8 488 $html_zoom
489 ->select('p')
490 ->set_attribute(class=>'paragraph')
491 ->then
94a3ddd9 492 ->add_to_attribute(class=>'divider');
f6644c71 493
434a11c8 494Attributes with more than one value will have a dividing space.
495
e225a4bd 496=head2 remove_attribute
434a11c8 497
498Removes an attribute and all its values.
499
500 $html_zoom
501 ->select('p')
502 ->set_attribute(class=>'paragraph')
503 ->then
504 ->remove_attribute('class');
505
506Removes attributes from the original stream or events already added.
f6644c71 507
5cac799e 508=head2 transform_attribute
509
510Transforms (or creates or deletes) an attribute by running the passed
511coderef on it. If the coderef returns nothing, the attribute is
512removed.
513
514 $html_zoom
515 ->select('a')
516 ->transform_attribute( href => sub {
517 ( my $a = shift ) =~ s/localhost/example.com/;
518 return $a;
519 },
520 );
521
f6644c71 522=head2 collect
523
ac3acd87 524Collects and extracts results of L<HTML::Zoom/select>. It takes the following
525optional common options as hash reference.
526
527=over
528
529=item into [ARRAY REFERENCE]
530
531Where to save collected events (selected elements).
532
533 $z1->select('#main-content')
534 ->collect({ into => \@body })
535 ->run;
536 $z2->select('#main-content')
537 ->replace(\@body)
538 ->memoize;
539
540=item filter [CODE]
541
542Run filter on collected elements (locally setting $_ to stream, and passing
543stream as an argument to given code reference). Filtered stream would be
544returned.
545
546 $z->select('.outer')
547 ->collect({
548 filter => sub { $_->select('.inner')->replace_content('bar!') },
549 passthrough => 1,
550 })
551
552It can be used to further filter selection. For example
553
554 $z->select('tr')
555 ->collect({
556 filter => sub { $_->select('td') },
557 passthrough => 1,
558 })
559
560is equivalent to (not implemented yet) descendant selector combination, i.e.
561
562 $z->select('tr td')
563
564=item passthrough [BOOLEAN]
565
566Extract copy of elements; the stream is unchanged (it does not remove collected
567elements). For example without 'passthrough'
568
569 HTML::Zoom->from_html('<foo><bar /></foo>')
570 ->select('foo')
571 ->collect({ content => 1 })
572 ->to_html
573
574returns '<foo></foo>', while with C<passthrough> option
575
576 HTML::Zoom->from_html('<foo><bar /></foo>')
577 ->select('foo')
578 ->collect({ content => 1, passthough => 1 })
579 ->to_html
580
581returns '<foo><bar /></foo>'.
582
583=item content [BOOLEAN]
584
585Collect content of the element, and not the element itself.
586
587For example
588
589 HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
590 ->select('h1')
591 ->collect
592 ->to_html
593
594would return '<p>foo</p>', while
595
596 HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
597 ->select('h1')
598 ->collect({ content => 1 })
599 ->to_html
600
601would return '<h1></h1><p>foo</p>'.
602
603See also L</collect_content>.
604
605=item flush_before [BOOLEAN]
606
607Generate C<flush> event before collecting, to ensure that the HTML generated up
608to selected element being collected is flushed throught to the browser. Usually
609used in L</repeat> or L</repeat_content>.
610
611=back
f6644c71 612
613=head2 collect_content
614
ac3acd87 615Collects contents of L<HTML::Zoom/select> result.
616
617 HTML::Zoom->from_file($foo)
618 ->select('#main-content')
619 ->collect_content({ into => \@foo_body })
620 ->run;
621 $z->select('#foo')
622 ->replace_content(\@foo_body)
623 ->memoize;
624
625Equivalent to running L</collect> with C<content> option set.
f6644c71 626
627=head2 add_before
628
ac3acd87 629Given a L<HTML::Zoom/select> result, add given content (which might be string,
630array or another L<HTML::Zoom> object) before it.
631
632 $html_zoom
633 ->select('input[name="foo"]')
634 ->add_before(\ '<span class="warning">required field</span>');
f6644c71 635
636=head2 add_after
637
ac3acd87 638Like L</add_before>, only after L<HTML::Zoom/select> result.
639
640 $html_zoom
641 ->select('p')
642 ->add_after("\n\n");
643
644You can add zoom events directly
645
646 $html_zoom
647 ->select('p')
648 ->add_after([ { type => 'TEXT', raw => 'O HAI' } ]);
f6644c71 649
650=head2 prepend_content
651
94a3ddd9 652Similar to add_before, but adds the content to the match.
653
654 HTML::Zoom
655 ->from_html(q[<p>World</p>])
656 ->select('p')
657 ->prepend_content("Hello ")
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 append_content
665
94a3ddd9 666Similar to add_after, but adds the content to the match.
667
668 HTML::Zoom
669 ->from_html(q[<p>Hello </p>])
670 ->select('p')
671 ->prepend_content("World")
672 ->to_html
673
674 ## <p>Hello World</p>
675
676Acceptable values are strings, scalar refs and L<HTML::Zoom> objects
f6644c71 677
678=head2 replace
679
ac3acd87 680Given a L<HTML::Zoom/select> result, replace it with a string, array or another
681L<HTML::Zoom> object. It takes the same optional common options as L</collect>
682(via hash reference).
f6644c71 683
684=head2 replace_content
685
244252e7 686Given a L<HTML::Zoom/select> result, replace the content with a string, array
687or another L<HTML::Zoom> object.
f6644c71 688
ac3acd87 689 $html_zoom
690 ->select('title, #greeting')
691 ->replace_content('Hello world!');
692
f6644c71 693=head2 repeat
694
94a3ddd9 695For a given selection, repeat over transformations, typically for the purposes
696of populating lists. Takes either an array of anonymous subroutines or a zoom-
697able object consisting of transformation.
ac3acd87 698
94a3ddd9 699Example of array reference style (when it doesn't matter that all iterations are
700pre-generated)
ac3acd87 701
702 $zoom->select('table')->repeat([
703 map {
704 my $elem = $_;
705 sub {
706 $_->select('td')->replace_content($e);
707 }
708 } @list
709 ]);
94a3ddd9 710
711Subroutines would be run with $_ localized to result of L<HTML::Zoom/select> (of
712collected elements), and with said result passed as parameter to subroutine.
713
714You might want to use CodeStream when you don't have all elements upfront
715
716 $zoom->select('.contents')->repeat(sub {
717 HTML::Zoom::CodeStream->new({
718 code => sub {
719 while (my $line = $fh->getline) {
720 return sub {
721 $_->select('.lno')->replace_content($fh->input_line_number)
722 ->select('.line')->replace_content($line)
723 }
724 }
725 return
726 },
727 })
728 });
ac3acd87 729
94a3ddd9 730In addition to common options as in L</collect>, it also supports:
ac3acd87 731
732=over
733
734=item repeat_between [SELECTOR]
735
736Selects object to be repeated between items. In the case of array this object
737is put between elements, in case of iterator it is put between results of
738subsequent iterations, in the case of streamable it is put between events
739(->to_stream->next).
740
741See documentation for L</repeat_content>
742
743=back
f6644c71 744
745=head2 repeat_content
746
ac3acd87 747Given a L<HTML::Zoom/select> result, run provided iterator passing content of
748this result to this iterator. Accepts the same options as L</repeat>.
749
750Equivalent to using C<contents> option with L</repeat>.
751
752 $html_zoom
753 ->select('#list')
754 ->repeat_content(
755 [
756 sub {
757 $_->select('.name')->replace_content('Matt')
758 ->select('.age')->replace_content('26')
759 },
760 sub {
761 $_->select('.name')->replace_content('Mark')
762 ->select('.age')->replace_content('0x29')
763 },
764 sub {
765 $_->select('.name')->replace_content('Epitaph')
766 ->select('.age')->replace_content('<redacted>')
767 },
768 ],
769 { repeat_between => '.between' }
770 );
771
f6644c71 772
556c8616 773=head1 ALSO SEE
774
775L<HTML::Zoom>
776
777=head1 AUTHORS
778
779See L<HTML::Zoom> for authors.
780
781=head1 LICENSE
782
783See L<HTML::Zoom> for the license.
784
785=cut
786