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