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