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