4 #use warnings; FIXME - Bug 2505
8 use VerboseWarnings qw( pedantic_p error_normal warn_normal warn_pedantic );
11 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
13 ###############################################################################
17 TmplTokenizer.pm - Simple-minded wrapper class for TTParser
21 A wrapper for the functionality found in TTParser to allow an easier transition to Template Toolkit
25 ###############################################################################
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 );
36 ###############################################################################
39 use vars qw( $re_xsl $re_end_entity $re_tmpl_var);
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
45 # End of the hideous stuff
47 use vars qw( $serial );
49 ###############################################################################
51 sub FATAL_P () {'fatal-p'}
52 sub SYNTAXERROR_P () {'syntaxerror-p'}
54 sub FILENAME () {'input'}
55 #sub HANDLE () {'handle'}
57 #sub READAHEAD () {'readahead'}
58 sub LINENUM_START () {'lc_0'}
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
65 sub ALLOW_CFORMAT_P () {'allow-cformat-p'}
70 #open my $handle,$filename or die "can't open $filename";
71 my $parser = TTParser->new;
72 $parser->build_tokens( $filename );
74 filename => $filename,
81 ###############################################################################
87 return $this->{filename};
92 return $this->{+FATAL_P};
95 # work around, currently not implemented
98 # return $this->{+SYNTAXERROR_P};
104 return $this->{+JS_MODE_P};
107 sub allow_cformat_p {
109 return $this->{+ALLOW_CFORMAT_P};
116 $this->{+FATAL_P} = $_[0];
122 $this->{+JS_MODE_P} = $_[0];
126 #used in xgettext, tmpl_process3 and text-extract2
127 sub set_allow_cformat {
129 $this->{+ALLOW_CFORMAT_P} = $_[0];
133 ###############################################################################
135 use vars qw( $js_EscapeSequence );
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})};
140 sub parenleft () { '(' }
141 sub parenright () { ')' }
147 if ($s0 =~ /^\s+/s) { # whitespace
150 } elsif ($s0 =~ /^\/\/[^\r\n]*(?:[\r\n]|$)/s) { # C++-style comment
153 } elsif ($s0 =~ /^\/\*(?:(?!\*\/).)*\*\//s) { # C-style comment
156 # Keyword or identifier, ECMA-262 p.13 (section 7.5)
157 } elsif ($s0 =~ /^[A-Z_\$][A-Z\d_\$]*/is) { # IdentifierName
160 # Punctuator, ECMA-262 p.13 (section 7.6)
161 } elsif ($s0 =~ /^(?:[\(\){}\[\];]|>>>=|<<=|>>=|[-\+\*\/\&\|\^\%]=|>>>|<<|>>|--|\+\+|\|\||\&\&|==|<=|>=|!=|[=><,!~\?:\.\-\+\*\/\&\|\^\%])/s) {
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) {
168 # HexIntegerLiteral, ECMA-262 p.15 (section 7.7.3)
169 } elsif ($s0 =~ /^0[xX][\da-fA-F]+/s) {
172 # OctalIntegerLiteral, ECMA-262 p.15 (section 7.7.3)
173 } elsif ($s0 =~ /^0[\da-fA-F]+/s) {
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) {
181 } elsif ($s0 =~ /^./) { # UNKNOWN TOKEN !!!
189 sub STATE_UNDERSCORE () { 1 }
190 sub STATE_PARENLEFT () { 2 }
191 sub STATE_STRING_LITERAL () { 3 }
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 (@) {
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) {
202 my $reset_state_p = 0;
203 push @output, [0, $input[$i]];
204 if ($input[$i] !~ /\S/s) {
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);
216 } elsif ($state == STATE_STRING_LITERAL) {
217 if ($input[$i] eq parenright) {
218 $output[$j] = [1, $output[$j]->[1], $q, $s];
222 die "identify_js_translatables internal error: Unknown state $state"
226 # warn Dumper \@output;
230 ###############################################################################
232 sub string_canon ($) {
234 # Fold all whitespace into single blanks
239 # safer version used internally, preserves new lines
240 sub string_canon_safe ($) {
242 # fold tabs and spaces into single spaces
243 $s =~ s/[\ \t]+/ /gs;
254 sub _formalize_string_cformat{
256 return _quote_cformat( string_canon_safe $s );
261 if( $t->type == TmplTokenType::DIRECTIVE ){
263 } elsif( $t->type == TmplTokenType::TEXT ){
264 return _formalize_string_cformat( $t->string );
265 } elsif( $t->type == TmplTokenType::TAG ){
266 if( $t->string =~ m/^a\b/is ){
268 } elsif( $t->string =~ m/^input\b/is ){
269 if( lc $t->attributes->{'type'}->[1] eq 'text' ){
275 return _quote_cformat $t->string;
278 return _quote_cformat $t->string;
282 # internal parametization, used within next_token
283 # method that takes in an array of TEXT and DIRECTIVE tokens (DIRECTIVEs must be GET) and return a TmplTokenType::TEXT_PARAMETRIZED
284 sub _parametrize_internal{
288 # for my $item (@parts){
289 # if( $item->type == TmplTokenType::TEXT ){
290 # $s .= $item->string;
292 # #must be a variable directive
296 my $s = join( "", map { _formalize $_ } @parts );
297 # should both the string and form be $s? maybe only the later? posibly the former....
298 # used line number from first token, should suffice
299 my $t = TmplToken->new( $s, TmplTokenType::TEXT_PARAMETRIZED, $parts[0]->line_number, $this->filename );
300 $t->set_children(@parts);
308 # warn "in next_token";
309 # parts that make up a text_parametrized (future children of the token)
312 $next = $self->{_parser}->next_token;
315 return $self->_parametrize_internal(@parts);
321 # if cformat mode is off, dont bother parametrizing, just return them as they come
322 return $next unless $self->allow_cformat_p;
323 if( $next->type == TmplTokenType::TEXT ){
326 elsif( $next->type == TmplTokenType::DIRECTIVE && $next->string =~ m/\[%\s*\w+\s*%\]/ ){
329 elsif ( $next->type == TmplTokenType::CDATA){
330 $self->_set_js_mode(1);
331 my $s0 = $next->string;
335 if ($s0 =~ /^(\s*\[%\s*)(.*)(\s%=]\s*)$/s) {
340 push @head, _split_js $s0;
341 $next->set_js_data(_identify_js_translatables(@head, @tail) );
346 # if there is nothing in parts, return this token
348 return $next unless @parts;
349 # OTHERWISE, put this token back and return the parametrized string of @parts
350 $self->{_parser}->unshift_token($next);
351 return $self->_parametrize_internal(@parts);
357 ###############################################################################
359 # function taken from old version
360 # used by tmpl_process3
361 sub parametrize ($$$$) {
362 my($fmt_0, $cformat_p, $t, $f) = @_;
365 my @params = $t->parameters_and_fields;
366 for (my $n = 0, my $fmt = $fmt_0; length $fmt;) {
367 if ($fmt =~ /^[^%]+/) {
370 } elsif ($fmt =~ /^%%/) {
373 } elsif ($fmt =~ /^%(?:(\d+)\$)?(?:(\d+)(?:\.(\d+))?)?s/s) {
375 my($i, $width, $prec) = ((defined $1? $1: $n), $2, $3);
377 if (defined $width && defined $prec && !$width && !$prec) {
379 } elsif (defined $params[$i - 1]) {
380 my $param = $params[$i - 1];
381 warn_normal "$fmt_0: $&: Expected a TMPL_VAR, but found a "
382 . $param->type->to_string . "\n", undef
383 if $param->type != TmplTokenType::DIRECTIVE;
384 warn_normal "$fmt_0: $&: Unsupported "
385 . "field width or precision\n", undef
386 if defined $width || defined $prec;
387 warn_normal "$fmt_0: $&: Parameter $i not known", undef
388 unless defined $param;
389 $it .= defined $f? &$f( $param ): $param->string;
391 } elsif ($fmt =~ /^%(?:(\d+)\$)?(?:(\d+)(?:\.(\d+))?)?([pS])/s) {
393 my($i, $width, $prec, $conv) = ((defined $1? $1: $n), $2, $3, $4);
396 my $param = $params[$i - 1];
397 if (!defined $param) {
398 warn_normal "$fmt_0: $&: Parameter $i not known", undef;
400 if ($param->type == TmplTokenType::TAG
401 && $param->string =~ /^<input\b/is) {
402 my $type = defined $param->attributes?
403 lc($param->attributes->{'type'}->[1]): undef;
405 warn_normal "$fmt_0: $&: Expected type=text, "
406 . "but found type=$type", undef
407 unless $type eq 'text';
408 } elsif ($conv eq 'p') {
409 warn_normal "$fmt_0: $&: Expected type=radio, "
410 . "but found type=$type", undef
411 unless $type eq 'radio';
414 warn_normal "$&: Expected an INPUT, but found a "
415 . $param->type->to_string . "\n", undef
417 warn_normal "$fmt_0: $&: Unsupported "
418 . "field width or precision\n", undef
419 if defined $width || defined $prec;
420 $it .= defined $f? &$f( $param ): $param->string;
422 } elsif ($fmt =~ /^%[^%a-zA-Z]*[a-zA-Z]/) {
425 die "$&: Unknown or unsupported format specification\n"; #XXX
427 die "$&: Completely confused parametrizing\n";#XXX
431 my @anchors = $t->anchors;
432 for (my $n = 0, my $fmt = $it, $it = ''; length $fmt;) {
433 if ($fmt =~ /^(?:(?!<a\d+>).)+/is) {
436 } elsif ($fmt =~ /^<a(\d+)>/is) {
440 my $anchor = $anchors[$i - 1];
441 warn_normal "$&: Anchor $1 not found for msgid \"$fmt_0\"", undef #FIXME
442 unless defined $anchor;
443 $it .= $anchor->string;
445 die "Completely confused decoding anchors: $fmt\n";#XXX
452 # Other simple functions (These are not methods)
456 return $s =~ /^(?:\s|\ $re_end_entity|$re_tmpl_var|$re_xsl)*$/osi;
463 $s =~ s/^(\s|\ $re_end_entity)+//os; my $l1 = $l0 - length $s;
464 $s =~ s/(\s|\ $re_end_entity)+$//os; my $l2 = $l0 - $l1 - length $s;
465 return wantarray? (substr($s0, 0, $l1), $s, substr($s0, $l0 - $l2)): $s;
470 # Locale::PO->quote is buggy, it doesn't quote newlines :-/
471 $s =~ s/([\\"])/\\\1/gs;
473 #$s =~ s/[\177-\377]/ sprintf("\\%03o", ord($&)) /egs;
477 sub charset_canon ($) {
479 $charset = uc($charset);
480 $charset = "$1-$2" if $charset =~ /^(ISO|UTF)(\d.*)/i;
481 $charset = 'Big5' if $charset eq 'BIG5'; # "Big5" must be in mixed case
485 use vars qw( @latin1_utf8 );
487 "\302\200", "\302\201", "\302\202", "\302\203", "\302\204", "\302\205",
488 "\302\206", "\302\207", "\302\210", "\302\211", "\302\212", "\302\213",
489 "\302\214", "\302\215", undef, undef, "\302\220", "\302\221",
490 "\302\222", "\302\223", "\302\224", "\302\225", "\302\226", "\302\227",
491 "\302\230", "\302\231", "\302\232", "\302\233", "\302\234", "\302\235",
492 "\302\236", "\302\237", "\302\240", "\302\241", "\302\242", "\302\243",
493 "\302\244", "\302\245", "\302\246", "\302\247", "\302\250", "\302\251",
494 "\302\252", "\302\253", "\302\254", "\302\255", "\302\256", "\302\257",
495 "\302\260", "\302\261", "\302\262", "\302\263", "\302\264", "\302\265",
496 "\302\266", "\302\267", "\302\270", "\302\271", "\302\272", "\302\273",
497 "\302\274", "\302\275", "\302\276", "\302\277", "\303\200", "\303\201",
498 "\303\202", "\303\203", "\303\204", "\303\205", "\303\206", "\303\207",
499 "\303\210", "\303\211", "\303\212", "\303\213", "\303\214", "\303\215",
500 "\303\216", "\303\217", "\303\220", "\303\221", "\303\222", "\303\223",
501 "\303\224", "\303\225", "\303\226", "\303\227", "\303\230", "\303\231",
502 "\303\232", "\303\233", "\303\234", "\303\235", "\303\236", "\303\237",
503 "\303\240", "\303\241", "\303\242", "\303\243", "\303\244", "\303\245",
504 "\303\246", "\303\247", "\303\250", "\303\251", "\303\252", "\303\253",
505 "\303\254", "\303\255", "\303\256", "\303\257", "\303\260", "\303\261",
506 "\303\262", "\303\263", "\303\264", "\303\265", "\303\266", "\303\267",
507 "\303\270", "\303\271", "\303\272", "\303\273", "\303\274", "\303\275",
508 "\303\276", "\303\277" );
510 sub charset_convert ($$$) {
511 my($s, $charset_in, $charset_out) = @_;
512 if ($s !~ /[\200-\377]/s) { # FIXME: don't worry about iso2022 for now
514 } elsif ($charset_in eq 'ISO-8859-1' && $charset_out eq 'UTF-8') {
515 $s =~ s/[\200-\377]/ $latin1_utf8[ord($&) - 128] /egs;
516 } elsif ($charset_in ne $charset_out) {
517 VerboseWarnings::warn_normal "conversion from $charset_in to $charset_out is not supported\n", undef;
522 ###############################################################################
526 In addition to the basic scanning, this class will also perform
533 Emulation of c-format strings (see below)
537 Display of warnings for certain things that affects either the
538 ability of this class to yield correct output, or things that
539 are known to cause the original template to cause trouble.
543 Automatic correction of some of the things warned about
544 (e.g., SGML "closed start tag" notation).
548 =head2 c-format strings emulation
550 Because English word order is not universal, a simple extraction
551 of translatable strings may yield some strings like "Accounts for"
552 or ambiguous strings like "in". This makes the resulting strings
553 difficult to translate, but does not affect all languages alike.
554 For example, Chinese (with a somewhat different word order) would
555 be hit harder, but French would be relatively unaffected.
557 To overcome this problem, the scanner can be configured to detect
558 patterns with <TMPL_VAR> directives (as well as certain HTML tags),
559 and try to construct a larger pattern that will appear in the PO
560 file as c-format strings with %s placeholders. This additional
561 step allows the translator to deal with cases where word order
562 is different (replacing %s with %1$s, %2$s, etc.), or when certain
563 words will require certain inflectional suffixes in sentences.
565 Because this is an incompatible change, this mode must be explicitly
566 turned on using the set_allow_cformat(1) method call.
568 =head2 The flag characters
570 The character % is followed by zero or more of the following flags:
576 The value comes from HTML <INPUT> elements.
577 This abuse of the flag character is somewhat reasonable,
578 since TMPL_VAR and INPUT are both variables, but of different kinds.
582 =head2 The field width and precision
584 An optional 0.0 can be specified for %s to specify
585 that the <TMPL_VAR> should be suppressed.
587 =head2 The conversion specifier
593 Specifies any input field that is neither text nor hidden
594 (which currently mean radio buttons).
595 The p conversion specifier is chosen because this does not
596 evoke any certain sensible data type.
600 Specifies a text input field (<INPUT TYPE=TEXT>).
601 This use of the S conversion specifier is somewhat reasonable,
602 since text input fields contain values of undeterminable type,
603 which can be treated as strings.
607 Specifies a <TMPL_VAR>.
608 This use of the o conversion specifier is somewhat reasonable,
609 since <TMPL_VAR> denotes values of undeterminable type, which
610 can be treated as strings.
616 There is no code to save the tag name anywhere in the scanned token.
618 The use of <AI<i>> to stand for the I<i>th anchor
619 is not very well thought out.
620 Some abuse of c-format specifies might have been more appropriate.
624 This tokenizer is mostly based
625 on Ambrose's hideous Perl script known as subst.pl.