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