eba4adfd3440eceef140dc36c3fff7c416ebce09
[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.009007';
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           # alternate form
217           sub {
218             $_->replace_content({'.name' => ['Mark'],'.age' => ['0x29'] })
219           },
220           #alternate alternate form
221           sub {
222             $_->replace_content('.name' => 'Epitaph')
223               ->replace_content('.age' => '<redacted>')
224           },
225         ],
226         { repeat_between => '.between' }
227       )
228     ->to_html;
229
230 will 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
271 =head1 DANGER WILL ROBINSON
272
273 This is a 0.9 release. That means that I'm fairly happy the API isn't going
274 to change in surprising and upsetting ways before 1.0 and a real compatibility
275 freeze. But it also means that if it turns out there's a mistake the size of
276 a politician's ego in the API design that I haven't spotted yet there may be
277 a bit of breakage between here and 1.0. Hopefully not though. Appendages
278 crossed and all that.
279
280 Worse still, the rest of the distribution isn't documented yet. I'm sorry.
281 I suck. But lots of people have been asking me to ship this, docs or no, so
282 having got this class itself at least somewhat documented I figured now was
283 a good time to cut a first real release.
284
285 =head1 DESCRIPTION
286
287 HTML::Zoom is a lazy, stream oriented, streaming capable, mostly functional,
288 CSS selector based semantic templating engine for HTML and HTML-like
289 document formats.
290
291 Which is, on the whole, a bit of a mouthful. So let me step back a moment
292 and explain why you care enough to understand what I mean:
293
294 =head2 JQUERY ENVY
295
296 HTML::Zoom is the cure for JQuery envy. When your javascript guy pushes a
297 piece of data into a document by doing:
298
299   $('.username').replaceAll(username);
300
301 In HTML::Zoom one can write
302
303   $zoom->select('.username')->replace_content($username);
304
305 which is, I hope, almost as clear, hampered only by the fact that Zoom can't
306 assume a global document and therefore has nothing quite so simple as the
307 $() function to get the initial selection.
308
309 L<HTML::Zoom::SelectorParser> implements a subset of the JQuery selector
310 specification, and will continue to track that rather than the W3C standards
311 for the forseeable future on grounds of pragmatism. Also on grounds of their
312 spec is written in EN_US rather than EN_W3C, and I read the former much better.
313
314 I am happy to admit that it's very, very much a subset at the moment - see the
315 L<HTML::Zoom::SelectorParser> POD for what's currently there, and expect more
316 and more to be supported over time as we need it and patch it in.
317
318 =head2 CLEAN TEMPLATES
319
320 HTML::Zoom is the cure for messy templates. How many times have you looked at
321 templates 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
330 and despaired of the fact that neither the HTML structure nor the logic are
331 remotely easy to read? Fortunately, with HTML::Zoom we can separate the two
332 cleanly:
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')
343        ->add_to_attribute( for => $field->{id} )
344        ->then
345        ->replace_content( $field->{label} )
346        ->add_to_attribute(
347         input => { 
348          name => $field->{name},
349          type => $field->{type},
350          value => $field->{value}
351        })
352     } } @fields
353   ]);
354
355 This is, admittedly, very much not shorter. However, it makes it extremely
356 clear what's happening and therefore less hassle to maintain. Especially
357 because it allows the designer to fiddle with the HTML without cutting
358 himself on sharp ELSE clauses, and the developer to add available data to
359 the template without getting angle bracket cuts on sensitive parts.
360
361 Better still, HTML::Zoom knows that it's inserting content into HTML and
362 can 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
371 and 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
374 it's the template system I hate the least - for text templating, I don't
375 honestly think I'll ever like anything except the next version of Template
376 Toolkit better - but HTML isn't text. Zoom knows that. Do you?)
377
378 =head2 PUTTING THE FUN INTO FUNCTIONAL
379
380 The principle of HTML::Zoom is to provide a reusable, functional container
381 object that lets you build up a set of transforms to be applied; every method
382 call you make on a zoom object returns a new object, so it's safe to do so
383 on one somebody else gave you without worrying about altering state (with
384 the notable exception of ->next for stream objects, which I'll come to later).
385
386 So:
387
388   my $z2 = $z1->select('.name')->replace_content($name);
389
390   my $z3 = $z2->select('.title')->replace_content('Ms.');
391
392 each time produces a new Zoom object. If you want to package up a set of
393 transforms 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
401 HTML::Zoom does its best to defer doing anything until it's absolutely
402 required. The only point at which it descends into state is when you force
403 it to create a stream, directly by:
404
405   my $stream = $zoom->to_stream;
406
407   while (my $evt = $stream->next) {
408     # handle zoom event here
409   }
410
411 or 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
421 Better still, the $fh returned doesn't create its stream until the first
422 call to getline, which means that until you call that and force it to be
423 stateful you can get back to the original stateless Zoom object via:
424
425   my $zoom = $fh->to_zoom;
426
427 which is exceedingly handy for filtering L<Plack> PSGI responses, among other
428 things.
429
430 Because HTML::Zoom doesn't try and evaluate everything up front, you can
431 generally put things together in whatever order is most appropriate. This
432 means that:
433
434   my $start = HTML::Zoom->from_html($html);
435
436   my $zoom = $start->select('div')->replace_content('THIS IS A DIV!');
437
438 and:
439
440   my $start = HTML::Zoom->select('div')->replace_content('THIS IS A DIV!');
441
442   my $zoom = $start->from_html($html);
443
444 will produce equivalent final $zoom objects, thus proving that there can be
445 more than one way to do it without one of them being a
446 L<bait and switch|Switch>.
447
448 =head2 STOCKTON TO DARLINGTON UNDER STREAM POWER
449
450 HTML::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
459 More importantly, all selectors and filters are also built as stream
460 operations, 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
472 Internally, things are marginally more complicated than that, but not enough
473 that you as a user should normally need to care.
474
475 In fact, an HTML::Zoom object is mostly just a container for the relevant
476 information from which to build the final stream that does the real work. A
477 stream built from a Zoom object is a stream of events from parsing the
478 initial HTML, wrapped in a filter stream per selector/filter pair provided
479 as described above.
480
481 The upshot of this is that the application of filters works just as well on
482 streams as on the original Zoom object - in fact, when you run a
483 L</repeat_content> operation your subroutines are applied to the stream for
484 that element of the repeat, rather than constructing a new zoom per repeat
485 element as well.
486
487 More concretely:
488
489   $_->select('div')->replace_content('I AM A DIV!');
490
491 works on both HTML::Zoom objects themselves and HTML::Zoom stream objects and
492 shares sufficient of the implementation that you can generally forget the
493 difference - barring the fact that a stream already has state attached so
494 things like to_fh are no longer available.
495
496 =head2 POP! GOES THE WEASEL
497
498 ... and by Weasel, I mean layout.
499
500 HTML::Zoom's filehandle object supports an additional event key, 'flush',
501 that is transparent to the rest of the system but indicates to the filehandle
502 object to end a getline operation at that point and return the HTML so far.
503
504 This means that in an environment where streaming output is available, such
505 as a number of the L<Plack> PSGI handlers, you can add the flush key to an
506 event in order to ensure that the HTML generated so far is flushed through
507 to the browser right now. This can be especially useful if you know you're
508 about to call a web service or a potentially slow database query or similar
509 to ensure that at least the header/layout of your page renders now, improving
510 perceived user responsiveness while your application waits around for the
511 data it needs.
512
513 This is currently exposed by the 'flush_before' option to the collect filter,
514 which incidentally also underlies the replace and repeat filters, so to
515 indicate we want this behaviour to happen before a query is executed we can
516 write 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
526 which should have the desired effect given a sufficiently lazy $db_thing (for
527 example a L<DBIx::Class::ResultSet> object).
528
529 =head2 A FISTFUL OF OBJECTS
530
531 At the core of an HTML::Zoom system lurks an L<HTML::Zoom::ZConfig> object,
532 whose purpose is to hang on to the various bits and pieces that things need
533 so that there's a common way of accessing shared functionality.
534
535 Were I a computer scientist I would probably call this an "Inversion of
536 Control" object - which you'd be welcome to google to learn more about, or
537 you can just imagine a computer scientist being suspended upside down over
538 a pit. Either way works for me, I'm a pure maths grad.
539
540 The 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
556 In theory you could replace any of these with anything you like, but in
557 practice you're probably best restricting yourself to subclasses, or at
558 least things that manage to look like the original if you squint a bit.
559
560 If you do something more clever than that, or find yourself overriding things
561 in your ZConfig a lot, please please tell us about it via one of the means
562 mentioned under L</SUPPORT>.
563
564 =head2 SEMANTIC DIDACTIC
565
566 Some will argue that overloading CSS selectors to do data stuff is a terrible
567 idea, 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
569 where it keeps reminding me of the late, great Tony Hart's plasticine friend).
570
571 To which I say, "eh", "meh", and possibly also "feh". If it really upsets
572 you, either use extra classes for this (and remove them afterwards) or
573 use special fake elements or, well, honestly, just use something different.
574 L<Template::Semantic> provides a similar idea to zoom except using XPath
575 and XML::LibXML transforms rather than a lightweight streaming approach -
576 maybe you'd like that better. Or maybe you really did want
577 L<Template Toolkit|Template> after all. It is still damn good at what it does,
578 after all.
579
580 So far, however, I've found that for new sites the designers I'm working with
581 generally want to produce nice semantic HTML with classes that represent the
582 nature of the data rather than the structure of the layout, so sharing them
583 as a common interface works really well for us.
584
585 In the absence of any evidence that overloading CSS selectors has killed
586 children or unexpectedly set fire to grandmothers - and given microformats
587 have been around for a while there's been plenty of opportunity for
588 octagenarian combustion - I'd suggest you give it a try and see if you like it.
589
590 =head2 GET THEE TO A SUMMARY!
591
592 Erm. Well.
593
594 HTML::Zoom is a lazy, stream oriented, streaming capable, mostly functional,
595 CSS selector based semantic templating engine for HTML and HTML-like
596 document formats.
597
598 But I said that already. Although hopefully by now you have some idea what I
599 meant when I said it. If you didn't have any idea the first time. I mean, I'm
600 not trying to call you stupid or anything. Just saying that maybe it wasn't
601 totally obvious without the explanation. Or something.
602
603 Er.
604
605 Maybe 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
615 Create a new empty Zoom object. You can optionally pass an
616 L<HTML::Zoom::ZConfig> instance if you're trying to override one or more of
617 the default components.
618
619 This method isn't often used directly since several other methods can also
620 act as constructors, notable L</select> and L</from_html>
621
622 =head2 zconfig
623
624   my $zconfig = $zoom->zconfig;
625
626 Retrieve the L<HTML::Zoom::ZConfig> instance used by this Zoom object. You
627 shouldn'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
635 Parses the HTML using the current zconfig's parser object and returns a new
636 zoom 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
644 Convenience method - slurps the contents of $file and calls from_html with it.
645
646 =head2 from_events
647
648   my $zoom = HTML::Zoom->from_events($evt);
649
650 Create a new Zoom object from collected events
651
652 =head2 to_stream
653
654   my $stream = $zoom->to_stream;
655
656   while (my ($evt) = $stream->next) {
657     ...
658
659 Creates a stream, starting with a stream of the events from the HTML supplied
660 via L</from_html> and then wrapping it in turn with each selector+filter pair
661 that 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
669 Returns an L<HTML::Zoom::ReadFH> instance that will create a stream the first
670 time its getline method is called and then return all HTML up to the next
671 event with 'flush' set.
672
673 You can pass this filehandle to compliant PSGI handlers (and probably most
674 web frameworks).
675
676 =head2 run
677
678   $zoom->run;
679
680 Runs the zoom object's transforms without doing anything with the results.
681
682 Normally used to get side effects of a zoom run - for example when using
683 L<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
691 Sets $_ to the zoom object and then runs the provided code. Basically syntax
692 sugar, 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
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
708 =head2 to_html
709
710   my $html = $zoom->to_html;
711
712 Runs the zoom processing and returns the resulting HTML.
713
714 =head2 memoize
715
716   my $z2 = $z1->memoize;
717
718 Creates a new zoom whose source HTML is the results of the original zoom's
719 processing. Effectively syntax sugar for:
720
721   my $z2 = HTML::Zoom->from_html($z1->to_html);
722
723 but 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
735 Lower level interface than L</select> to adding filters to your zoom object.
736
737 In 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
745 Returns an intermediary object of the class L<HTML::Zoom::TransformBuilder>
746 on which methods of your L<HTML::Zoom::FilterBuilder> object can be called.
747
748 In normal usage you should generally always put the pair of method calls
749 together; the intermediary object isn't designed or expected to stick around.
750
751 =head2 then
752
753   my $z2 = $z1->select('div')->add_to_attribute(class => 'spoon')
754                              ->then
755                              ->replace_content('I AM A DIV!');
756
757 Re-runs the previous select to allow you to chain actions together on the
758 same selector.
759
760 =head1 AUTOLOAD METHODS
761
762 L<HTML::Zoom> AUTOLOADS methods against L</select> so that you can reduce a
763 certain amount of boilerplate typing.  This allows you to replace:
764
765   $z->select('div')->replace_content("Hello World");
766   
767 With:
768
769   $z->replace_content(div => "Hello World");
770   
771 Besides saving a few keys per invocations, you may feel this looks neater
772 in your code and increases understanding.
773
774 =head1 AUTHOR
775
776 mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
777
778 =head1 CONTRIBUTORS
779
780 Oliver Charles
781
782 Jakub Nareski
783
784 Simon Elliott
785
786 Joe Highton
787
788 John Napiorkowski
789
790 Robert Buels
791
792 David Dorward
793
794 =head1 COPYRIGHT
795
796 Copyright (c) 2010-2011 the HTML::Zoom L</AUTHOR> and L</CONTRIBUTORS>
797 as listed above.
798
799 =head1 LICENSE
800
801 This library is free software, you can redistribute it and/or modify
802 it under the same terms as Perl itself.
803
804 =cut
805