Commit | Line | Data |
456a815d |
1 | package HTML::Zoom::FilterBuilder; |
2 | |
1cf03540 |
3 | use strictures 1; |
d80786d0 |
4 | use base qw(HTML::Zoom::SubObject); |
456a815d |
5 | use HTML::Zoom::CodeStream; |
6 | |
456a815d |
7 | sub _stream_from_code { |
d80786d0 |
8 | shift->_zconfig->stream_utils->stream_from_code(@_) |
456a815d |
9 | } |
10 | |
11 | sub _stream_from_array { |
d80786d0 |
12 | shift->_zconfig->stream_utils->stream_from_array(@_) |
456a815d |
13 | } |
14 | |
3cdbc13f |
15 | sub _stream_from_proto { |
d80786d0 |
16 | shift->_zconfig->stream_utils->stream_from_proto(@_) |
3cdbc13f |
17 | } |
18 | |
456a815d |
19 | sub _stream_concat { |
d80786d0 |
20 | shift->_zconfig->stream_utils->stream_concat(@_) |
456a815d |
21 | } |
22 | |
6d0f20a6 |
23 | sub _flatten_stream_of_streams { |
24 | shift->_zconfig->stream_utils->flatten_stream_of_streams(@_) |
25 | } |
26 | |
f0ddc273 |
27 | sub set_attr { shift->set_attribute(@_); } |
28 | |
456a815d |
29 | sub 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 |
42 | sub _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 |
50 | sub add_attribute { |
2daa653a |
51 | die "renamed to add_to_attribute. killing this entirely for 1.0"; |
52 | } |
53 | |
f0ddc273 |
54 | sub add_class { shift->add_to_attribute('class',@_) } |
55 | |
c84b68f3 |
56 | sub remove_class { shift->remove_from_attribute('class',@_) } |
f0ddc273 |
57 | |
58 | sub set_class { shift->set_attribute('class',@_) } |
59 | |
60 | sub set_id { shift->set_attribute('id',@_) } |
61 | |
2daa653a |
62 | sub 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 |
79 | sub 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 |
96 | sub 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 |
110 | sub 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 |
142 | sub 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 |
207 | sub collect_content { |
208 | my ($self, $options) = @_; |
209 | $self->collect({ %{$options||{}}, content => 1 }) |
210 | } |
211 | |
456a815d |
212 | sub 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 | |
231 | sub 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 |
244 | sub 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 |
261 | sub 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 |
278 | sub 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 |
318 | sub replace_content { |
319 | my ($self, $replace_with, $options) = @_; |
320 | $self->replace($replace_with, { %{$options||{}}, content => 1 }) |
321 | } |
322 | |
3cdbc13f |
323 | sub 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 |
361 | sub repeat_content { |
362 | my ($self, $repeat_for, $options) = @_; |
363 | $self->repeat($repeat_for, { %{$options||{}}, content => 1 }) |
364 | } |
365 | |
abe0a338 |
366 | sub extract_names { |
367 | my ($self, $to) = @_; |
368 | sub { |
369 | my ($evt) = @_; |
370 | push @$to, $evt->{'attrs'}->{'name'}; |
371 | $evt; |
372 | } |
373 | }; |
374 | |
375 | sub 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 |
387 | sub 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 | |
400 | sub 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 |
410 | sub 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 |
1105000a |
428 | if(exists $attrs->{'type'} && $attrs->{'type'} eq 'checkbox') { |
ff52d017 |
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 |
444 | 1; |
556c8616 |
445 | |
446 | =head1 NAME |
447 | |
448 | HTML::Zoom::FilterBuilder - Add Filters to a Stream |
449 | |
244252e7 |
450 | =head1 SYNOPSIS |
451 | |
a42917f6 |
452 | Create 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 |
467 | Create a new attribute on the C<body> tag: |
468 | |
469 | $root = $root |
470 | ->select('body') |
471 | ->set_attribute(class=>'main'); |
472 | |
473 | Add a extra value to an existing attribute: |
474 | |
475 | $root = $root |
476 | ->select('body') |
477 | ->add_to_attribute(class=>'one-column'); |
478 | |
479 | Set the content of the C<title> tag: |
480 | |
481 | $root = $root |
482 | ->select('title') |
483 | ->replace_content('Hello World'); |
484 | |
485 | Set 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 | |
499 | Set an attribute on multiple matches: |
500 | |
501 | $root = $root |
f8ad684d |
502 | ->select('p') |
a42917f6 |
503 | ->set_attribute(class=>'para'); |
504 | |
505 | Remove an attribute: |
506 | |
507 | $root = $root |
508 | ->select('body') |
509 | ->remove_attribute('bad_attr'); |
0d8f057e |
510 | |
511 | will 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 | |
540 | Given a L<HTML::Zoom> stream, provide methods to apply filters which |
541 | alter the content of that stream. |
542 | |
f6644c71 |
543 | =head1 METHODS |
544 | |
545 | This class defines the following public API |
546 | |
e225a4bd |
547 | =head2 set_attribute |
f6644c71 |
548 | |
f8ad684d |
549 | Sets 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 |
557 | Overrides existing values, if such exist. When multiple L</set_attribute> |
558 | calls are made against the same or overlapping selection sets, the final |
559 | call wins. |
f6644c71 |
560 | |
e225a4bd |
561 | =head2 add_to_attribute |
f6644c71 |
562 | |
434a11c8 |
563 | Adds a value to an existing attribute, or creates one if the attribute does not |
94a3ddd9 |
564 | yet 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 |
572 | Attributes with more than one value will have a dividing space. |
573 | |
e225a4bd |
574 | =head2 remove_attribute |
434a11c8 |
575 | |
576 | Removes 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 | |
586 | Removes 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 |
594 | Removes attributes from the original stream or events already added. |
f6644c71 |
595 | |
c84b68f3 |
596 | =head2 add_class |
597 | |
598 | Add to a class attribute |
599 | |
600 | =head2 remove_class |
601 | |
602 | Remove from a class attribute |
603 | |
5cac799e |
604 | =head2 transform_attribute |
605 | |
606 | Transforms (or creates or deletes) an attribute by running the passed |
607 | coderef on it. If the coderef returns nothing, the attribute is |
608 | removed. |
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 |
620 | Collects and extracts results of L<HTML::Zoom/select>. It takes the following |
621 | optional common options as hash reference. |
622 | |
623 | =over |
624 | |
625 | =item into [ARRAY REFERENCE] |
626 | |
627 | Where 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 | |
638 | Run filter on collected elements (locally setting $_ to stream, and passing |
639 | stream as an argument to given code reference). Filtered stream would be |
640 | returned. |
641 | |
642 | $z->select('.outer') |
643 | ->collect({ |
644 | filter => sub { $_->select('.inner')->replace_content('bar!') }, |
645 | passthrough => 1, |
646 | }) |
647 | |
648 | It 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 | |
656 | is equivalent to (not implemented yet) descendant selector combination, i.e. |
657 | |
658 | $z->select('tr td') |
659 | |
660 | =item passthrough [BOOLEAN] |
661 | |
662 | Extract copy of elements; the stream is unchanged (it does not remove collected |
663 | elements). For example without 'passthrough' |
664 | |
665 | HTML::Zoom->from_html('<foo><bar /></foo>') |
666 | ->select('foo') |
667 | ->collect({ content => 1 }) |
668 | ->to_html |
669 | |
670 | returns '<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 | |
677 | returns '<foo><bar /></foo>'. |
678 | |
679 | =item content [BOOLEAN] |
680 | |
681 | Collect content of the element, and not the element itself. |
682 | |
683 | For example |
684 | |
685 | HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>') |
686 | ->select('h1') |
687 | ->collect |
688 | ->to_html |
689 | |
690 | would 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 | |
697 | would return '<h1></h1><p>foo</p>'. |
698 | |
699 | See also L</collect_content>. |
700 | |
701 | =item flush_before [BOOLEAN] |
702 | |
703 | Generate C<flush> event before collecting, to ensure that the HTML generated up |
704 | to selected element being collected is flushed throught to the browser. Usually |
705 | used in L</repeat> or L</repeat_content>. |
706 | |
707 | =back |
f6644c71 |
708 | |
709 | =head2 collect_content |
710 | |
ac3acd87 |
711 | Collects 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 | |
721 | Equivalent to running L</collect> with C<content> option set. |
f6644c71 |
722 | |
723 | =head2 add_before |
724 | |
ac3acd87 |
725 | Given a L<HTML::Zoom/select> result, add given content (which might be string, |
726 | array 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 |
734 | Like L</add_before>, only after L<HTML::Zoom/select> result. |
735 | |
736 | $html_zoom |
737 | ->select('p') |
738 | ->add_after("\n\n"); |
739 | |
740 | You 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 |
748 | Similar 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 | |
758 | Acceptable values are strings, scalar refs and L<HTML::Zoom> objects |
f6644c71 |
759 | |
760 | =head2 append_content |
761 | |
94a3ddd9 |
762 | Similar 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 | |
772 | Acceptable values are strings, scalar refs and L<HTML::Zoom> objects |
f6644c71 |
773 | |
774 | =head2 replace |
775 | |
ac3acd87 |
776 | Given a L<HTML::Zoom/select> result, replace it with a string, array or another |
777 | L<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 |
782 | Given a L<HTML::Zoom/select> result, replace the content with a string, array |
783 | or 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 |
791 | For a given selection, repeat over transformations, typically for the purposes |
792 | of populating lists. Takes either an array of anonymous subroutines or a zoom- |
793 | able object consisting of transformation. |
ac3acd87 |
794 | |
94a3ddd9 |
795 | Example of array reference style (when it doesn't matter that all iterations are |
796 | pre-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 | |
807 | Subroutines would be run with $_ localized to result of L<HTML::Zoom/select> (of |
808 | collected elements), and with said result passed as parameter to subroutine. |
809 | |
810 | You 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 |
826 | In addition to common options as in L</collect>, it also supports: |
ac3acd87 |
827 | |
828 | =over |
829 | |
830 | =item repeat_between [SELECTOR] |
831 | |
832 | Selects object to be repeated between items. In the case of array this object |
833 | is put between elements, in case of iterator it is put between results of |
834 | subsequent iterations, in the case of streamable it is put between events |
835 | (->to_stream->next). |
836 | |
837 | See documentation for L</repeat_content> |
838 | |
839 | =back |
f6644c71 |
840 | |
841 | =head2 repeat_content |
842 | |
ac3acd87 |
843 | Given a L<HTML::Zoom/select> result, run provided iterator passing content of |
844 | this result to this iterator. Accepts the same options as L</repeat>. |
845 | |
846 | Equivalent 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 | |
871 | L<HTML::Zoom> |
872 | |
873 | =head1 AUTHORS |
874 | |
875 | See L<HTML::Zoom> for authors. |
876 | |
877 | =head1 LICENSE |
878 | |
879 | See L<HTML::Zoom> for the license. |
880 | |
881 | =cut |
882 | |