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