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