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