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