1package Locale::Maketext::Extract;
2$Locale::Maketext::Extract::VERSION = '1.00';
3use strict;
4use Locale::Maketext::Lexicon();
5
6# ABSTRACT: Extract translatable strings from source
7
8
9our %Known_Plugins = (
10    perl    => 'Locale::Maketext::Extract::Plugin::Perl',
11    yaml    => 'Locale::Maketext::Extract::Plugin::YAML',
12    tt2     => 'Locale::Maketext::Extract::Plugin::TT2',
13    text    => 'Locale::Maketext::Extract::Plugin::TextTemplate',
14    mason   => 'Locale::Maketext::Extract::Plugin::Mason',
15    generic => 'Locale::Maketext::Extract::Plugin::Generic',
16    formfu  => 'Locale::Maketext::Extract::Plugin::FormFu',
17    haml    => 'Locale::Maketext::Extract::Plugin::Haml',
18);
19
20sub new {
21    my $class   = shift;
22    my %params  = @_;
23    my $plugins = delete $params{plugins}
24        || { map { $_ => undef } keys %Known_Plugins };
25
26    Locale::Maketext::Lexicon::set_option( 'keep_fuzzy' => 1 );
27    my $self = bless(
28        {   header           => '',
29            entries          => {},
30            compiled_entries => {},
31            lexicon          => {},
32            warnings         => 0,
33            verbose          => 0,
34            wrap             => 0,
35            %params,
36        },
37        $class
38    );
39    $self->{verbose} ||= 0;
40    die "No plugins defined in new()"
41        unless $plugins;
42    $self->plugins($plugins);
43    return $self;
44}
45
46
47sub header { $_[0]{header} || _default_header() }
48sub set_header { $_[0]{header} = $_[1] }
49
50sub lexicon { $_[0]{lexicon} }
51sub set_lexicon { $_[0]{lexicon} = $_[1] || {}; delete $_[0]{lexicon}{''}; }
52
53sub msgstr { $_[0]{lexicon}{ $_[1] } }
54sub set_msgstr { $_[0]{lexicon}{ $_[1] } = $_[2] }
55
56sub entries { $_[0]{entries} }
57sub set_entries { $_[0]{entries} = $_[1] || {} }
58
59sub compiled_entries { $_[0]{compiled_entries} }
60sub set_compiled_entries { $_[0]{compiled_entries} = $_[1] || {} }
61
62sub entry { @{ $_[0]->entries->{ $_[1] } || [] } }
63sub add_entry { push @{ $_[0]->entries->{ $_[1] } }, $_[2] }
64sub del_entry { delete $_[0]->entries->{ $_[1] } }
65
66sub compiled_entry { @{ $_[0]->compiled_entries->{ $_[1] } || [] } }
67sub add_compiled_entry { push @{ $_[0]->compiled_entries->{ $_[1] } }, $_[2] }
68sub del_compiled_entry { delete $_[0]->compiled_entries->{ $_[1] } }
69
70sub plugins {
71    my $self = shift;
72    if (@_) {
73        my @plugins;
74        my %params = %{ shift @_ };
75
76        foreach my $name ( keys %params ) {
77            my $plugin_class = $Known_Plugins{$name} || $name;
78            my $filename = $plugin_class . '.pm';
79            $filename =~ s/::/\//g;
80            local $@;
81            eval {
82                require $filename && 1;
83                1;
84            } or do {
85                my $error = $@ || 'Unknown';
86                print STDERR "Error loading $plugin_class: $error\n"
87                    if $self->{warnings};
88                next;
89            };
90
91            my $plugin
92                = $params{$name}
93                ? $plugin_class->new( $params{$name} )
94                : $plugin_class->new;
95            push @plugins, $plugin;
96        }
97        $self->{plugins} = \@plugins;
98    }
99    return $self->{plugins} || [];
100}
101
102sub clear {
103    $_[0]->set_header;
104    $_[0]->set_lexicon;
105    $_[0]->set_comments;
106    $_[0]->set_fuzzy;
107    $_[0]->set_entries;
108    $_[0]->set_compiled_entries;
109}
110
111
112sub read_po {
113    my ( $self, $file ) = @_;
114    print STDERR "READING PO FILE : $file\n"
115        if $self->{verbose};
116
117    my $header = '';
118
119    local ( *LEXICON, $_ );
120    open LEXICON, $file or die $!;
121    while (<LEXICON>) {
122        ( 1 .. /^$/ ) or last;
123        $header .= $_;
124    }
125    1 while chomp $header;
126
127    $self->set_header("$header\n");
128
129    require Locale::Maketext::Lexicon::Gettext;
130    my $lexicon  = {};
131    my $comments = {};
132    my $fuzzy    = {};
133    $self->set_compiled_entries( {} );
134
135    if ( defined($_) ) {
136        ( $lexicon, $comments, $fuzzy )
137            = Locale::Maketext::Lexicon::Gettext->parse( $_, <LEXICON> );
138    }
139
140    # Internally the lexicon is in gettext format already.
141    $self->set_lexicon( { map _maketext_to_gettext($_), %$lexicon } );
142    $self->set_comments($comments);
143    $self->set_fuzzy($fuzzy);
144
145    close LEXICON;
146}
147
148sub msg_comment {
149    my $self    = shift;
150    my $msgid   = shift;
151    my $comment = $self->{comments}->{$msgid};
152    return $comment;
153}
154
155sub msg_fuzzy {
156    return $_[0]->{fuzzy}{ $_[1] } ? ', fuzzy' : '';
157}
158
159sub set_comments {
160    $_[0]->{comments} = $_[1];
161}
162
163sub set_fuzzy {
164    $_[0]->{fuzzy} = $_[1];
165}
166
167
168sub write_po {
169    my ( $self, $file, $add_format_marker ) = @_;
170    print STDERR "WRITING PO FILE : $file\n"
171        if $self->{verbose};
172
173    local *LEXICON;
174    open LEXICON, ">$file" or die "Can't write to $file$!\n";
175
176    print LEXICON $self->header;
177
178    foreach my $msgid ( $self->msgids ) {
179        $self->normalize_space($msgid);
180        print LEXICON "\n";
181        if ( my $comment = $self->msg_comment($msgid) ) {
182            my @lines = split "\n", $comment;
183            print LEXICON map {"# $_\n"} @lines;
184        }
185        print LEXICON $self->msg_variables($msgid);
186        print LEXICON $self->msg_positions($msgid);
187        my $flags = $self->msg_fuzzy($msgid);
188        $flags .= $self->msg_format($msgid) if $add_format_marker;
189        print LEXICON "#$flags\n" if $flags;
190        print LEXICON $self->msg_out($msgid);
191    }
192
193    print STDERR "DONE\n\n"
194        if $self->{verbose};
195
196}
197
198
199sub extract {
200    my $self    = shift;
201    my $file    = shift;
202    my $content = shift;
203
204    local $@;
205
206    my ( @messages, $total, $error_found );
207    $total = 0;
208    my $verbose = $self->{verbose};
209
210    my @plugins = $self->_plugins_specifically_for_file($file);
211
212    # If there's no plugin which can handle this file
213    # specifically, fall back trying with all known plugins.
214    @plugins = @{ $self->plugins } if not @plugins;
215
216    foreach my $plugin (@plugins) {
217        pos($content) = 0;
218        my $success = eval { $plugin->extract($content); 1; };
219        if ($success) {
220            my $entries = $plugin->entries;
221            if ( $verbose > 1 && @$entries ) {
222                push @messages,
223                      "     - "
224                    . ref($plugin)
225                    . ' - Strings extracted : '
226                    . ( scalar @$entries );
227            }
228            for my $entry (@$entries) {
229                my ( $string, $line, $vars ) = @$entry;
230                $self->add_entry( $string => [ $file, $line, $vars ] );
231                if ( $verbose > 2 ) {
232                    $vars = '' if !defined $vars;
233
234                    # pad string
235                    $string =~ s/\n/\n               /g;
236                    push @messages,
237                        sprintf(
238                        qq[       - %-8s "%s" (%s)],
239                        $line . ':',
240                        $string, $vars
241                        ),
242                        ;
243                }
244            }
245            $total += @$entries;
246        }
247        else {
248            $error_found++;
249            if ( $self->{warnings} ) {
250                push @messages,
251                      "Error parsing '$file' with plugin "
252                    . ( ref $plugin )
253                    . ": \n $@\n";
254            }
255        }
256        $plugin->clear;
257    }
258
259    print STDERR " * $file\n   - Total strings extracted : $total"
260        . ( $error_found ? ' [ERROR ] ' : '' ) . "\n"
261        if $verbose
262        && ( $total || $error_found );
263    print STDERR join( "\n", @messages ) . "\n"
264        if @messages;
265
266}
267
268sub extract_file {
269    my ( $self, $file ) = @_;
270
271    local ( *FH );
272    open FH, $file or die "Error reading from file '$file' : $!";
273    my $content = do {
274        local $/;
275        scalar <FH>;
276    };
277
278    $self->extract( $file => $content );
279    close FH;
280}
281
282
283sub compile {
284    my ( $self, $entries_are_in_gettext_style ) = @_;
285    my $entries = $self->entries;
286    my $lexicon = $self->lexicon;
287    my $comp    = $self->compiled_entries;
288
289    while ( my ( $k, $v ) = each %$entries ) {
290        my $compiled_key = (
291            ($entries_are_in_gettext_style)
292            ? $k
293            : _maketext_to_gettext($k)
294        );
295        $comp->{$compiled_key}    = $v;
296        $lexicon->{$compiled_key} = ''
297            unless exists $lexicon->{$compiled_key};
298    }
299
300    return %$lexicon;
301}
302
303
304my %Escapes = map { ( "\\$_" => eval("qq(\\$_)") ) } qw(t r f b a e);
305
306sub normalize_space {
307    my ( $self, $msgid ) = @_;
308    my $nospace = $msgid;
309    $nospace =~ s/ +$//;
310
311    return
312        unless ( !$self->has_msgid($msgid) and $self->has_msgid($nospace) );
313
314    $self->set_msgstr( $msgid => $self->msgstr($nospace)
315            . ( ' ' x ( length($msgid) - length($nospace) ) ) );
316}
317
318
319sub msgids { sort keys %{ $_[0]{lexicon} } }
320
321sub has_msgid {
322    my $msg_str = $_[0]->msgstr( $_[1] );
323    return defined $msg_str ? length $msg_str : 0;
324}
325
326sub msg_positions {
327    my ( $self, $msgid ) = @_;
328    my %files = ( map { ( " $_->[0]:$_->[1]" => 1 ) }
329            $self->compiled_entry($msgid) );
330    return $self->{wrap}
331        ? join( "\n", ( map { '#:' . $_ } sort( keys %files ) ), '' )
332        : join( '', '#:', sort( keys %files ), "\n" );
333}
334
335sub msg_variables {
336    my ( $self, $msgid ) = @_;
337    my $out = '';
338
339    my %seen;
340    foreach my $entry ( grep { $_->[2] } $self->compiled_entry($msgid) ) {
341        my ( $file, $line, $var ) = @$entry;
342        $var =~ s/^\s*,\s*//;
343        $var =~ s/\s*$//;
344        $out .= "#. ($var)\n" unless !length($var) or $seen{$var}++;
345    }
346
347    return $out;
348}
349
350sub msg_format {
351    my ( $self, $msgid ) = @_;
352    return ", perl-maketext-format"
353        if $msgid =~ /%(?:[1-9]\d*|\w+\([^\)]*\))/;
354    return '';
355}
356
357sub msg_out {
358    my ( $self, $msgid ) = @_;
359    my $msgstr = $self->msgstr($msgid);
360
361    return "msgid " . _format($msgid) . "msgstr " . _format($msgstr);
362}
363
364
365sub _default_header {
366    return << '.';
367# SOME DESCRIPTIVE TITLE.
368# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
369# This file is distributed under the same license as the PACKAGE package.
370# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
371#
372#, fuzzy
373msgid ""
374msgstr ""
375"Project-Id-Version: PACKAGE VERSION\n"
376"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
377"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
378"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
379"Language-Team: LANGUAGE <LL@li.org>\n"
380"MIME-Version: 1.0\n"
381"Content-Type: text/plain; charset=CHARSET\n"
382"Content-Transfer-Encoding: 8bit\n"
383.
384}
385
386sub _maketext_to_gettext {
387    my $text = shift;
388    return '' unless defined $text;
389
390    $text =~ s{((?<!~)(?:~~)*)\[_([1-9]\d*|\*)\]}
391              {$1%$2}g;
392    $text =~ s{((?<!~)(?:~~)*)\[([A-Za-z#*]\w*),([^\]]+)\]}
393              {"$1%$2(" . _escape($3) . ')'}eg;
394
395    $text =~ s/~([\~\[\]])/$1/g;
396    return $text;
397}
398
399sub _escape {
400    my $text = shift;
401    $text =~ s/\b_([1-9]\d*)/%$1/g;
402    return $text;
403}
404
405sub _format {
406    my $str = shift;
407
408    $str =~ s/(?=[\\"])/\\/g;
409
410    while ( my ( $char, $esc ) = each %Escapes ) {
411        $str =~ s/$esc/$char/g;
412    }
413
414    return "\"$str\"\n" unless $str =~ /\n/;
415    my $multi_line = ( $str =~ /\n(?!\z)/ );
416    $str =~ s/\n/\\n"\n"/g;
417    if ( $str =~ /\n"$/ ) {
418        chop $str;
419    }
420    else {
421        $str .= "\"\n";
422    }
423    return $multi_line ? qq(""\n"$str) : qq("$str);
424}
425
426sub _plugins_specifically_for_file {
427    my ( $self, $file ) = @_;
428
429    return () if not $file;
430
431    my @plugins = grep {
432        my $plugin     = $_;
433        my @file_types = $plugin->file_types;
434        my $is_generic
435            = ( scalar @file_types == 1 and $file_types[0] eq '*' );
436        ( not $is_generic and $plugin->known_file_type($file) );
437    } @{ $self->plugins };
438
439    return @plugins;
440}
441
4421;
443
444__END__
445
446=pod
447
448=encoding UTF-8
449
450=head1 NAME
451
452Locale::Maketext::Extract - Extract translatable strings from source
453
454=head1 VERSION
455
456version 1.00
457
458=head1 SYNOPSIS
459
460    my $Ext = Locale::Maketext::Extract->new;
461    $Ext->read_po('messages.po');
462    $Ext->extract_file($_) for <*.pl>;
463
464    # Set $entries_are_in_gettext_format if the .pl files above use
465    # loc('%1') instead of loc('[_1]')
466    $Ext->compile($entries_are_in_gettext_format);
467
468    $Ext->write_po('messages.po');
469
470    -----------------------------------
471
472    ### Specifying parser plugins ###
473
474    my $Ext = Locale::Maketext::Extract->new(
475
476        # Specify which parser plugins to use
477        plugins => {
478
479            # Use Perl parser, process files with extension .pl .pm .cgi
480            perl => [],
481
482            # Use YAML parser, process all files
483            yaml => ['*'],
484
485            # Use TT2 parser, process files with extension .tt2 .tt .html
486            # or which match the regex
487            tt2  => [
488                'tt2',
489                'tt',
490                'html',
491                qr/\.tt2?\./
492            ],
493
494            # Use My::Module as a parser for all files
495            'My::Module' => ['*'],
496
497        },
498
499        # Warn if a parser can't process a file or problems loading a plugin
500        warnings => 1,
501
502        # List processed files
503        verbose => 1,
504
505    );
506
507=head1 DESCRIPTION
508
509This module can extract translatable strings from files, and write
510them back to PO files.  It can also parse existing PO files and merge
511their contents with newly extracted strings.
512
513A command-line utility, L<xgettext.pl>, is installed with this module
514as well.
515
516The format parsers are loaded as plugins, so it is possible to define
517your own parsers.
518
519Following formats of input files are supported:
520
521=over 4
522
523=item Perl source files  (plugin: perl)
524
525Valid localization function names are: C<translate>, C<maketext>,
526C<gettext>, C<l>, C<loc>, C<x>, C<_> and C<__>.
527
528For a slightly more accurate, but much slower Perl parser, you can  use the PPI
529plugin. This does not have a short name (like C<perl>), but must be specified
530in full.
531
532=item HTML::Mason (Mason 1) and Mason (Mason 2) (plugin: mason)
533
534HTML::Mason (aka Mason 1)
535 Strings inside <&|/l>...</&> and <&|/loc>...</&> are extracted.
536
537Mason (aka Mason 2)
538Strings inside <% $.floc { %>...</%> or <% $.fl { %>...</%> or
539<% $self->floc { %>...</%> or <% $self->fl { %>...</%> are extracted.
540
541=item Template Toolkit (plugin: tt2)
542
543Valid forms are:
544
545  [% | l(arg1,argn) %]string[% END %]
546  [% 'string' | l(arg1,argn) %]
547  [% l('string',arg1,argn) %]
548
549  FILTER and | are interchangeable
550  l and loc are interchangeable
551  args are optional
552
553=item Text::Template (plugin: text)
554
555Sentences between C<STARTxxx> and C<ENDxxx> are extracted individually.
556
557=item YAML (plugin: yaml)
558
559Valid forms are _"string" or _'string', eg:
560
561    title: _"My title"
562    desc:  _'My "quoted" string'
563
564Quotes do not have to be escaped, so you could also do:
565
566    desc:  _"My "quoted" string"
567
568=item HTML::FormFu (plugin: formfu)
569
570HTML::FormFu uses a config-file to generate forms, with built in
571support for localizing errors, labels etc.
572
573We extract the text after C<_loc: >:
574    content_loc: this is the string
575    message_loc: ['Max string length: [_1]', 10]
576
577=item Generic Template (plugin: generic)
578
579Strings inside {{...}} are extracted.
580
581=back
582
583=head1 METHODS
584
585=head2 Constructor
586
587    new()
588
589    new(
590        plugins   => {...},
591        warnings  => 1 | 0,
592        verbose   => 0 | 1 | 2 | 3,
593    )
594
595See L</"Plugins">, L</"Warnings"> and L</"Verbose"> for details
596
597=head2 Plugins
598
599    $ext->plugins({...});
600
601Locale::Maketext::Extract uses plugins (see below for the list)
602to parse different formats.
603
604Each plugin can also specify which file types it can parse.
605
606    # use only the YAML plugin
607    # only parse files with the default extension list defined in the plugin
608    # ie .yaml .yml .conf
609
610    $ext->plugins({
611        yaml => [],
612    })
613
614
615    # use only the Perl plugin
616    # parse all file types
617
618    $ext->plugins({
619        perl => '*'
620    })
621
622    $ext->plugins({
623        tt2  => [
624            'tt',              # matches base filename against /\.tt$/
625            qr/\.tt2?\./,      # matches base filename against regex
626            \&my_filter,       # codref called
627        ]
628    })
629
630    sub my_filter {
631        my ($base_filename,$path_to_file) = @_;
632
633        return 1 | 0;
634    }
635
636    # Specify your own parser
637    # only parse files with the default extension list defined in the plugin
638
639    $ext->plugins({
640        'My::Extract::Parser'  => []
641    })
642
643By default, if no plugins are specified, it first tries to determine which
644plugins are intended specifically for the file type and uses them. If no
645such plugins are found, it then uses all of the builtin plugins, overriding
646the file types specified in each.
647
648=head3 Available plugins
649
650=over 4
651
652=item C<perl>    : L<Locale::Maketext::Extract::Plugin::Perl>
653
654For a slightly more accurate but much slower Perl parser, you can use
655the PPI plugin. This does not have a short name, but must be specified in
656full, ie: L<Locale::Maketext::Extract::Plugin::PPI>
657
658=item C<tt2>     : L<Locale::Maketext::Extract::Plugin::TT2>
659
660=item C<yaml>    : L<Locale::Maketext::Extract::Plugin::YAML>
661
662=item C<formfu>  : L<Locale::Maketext::Extract::Plugin::FormFu>
663
664=item C<mason>   : L<Locale::Maketext::Extract::Plugin::Mason>
665
666=item C<text>    : L<Locale::Maketext::Extract::Plugin::TextTemplate>
667
668=item C<generic> : L<Locale::Maketext::Extract::Plugin::Generic>
669
670=back
671
672Also, see L<Locale::Maketext::Extract::Plugin::Base> for details of how to
673write your own plugin.
674
675=head2 Warnings
676
677Because the YAML and TT2 plugins use proper parsers, rather than just regexes,
678if a source file is not valid and it is unable to parse the file, then the
679parser will throw an error and abort parsing.
680
681The next enabled plugin will be tried.
682
683By default, you will not see these errors.  If you would like to see them,
684then enable warnings via new(). All parse errors will be printed to STDERR.
685
686Also, if developing your own plugin, turn on warnings to see any errors that
687result from loading your plugin.
688
689=head2 Verbose
690
691If you would like to see which files have been processed, which plugins were
692used, and which strings were extracted, then enable C<verbose>. If no
693acceptable plugin was found, or no strings were extracted, then the file
694is not listed:
695
696      $ext = Locale::Extract->new( verbose => 1 | 2 | 3);
697
698   OR
699      xgettext.pl ... -v           # files reported
700      xgettext.pl ... -v -v        # files and plugins reported
701      xgettext.pl ... -v -v -v     # files, plugins and strings reported
702
703=head2 Accessors
704
705    header, set_header
706    lexicon, set_lexicon, msgstr, set_msgstr
707    entries, set_entries, entry, add_entry, del_entry
708    compiled_entries, set_compiled_entries, compiled_entry,
709    add_compiled_entry, del_compiled_entry
710    clear
711
712=head2 PO File manipulation
713
714=head3 method read_po ($file)
715
716=head3 method write_po ($file, $add_format_marker?)
717
718=head2 Extraction
719
720    extract
721    extract_file
722
723=head2 Compilation
724
725=head3 compile($entries_are_in_gettext_style?)
726
727Merges the C<entries> into C<compiled_entries>.
728
729If C<$entries_are_in_gettext_style> is true, the previously extracted entries
730are assumed to be in the B<Gettext> style (e.g. C<%1>).
731
732Otherwise they are assumed to be in B<Maketext> style (e.g. C<[_1]>) and are
733converted into B<Gettext> style before merging into C<compiled_entries>.
734
735The C<entries> are I<not> cleared after each compilation; use
736C<->set_entries()> to clear them if you need to extract from sources with
737varying styles.
738
739=head3 normalize_space
740
741=head2 Lexicon accessors
742
743    msgids, has_msgid,
744    msgstr, set_msgstr
745    msg_positions, msg_variables, msg_format, msg_out
746
747=head2 Internal utilities
748
749    _default_header
750    _maketext_to_gettext
751    _escape
752    _format
753    _plugins_specifically_for_file
754
755=head1 ACKNOWLEDGMENTS
756
757Thanks to Jesse Vincent for contributing to an early version of this
758module.
759
760Also to Alain Barbet, who effectively re-wrote the source parser with a
761flex-like algorithm.
762
763=head1 SEE ALSO
764
765L<xgettext.pl>, L<Locale::Maketext>, L<Locale::Maketext::Lexicon>
766
767=head1 AUTHORS
768
769Audrey Tang E<lt>cpan@audreyt.orgE<gt>
770
771=head1 COPYRIGHT
772
773Copyright 2003-2013 by Audrey Tang E<lt>cpan@audreyt.orgE<gt>.
774
775This software is released under the MIT license cited below.
776
777=head2 The "MIT" License
778
779Permission is hereby granted, free of charge, to any person obtaining a copy
780of this software and associated documentation files (the "Software"), to deal
781in the Software without restriction, including without limitation the rights
782to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
783copies of the Software, and to permit persons to whom the Software is
784furnished to do so, subject to the following conditions:
785
786The above copyright notice and this permission notice shall be included in
787all copies or substantial portions of the Software.
788
789THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
790OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
791FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
792THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
793LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
794FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
795DEALINGS IN THE SOFTWARE.
796
797=head1 AUTHORS
798
799=over 4
800
801=item *
802
803Clinton Gormley <drtech@cpan.org>
804
805=item *
806
807Audrey Tang <cpan@audreyt.org>
808
809=back
810
811=head1 COPYRIGHT AND LICENSE
812
813This software is Copyright (c) 2014 by Audrey Tang.
814
815This is free software, licensed under:
816
817  The MIT (X11) License
818
819=cut
820