bump version
[catagits/HTML-Zoom.git] / lib / HTML / Zoom.pm
CommitLineData
d80786d0 1package HTML::Zoom;
2
1cf03540 3use strictures 1;
d80786d0 4
5use HTML::Zoom::ZConfig;
bf5a23d0 6use HTML::Zoom::ReadFH;
655965b3 7use HTML::Zoom::Transform;
eeeb0921 8use HTML::Zoom::TransformBuilder;
94a3ddd9 9use Scalar::Util ();
d80786d0 10
9d554d62 11our $VERSION = '0.009008';
7af7362d 12
13$VERSION = eval $VERSION;
14
d80786d0 15sub new {
16 my ($class, $args) = @_;
17 my $new = {};
18 $new->{zconfig} = HTML::Zoom::ZConfig->new($args->{zconfig}||{});
19 bless($new, $class);
20}
21
22sub zconfig { shift->_self_or_new->{zconfig} }
23
24sub _self_or_new {
25 ref($_[0]) ? $_[0] : $_[0]->new
26}
27
28sub _with {
29 bless({ %{$_[0]}, %{$_[1]} }, ref($_[0]));
30}
31
7567494d 32sub from_events {
d80786d0 33 my $self = shift->_self_or_new;
34 $self->_with({
7567494d 35 initial_events => shift,
d80786d0 36 });
37}
38
7567494d 39sub from_html {
40 my $self = shift->_self_or_new;
41 $self->from_events($self->zconfig->parser->html_to_events($_[0]))
42}
43
bf5a23d0 44sub from_file {
45 my $self = shift->_self_or_new;
46 my $filename = shift;
47 $self->from_html(do { local (@ARGV, $/) = ($filename); <> });
48}
49
d80786d0 50sub to_stream {
51 my $self = shift;
52 die "No events to build from - forgot to call from_html?"
53 unless $self->{initial_events};
54 my $sutils = $self->zconfig->stream_utils;
55 my $stream = $sutils->stream_from_array(@{$self->{initial_events}});
2f0c6a86 56 $stream = $_->apply_to_stream($stream) for @{$self->{transforms}||[]};
d80786d0 57 $stream
58}
59
bf5a23d0 60sub to_fh {
61 HTML::Zoom::ReadFH->from_zoom(shift);
62}
63
7567494d 64sub to_events {
65 my $self = shift;
66 [ $self->zconfig->stream_utils->stream_to_array($self->to_stream) ];
67}
68
bf5a23d0 69sub run {
70 my $self = shift;
7567494d 71 $self->to_events;
bf5a23d0 72 return
73}
74
75sub apply {
76 my ($self, $code) = @_;
77 local $_ = $self;
78 $self->$code;
79}
80
fdb039c6 81sub apply_if {
82 my ($self, $predicate, $code) = @_;
83 if($predicate) {
84 local $_ = $self;
85 $self->$code;
86 }
87 else {
88 $self;
89 }
90}
91
d80786d0 92sub to_html {
93 my $self = shift;
94 $self->zconfig->producer->html_from_stream($self->to_stream);
95}
96
97sub memoize {
98 my $self = shift;
99 ref($self)->new($self)->from_html($self->to_html);
100}
101
eeeb0921 102sub with_transform {
1c4455ae 103 my $self = shift->_self_or_new;
eeeb0921 104 my ($transform) = @_;
d80786d0 105 $self->_with({
2f0c6a86 106 transforms => [
107 @{$self->{transforms}||[]},
eeeb0921 108 $transform
2f0c6a86 109 ]
d80786d0 110 });
111}
eeeb0921 112
113sub with_filter {
114 my $self = shift->_self_or_new;
115 my ($selector, $filter) = @_;
116 $self->with_transform(
117 HTML::Zoom::Transform->new({
118 zconfig => $self->zconfig,
119 selector => $selector,
120 filters => [ $filter ]
121 })
122 );
123}
d80786d0 124
125sub select {
1c4455ae 126 my $self = shift->_self_or_new;
127 my ($selector) = @_;
eeeb0921 128 return HTML::Zoom::TransformBuilder->new({
129 zconfig => $self->zconfig,
130 selector => $selector,
131 proto => $self
132 });
d80786d0 133}
134
135# There's a bug waiting to happen here: if you do something like
136#
137# $zoom->select('.foo')
1c4455ae 138# ->remove_attribute(class => 'foo')
d80786d0 139# ->then
140# ->well_anything_really
141#
142# the second action won't execute because it doesn't match anymore.
143# Ideally instead we'd merge the match subs but that's more complex to
144# implement so I'm deferring it for the moment.
145
146sub then {
147 my $self = shift;
2f0c6a86 148 die "Can't call ->then without a previous transform"
149 unless $self->{transforms};
150 $self->select($self->{transforms}->[-1]->selector);
d80786d0 151}
152
94a3ddd9 153sub AUTOLOAD {
154 my ($self, $selector, @args) = @_;
155 my $sel = $self->select($selector);
156 my $meth = our $AUTOLOAD;
157 $meth =~ s/.*:://;
6dc05a36 158 if (ref($selector) eq 'HASH') {
159 my $ret = $self;
160 $ret = $ret->_do($_, $meth, @{$selector->{$_}}) for keys %$selector;
161 $ret;
162 } else {
163 $self->_do($selector, $meth, @args);
164 }
165}
166
167sub _do {
168 my ($self, $selector, $meth, @args) = @_;
169 my $sel = $self->select($selector);
170 if( my $cr = $sel->_zconfig->filter_builder->can($meth)) {
94a3ddd9 171 return $sel->$meth(@args);
172 } else {
173 die "We can't do $meth on ->select('$selector')";
174 }
175}
176
268f2c35 177sub DESTROY {}
178
d80786d0 1791;
180
181=head1 NAME
182
183HTML::Zoom - selector based streaming template engine
184
185=head1 SYNOPSIS
186
187 use HTML::Zoom;
188
189 my $template = <<HTML;
190 <html>
191 <head>
192 <title>Hello people</title>
193 </head>
194 <body>
195 <h1 id="greeting">Placeholder</h1>
196 <div id="list">
197 <span>
198 <p>Name: <span class="name">Bob</span></p>
199 <p>Age: <span class="age">23</span></p>
200 </span>
201 <hr class="between" />
202 </div>
203 </body>
204 </html>
205 HTML
206
207 my $output = HTML::Zoom
208 ->from_html($template)
209 ->select('title, #greeting')->replace_content('Hello world & dog!')
210 ->select('#list')->repeat_content(
211 [
212 sub {
213 $_->select('.name')->replace_content('Matt')
214 ->select('.age')->replace_content('26')
215 },
2d0662c0 216 # alternate form
d80786d0 217 sub {
2d0662c0 218 $_->replace_content({'.name' => ['Mark'],'.age' => ['0x29'] })
d80786d0 219 },
2d0662c0 220 #alternate alternate form
d80786d0 221 sub {
2d0662c0 222 $_->replace_content('.name' => 'Epitaph')
223 ->replace_content('.age' => '<redacted>')
d80786d0 224 },
225 ],
226 { repeat_between => '.between' }
227 )
228 ->to_html;
229
230will produce:
231
232=begin testinfo
233
234 my $expect = <<HTML;
235
236=end testinfo
237
238 <html>
239 <head>
240 <title>Hello world &amp; dog!</title>
241 </head>
242 <body>
243 <h1 id="greeting">Hello world &amp; dog!</h1>
244 <div id="list">
245 <span>
246 <p>Name: <span class="name">Matt</span></p>
247 <p>Age: <span class="age">26</span></p>
248 </span>
249 <hr class="between" />
250 <span>
251 <p>Name: <span class="name">Mark</span></p>
252 <p>Age: <span class="age">0x29</span></p>
253 </span>
254 <hr class="between" />
255 <span>
256 <p>Name: <span class="name">Epitaph</span></p>
257 <p>Age: <span class="age">&lt;redacted&gt;</span></p>
258 </span>
259
260 </div>
261 </body>
262 </html>
263
264=begin testinfo
265
266 HTML
267 is($output, $expect, 'Synopsis code works ok');
268
269=end testinfo
270
1c4455ae 271=head1 DANGER WILL ROBINSON
272
273This is a 0.9 release. That means that I'm fairly happy the API isn't going
274to change in surprising and upsetting ways before 1.0 and a real compatibility
275freeze. But it also means that if it turns out there's a mistake the size of
276a politician's ego in the API design that I haven't spotted yet there may be
277a bit of breakage between here and 1.0. Hopefully not though. Appendages
278crossed and all that.
279
280Worse still, the rest of the distribution isn't documented yet. I'm sorry.
281I suck. But lots of people have been asking me to ship this, docs or no, so
282having got this class itself at least somewhat documented I figured now was
283a good time to cut a first real release.
284
285=head1 DESCRIPTION
286
287HTML::Zoom is a lazy, stream oriented, streaming capable, mostly functional,
288CSS selector based semantic templating engine for HTML and HTML-like
289document formats.
290
291Which is, on the whole, a bit of a mouthful. So let me step back a moment
292and explain why you care enough to understand what I mean:
293
294=head2 JQUERY ENVY
295
296HTML::Zoom is the cure for JQuery envy. When your javascript guy pushes a
297piece of data into a document by doing:
298
299 $('.username').replaceAll(username);
300
301In HTML::Zoom one can write
302
303 $zoom->select('.username')->replace_content($username);
304
305which is, I hope, almost as clear, hampered only by the fact that Zoom can't
306assume a global document and therefore has nothing quite so simple as the
307$() function to get the initial selection.
308
309L<HTML::Zoom::SelectorParser> implements a subset of the JQuery selector
310specification, and will continue to track that rather than the W3C standards
311for the forseeable future on grounds of pragmatism. Also on grounds of their
312spec is written in EN_US rather than EN_W3C, and I read the former much better.
313
314I am happy to admit that it's very, very much a subset at the moment - see the
315L<HTML::Zoom::SelectorParser> POD for what's currently there, and expect more
316and more to be supported over time as we need it and patch it in.
317
318=head2 CLEAN TEMPLATES
319
320HTML::Zoom is the cure for messy templates. How many times have you looked at
321templates like this:
322
323 <form action="/somewhere">
324 [% FOREACH field IN fields %]
325 <label for="[% field.id %]">[% field.label %]</label>
326 <input name="[% field.name %]" type="[% field.type %]" value="[% field.value %]" />
327 [% END %]
328 </form>
329
330and despaired of the fact that neither the HTML structure nor the logic are
331remotely easy to read? Fortunately, with HTML::Zoom we can separate the two
332cleanly:
333
334 <form class="myform" action="/somewhere">
335 <label />
336 <input />
337 </form>
338
339 $zoom->select('.myform')->repeat_content([
340 map { my $field = $_; sub {
341
342 $_->select('label')
2daa653a 343 ->add_to_attribute( for => $field->{id} )
1c4455ae 344 ->then
345 ->replace_content( $field->{label} )
2d0662c0 346 ->add_to_attribute(
347 input => {
348 name => $field->{name},
349 type => $field->{type},
350 value => $field->{value}
351 })
1c4455ae 352 } } @fields
353 ]);
354
355This is, admittedly, very much not shorter. However, it makes it extremely
356clear what's happening and therefore less hassle to maintain. Especially
357because it allows the designer to fiddle with the HTML without cutting
358himself on sharp ELSE clauses, and the developer to add available data to
359the template without getting angle bracket cuts on sensitive parts.
360
361Better still, HTML::Zoom knows that it's inserting content into HTML and
362can escape it for you - the example template should really have been:
363
364 <form action="/somewhere">
365 [% FOREACH field IN fields %]
366 <label for="[% field.id | html %]">[% field.label | html %]</label>
367 <input name="[% field.name | html %]" type="[% field.type | html %]" value="[% field.value | html %]" />
368 [% END %]
369 </form>
370
371and frankly I'll take slightly more code any day over *that* crawling horror.
372
373(addendum: I pick on L<Template Toolkit|Template> here specifically because
374it's the template system I hate the least - for text templating, I don't
375honestly think I'll ever like anything except the next version of Template
376Toolkit better - but HTML isn't text. Zoom knows that. Do you?)
377
378=head2 PUTTING THE FUN INTO FUNCTIONAL
379
380The principle of HTML::Zoom is to provide a reusable, functional container
381object that lets you build up a set of transforms to be applied; every method
382call you make on a zoom object returns a new object, so it's safe to do so
383on one somebody else gave you without worrying about altering state (with
384the notable exception of ->next for stream objects, which I'll come to later).
385
386So:
387
388 my $z2 = $z1->select('.name')->replace_content($name);
389
390 my $z3 = $z2->select('.title')->replace_content('Ms.');
391
392each time produces a new Zoom object. If you want to package up a set of
393transforms to re-use, HTML::Zoom provides an 'apply' method:
394
395 my $add_name = sub { $_->select('.name')->replace_content($name) };
396
397 my $same_as_z2 = $z1->apply($add_name);
398
399=head2 LAZINESS IS A VIRTUE
400
401HTML::Zoom does its best to defer doing anything until it's absolutely
402required. The only point at which it descends into state is when you force
403it to create a stream, directly by:
404
c9e76777 405 my $stream = $zoom->to_stream;
1c4455ae 406
407 while (my $evt = $stream->next) {
408 # handle zoom event here
409 }
410
411or indirectly via:
412
413 my $final_html = $zoom->to_html;
414
415 my $fh = $zoom->to_fh;
416
417 while (my $chunk = $fh->getline) {
418 ...
419 }
420
421Better still, the $fh returned doesn't create its stream until the first
422call to getline, which means that until you call that and force it to be
423stateful you can get back to the original stateless Zoom object via:
424
425 my $zoom = $fh->to_zoom;
426
427which is exceedingly handy for filtering L<Plack> PSGI responses, among other
428things.
429
430Because HTML::Zoom doesn't try and evaluate everything up front, you can
431generally put things together in whatever order is most appropriate. This
432means that:
433
434 my $start = HTML::Zoom->from_html($html);
435
436 my $zoom = $start->select('div')->replace_content('THIS IS A DIV!');
437
438and:
439
440 my $start = HTML::Zoom->select('div')->replace_content('THIS IS A DIV!');
441
442 my $zoom = $start->from_html($html);
443
444will produce equivalent final $zoom objects, thus proving that there can be
445more than one way to do it without one of them being a
446L<bait and switch|Switch>.
447
448=head2 STOCKTON TO DARLINGTON UNDER STREAM POWER
449
450HTML::Zoom's execution always happens in terms of streams under the hood
451- that is, the basic pattern for doing anything is -
452
453 my $stream = get_stream_from_somewhere
454
455 while (my ($evt) = $stream->next) {
456 # do something with the event
457 }
458
459More importantly, all selectors and filters are also built as stream
460operations, so a selector and filter pair is effectively:
461
462 sub next {
463 my ($self) = @_;
464 my $next_evt = $self->parent_stream->next;
465 if ($self->selector_matches($next_evt)) {
466 return $self->apply_filter_to($next_evt);
467 } else {
468 return $next_evt;
469 }
470 }
471
472Internally, things are marginally more complicated than that, but not enough
473that you as a user should normally need to care.
474
475In fact, an HTML::Zoom object is mostly just a container for the relevant
476information from which to build the final stream that does the real work. A
477stream built from a Zoom object is a stream of events from parsing the
478initial HTML, wrapped in a filter stream per selector/filter pair provided
479as described above.
480
481The upshot of this is that the application of filters works just as well on
482streams as on the original Zoom object - in fact, when you run a
483L</repeat_content> operation your subroutines are applied to the stream for
484that element of the repeat, rather than constructing a new zoom per repeat
485element as well.
486
487More concretely:
488
489 $_->select('div')->replace_content('I AM A DIV!');
490
491works on both HTML::Zoom objects themselves and HTML::Zoom stream objects and
492shares sufficient of the implementation that you can generally forget the
493difference - barring the fact that a stream already has state attached so
494things like to_fh are no longer available.
495
496=head2 POP! GOES THE WEASEL
497
498... and by Weasel, I mean layout.
499
500HTML::Zoom's filehandle object supports an additional event key, 'flush',
501that is transparent to the rest of the system but indicates to the filehandle
502object to end a getline operation at that point and return the HTML so far.
503
504This means that in an environment where streaming output is available, such
505as a number of the L<Plack> PSGI handlers, you can add the flush key to an
506event in order to ensure that the HTML generated so far is flushed through
507to the browser right now. This can be especially useful if you know you're
508about to call a web service or a potentially slow database query or similar
509to ensure that at least the header/layout of your page renders now, improving
510perceived user responsiveness while your application waits around for the
511data it needs.
512
513This is currently exposed by the 'flush_before' option to the collect filter,
514which incidentally also underlies the replace and repeat filters, so to
515indicate we want this behaviour to happen before a query is executed we can
516write something like:
517
518 $zoom->select('.item')->repeat(sub {
519 if (my $row = $db_thing->next) {
520 return sub { $_->select('.item-name')->replace_content($row->name) }
521 } else {
522 return
523 }
524 }, { flush_before => 1 });
525
526which should have the desired effect given a sufficiently lazy $db_thing (for
527example a L<DBIx::Class::ResultSet> object).
528
529=head2 A FISTFUL OF OBJECTS
530
531At the core of an HTML::Zoom system lurks an L<HTML::Zoom::ZConfig> object,
532whose purpose is to hang on to the various bits and pieces that things need
533so that there's a common way of accessing shared functionality.
534
535Were I a computer scientist I would probably call this an "Inversion of
536Control" object - which you'd be welcome to google to learn more about, or
537you can just imagine a computer scientist being suspended upside down over
538a pit. Either way works for me, I'm a pure maths grad.
539
540The ZConfig object hangs on to one each of the following for you:
541
542=over 4
543
544=item * An HTML parser, normally L<HTML::Zoom::Parser::BuiltIn>
545
546=item * An HTML producer (emitter), normally L<HTML::Zoom::Producer::BuiltIn>
547
548=item * An object to build event filters, normally L<HTML::Zoom::FilterBuilder>
549
550=item * An object to parse CSS selectors, normally L<HTML::Zoom::SelectorParser>
551
552=item * An object to build streams, normally L<HTML::Zoom::StreamUtils>
553
554=back
555
556In theory you could replace any of these with anything you like, but in
557practice you're probably best restricting yourself to subclasses, or at
558least things that manage to look like the original if you squint a bit.
559
560If you do something more clever than that, or find yourself overriding things
561in your ZConfig a lot, please please tell us about it via one of the means
562mentioned under L</SUPPORT>.
563
564=head2 SEMANTIC DIDACTIC
565
566Some will argue that overloading CSS selectors to do data stuff is a terrible
567idea, and possibly even a step towards the "Concrete Javascript" pattern
568(which I abhor) or Smalltalk's Morphic (which I ignore, except for the part
569where it keeps reminding me of the late, great Tony Hart's plasticine friend).
570
571To which I say, "eh", "meh", and possibly also "feh". If it really upsets
572you, either use extra classes for this (and remove them afterwards) or
573use special fake elements or, well, honestly, just use something different.
574L<Template::Semantic> provides a similar idea to zoom except using XPath
575and XML::LibXML transforms rather than a lightweight streaming approach -
576maybe you'd like that better. Or maybe you really did want
577L<Template Toolkit|Template> after all. It is still damn good at what it does,
578after all.
579
580So far, however, I've found that for new sites the designers I'm working with
581generally want to produce nice semantic HTML with classes that represent the
582nature of the data rather than the structure of the layout, so sharing them
583as a common interface works really well for us.
584
585In the absence of any evidence that overloading CSS selectors has killed
586children or unexpectedly set fire to grandmothers - and given microformats
587have been around for a while there's been plenty of opportunity for
588octagenarian combustion - I'd suggest you give it a try and see if you like it.
589
590=head2 GET THEE TO A SUMMARY!
591
592Erm. Well.
593
594HTML::Zoom is a lazy, stream oriented, streaming capable, mostly functional,
595CSS selector based semantic templating engine for HTML and HTML-like
596document formats.
597
598But I said that already. Although hopefully by now you have some idea what I
599meant when I said it. If you didn't have any idea the first time. I mean, I'm
600not trying to call you stupid or anything. Just saying that maybe it wasn't
601totally obvious without the explanation. Or something.
602
603Er.
604
605Maybe we should just move on to the method docs.
606
607=head1 METHODS
608
609=head2 new
610
611 my $zoom = HTML::Zoom->new;
612
613 my $zoom = HTML::Zoom->new({ zconfig => $zconfig });
614
615Create a new empty Zoom object. You can optionally pass an
616L<HTML::Zoom::ZConfig> instance if you're trying to override one or more of
617the default components.
618
619This method isn't often used directly since several other methods can also
620act as constructors, notable L</select> and L</from_html>
621
622=head2 zconfig
623
624 my $zconfig = $zoom->zconfig;
625
626Retrieve the L<HTML::Zoom::ZConfig> instance used by this Zoom object. You
627shouldn't usually need to call this yourself.
628
629=head2 from_html
630
631 my $zoom = HTML::Zoom->from_html($html);
632
633 my $z2 = $z1->from_html($html);
634
635Parses the HTML using the current zconfig's parser object and returns a new
636zoom instance with that as the source HTML to be transformed.
637
638=head2 from_file
639
640 my $zoom = HTML::Zoom->from_file($file);
641
642 my $z2 = $z1->from_file($file);
643
644Convenience method - slurps the contents of $file and calls from_html with it.
645
f3a758ad 646=head2 from_events
647
648 my $zoom = HTML::Zoom->from_events($evt);
649
650Create a new Zoom object from collected events
651
1c4455ae 652=head2 to_stream
653
654 my $stream = $zoom->to_stream;
655
656 while (my ($evt) = $stream->next) {
657 ...
658
659Creates a stream, starting with a stream of the events from the HTML supplied
660via L</from_html> and then wrapping it in turn with each selector+filter pair
661that have been applied to the zoom object.
662
663=head2 to_fh
664
665 my $fh = $zoom->to_fh;
666
667 call_something_expecting_a_filehandle($fh);
668
669Returns an L<HTML::Zoom::ReadFH> instance that will create a stream the first
670time its getline method is called and then return all HTML up to the next
671event with 'flush' set.
672
673You can pass this filehandle to compliant PSGI handlers (and probably most
674web frameworks).
675
676=head2 run
677
678 $zoom->run;
679
680Runs the zoom object's transforms without doing anything with the results.
681
682Normally used to get side effects of a zoom run - for example when using
683L<HTML::Zoom::FilterBuilder/collect> to slurp events for scraping or layout.
684
685=head2 apply
686
687 my $z2 = $z1->apply(sub {
688 $_->select('div')->replace_content('I AM A DIV!') })
689 });
690
691Sets $_ to the zoom object and then runs the provided code. Basically syntax
692sugar, the following is entirely equivalent:
693
694 my $sub = sub {
695 shift->select('div')->replace_content('I AM A DIV!') })
696 };
697
698 my $z2 = $sub->($z1);
699
3959a04d 700=head2 apply_if
701
702 my $z2 = $z1->apply_if($cond, sub {
703 $_->select('div')->replace_content('I AM A DIV!') })
704 });
705
706->apply but will only run the tranform if $cond is true
707
1c4455ae 708=head2 to_html
709
710 my $html = $zoom->to_html;
711
712Runs the zoom processing and returns the resulting HTML.
713
714=head2 memoize
715
716 my $z2 = $z1->memoize;
717
718Creates a new zoom whose source HTML is the results of the original zoom's
719processing. Effectively syntax sugar for:
720
721 my $z2 = HTML::Zoom->from_html($z1->to_html);
722
723but preserves your L<HTML::Zoom::ZConfig> object.
724
725=head2 with_filter
726
727 my $zoom = HTML::Zoom->with_filter(
728 'div', $filter_builder->replace_content('I AM A DIV!')
729 );
730
731 my $z2 = $z1->with_filter(
732 'div', $filter_builder->replace_content('I AM A DIV!')
733 );
734
735Lower level interface than L</select> to adding filters to your zoom object.
736
737In normal usage, you probably don't need to call this yourself.
738
739=head2 select
740
741 my $zoom = HTML::Zoom->select('div')->replace_content('I AM A DIV!');
742
743 my $z2 = $z1->select('div')->replace_content('I AM A DIV!');
744
97192b02 745Returns an intermediary object of the class L<HTML::Zoom::TransformBuilder>
1c4455ae 746on which methods of your L<HTML::Zoom::FilterBuilder> object can be called.
747
748In normal usage you should generally always put the pair of method calls
749together; the intermediary object isn't designed or expected to stick around.
750
751=head2 then
752
2daa653a 753 my $z2 = $z1->select('div')->add_to_attribute(class => 'spoon')
1c4455ae 754 ->then
755 ->replace_content('I AM A DIV!');
756
757Re-runs the previous select to allow you to chain actions together on the
758same selector.
759
94a3ddd9 760=head1 AUTOLOAD METHODS
761
762L<HTML::Zoom> AUTOLOADS methods against L</select> so that you can reduce a
763certain amount of boilerplate typing. This allows you to replace:
764
765 $z->select('div')->replace_content("Hello World");
766
767With:
768
769 $z->replace_content(div => "Hello World");
770
771Besides saving a few keys per invocations, you may feel this looks neater
772in your code and increases understanding.
773
f107bef7 774=head1 AUTHOR
45b4cea1 775
f107bef7 776mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
45b4cea1 777
f107bef7 778=head1 CONTRIBUTORS
45b4cea1 779
f107bef7 780Oliver Charles
781
782Jakub Nareski
783
6dc05a36 784Simon Elliott
f107bef7 785
786Joe Highton
787
788John Napiorkowski
789
5cac799e 790Robert Buels
791
b0ed2859 792David Dorward
793
f107bef7 794=head1 COPYRIGHT
795
796Copyright (c) 2010-2011 the HTML::Zoom L</AUTHOR> and L</CONTRIBUTORS>
797as listed above.
45b4cea1 798
799=head1 LICENSE
800
801This library is free software, you can redistribute it and/or modify
802it under the same terms as Perl itself.
803
d80786d0 804=cut
45b4cea1 805