File Coverage

blib/lib/Locale/XGettext/TT2.pm
Criterion Covered Total %
statement 136 164 82.9
branch 62 92 67.3
condition 36 57 63.1
subroutine 13 18 72.2
pod 8 8 100.0
total 255 339 75.2


line stmt bran cond sub pod time code
1             #! /bin/false
2              
3             # Copyright (C) 2016-2018 Guido Flohr ,
4             # all rights reserved.
5              
6             # This program is free software; you can redistribute it and/or modify it
7             # under the terms of the GNU Library General Public License as published
8             # by the Free Software Foundation; either version 2, or (at your option)
9             # any later version.
10              
11             # This program is distributed in the hope that it will be useful,
12             # but WITHOUT ANY WARRANTY; without even the implied warranty of
13             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14             # Library General Public License for more details.
15              
16             # You should have received a copy of the GNU Library General Public
17             # License along with this program; if not, write to the Free Software
18             # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
19             # USA.
20              
21             package Locale::XGettext::TT2;
22             $Locale::XGettext::TT2::VERSION = '0.7';
23 7     7   479065 use strict;
  7         69  
  7         241  
24              
25 7     7   4108 use Locale::TextDomain qw(Template-Plugin-Gettext);
  7         134330  
  7         46  
26 7     7   201695 use Template;
  7         135655  
  7         246  
27              
28 7     7   54 use base qw(Locale::XGettext);
  7         21  
  7         4307  
29              
30             sub versionInformation {
31 0     0 1 0 return __x('{program} (Template-Plugin-Gettext) {version}
32             Copyright (C) {years} Cantanea EOOD (http://www.cantanea.com/).
33             License GPLv3+: GNU GPL version 3 or later
34             This is free software: you are free to change and redistribute it.
35             There is NO WARRANTY, to the extent permitted by law.
36             Written by Guido Flohr (http://www.guido-flohr.net/).
37             ',
38             program => $0, years => '2016-2018',
39             version => $Locale::XGettext::TT2::VERSION);
40             }
41              
42             sub fileInformation {
43 0     0 1 0 return __(<
44             The input files should be templates for the Template::Toolkit
45             (http://www.template-toolkit.org/). The strings are usually marked and
46             made translatable with the help of "Template::Plugin::Gettext". Try the
47             command "perldoc Template::Plugin::Gettext" for more information.
48             EOF
49             }
50              
51             sub canExtractAll {
52 0     0 1 0 shift;
53             }
54              
55             sub canKeywords {
56 0     0 1 0 shift;
57             }
58              
59             sub languageSpecificOptions {
60             return [
61             [
62 0     0 1 0 'plugin|plug-in:s',
63             'plug_in',
64             ' --plug-in=PLUG-IN, --plugin=PLUG-IN',
65             __"the plug-in name (defaults to 'Gettext'), can be an empty string",
66             ]
67             ];
68             }
69              
70             sub defaultKeywords {
71             return [
72 10     10 1 41501 'gettext:1',
73             'ngettext:1,2',
74             'pgettext:1c,2',
75             'gettextp:1,2c',
76             'npgettext:1c,2,3',
77             'ngettextp:1,2,3c',
78             'xgettext:1',
79             'nxgettext:1,2',
80             'pxgettext:1c,2',
81             'xgettextp:1,2c',
82             'npxgettext:1c,2,3',
83             'nxgettextp:1,2,3c',
84             ];
85             }
86              
87             sub defaultFlags {
88             return [
89 10     10 1 11457 "xgettext:1:perl-brace-format",
90             "nxgettext:1:perl-brace-format",
91             "nxgettext:2:perl-brace-format",
92             "pxgettext:2:perl-brace-format",
93             "xgettextp:1:perl-brace-format",
94             "npxgettext:2:perl-brace-format",
95             "npxgettext:3:perl-brace-format",
96             "nxgettextp:1:perl-brace-format",
97             "nxgettextp:2:perl-brace-format",
98             ];
99             }
100              
101             sub readFile {
102 10     10 1 6057 my ($self, $filename) = @_;
103              
104 10         43 my %options = (
105             ABSOLUTE => 1,
106             # Needed for reading from POTFILES
107             RELATIVE => 1
108             );
109              
110 10         119 my $parser = Locale::XGettext::TT2::Parser->new(\%options);
111              
112 10         2280 my $tt = Template->new({
113             %options,
114             PARSER => $parser,
115             });
116              
117 10         166180 my $sink;
118 10         32 $parser->{__xgettext} = $self;
119 10         91 $parser->{__xgettext_filename} = $filename;
120              
121 10 100       48 $tt->process($filename, {}, \$sink) or die $tt->error;
122              
123 9         7069 return $self;
124             }
125              
126             package Locale::XGettext::TT2::Parser;
127             $Locale::XGettext::TT2::Parser::VERSION = '0.7';
128 7     7   227025 use strict;
  7         20  
  7         206  
129              
130 7     7   54 use Locale::TextDomain qw(Template-Plugin-Gettext);
  7         16  
  7         34  
131              
132 7     7   1579 use base qw(Template::Parser);
  7         21  
  7         4506  
133              
134             sub split_text {
135 10     10   4429 my ($self, $text) = @_;
136              
137 10 50       54 my $chunks = $self->SUPER::split_text($text) or return;
138              
139 10         7902 my $keywords = $self->{__xgettext}->keywords;
140 10         239 my $plug_in = $self->{__xgettext}->option('plug_in');
141 10 100       91 $plug_in = 'Gettext' if !defined $plug_in;
142              
143 10         29 my $ident;
144 10         22 my $lplug_in = length $plug_in;
145 10         40 while (my $chunk = shift @$chunks) {
146 75 100       177 if (!ref $chunk) {
147 37         56 shift @$chunks;
148 37         111 next;
149             }
150              
151 38         94 my ($text, $lineno, $tokens) = @$chunk;
152              
153 38 50       81 next if !ref $tokens;
154              
155 38 100       93 if ($lplug_in) {
156 37 100 66     143 if ('USE' eq $tokens->[0] && 'IDENT' eq $tokens->[2]) {
157 9 50 0     130 if ($plug_in eq $tokens->[3]
    50 33        
      33        
      33        
158             && (4 == @$tokens
159             || '(' eq $tokens->[4])) {
160 0         0 $ident = $plug_in;
161             } elsif ('ASSIGN' eq $tokens->[4] && 'IDENT' eq $tokens->[6]
162             && $plug_in eq $tokens->[7]) {
163 9         23 $ident = $tokens->[3];
164             }
165 9         45 next;
166             }
167              
168 28 50       64 next if !defined $ident;
169             } else {
170 1         2 $ident = '';
171             }
172              
173 29         79 for (my $i = 0; $i < @$tokens; $i += 2) {
174             # FIXME! It would be better to copy $tokens into an array
175             # @tokens because we modify the array reference $tokens.
176             # That implies that we iterate over tokens that do ot exist
177             # and that is an unnecessary risk.
178 232 100 100     1709 if ($lplug_in
    100 100        
    100 66        
      66        
      33        
      66        
      100        
      100        
      66        
179             && 'IDENT' eq $tokens->[$i] && $ident eq $tokens->[$i + 1]
180             && 'DOT' eq $tokens->[$i + 2] && 'IDENT' eq $tokens->[$i + 4]
181             && exists $keywords->{$tokens->[$i + 5]}) {
182 13         32 my $keyword = $keywords->{$tokens->[$i + 5]};
183 13         65 $self->__extractEntry($text, $lineno, $keyword,
184             @$tokens[$i + 6 .. $#$tokens]);
185             } elsif ('FILTER' eq $tokens->[$i]
186             && 'IDENT' eq $tokens->[$i + 2]
187             && exists $keywords->{$tokens->[$i + 3]}) {
188 12         31 my $keyword = $keywords->{$tokens->[$i + 3]};
189             # Inject the block contents as the first argument.
190 12 100       40 if ($i) {
191 9         18 my $first_arg;
192 9 50       35 if ($tokens->[$i - 2] eq 'LITERAL') {
193 9         23 $first_arg = $tokens->[$i - 1];
194             } else {
195 0         0 next;
196             }
197             # May have been called without parentheses, see
198             # https://github.com/gflohr/Template-Plugin-Gettext/issues/4
199 9 100 100     86 if (!defined $tokens->[4 + $i]) {
    100          
200 2         7 $tokens->[4 + $i] = $tokens->[5 + $i] = '(';
201 2         6 $tokens->[6 + $i] = $tokens->[7 + $i] = ')';
202 2         9 splice @$tokens, 6 + $i, 0, LITERAL => $first_arg;
203             # Or without parentheses and another filter is immediately
204             # following or the value gets dereferenced with a dot.
205             # The latter is kind of nonsense but we support it
206             # elsewhere as well and it is hard to catch.
207             } elsif ('FILTER' eq $tokens->[4 + $i]
208             || 'DOT' eq $tokens->[4 + $i]) {
209 4         19 splice @$tokens, 4 + $i, 0,
210             '(', '(', LITERAL => $first_arg, ')', ')';
211             } else {
212 3         16 splice @$tokens, 6 + $i, 0,
213             LITERAL => $first_arg, COMMA => ',';
214             }
215             } else {
216 3 50       12 next if !@$chunks;
217 3         5 my $first_arg;
218 3 50       17 if (ref $chunks->[0]) {
    50          
219 0 0       0 next if $chunks->[0]->[2] ne 'ITEXT';
220 0         0 $first_arg = $chunks->[0]->[0];
221             } elsif ('TEXT' eq $chunks->[0]) {
222 3         9 $first_arg = $chunks->[1];
223             } else {
224 0         0 next;
225             }
226 3         24 splice @$tokens, 6, 0,
227             'LITERAL', $first_arg, 'COMMA', ',';
228             }
229 12         75 $self->__extractEntry($text, $lineno, $keyword,
230             @$tokens[$i + 4 .. $#$tokens]);
231             } elsif (!$lplug_in && 'IDENT' eq $tokens->[$i]
232             && exists $keywords->{$tokens->[$i + 1]}) {
233 1         3 my $keyword = $keywords->{$tokens->[$i + 1]};
234 1         5 $self->__extractEntry($text, $lineno, $keyword,
235             @$tokens[$i + 2 .. $#$tokens]);
236             }
237             }
238             }
239              
240             # Stop processing here, so that for example includes are ignored.
241 9         52 return [];
242             }
243              
244             sub __extractEntry {
245 26     26   120 my ($self, $text, $lineno, $keyword, @tokens) = @_;
246              
247             my $args = sub {
248 26     26   144 my (@tokens) = @_;
249              
250 26 50       75 return if '(' ne $tokens[0];
251              
252 26         52 splice @tokens, 0, 2;
253              
254 26         62 my @values;
255 26         78 while (@tokens) {
256 53 100       131 if ('LITERAL' eq $tokens[0]) {
    100          
    50          
    50          
    0          
257 36         85 my $string = substr $tokens[1], 1, -1;
258 36         83 $string =~ s/\\([\\'])/$1/gs;
259 36         69 push @values, $string;
260 36         69 splice @tokens, 0, 2;
261             } elsif ('"' eq $tokens[0]) {
262 8 100 66     78 if ('TEXT' eq $tokens[2]
      33        
      66        
263             && '"' eq $tokens[4]
264             && ('COMMA' eq $tokens[6]
265             || ')' eq $tokens[6])) {
266 7         16 push @values, $tokens[3];
267 7         20 splice @tokens, 6;
268             } else {
269             # String containing interpolated variables.
270 1         26 my $msg = __"Illegal variable interpolation at \"\$\"!";
271 1         120 push @values, \$msg;
272 1         4 while (@tokens) {
273 10 50       18 last if 'COMMA' eq $tokens[0];
274 10 100       21 last if ')' eq $tokens[0];
275 9         14 shift @tokens;
276             }
277             }
278             } elsif ('NUMBER' eq $tokens[0]) {
279 0         0 push @values, $tokens[1];
280 0         0 splice @tokens, 0, 2;
281             } elsif ('IDENT' eq $tokens[0]) {
282             # We store undef as the value because we cannot use it
283             # anyway.
284 9         18 push @values, undef;
285 9         15 splice @tokens, 0, 2;
286             } elsif ('(' eq $tokens[0]) {
287 0         0 splice @tokens, 0, 2;
288 0         0 my $nested = 1;
289 0         0 while (@tokens) {
290 0 0       0 if ('(' eq $tokens[0]) {
    0          
291 0         0 ++$nested;
292 0         0 splice @tokens, 0, 2;
293             } elsif (')' eq $tokens[0]) {
294 0         0 --$nested;
295 0         0 splice @tokens, 0, 2;
296 0 0       0 if (!$nested) {
297 0         0 push @values, undef;
298 0         0 last;
299             }
300             } else {
301 0         0 splice @tokens, 0, 2;
302             }
303             }
304             } else {
305 0         0 return @values;
306             }
307              
308 53 50       116 return @values if !@tokens;
309              
310 53         85 my $next = shift @tokens;
311 53 100 66     234 if ('COMMA' eq $next) {
    100          
312 18         35 shift @tokens;
313 18         43 next;
314             } elsif ('ASSIGN' eq $next && '=>' eq $tokens[0]) {
315 9         13 shift @tokens;
316 9         23 next;
317             }
318              
319 26         111 return @values;
320             }
321              
322 0         0 return @values;
323 26         141 };
324              
325 26         101 my $min_args = $keyword->singular;
326 26         153 my %forms = (msgid => $keyword->singular);
327 26 50       141 if ($keyword->plural) {
328 0 0       0 $min_args = $keyword->plural if $keyword->plural > $min_args;
329 0         0 $forms{msgid_plural} = $keyword->plural;
330             }
331              
332 26 100       152 if ($keyword->context) {
333 9 100       44 $min_args = $keyword->context if $keyword->context > $min_args;
334 9         76 $forms{msgctxt} = $keyword->context;
335             }
336              
337 26         181 my @args = $args->(@tokens);
338              
339             # Do we have enough arguments?
340 26 50       80 return if $min_args > @args;
341              
342             my $entry = {
343             keyword => $keyword->{function}
344 26         131 };
345 26         93 foreach my $prop (keys %forms) {
346 35         78 my $argno = $forms{$prop} - 1;
347              
348             # We are only interested in literal values. Whatever is
349             # undefined is not parsable or not valid.
350 35 50       82 return if !defined $args[$argno];
351 35 100       80 if (ref $args[$argno]) {
352 1         2 my $filename = $self->{__xgettext_filename};
353 1 50       8 die "$filename:$lineno: ${$args[$argno]}\n" if ref $args[$argno];
  1         27  
354             }
355 34         77 $entry->{$prop} = $args[$argno];
356             }
357              
358 25         114 my $reference = $self->{__xgettext_filename} . ':' . $lineno;
359 25         103 $reference =~ s/-[1-9][0-9]*$//;
360 25         57 $entry->{reference} = $reference;
361              
362 25 100       70 if ($text =~ /^#/) {
363 3         8 my $comment = '';
364 3         13 my @lines = split /\n/, $text;
365 3         10 foreach my $line (@lines) {
366 6 100       28 last if $line !~ s/^[ \t\r\f\013]*#[ \t\r\f\013]?//;
367              
368 3         13 $comment .= $line . "\n";
369             }
370 3         9 $entry->{automatic} = $comment;
371             }
372              
373 25         137 $self->{__xgettext}->addEntry($entry);
374              
375 25         8063 return $self;
376             }
377              
378             1;