Merge remote branch 'kc/new/enh/bug_5692' 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         my $reset_state_p = 0;
202         push @output, [0, $input[$i]];
203         if ($input[$i] !~ /\S/s) {
204           ;
205         } elsif ($state == 0) {
206           $state = STATE_UNDERSCORE if $input[$i] eq '_';
207         } elsif ($state == STATE_UNDERSCORE) {
208           $state = $input[$i] eq parenleft ? STATE_PARENLEFT : 0;
209         } elsif ($state == STATE_PARENLEFT) {
210           if ($input[$i] =~ /^(['"])(.*)\1$/s) {
211             ($state, $j, $q, $s) = (STATE_STRING_LITERAL, $#output, $1, $2);
212           } else {
213             $state = 0;
214           }
215         } elsif ($state == STATE_STRING_LITERAL) {
216           if ($input[$i] eq parenright) {
217             $output[$j] = [1, $output[$j]->[1], $q, $s];
218           }
219           $state = 0;
220         } else {
221           die "identify_js_translatables internal error: Unknown state $state"
222         }
223     }
224     return \@output;
225 }
226
227 ###############################################################################
228
229 sub string_canon ($) {
230   my $s = shift;
231   # Fold all whitespace into single blanks
232   $s =~ s/\s+/ /g;
233   return $s;
234 }
235
236 # safer version used internally, preserves new lines
237 sub string_canon_safe ($) {
238   my $s = shift;
239   # fold tabs and spaces into single spaces
240   $s =~ s/[\ \t]+/ /gs;
241   return $s;
242 }
243
244
245 sub _quote_cformat{
246   my $s = shift;
247   $s =~ s/%/%%/g;
248   return $s;
249 }
250
251 sub _formalize_string_cformat{
252   my $s = shift;
253   return _quote_cformat( string_canon_safe $s );
254 }
255
256 sub _formalize{
257   my $t = shift;
258   if( $t->type == TmplTokenType::DIRECTIVE ){
259     return '%s';
260   } elsif( $t->type == TmplTokenType::TEXT ){
261     return _formalize_string_cformat( $t->string );
262   } elsif( $t->type == TmplTokenType::TAG ){
263     if( $t->string =~ m/^a\b/is ){
264       return '<a>';
265     } elsif( $t->string =~ m/^input\b/is ){
266       if( lc $t->attributes->{'type'}->[1] eq 'text' ){
267         return '%S';
268       } else{
269         return '%p';
270       }
271     } else{
272       return _quote_cformat $t->string;
273     }
274   } else{
275     return _quote_cformat $t->string;
276   }
277 }
278
279 # internal parametization, used within next_token
280 # method that takes in an array of TEXT and DIRECTIVE tokens (DIRECTIVEs must be GET) and return a TmplTokenType::TEXT_PARAMETRIZED
281 sub _parametrize_internal{
282     my $this = shift;
283     my @parts = @_;
284     # my $s = "";
285     # for my $item (@parts){
286     #     if( $item->type == TmplTokenType::TEXT ){
287     #         $s .= $item->string;
288     #     } else {
289     #         #must be a variable directive
290     #         $s .= "%s";
291     #     }
292     # }
293     my $s = join( "", map { _formalize $_ } @parts );
294     # should both the string and form be $s? maybe only the later? posibly the former....
295     # used line number from first token, should suffice
296     my $t = TmplToken->new( $s, TmplTokenType::TEXT_PARAMETRIZED, $parts[0]->line_number, $this->filename );
297     $t->set_children(@parts);
298     $t->set_form($s);
299     return $t;
300 }
301
302 sub next_token {
303     my $self = shift;
304     my $next;
305     # parts that make up a text_parametrized (future children of the token)
306     my @parts = ();
307     while(1){
308         # warn Dumper @parts;
309         $next = $self->{_parser}->next_token;
310         if (! $next){
311             if (@parts){
312                 return $self->_parametrize_internal(@parts);
313             }
314             else {
315                 return undef;
316             }
317         }
318         # if cformat mode is off, dont bother parametrizing, just return them as they come
319         return $next unless $self->allow_cformat_p;
320         if( $next->type == TmplTokenType::TEXT ){
321             push @parts, $next;
322         } 
323         elsif( $next->type == TmplTokenType::DIRECTIVE && $next->string =~ m/\[%\s*\w+\s*%\]/ ){
324             push @parts, $next;
325         } 
326         else {
327             # if there is nothing in parts, return this token
328  
329            return $next unless @parts;
330             # OTHERWISE, put this token back and return the parametrized string of @parts
331             $self->{_parser}->unshift_token($next);
332             return $self->_parametrize_internal(@parts);
333         }
334
335     }
336 }
337
338 ###############################################################################
339
340 # function taken from old version
341 # used by tmpl_process3
342 sub parametrize ($$$$) {
343     my($fmt_0, $cformat_p, $t, $f) = @_;
344     my $it = '';
345     if ($cformat_p) {
346         my @params = $t->parameters_and_fields;
347         for (my $n = 0, my $fmt = $fmt_0; length $fmt;) {
348             if ($fmt =~ /^[^%]+/) {
349                 $fmt = $';
350                 $it .= $&;
351             } elsif ($fmt =~ /^%%/) {
352                 $fmt = $';
353                 $it .= '%';
354             } elsif ($fmt =~ /^%(?:(\d+)\$)?(?:(\d+)(?:\.(\d+))?)?s/s) {
355                 $n += 1;
356                 my($i, $width, $prec) = ((defined $1? $1: $n), $2, $3);
357                 $fmt = $';
358                 if (defined $width && defined $prec && !$width && !$prec) {
359                     ;
360                 } elsif (defined $params[$i - 1]) {
361                     my $param = $params[$i - 1];
362                     warn_normal "$fmt_0: $&: Expected a TMPL_VAR, but found a "
363                             . $param->type->to_string . "\n", undef
364                             if $param->type != TmplTokenType::DIRECTIVE;
365                     warn_normal "$fmt_0: $&: Unsupported "
366                                 . "field width or precision\n", undef
367                             if defined $width || defined $prec;
368                     warn_normal "$fmt_0: $&: Parameter $i not known", undef
369                             unless defined $param;
370                     $it .= defined $f? &$f( $param ): $param->string;
371                 }
372             } elsif ($fmt =~ /^%(?:(\d+)\$)?(?:(\d+)(?:\.(\d+))?)?([pS])/s) {
373                 $n += 1;
374                 my($i, $width, $prec, $conv) = ((defined $1? $1: $n), $2, $3, $4);
375                 $fmt = $';
376
377                 my $param = $params[$i - 1];
378                 if (!defined $param) {
379                     warn_normal "$fmt_0: $&: Parameter $i not known", undef;
380                 } else {
381                     if ($param->type == TmplTokenType::TAG
382                             && $param->string =~ /^<input\b/is) {
383                         my $type = defined $param->attributes?
384                                 lc($param->attributes->{'type'}->[1]): undef;
385                         if ($conv eq 'S') {
386                             warn_normal "$fmt_0: $&: Expected type=text, "
387                                         . "but found type=$type", undef
388                                     unless $type eq 'text';
389                         } elsif ($conv eq 'p') {
390                             warn_normal "$fmt_0: $&: Expected type=radio, "
391                                         . "but found type=$type", undef
392                                     unless $type eq 'radio';
393                         }
394                     } else {
395                         warn_normal "$&: Expected an INPUT, but found a "
396                                 . $param->type->to_string . "\n", undef
397                     }
398                     warn_normal "$fmt_0: $&: Unsupported "
399                                 . "field width or precision\n", undef
400                             if defined $width || defined $prec;
401                     $it .= defined $f? &$f( $param ): $param->string;
402                 }
403             } elsif ($fmt =~ /^%[^%a-zA-Z]*[a-zA-Z]/) {
404                 $fmt = $';
405                 $it .= $&;
406                 die "$&: Unknown or unsupported format specification\n"; #XXX
407             } else {
408                 die "$&: Completely confused parametrizing\n";#XXX
409             }
410         }
411     }
412     my @anchors = $t->anchors;
413     for (my $n = 0, my $fmt = $it, $it = ''; length $fmt;) {
414         if ($fmt =~ /^(?:(?!<a\d+>).)+/is) {
415             $fmt = $';
416             $it .= $&;
417         } elsif ($fmt =~ /^<a(\d+)>/is) {
418             $n += 1;
419             my $i  = $1;
420             $fmt = $';
421             my $anchor = $anchors[$i - 1];
422             warn_normal "$&: Anchor $1 not found for msgid \"$fmt_0\"", undef #FIXME
423                     unless defined $anchor;
424             $it .= $anchor->string;
425         } else {
426             die "Completely confused decoding anchors: $fmt\n";#XXX
427         }
428     }
429     return $it;
430 }
431
432
433 # Other simple functions (These are not methods)
434
435 sub blank_p ($) {
436     my($s) = @_;
437     return $s =~ /^(?:\s|\&nbsp$re_end_entity|$re_tmpl_var|$re_xsl)*$/osi;
438 }
439
440 sub trim ($) {
441     my($s0) = @_;
442     my $l0 = length $s0;
443     my $s = $s0;
444     $s =~ s/^(\s|\&nbsp$re_end_entity)+//os; my $l1 = $l0 - length $s;
445     $s =~ s/(\s|\&nbsp$re_end_entity)+$//os; my $l2 = $l0 - $l1 - length $s;
446     return wantarray? (substr($s0, 0, $l1), $s, substr($s0, $l0 - $l2)): $s;
447 }
448
449 sub quote_po ($) {
450     my($s) = @_;
451     # Locale::PO->quote is buggy, it doesn't quote newlines :-/
452     $s =~ s/([\\"])/\\\1/gs;
453     $s =~ s/\n/\\n/g;
454     #$s =~ s/[\177-\377]/ sprintf("\\%03o", ord($&)) /egs;
455     return "\"$s\"";
456 }
457
458 sub charset_canon ($) {
459     my($charset) = @_;
460     $charset = uc($charset);
461     $charset = "$1-$2" if $charset =~ /^(ISO|UTF)(\d.*)/i;
462     $charset = 'Big5' if $charset eq 'BIG5'; # "Big5" must be in mixed case
463     return $charset;
464 }
465
466 use vars qw( @latin1_utf8 );
467 @latin1_utf8 = (
468     "\302\200", "\302\201", "\302\202", "\302\203", "\302\204", "\302\205",
469     "\302\206", "\302\207", "\302\210", "\302\211", "\302\212", "\302\213",
470     "\302\214", "\302\215",   undef,      undef,    "\302\220", "\302\221",
471     "\302\222", "\302\223", "\302\224", "\302\225", "\302\226", "\302\227",
472     "\302\230", "\302\231", "\302\232", "\302\233", "\302\234", "\302\235",
473     "\302\236", "\302\237", "\302\240", "\302\241", "\302\242", "\302\243",
474     "\302\244", "\302\245", "\302\246", "\302\247", "\302\250", "\302\251",
475     "\302\252", "\302\253", "\302\254", "\302\255", "\302\256", "\302\257",
476     "\302\260", "\302\261", "\302\262", "\302\263", "\302\264", "\302\265",
477     "\302\266", "\302\267", "\302\270", "\302\271", "\302\272", "\302\273",
478     "\302\274", "\302\275", "\302\276", "\302\277", "\303\200", "\303\201",
479     "\303\202", "\303\203", "\303\204", "\303\205", "\303\206", "\303\207",
480     "\303\210", "\303\211", "\303\212", "\303\213", "\303\214", "\303\215",
481     "\303\216", "\303\217", "\303\220", "\303\221", "\303\222", "\303\223",
482     "\303\224", "\303\225", "\303\226", "\303\227", "\303\230", "\303\231",
483     "\303\232", "\303\233", "\303\234", "\303\235", "\303\236", "\303\237",
484     "\303\240", "\303\241", "\303\242", "\303\243", "\303\244", "\303\245",
485     "\303\246", "\303\247", "\303\250", "\303\251", "\303\252", "\303\253",
486     "\303\254", "\303\255", "\303\256", "\303\257", "\303\260", "\303\261",
487     "\303\262", "\303\263", "\303\264", "\303\265", "\303\266", "\303\267",
488     "\303\270", "\303\271", "\303\272", "\303\273", "\303\274", "\303\275",
489     "\303\276", "\303\277" );
490
491 sub charset_convert ($$$) {
492     my($s, $charset_in, $charset_out) = @_;
493     if ($s !~ /[\200-\377]/s) { # FIXME: don't worry about iso2022 for now
494         ;
495     } elsif ($charset_in eq 'ISO-8859-1' && $charset_out eq 'UTF-8') {
496         $s =~ s/[\200-\377]/ $latin1_utf8[ord($&) - 128] /egs;
497     } elsif ($charset_in ne $charset_out) {
498         VerboseWarnings::warn_normal "conversion from $charset_in to $charset_out is not supported\n", undef;
499     }
500     return $s;
501 }
502
503 ###############################################################################
504
505 =pod
506
507 In addition to the basic scanning, this class will also perform
508 the following:
509
510 =over
511
512 =item -
513
514 Emulation of c-format strings (see below)
515
516 =item -
517
518 Display of warnings for certain things that affects either the
519 ability of this class to yield correct output, or things that
520 are known to cause the original template to cause trouble.
521
522 =item -
523
524 Automatic correction of some of the things warned about
525 (e.g., SGML "closed start tag" notation).
526
527 =back
528
529 =head2 c-format strings emulation
530
531 Because English word order is not universal, a simple extraction
532 of translatable strings may yield some strings like "Accounts for"
533 or ambiguous strings like "in". This makes the resulting strings
534 difficult to translate, but does not affect all languages alike.
535 For example, Chinese (with a somewhat different word order) would
536 be hit harder, but French would be relatively unaffected.
537
538 To overcome this problem, the scanner can be configured to detect
539 patterns with <TMPL_VAR> directives (as well as certain HTML tags),
540 and try to construct a larger pattern that will appear in the PO
541 file as c-format strings with %s placeholders. This additional
542 step allows the translator to deal with cases where word order
543 is different (replacing %s with %1$s, %2$s, etc.), or when certain
544 words will require certain inflectional suffixes in sentences.
545
546 Because this is an incompatible change, this mode must be explicitly
547 turned on using the set_allow_cformat(1) method call.
548
549 =head2 The flag characters
550
551 The character % is followed by zero or more of the following flags:
552
553 =over
554
555 =item #
556
557 The value comes from HTML <INPUT> elements.
558 This abuse of the flag character is somewhat reasonable,
559 since TMPL_VAR and INPUT are both variables, but of different kinds.
560
561 =back
562
563 =head2 The field width and precision
564
565 An optional 0.0 can be specified for %s to specify
566 that the <TMPL_VAR> should be suppressed.
567
568 =head2 The conversion specifier
569
570 =over
571
572 =item p
573
574 Specifies any input field that is neither text nor hidden
575 (which currently mean radio buttons).
576 The p conversion specifier is chosen because this does not
577 evoke any certain sensible data type.
578
579 =item S
580
581 Specifies a text input field (<INPUT TYPE=TEXT>).
582 This use of the S conversion specifier is somewhat reasonable,
583 since text input fields contain values of undeterminable type,
584 which can be treated as strings.
585
586 =item s
587
588 Specifies a <TMPL_VAR>.
589 This use of the o conversion specifier is somewhat reasonable,
590 since <TMPL_VAR> denotes values of undeterminable type, which
591 can be treated as strings.
592
593 =back
594
595 =head1 BUGS
596
597 There is no code to save the tag name anywhere in the scanned token.
598
599 The use of <AI<i>> to stand for the I<i>th anchor
600 is not very well thought out.
601 Some abuse of c-format specifies might have been more appropriate.
602
603 =head1 HISTORY
604
605 This tokenizer is mostly based
606 on Ambrose's hideous Perl script known as subst.pl.
607
608 =cut
609
610 1;