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