Merge remote-tracking branch 'kc/new/enh/bug_5917' into kcmaster
[koha.git] / misc / translator / TmplTokenizer.pm
1 package TmplTokenizer;
2
3 use strict;
4 #use warnings; FIXME - Bug 2505
5 use TmplTokenType;
6 use TmplToken;
7 use TTParser;
8 use VerboseWarnings qw( pedantic_p error_normal warn_normal warn_pedantic );
9 require Exporter;
10
11 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
12
13 ###############################################################################
14
15 =head1 NAME
16
17 TmplTokenizer.pm - Simple-minded wrapper class for TTParser
18
19 =head1 DESCRIPTION
20
21 A wrapper for the functionality found in TTParser to allow an easier transition to Template Toolkit
22
23 =cut
24
25 ###############################################################################
26
27 $VERSION = 0.02;
28
29 @ISA = qw(Exporter);
30 @EXPORT_OK = qw();
31
32 use vars qw( $pedantic_attribute_error_in_nonpedantic_mode_p );
33 use vars qw( $pedantic_tmpl_var_use_in_nonpedantic_mode_p );
34 use vars qw( $pedantic_error_markup_in_pcdata_p );
35
36 ###############################################################################
37
38 # Hideous stuff
39 use vars qw( $re_xsl $re_end_entity $re_tmpl_var);
40 BEGIN {
41     $re_tmpl_var = q{\[%\s*[get|set|default]?\s*[\w\.]+\s*[|.*?]?\s*%\]};
42     $re_xsl = q{<\/?(?:xsl:)(?:[\s\-a-zA-Z0-9"'\/\.\[\]\@\(\):=,$]+)\/?>};
43     $re_end_entity = '(?:;|$|(?=\s))'; # semicolon or before-whitespace
44 }
45 # End of the hideous stuff
46
47 use vars qw( $serial );
48
49 ###############################################################################
50
51 sub FATAL_P             () {'fatal-p'}
52 sub SYNTAXERROR_P       () {'syntaxerror-p'}
53
54 sub FILENAME            () {'input'}
55 #sub HANDLE             () {'handle'}
56
57 #sub READAHEAD          () {'readahead'}
58 sub LINENUM_START       () {'lc_0'}
59 sub LINENUM             () {'lc'}
60 sub CDATA_MODE_P        () {'cdata-mode-p'}
61 sub CDATA_CLOSE         () {'cdata-close'}
62 #sub PCDATA_MODE_P      () {'pcdata-mode-p'}    # additional submode for CDATA
63 sub JS_MODE_P           () {'js-mode-p'}        # cdata-mode-p must also be true
64
65 sub ALLOW_CFORMAT_P     () {'allow-cformat-p'}
66
67 sub new {
68     shift;
69     my ($filename) = @_;
70     #open my $handle,$filename or die "can't open $filename";
71     my $parser = TTParser->new;
72     $parser->build_tokens( $filename );
73     bless {
74       filename => $filename,
75       _parser => $parser
76 #     , handle => $handle
77 #     , readahead => []
78     } , __PACKAGE__;
79 }
80
81 ###############################################################################
82
83 # Simple getters
84
85 sub filename {
86     my $this = shift;
87     return $this->{filename};
88 }
89
90 sub fatal_p {
91     my $this = shift;
92     return $this->{+FATAL_P};
93 }
94
95 # work around, currently not implemented
96 sub syntaxerror_p {
97 #    my $this = shift;
98 #    return $this->{+SYNTAXERROR_P};
99     return 0;
100 }
101
102 sub js_mode_p {
103     my $this = shift;
104     return $this->{+JS_MODE_P};
105 }
106
107 sub allow_cformat_p {
108     my $this = shift;
109     return $this->{+ALLOW_CFORMAT_P};
110 }
111
112 # Simple setters
113
114 sub _set_fatal {
115     my $this = shift;
116     $this->{+FATAL_P} = $_[0];
117     return $this;
118 }
119
120 sub _set_js_mode {
121     my $this = shift;
122     $this->{+JS_MODE_P} = $_[0];
123     return $this;
124 }
125
126 #used in xgettext, tmpl_process3 and text-extract2
127 sub set_allow_cformat {
128     my $this = shift;
129     $this->{+ALLOW_CFORMAT_P} = $_[0];
130     return $this;
131 }
132
133 ###############################################################################
134
135 use vars qw( $js_EscapeSequence );
136 BEGIN {
137     # Perl quoting is really screwed up, but this common subexp is way too long
138     $js_EscapeSequence = q{\\\\(?:['"\\\\bfnrt]|[^0-7xu]|[0-3]?[0-7]{1,2}|x[\da-fA-F]{2}|u[\da-fA-F]{4})};
139 }
140 sub parenleft  () { '(' }
141 sub parenright () { ')' }
142
143 sub _split_js ($) {
144     my ($s0) = @_;
145     my @it = ();
146     while (length $s0) {
147         if ($s0 =~ /^\s+/s) {                           # whitespace
148           push @it, $&;
149           $s0 = $';
150         } elsif ($s0 =~ /^\/\/[^\r\n]*(?:[\r\n]|$)/s) { # C++-style comment
151         push @it, $&;
152         $s0 = $';
153         } elsif ($s0 =~ /^\/\*(?:(?!\*\/).)*\*\//s) {   # C-style comment
154             push @it, $&;
155             $s0 = $';
156         # Keyword or identifier, ECMA-262 p.13 (section 7.5)
157         } elsif ($s0 =~ /^[A-Z_\$][A-Z\d_\$]*/is) {     # IdentifierName
158             push @it, $&;
159             $s0 = $';
160         # Punctuator, ECMA-262 p.13 (section 7.6)
161         } elsif ($s0 =~ /^(?:[\(\){}\[\];]|>>>=|<<=|>>=|[-\+\*\/\&\|\^\%]=|>>>|<<|>>|--|\+\+|\|\||\&\&|==|<=|>=|!=|[=><,!~\?:\.\-\+\*\/\&\|\^\%])/s) {
162             push @it, $&;
163             $s0 = $';
164         # DecimalLiteral, ECMA-262 p.14 (section 7.7.3); note: bug in the spec
165         } elsif ($s0 =~ /^(?:0|[1-9]\d+(?:\.\d*(?:[eE][-\+]?\d+)?)?)/s) {
166             push @it, $&;
167             $s0 = $';
168         # HexIntegerLiteral, ECMA-262 p.15 (section 7.7.3)
169         } elsif ($s0 =~ /^0[xX][\da-fA-F]+/s) {
170             push @it, $&;
171             $s0 = $';
172         # OctalIntegerLiteral, ECMA-262 p.15 (section 7.7.3)
173         } elsif ($s0 =~ /^0[\da-fA-F]+/s) {
174             push @it, $&;
175             $s0 = $';
176         # StringLiteral, ECMA-262 p.17 (section 7.7.4)
177         # XXX SourceCharacter doesn't seem to be defined (?)
178         } elsif ($s0 =~ /^(?:"(?:(?!["\\\r\n]).|$js_EscapeSequence)*"|'(?:(?!['\\\r\n]).|$js_EscapeSequence)*')/os) {
179             push @it, $&;
180             $s0 = $';
181         } elsif ($s0 =~ /^./) {                         # UNKNOWN TOKEN !!!
182             push @it, $&;
183             $s0 = $';
184         }
185     }
186     return @it;
187 }
188
189 sub STATE_UNDERSCORE     () { 1 }
190 sub STATE_PARENLEFT      () { 2 }
191 sub STATE_STRING_LITERAL () { 3 }
192
193 # XXX This is a crazy hack. I don't want to write an ECMAScript parser.
194 # XXX A scanner is one thing; a parser another thing.
195 sub _identify_js_translatables (@) {
196     my @input = @_;
197     my @output = ();
198     # We mark a JavaScript translatable string as in C, i.e., _("literal")
199     # For simplicity, we ONLY look for "_" "(" StringLiteral ")"
200     for (my $i = 0, my $state = 0, my($j, $q, $s); $i <= $#input; $i += 1) {
201 #        warn $input[$i];
202         my $reset_state_p = 0;
203         push @output, [0, $input[$i]];
204         if ($input[$i] !~ /\S/s) {
205           ;
206         } elsif ($state == 0) {
207           $state = STATE_UNDERSCORE if $input[$i] eq '_';
208         } elsif ($state == STATE_UNDERSCORE) {
209           $state = $input[$i] eq parenleft ? STATE_PARENLEFT : 0;
210         } elsif ($state == STATE_PARENLEFT) {
211           if ($input[$i] =~ /^(['"])(.*)\1$/s) {
212             ($state, $j, $q, $s) = (STATE_STRING_LITERAL, $#output, $1, $2);
213           } else {
214             $state = 0;
215           }
216         } elsif ($state == STATE_STRING_LITERAL) {
217           if ($input[$i] eq parenright) {
218             $output[$j] = [1, $output[$j]->[1], $q, $s];
219           }
220           $state = 0;
221         } else {
222           die "identify_js_translatables internal error: Unknown state $state"
223         }
224     }
225 #    use Data::Dumper;
226 #    warn Dumper \@output;
227     return \@output;
228 }
229
230 ###############################################################################
231
232 sub string_canon ($) {
233   my $s = shift;
234   # Fold all whitespace into single blanks
235   $s =~ s/\s+/ /g;
236   $s =~ s/^\s+//g;
237   return $s;
238 }
239
240 # safer version used internally, preserves new lines
241 sub string_canon_safe ($) {
242   my $s = shift;
243   # fold tabs and spaces into single spaces
244   $s =~ s/[\ \t]+/ /gs;
245   return $s;
246 }
247
248
249 sub _quote_cformat{
250   my $s = shift;
251   $s =~ s/%/%%/g;
252   return $s;
253 }
254
255 sub _formalize_string_cformat{
256   my $s = shift;
257   return _quote_cformat( string_canon_safe $s );
258 }
259
260 sub _formalize{
261   my $t = shift;
262   if( $t->type == TmplTokenType::DIRECTIVE ){
263     return '%s';
264   } elsif( $t->type == TmplTokenType::TEXT ){
265     return _formalize_string_cformat( $t->string );
266   } elsif( $t->type == TmplTokenType::TAG ){
267     if( $t->string =~ m/^a\b/is ){
268       return '<a>';
269     } elsif( $t->string =~ m/^input\b/is ){
270       if( lc $t->attributes->{'type'}->[1] eq 'text' ){
271         return '%S';
272       } else{
273         return '%p';
274       }
275     } else{
276       return _quote_cformat $t->string;
277     }     
278   } else{
279     return _quote_cformat $t->string;
280   }
281 }
282
283 # internal parametization, used within next_token
284 # method that takes in an array of TEXT and DIRECTIVE tokens (DIRECTIVEs must be GET) and return a TmplTokenType::TEXT_PARAMETRIZED
285 sub _parametrize_internal{
286     my $this = shift;
287     my @parts = @_;
288     # my $s = "";
289     # for my $item (@parts){
290     #     if( $item->type == TmplTokenType::TEXT ){
291     #         $s .= $item->string;
292     #     } else {
293     #         #must be a variable directive
294     #         $s .= "%s";
295     #     }
296     # }
297     my $s = join( "", map { _formalize $_ } @parts );
298     # should both the string and form be $s? maybe only the later? posibly the former....
299     # used line number from first token, should suffice
300     my $t = TmplToken->new( $s, TmplTokenType::TEXT_PARAMETRIZED, $parts[0]->line_number, $this->filename );
301     $t->set_children(@parts);
302     $t->set_form($s);
303     return $t;
304 }
305
306 sub next_token {
307     my $self = shift;
308     my $next;
309 #    warn "in next_token";
310     # parts that make up a text_parametrized (future children of the token)
311     my @parts = ();
312     while(1){
313         $next = $self->{_parser}->next_token;
314         if (! $next){
315             if (@parts){
316                 return $self->_parametrize_internal(@parts);
317             }
318             else {
319                 return undef;
320             }
321         }
322         # if cformat mode is off, dont bother parametrizing, just return them as they come
323         return $next unless $self->allow_cformat_p;
324         if( $next->type == TmplTokenType::TEXT ){
325             push @parts, $next;
326         } 
327 #        elsif( $next->type == TmplTokenType::DIRECTIVE && $next->string =~ m/\[%\s*\w+\s*%\]/ ){
328         elsif( $next->type == TmplTokenType::DIRECTIVE ){
329             push @parts, $next;
330         } 
331         elsif ( $next->type == TmplTokenType::CDATA){
332             $self->_set_js_mode(1);
333             my $s0 = $next->string;
334             my @head = ();
335             my @tail = ();
336
337             if ($s0 =~ /^(\s*\[%\s*)(.*)(\s%=]\s*)$/s) {
338                 push @head, $1;
339                  push @tail, $3;
340                 $s0 = $2;
341             }
342             push @head, _split_js $s0;
343             $next->set_js_data(_identify_js_translatables(@head, @tail) );
344             return $next unless @parts;     
345             $self->{_parser}->unshift_token($next);
346             return $self->_parametrize_internal(@parts);
347         }
348         else {
349             # if there is nothing in parts, return this token
350             return $next unless @parts;
351
352             # OTHERWISE, put this token back and return the parametrized string of @parts
353             $self->{_parser}->unshift_token($next);
354             return $self->_parametrize_internal(@parts);
355         }
356
357     }
358 }
359
360 ###############################################################################
361
362 # function taken from old version
363 # used by tmpl_process3
364 sub parametrize ($$$$) {
365     my($fmt_0, $cformat_p, $t, $f) = @_;
366     my $it = '';
367     if ($cformat_p) {
368         my @params = $t->parameters_and_fields;
369         for (my $n = 0, my $fmt = $fmt_0; length $fmt;) {
370             if ($fmt =~ /^[^%]+/) {
371                 $fmt = $';
372                 $it .= $&;
373             } elsif ($fmt =~ /^%%/) {
374                 $fmt = $';
375                 $it .= '%';
376             } elsif ($fmt =~ /^%(?:(\d+)\$)?(?:(\d+)(?:\.(\d+))?)?s/s) {
377                 $n += 1;
378                 my($i, $width, $prec) = ((defined $1? $1: $n), $2, $3);
379                 $fmt = $';
380                 if (defined $width && defined $prec && !$width && !$prec) {
381                     ;
382                 } elsif (defined $params[$i - 1]) {
383                     my $param = $params[$i - 1];
384                     warn_normal "$fmt_0: $&: Expected a TMPL_VAR, but found a "
385                             . $param->type->to_string . "\n", undef
386                             if $param->type != TmplTokenType::DIRECTIVE;
387                     warn_normal "$fmt_0: $&: Unsupported "
388                                 . "field width or precision\n", undef
389                             if defined $width || defined $prec;
390                     warn_normal "$fmt_0: $&: Parameter $i not known", undef
391                             unless defined $param;
392                     $it .= defined $f? &$f( $param ): $param->string;
393                 }
394             } elsif ($fmt =~ /^%(?:(\d+)\$)?(?:(\d+)(?:\.(\d+))?)?([pS])/s) {
395                 $n += 1;
396                 my($i, $width, $prec, $conv) = ((defined $1? $1: $n), $2, $3, $4);
397                 $fmt = $';
398
399                 my $param = $params[$i - 1];
400                 if (!defined $param) {
401                     warn_normal "$fmt_0: $&: Parameter $i not known", undef;
402                 } else {
403                     if ($param->type == TmplTokenType::TAG
404                             && $param->string =~ /^<input\b/is) {
405                         my $type = defined $param->attributes?
406                                 lc($param->attributes->{'type'}->[1]): undef;
407                         if ($conv eq 'S') {
408                             warn_normal "$fmt_0: $&: Expected type=text, "
409                                         . "but found type=$type", undef
410                                     unless $type eq 'text';
411                         } elsif ($conv eq 'p') {
412                             warn_normal "$fmt_0: $&: Expected type=radio, "
413                                         . "but found type=$type", undef
414                                     unless $type eq 'radio';
415                         }
416                     } else {
417                         warn_normal "$&: Expected an INPUT, but found a "
418                                 . $param->type->to_string . "\n", undef
419                     }
420                     warn_normal "$fmt_0: $&: Unsupported "
421                                 . "field width or precision\n", undef
422                             if defined $width || defined $prec;
423                     $it .= defined $f? &$f( $param ): $param->string;
424                 }
425             } elsif ($fmt =~ /^%[^%a-zA-Z]*[a-zA-Z]/) {
426                 $fmt = $';
427                 $it .= $&;
428                 die "$&: Unknown or unsupported format specification\n"; #XXX
429             } else {
430                 die "$&: Completely confused parametrizing -- msgid: $fmt_0\n";#XXX
431             }
432         }
433     }
434     my @anchors = $t->anchors;
435     for (my $n = 0, my $fmt = $it, $it = ''; length $fmt;) {
436         if ($fmt =~ /^(?:(?!<a\d+>).)+/is) {
437             $fmt = $';
438             $it .= $&;
439         } elsif ($fmt =~ /^<a(\d+)>/is) {
440             $n += 1;
441             my $i  = $1;
442             $fmt = $';
443             my $anchor = $anchors[$i - 1];
444             warn_normal "$&: Anchor $1 not found for msgid \"$fmt_0\"", undef #FIXME
445                     unless defined $anchor;
446             $it .= $anchor->string;
447         } else {
448             die "Completely confused decoding anchors: $fmt\n";#XXX
449         }
450     }
451     return $it;
452 }
453
454
455 # Other simple functions (These are not methods)
456
457 sub blank_p ($) {
458     my($s) = @_;
459     return $s =~ /^(?:\s|\&nbsp$re_end_entity|$re_tmpl_var|$re_xsl)*$/osi;
460 }
461
462 sub trim ($) {
463     my($s0) = @_;
464     my $l0 = length $s0;
465     my $s = $s0;
466     $s =~ s/^(\s|\&nbsp$re_end_entity)+//os; my $l1 = $l0 - length $s;
467     $s =~ s/(\s|\&nbsp$re_end_entity)+$//os; my $l2 = $l0 - $l1 - length $s;
468     return wantarray? (substr($s0, 0, $l1), $s, substr($s0, $l0 - $l2)): $s;
469 }
470
471 sub quote_po ($) {
472     my($s) = @_;
473     # Locale::PO->quote is buggy, it doesn't quote newlines :-/
474     $s =~ s/([\\"])/\\\1/gs;
475     $s =~ s/\n/\\n/g;
476     #$s =~ s/[\177-\377]/ sprintf("\\%03o", ord($&)) /egs;
477     return "\"$s\"";
478 }
479
480 sub charset_canon ($) {
481     my($charset) = @_;
482     $charset = uc($charset);
483     $charset = "$1-$2" if $charset =~ /^(ISO|UTF)(\d.*)/i;
484     $charset = 'Big5' if $charset eq 'BIG5'; # "Big5" must be in mixed case
485     return $charset;
486 }
487
488 use vars qw( @latin1_utf8 );
489 @latin1_utf8 = (
490     "\302\200", "\302\201", "\302\202", "\302\203", "\302\204", "\302\205",
491     "\302\206", "\302\207", "\302\210", "\302\211", "\302\212", "\302\213",
492     "\302\214", "\302\215",   undef,      undef,    "\302\220", "\302\221",
493     "\302\222", "\302\223", "\302\224", "\302\225", "\302\226", "\302\227",
494     "\302\230", "\302\231", "\302\232", "\302\233", "\302\234", "\302\235",
495     "\302\236", "\302\237", "\302\240", "\302\241", "\302\242", "\302\243",
496     "\302\244", "\302\245", "\302\246", "\302\247", "\302\250", "\302\251",
497     "\302\252", "\302\253", "\302\254", "\302\255", "\302\256", "\302\257",
498     "\302\260", "\302\261", "\302\262", "\302\263", "\302\264", "\302\265",
499     "\302\266", "\302\267", "\302\270", "\302\271", "\302\272", "\302\273",
500     "\302\274", "\302\275", "\302\276", "\302\277", "\303\200", "\303\201",
501     "\303\202", "\303\203", "\303\204", "\303\205", "\303\206", "\303\207",
502     "\303\210", "\303\211", "\303\212", "\303\213", "\303\214", "\303\215",
503     "\303\216", "\303\217", "\303\220", "\303\221", "\303\222", "\303\223",
504     "\303\224", "\303\225", "\303\226", "\303\227", "\303\230", "\303\231",
505     "\303\232", "\303\233", "\303\234", "\303\235", "\303\236", "\303\237",
506     "\303\240", "\303\241", "\303\242", "\303\243", "\303\244", "\303\245",
507     "\303\246", "\303\247", "\303\250", "\303\251", "\303\252", "\303\253",
508     "\303\254", "\303\255", "\303\256", "\303\257", "\303\260", "\303\261",
509     "\303\262", "\303\263", "\303\264", "\303\265", "\303\266", "\303\267",
510     "\303\270", "\303\271", "\303\272", "\303\273", "\303\274", "\303\275",
511     "\303\276", "\303\277" );
512
513 sub charset_convert ($$$) {
514     my($s, $charset_in, $charset_out) = @_;
515     if ($s !~ /[\200-\377]/s) { # FIXME: don't worry about iso2022 for now
516         ;
517     } elsif ($charset_in eq 'ISO-8859-1' && $charset_out eq 'UTF-8') {
518         $s =~ s/[\200-\377]/ $latin1_utf8[ord($&) - 128] /egs;
519     } elsif ($charset_in ne $charset_out) {
520         VerboseWarnings::warn_normal "conversion from $charset_in to $charset_out is not supported\n", undef;
521     }
522     return $s;
523 }
524
525 ###############################################################################
526
527 =pod
528
529 In addition to the basic scanning, this class will also perform
530 the following:
531
532 =over
533
534 =item -
535
536 Emulation of c-format strings (see below)
537
538 =item -
539
540 Display of warnings for certain things that affects either the
541 ability of this class to yield correct output, or things that
542 are known to cause the original template to cause trouble.
543
544 =item -
545
546 Automatic correction of some of the things warned about
547 (e.g., SGML "closed start tag" notation).
548
549 =back
550
551 =head2 c-format strings emulation
552
553 Because English word order is not universal, a simple extraction
554 of translatable strings may yield some strings like "Accounts for"
555 or ambiguous strings like "in". This makes the resulting strings
556 difficult to translate, but does not affect all languages alike.
557 For example, Chinese (with a somewhat different word order) would
558 be hit harder, but French would be relatively unaffected.
559
560 To overcome this problem, the scanner can be configured to detect
561 patterns with <TMPL_VAR> directives (as well as certain HTML tags),
562 and try to construct a larger pattern that will appear in the PO
563 file as c-format strings with %s placeholders. This additional
564 step allows the translator to deal with cases where word order
565 is different (replacing %s with %1$s, %2$s, etc.), or when certain
566 words will require certain inflectional suffixes in sentences.
567
568 Because this is an incompatible change, this mode must be explicitly
569 turned on using the set_allow_cformat(1) method call.
570
571 =head2 The flag characters
572
573 The character % is followed by zero or more of the following flags:
574
575 =over
576
577 =item #
578
579 The value comes from HTML <INPUT> elements.
580 This abuse of the flag character is somewhat reasonable,
581 since TMPL_VAR and INPUT are both variables, but of different kinds.
582
583 =back
584
585 =head2 The field width and precision
586
587 An optional 0.0 can be specified for %s to specify
588 that the <TMPL_VAR> should be suppressed.
589
590 =head2 The conversion specifier
591
592 =over
593
594 =item p
595
596 Specifies any input field that is neither text nor hidden
597 (which currently mean radio buttons).
598 The p conversion specifier is chosen because this does not
599 evoke any certain sensible data type.
600
601 =item S
602
603 Specifies a text input field (<INPUT TYPE=TEXT>).
604 This use of the S conversion specifier is somewhat reasonable,
605 since text input fields contain values of undeterminable type,
606 which can be treated as strings.
607
608 =item s
609
610 Specifies a <TMPL_VAR>.
611 This use of the o conversion specifier is somewhat reasonable,
612 since <TMPL_VAR> denotes values of undeterminable type, which
613 can be treated as strings.
614
615 =back
616
617 =head1 BUGS
618
619 There is no code to save the tag name anywhere in the scanned token.
620
621 The use of <AI<i>> to stand for the I<i>th anchor
622 is not very well thought out.
623 Some abuse of c-format specifies might have been more appropriate.
624
625 =head1 HISTORY
626
627 This tokenizer is mostly based
628 on Ambrose's hideous Perl script known as subst.pl.
629
630 =cut
631
632 1;