File Coverage

blib/lib/DBD/PO/Locale/PO.pm
Criterion Covered Total %
statement 31 123 25.2
branch 2 56 3.5
condition 1 9 11.1
subroutine 9 17 52.9
pod n/a
total 43 205 20.9


line stmt bran cond sub pod time code
1             package DBD::PO::Locale::PO;
2            
3 20     20   140667 use strict;
  20         49  
  20         726  
4 20     20   117 use warnings;
  20         47  
  20         561  
5            
6 20     20   19728 use version; our $VERSION = qv('0.21.5');
  20         70282  
  20         135  
7            
8 20     20   2076 use Carp qw(croak);
  20         48  
  20         1258  
9 20     20   301 use English qw(-no_match_vars $EVAL_ERROR $OS_ERROR);
  20         55  
  20         192  
10            
11 20     20   3401 use parent qw(Exporter);
  20         46  
  20         180  
12             our @EXPORT_OK = qw(
13             @FORMAT_FLAGS
14             $ALLOW_LOST_BLANK_LINES
15             );
16            
17             our @FORMAT_FLAGS = qw(
18             c-format
19             objc-format
20             sh-format
21             python-format
22             lisp-format
23             elisp-format
24             librep-format
25             scheme-format
26             smalltalk-format
27             java-format
28             csharp-format
29             awk-format
30             object-pascal-format
31             ycp-format
32             tcl-format
33             perl-format
34             perl-brace-format
35             php-format
36             gcc-internal-format
37             qt-format
38             kde-format
39             boost-format
40             );
41            
42             our $ALLOW_LOST_BLANK_LINES = 1;
43            
44             sub new {
45 1     1   14964 my ($this, %options) = @_;
46            
47 1   33     13 my $class = ref $this || $this;
48 1         3 my $self = bless {}, $class;
49 1         18 $self->eol( $options{eol} );
50 1         248 $self->_flags({});
51 0         0 for (qw(
52             msgctxt msgid msgid_plural
53             previous_msgctxt previous_msgid previous_msgid_plural
54             msgstr msgstr_n
55             comment automatic reference fuzzy obsolete
56             loaded_line_number
57             )) {
58 0 0       0 if ( defined $options{"-$_"} ) {
59 0         0 $self->$_( $options{"-$_"} );
60             }
61             }
62 0         0 for my $format (@FORMAT_FLAGS) {
63 0 0       0 if ( defined $options{"-$format"} ) {
64 0         0 $self->format_flag($format => 1);
65             }
66 0 0       0 if ( defined $options{"-no-$format"} ) {
67 0         0 $self->format_flag($format => 0);
68             }
69             }
70            
71 0         0 return $self;
72             }
73            
74             sub eol {
75 1     1   5 my ($self, @params) = @_;
76            
77 1 50       5 if (@params) {
78 1         3 my $eol = shift @params;
79 1         9 $self->{eol} = $eol;
80             }
81            
82 1 50       6 return defined $self->{eol}
83             ? $self->{eol}
84             : "\n";
85             }
86            
87             # create methods
88             for (qw(
89             msgctxt msgid msgid_plural
90             previous_msgctxt previous_msgid previous_msgid_plural
91             msgstr
92             comment automatic reference obsolete
93             _flags loaded_line_number
94             )) {
95             my $name = $_;
96 20     20   12930 no strict 'refs'; ## no critic (NoStrict)
  20         111  
  20         47512  
97             *{$name} = sub {
98             my ($self, @params) = @_;
99            
100             return @params
101             ? $self->{$name} = shift @params
102             : $self->{$name};
103             };
104             }
105            
106             sub msgstr_n {
107 0     0     my ($self, @params) = @_;
108            
109 0 0         if (@params) {
110 0           my $hashref = shift @params;
111            
112             # check that we have a hashref.
113 0 0         ref $hashref eq 'HASH'
114             or croak 'Argument to msgstr_n must be a hashref: { n => "string n", ... }.';
115            
116             # Check that the keys are all numbers.
117 0           for ( keys %{$hashref} ) {
  0            
118 0 0 0       croak 'Keys to msgstr_n hashref must be numbers'
119             if ! defined $_ || m{\D}xms;
120             }
121            
122             # Write all the values in the hashref.
123 0           @{ $self->{msgstr_n} }{ keys %{$hashref} } = values %{$hashref};
  0            
  0            
  0            
124             }
125            
126 0           return $self->{msgstr_n};
127             }
128            
129             sub add_flag {
130 0     0     my ($self, $flag_name) = @_;
131            
132 0           $self->_flags()->{$flag_name} = 1;
133            
134 0           return $self;
135             }
136            
137             sub remove_flag {
138 0     0     my ($self, $flag_name) = @_;
139            
140 0           delete $self->_flags()->{$flag_name};
141            
142 0           return $self;
143             }
144            
145             sub has_flag {
146 0     0     my ($self, $flag_name) = @_;
147            
148 0           my $flags = $self->_flags();
149 0 0         exists $flags->{$flag_name}
150             or return;
151            
152 0           return $flags->{$flag_name};
153             }
154            
155             sub fuzzy {
156 0     0     my ($self, @params) = @_;
157            
158 0 0         if (@params) {
159 0           my $value = shift @params;
160             return
161 0 0         $value
162             ? $self->add_flag('fuzzy')
163             : $self->remove_flag('fuzzy');
164             }
165            
166 0           return $self->has_flag('fuzzy');
167             }
168            
169             sub format_flag {
170 0     0     my ($self, $flag_name, @params) = @_;
171            
172 0 0         if (@params) { # set or clear the flags
173 0           my $value = shift @params;
174 0 0 0       if (! defined($value) || ! length $value) {
    0          
175 0           $self->remove_flag($flag_name);
176 0           $self->remove_flag("no-$flag_name");
177 0           return;
178             }
179             elsif ($value) {
180 0           $self->add_flag($flag_name);
181 0           $self->remove_flag("no-$flag_name");
182 0           return 1;
183             }
184             else {
185 0           $self->add_flag("no-$flag_name");
186 0           $self->remove_flag($flag_name);
187 0           return 0;
188             }
189             }
190             # check the flags
191 0 0         return 1 if $self->has_flag($flag_name);
192 0 0         return 0 if $self->has_flag("no-$flag_name");
193            
194 0           return;
195             }
196            
197             sub dump { ## no critic (BuiltinHomonyms)
198 0     0     my $self = shift;
199            
200 0 0         my $obsolete = $self->obsolete() ? '#~ ' : q{};
201 0           my $dump = q{};
202 0 0         if ( defined $self->comment() ) {
203 0           $dump .= $self->_dump_multi_comment( $self->comment(), '# ' );
204             }
205 0 0         if ( defined $self->automatic() ) {
206 0           $dump .= $self->_dump_multi_comment( $self->automatic(), '#. ' );
207             }
208 0 0         if ( defined $self->reference() ) {
209 0           $dump .= $self->_dump_multi_comment( $self->reference(), '#: ' );
210             }
211 0           my $flags = join q{}, map {", $_"} sort keys %{ $self->_flags() };
  0            
  0            
212 0 0         if ($flags) {
213 0           $dump .= "#$flags"
214             . $self->eol();
215             }
216 0 0         if ( defined $self->previous_msgctxt() ) {
217 0           $dump .= '#| msgctxt '
218             . $self->quote( $self->previous_msgctxt() );
219             }
220 0 0         if ( defined $self->previous_msgid() ) {
221 0           $dump .= '#| msgid '
222             . $self->quote( $self->previous_msgid() );
223             }
224 0 0         if ( defined $self->previous_msgid_plural() ) {
225 0           $dump .= '#| msgid_plural '
226             . $self->quote( $self->previous_msgid_plural() );
227             }
228 0 0         if ( defined $self->msgctxt() ) {
229 0           $dump .= "${obsolete}msgctxt "
230             . $self->quote( $self->msgctxt() );
231             }
232 0           $dump .= "${obsolete}msgid "
233             . $self->quote( $self->msgid() );
234 0 0         if ( defined $self->msgid_plural() ) {
235 0           $dump .= "${obsolete}msgid_plural "
236             . $self->quote( $self->msgid_plural() );
237             }
238 0 0         if ( defined $self->msgstr() ) {
239 0           $dump .= "${obsolete}msgstr "
240             . $self->quote( $self->msgstr() );
241             }
242 0 0         if ( my $msgstr_n = $self->msgstr_n() ) {
243 0           $dump .= join
244             q{},
245             map {
246 0           "${obsolete}msgstr[$_] "
247             . $self->quote( $msgstr_n->{$_} );
248             } sort {
249 0           $a <=> $b
250 0           } keys %{$msgstr_n};
251             }
252            
253 0           $dump .= $self->eol();
254            
255 0           return $dump;
256             }
257            
258             sub _dump_multi_comment {
259 0     0     my $self = shift;
260 0           my $comment = shift;
261 0           my $leader = shift;
262            
263 0           my $eol = $self->eol();
264            
265 0           return join q{}, map {
266 0           "$leader$_$eol";
267             } split m{\Q$eol\E}xms, $comment;
268             }
269            
270             # Quote a string properly
271             sub quote {
272             my $self = shift;
273             my $string = shift;
274            
275             if (! defined $string) {
276             return q{""};
277             }
278             my %named = (
279             ## no critic (InterpolationOfLiterals)
280             #qq{\a} => qq{\\a}, # BEL
281             #qq{\b} => qq{\\b}, # BS
282             #qq{\t} => qq{\\t}, # TAB
283             qq{\n} => qq{\\n}, # LF
284             #qq{\f} => qq{\\f}, # FF
285             #qq{\r} => qq{\\r}, # CR
286             qq{"} => qq{\\"},
287             qq{\\} => qq{\\\\},
288             ## use critic (InterpolationOfLiterals)
289             );
290             $string =~ s{
291             ( [^ !#$%&'()*+,\-.\/0-9:;<=>?@A-Z\[\]\^_`a-z{|}~] )
292             }{
293             ord $1 < 0x80
294             ? (
295             exists $named{$1}
296             ? $named{$1}
297             : sprintf '\x%02x', ord $1
298             )
299             : $1;
300             }xmsge;
301             $string = qq{"$string"};
302             # multiline
303             my $eol = $self->eol();
304             if ($string =~ s{\A ( " .*? \\n )}{""$eol$1}xms) {
305             $string =~ s{\\n}{\\n"$eol"}xmsg;
306             }
307            
308             return "$string$eol";
309             }
310            
311             sub dequote {
312             my $self = shift;
313             my $string = shift;
314             my $eol = shift || $self->eol();
315            
316             if (! defined $string) {
317             $string = q{};
318             }
319             # multiline
320             if ($string =~ s{\A "" \Q$eol\E}{}xms) {
321             $string =~ s{\\n"\Q$eol\E"}{\\n}xmsg;
322             }
323             $string =~ s{( [\$\@] )}{\\$1}xmsg; # make uncritical
324             ($string) = $string =~ m{
325             \A
326             (
327             "
328             (?: \\\\ | \\" | [^"] )*
329             "
330             # eol
331             )
332             }xms; # check the quoted string and untaint
333             return q{} if ! defined $string;
334             my $dequoted = eval $string; ## no critic (StringyEval)
335             croak qq{Can not eval string "$string": $EVAL_ERROR} if $EVAL_ERROR;
336            
337             return $dequoted;
338             }
339            
340             sub save_file_fromarray {
341             my ($self, @params) = @_;
342            
343             return $self->_save_file(@params, 0);
344             }
345            
346             sub save_file_fromhash {
347             my ($self, @params) = @_;
348            
349             return $self->_save_file(@params, 1);
350             }
351            
352             sub _save_file {
353             my $self = shift;
354             my $file = shift;
355             my $entries = shift;
356             my $as_hash = shift;
357            
358             open my $out, '>', $file ## no critic (BriefOpen)
359             or croak "Open $file: $OS_ERROR";
360             if ($as_hash) {
361             for (sort keys %{$entries}) {
362             print {$out} $entries->{$_}->dump()
363             or croak "Print $file: $OS_ERROR";
364             }
365             }
366             else {
367             for (@{$entries}) {
368             print {$out} $_->dump()
369             or croak "Print $file: $OS_ERROR";
370             }
371             }
372             close $out
373             or croak "Close $file $OS_ERROR";
374            
375             return $self;
376             }
377            
378             sub load_file_asarray {
379             my $self = shift;
380             my $file = shift;
381             my $eol = shift || "\n";
382            
383             if (ref $file) {
384             return $self->_load_file($file, $file, $eol, 0);
385             }
386             open my $in, '<', $file
387             or croak "Open $file: $OS_ERROR";
388             my $array_ref = $self->_load_file($file, $in, $eol, 0);
389             close $in
390             or croak "Close $file: $OS_ERROR";
391            
392             return $array_ref;
393             }
394            
395             sub load_file_ashash {
396             my $self = shift;
397             my $file = shift;
398             my $eol = shift || "\n";
399            
400             if (ref $file) {
401             return $self->_load_file($file, $file, $eol, 1);
402             }
403             open my $in, '<', $file
404             or croak "Open $file: $OS_ERROR";
405             my $hash_ref = $self->_load_file($file, $in, $eol, 1);
406             close $in
407             or croak "Close $file: $OS_ERROR";
408            
409             return $hash_ref;
410             }
411            
412             sub _load_file {
413             my $self = shift;
414             my $file_name = shift;
415             my $file_handle = shift;
416             my $eol = shift;
417             my $ashash = shift;
418            
419             my $line_number = 0;
420             my (@entries, %entries);
421             while (
422             my $po = $self->load_entry(
423             $file_name,
424             $file_handle,
425             \$line_number,
426             $eol,
427             )
428             ) {
429             # ashash
430             if ($ashash) {
431             if ( $po->_hash_key_ok(\%entries) ) {
432             $entries{ $po->msgid() } = $po;
433             }
434             }
435             # asarray
436             else {
437             push @entries, $po;
438             }
439             }
440            
441             return $ashash
442             ? \%entries
443             : \@entries;
444             }
445            
446             sub load_entry { ## no critic (ExcessComplexity)
447             my $self = shift;
448             my $file_name = shift;
449             my $file_handle = shift;
450             my $line_number_ref = shift;
451             my $eol = shift || "\n";
452            
453             my $class = ref $self || $self;
454             my %last_line_of_section; # to find the end of an entry
455             my $current_section_key; # to add lines
456            
457             my ($current_line_number, $current_pos);
458             my $safe_current_position = sub {
459             # safe information to can roll back
460             $current_line_number = ${$line_number_ref};
461             $ALLOW_LOST_BLANK_LINES
462             or return;
463             $current_pos = tell $file_handle;
464             defined $current_pos
465             or croak "Can not tell file pointer of file $file_name: $OS_ERROR";
466             };
467             $safe_current_position->();
468            
469             my $is_new_entry = sub {
470             $current_section_key = shift;
471             if (
472             $ALLOW_LOST_BLANK_LINES
473             && exists $last_line_of_section{ $current_section_key }
474             && $last_line_of_section{ $current_section_key }
475             != ${$line_number_ref} - 1
476             ) {
477             # roll back
478             ${$line_number_ref} = $current_line_number;
479             seek $file_handle, $current_pos, 0
480             or croak "Can not seek file pointer of file $file_name: $OS_ERROR";
481             return 1; # this is a new entry
482             }
483             $last_line_of_section{ $current_section_key } = ${$line_number_ref};
484             return;
485             };
486            
487             my $po; # build an object during read an entry
488             my %buffer; # find the different msg...
489             my $current_buffer; # to add lines
490             LINE:
491             while (my $line = <$file_handle>) {
492             $line =~ s{\Q$eol\E \z}{}xms;
493             my $line_number = ++${$line_number_ref};
494             my ($obsolete, $key, $value);
495             # Empty line. End of an entry.
496             if ( $line =~ m{\A \s* \z}xms ) { ## no critic (CascadingIfElse)
497             last LINE if $po;
498             }
499             # strings
500             elsif (
501             ($obsolete, $key, $value)
502             = $line =~ m{\A ( \# ~ \s+ )? ( msgctxt | msgid | msgid_plural | msgstr ) \s+ (.*)}xms
503             ) {
504             last LINE if $is_new_entry->($key);
505             $po ||= $class->new(eol => $eol, -loaded_line_number => $line_number);
506             $buffer{$key} = $self->dequote($value, $eol);
507             $current_buffer = \$buffer{$key};
508             if ($obsolete) {
509             $po->obsolete(1);
510             }
511             }
512             # contined string
513             elsif ( $line =~ m{\A (?: \# ~ \s+ )? "}xms ) {
514             ${$current_buffer} .= $self->dequote($line, $eol);
515             $last_line_of_section{ $current_section_key } = $line_number;
516             }
517             # translated string, plural
518             elsif (
519             ($obsolete, $key, $value)
520             = $line =~ m{\A ( \# ~ \s+ )? msgstr \[ (\d+) \] \s+ (.*)}xms
521             ) {
522             last LINE if $is_new_entry->('msgstr_n');
523             $buffer{msgstr_n}->{$key} = $self->dequote($value, $eol);
524             $current_buffer = \$buffer{msgstr_n}->{$key};
525             if ($obsolete) {
526             $po->obsolete(1);
527             }
528             }
529             # reference
530             elsif ( ($value) = $line =~ m{\A \# : \s+ (.*)}xms ) {
531             last LINE if $is_new_entry->('comment');
532             $po ||= $class->new(eol => $eol, -loaded_line_number => $line_number);
533             # maybe more in 1 line
534             $value = join $eol, split m{\s+}xms, $value;
535             $po->reference(
536             defined $po->reference()
537             ? $po->reference() . "$eol$value"
538             : $value
539             );
540             }
541             # flags
542             elsif ( ($value) = $line =~ m{\A \# , \s+ (.*)}xms) {
543             last LINE if $is_new_entry->('comment');
544             $po ||= $class->new(eol => $eol, -loaded_line_number => $line_number);
545             for my $flag ( split m{\s* , \s*}xms, $value ) {
546             $po->add_flag($flag);
547             }
548             }
549             # Translator comments
550             elsif (
551             $line =~ m{\A \# \s+ (.*)}xms
552             || $line =~ m{\A \# ()\z}xms
553             ) {
554             $value = $1;
555             last LINE if $is_new_entry->('comment');
556             $po ||= $class->new(eol => $eol, -loaded_line_number => $line_number);
557             $po->comment(
558             defined $po->comment()
559             ? $po->comment() . "$eol$value"
560             : $value
561             );
562             }
563             # Automatic comments
564             elsif ( ($value) = $line =~ m{\A \# \. \s* (.*)}xms ) {
565             last LINE if $is_new_entry->('comment');
566             $po ||= $class->new(eol => $eol, -loaded_line_number => $line_number);
567             $po->automatic(
568             defined $po->automatic()
569             ? $po->automatic() . "$eol$value"
570             : $value
571             );
572             }
573             # previous
574             elsif (
575             ($key, $value)
576             = $line =~ m{\A \# \| \s+ ( msgctxt | msgid | msgid_plural ) \s+ (.*)}xms
577             ) {
578             last LINE if $is_new_entry->('comment');
579             $po ||= $class->new(eol => $eol, -loaded_line_number => $line_number);
580             $key = "previous_$key";
581             $buffer{$key} = $self->dequote($value, $eol);
582             $current_buffer = \$buffer{$key};
583             }
584             else {
585             warn "Strange line at $file_name line $line_number: $line\n";
586             }
587             $safe_current_position->();
588             }
589             if ($po) {
590             for my $key (qw(
591             msgctxt msgid msgid_plural
592             previous_msgctxt previous_msgid previous_msgid_plural
593             msgstr msgstr_n
594             )) {
595             if ( defined $buffer{$key} ) {
596             $po->$key( $buffer{$key} );
597             }
598             }
599             return $po;
600             }
601            
602             return; # no entry found
603             }
604            
605             sub _hash_key_ok {
606             my ($self, $entries) = @_;
607            
608             my $key = $self->msgid();
609            
610             if ($entries->{$key}) {
611             # don't overwrite non-obsolete entries with obsolete ones
612             return if $self->obsolete() && ! $entries->{$key}->obsolete();
613             # don't overwrite translated entries with untranslated ones
614             return if $self->msgstr() !~ m{\w}xms
615             && $entries->{$key}->msgstr() =~ m{\w}xms;
616             }
617            
618             return 1;
619             }
620            
621             1;
622            
623             __END__