File Coverage

blib/lib/Locale/XGettext/TT2.pm
Criterion Covered Total %
statement 139 167 83.2
branch 62 92 67.3
condition 36 57 63.1
subroutine 14 19 73.6
pod 8 8 100.0
total 259 343 75.5


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