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