4 #use warnings; FIXME - Bug 2505
8 use VerboseWarnings
qw( pedantic_p error_normal warn_normal warn_pedantic );
11 use vars
qw(@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 ###############################################################################
31 use vars
qw( $pedantic_attribute_error_in_nonpedantic_mode_p );
32 use vars qw( $pedantic_tmpl_var_use_in_nonpedantic_mode_p );
33 use vars qw( $pedantic_error_markup_in_pcdata_p );
35 ###############################################################################
38 use vars qw( $re_xsl $re_end_entity $re_tmpl_var);
40 $re_tmpl_var = q{\[%\s*[get|set|default]?\s*[\w\.]+\s*[|.*?]?\s*%\]};
41 $re_xsl = q{<\/?(?:xsl:)(?:[\s\-a-zA-Z0-9"'\/\.\[\]\@\(\):=,$]+)\/?>};
42 $re_end_entity = '(?:;|$|(?=\s))'; # semicolon or before-whitespace
44 # End of the hideous stuff
46 use vars
qw( $serial );
48 ###############################################################################
50 sub FATAL_P () {'fatal-p'}
51 sub SYNTAXERROR_P () {'syntaxerror-p'}
53 sub FILENAME () {'input'}
54 #sub HANDLE () {'handle'}
56 #sub READAHEAD () {'readahead'}
57 sub LINENUM_START () {'lc_0'}
59 sub CDATA_MODE_P () {'cdata-mode-p'}
60 sub CDATA_CLOSE () {'cdata-close'}
61 #sub PCDATA_MODE_P () {'pcdata-mode-p'} # additional submode for CDATA
62 sub JS_MODE_P () {'js-mode-p'} # cdata-mode-p must also be true
64 sub ALLOW_CFORMAT_P () {'allow-cformat-p'}
69 #open my $handle,$filename or die "can't open $filename";
70 my $parser = C4::TTParser->new;
71 $parser->build_tokens( $filename );
73 filename => $filename,
80 ###############################################################################
86 return $this->{filename};
91 return $this->{+FATAL_P};
94 # work around, currently not implemented
97 # return $this->{+SYNTAXERROR_P};
103 return $this->{+JS_MODE_P};
106 sub allow_cformat_p {
108 return $this->{+ALLOW_CFORMAT_P};
115 $this->{+FATAL_P} = $_[0];
121 $this->{+JS_MODE_P} = $_[0];
125 #used in xgettext, tmpl_process3
126 sub set_allow_cformat {
128 $this->{+ALLOW_CFORMAT_P} = $_[0];
132 ###############################################################################
134 use vars qw( $js_EscapeSequence );
136 # Perl quoting is really screwed up, but this common subexp is way too long
137 $js_EscapeSequence = q{\\\\(?:['"\\\\bfnrt]|[^0-7xu]|[0-3]?[0-7]{1,2}|x[\da-fA-F]{2}|u[\da-fA-F]{4})};
139 sub parenleft () { '(' }
140 sub parenright () { ')' }
146 if ($s0 =~ /^\s+/s) { # whitespace
149 } elsif ($s0 =~ /^\/\/[^\r\n]*(?:[\r\n]|$)/s) { # C++-style comment
152 } elsif ($s0 =~ /^\/\*(?:(?!\*\/).)*\*\//s) { # C-style comment
155 # Keyword or identifier, ECMA-262 p.13 (section 7.5)
156 } elsif ($s0 =~ /^[A-Z_\$][A-Z\d_\$]*/is) { # IdentifierName
159 # Punctuator, ECMA-262 p.13 (section 7.6)
160 } elsif ($s0 =~ /^(?:[\(\){}\[\];]|>>>=|<<=|>>=|[-\+\*\/\&\|\^\%]=|>>>|<<|>>|--|\+\+|\|\||\&\&|==|<=|>=|!=|[=><,!~\?:\.\-\+\*\/\&\|\^\%])/s) {
163 # DecimalLiteral, ECMA-262 p.14 (section 7.7.3); note: bug in the spec
164 } elsif ($s0 =~ /^(?:0|[1-9]\d+(?:\.\d*(?:[eE][-\+]?\d+)?)?)/s) {
167 # HexIntegerLiteral, ECMA-262 p.15 (section 7.7.3)
168 } elsif ($s0 =~ /^0[xX][\da-fA-F]+/s) {
171 # OctalIntegerLiteral, ECMA-262 p.15 (section 7.7.3)
172 } elsif ($s0 =~ /^0[\da-fA-F]+/s) {
175 # StringLiteral, ECMA-262 p.17 (section 7.7.4)
176 # XXX SourceCharacter doesn't seem to be defined (?)
177 } elsif ($s0 =~ /^(?:"(?:(?!["\\\r\n]).|$js_EscapeSequence)*"|'(?:(?!['\\\r\n]).|$js_EscapeSequence)*')/os) {
180 } elsif ($s0 =~ /^./) { # UNKNOWN TOKEN !!!
188 sub STATE_UNDERSCORE () { 1 }
189 sub STATE_PARENLEFT () { 2 }
190 sub STATE_STRING_LITERAL () { 3 }
192 # XXX This is a crazy hack. I don't want to write an ECMAScript parser.
193 # XXX A scanner is one thing; a parser another thing.
194 sub _identify_js_translatables (@) {
197 # We mark a JavaScript translatable string as in C, i.e., _("literal")
198 # For simplicity, we ONLY look for "_" "(" StringLiteral ")"
199 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) {
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);
215 } elsif ($state == STATE_STRING_LITERAL) {
216 if ($input[$i] eq parenright) {
217 $output[$j] = [1, $output[$j]->[1], $q, $s];
221 die "identify_js_translatables internal error: Unknown state $state"
225 # warn Dumper \@output;
229 ###############################################################################
231 sub string_canon ($) {
233 # 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 == C4::TmplTokenType::DIRECTIVE ){
263 } elsif( $t->type == C4::TmplTokenType::TEXT ){
264 return _formalize_string_cformat( $t->string );
265 } elsif( $t->type == C4::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 C4::TmplTokenType::TEXT_PARAMETRIZED
284 sub _parametrize_internal{
288 # for my $item (@parts){
289 # if( $item->type == C4::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 = C4::TmplToken->new( $s, C4::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 == C4::TmplTokenType::TEXT ){
326 # elsif( $next->type == C4::TmplTokenType::DIRECTIVE && $next->string =~ m/\[%\s*\w+\s*%\]/ ){
327 elsif( $next->type == C4::TmplTokenType::DIRECTIVE ){
330 elsif ( $next->type == C4::TmplTokenType::CDATA){
331 $self->_set_js_mode(1);
332 my $s0 = $next->string;
336 if ($s0 =~ /^(\s*\[%\s*)(.*)(\s%=]\s*)$/s) {
341 push @head, _split_js $s0;
342 $next->set_js_data(_identify_js_translatables(@head, @tail) );
343 return $next unless @parts;
344 $self->{_parser}->unshift_token($next);
345 return $self->_parametrize_internal(@parts);
348 # if there is nothing in parts, return this token
349 return $next unless @parts;
351 # OTHERWISE, put this token back and return the parametrized string of @parts
352 $self->{_parser}->unshift_token($next);
353 return $self->_parametrize_internal(@parts);
359 ###############################################################################
361 # function taken from old version
362 # used by tmpl_process3
363 sub parametrize ($$$$) {
364 my($fmt_0, $cformat_p, $t, $f) = @_;
367 my @params = $t->parameters_and_fields;
368 for (my $n = 0, my $fmt = $fmt_0; length $fmt;) {
369 if ($fmt =~ /^[^%]+/) {
372 } elsif ($fmt =~ /^%%/) {
375 } elsif ($fmt =~ /^%(?:(\d+)\$)?(?:(\d+)(?:\.(\d+))?)?s/s) {
377 my($i, $width, $prec) = ((defined $1? $1: $n), $2, $3);
379 if (defined $width && defined $prec && !$width && !$prec) {
381 } elsif (defined $params[$i - 1]) {
382 my $param = $params[$i - 1];
383 warn_normal "$fmt_0: $&: Expected a TMPL_VAR, but found a "
384 . $param->type->to_string . "\n", undef
385 if $param->type != C4::TmplTokenType::DIRECTIVE;
386 warn_normal "$fmt_0: $&: Unsupported "
387 . "field width or precision\n", undef
388 if defined $width || defined $prec;
389 warn_normal "$fmt_0: $&: Parameter $i not known", undef
390 unless defined $param;
391 $it .= defined $f? &$f( $param ): $param->string;
393 } elsif ($fmt =~ /^%(?:(\d+)\$)?(?:(\d+)(?:\.(\d+))?)?([pS])/s) {
395 my($i, $width, $prec, $conv) = ((defined $1? $1: $n), $2, $3, $4);
398 my $param = $params[$i - 1];
399 if (!defined $param) {
400 warn_normal "$fmt_0: $&: Parameter $i not known", undef;
402 if ($param->type == C4::TmplTokenType::TAG
403 && $param->string =~ /^<input\b/is) {
404 my $type = defined $param->attributes?
405 lc($param->attributes->{'type'}->[1]): undef;
407 warn_normal "$fmt_0: $&: Expected type=text, "
408 . "but found type=$type", undef
409 unless $type eq 'text';
410 } elsif ($conv eq 'p') {
411 warn_normal "$fmt_0: $&: Expected type=radio, "
412 . "but found type=$type", undef
413 unless $type eq 'radio';
416 warn_normal "$&: Expected an INPUT, but found a "
417 . $param->type->to_string . "\n", undef
419 warn_normal "$fmt_0: $&: Unsupported "
420 . "field width or precision\n", undef
421 if defined $width || defined $prec;
422 $it .= defined $f? &$f( $param ): $param->string;
424 } elsif ($fmt =~ /^%[^%a-zA-Z]*[a-zA-Z]/) {
427 die "$&: Unknown or unsupported format specification\n"; #XXX
429 die "$&: Completely confused parametrizing -- msgid: $fmt_0\n";#XXX
433 my @anchors = $t->anchors;
434 for (my $n = 0, my $fmt = $it, $it = ''; length $fmt;) {
435 if ($fmt =~ /^(?:(?!<a\d+>).)+/is) {
438 } elsif ($fmt =~ /^<a(\d+)>/is) {
442 my $anchor = $anchors[$i - 1];
443 warn_normal "$&: Anchor $1 not found for msgid \"$fmt_0\"", undef #FIXME
444 unless defined $anchor;
445 $it .= $anchor->string;
447 die "Completely confused decoding anchors: $fmt\n";#XXX
454 # Other simple functions (These are not methods)
458 return $s =~ /^(?:\s|\ $re_end_entity|$re_tmpl_var|$re_xsl)*$/osi;
465 $s =~ s/^(\s|\ $re_end_entity)+//os; my $l1 = $l0 - length $s;
466 $s =~ s/(\s|\ $re_end_entity)+$//os; my $l2 = $l0 - $l1 - length $s;
467 return wantarray? (substr($s0, 0, $l1), $s, substr($s0, $l0 - $l2)): $s;
472 # Locale::PO->quote is buggy, it doesn't quote newlines :-/
473 $s =~ s/([\\"])/\\$1/gs;
475 #$s =~ s/[\177-\377]/ sprintf("\\%03o", ord($&)) /egs;
479 sub charset_canon ($) {
481 $charset = uc($charset);
482 $charset = "$1-$2" if $charset =~ /^(ISO|UTF)(\d.*)/i;
483 $charset = 'Big5' if $charset eq 'BIG5'; # "Big5" must be in mixed case
487 use vars qw( @latin1_utf8 );
489 "\302\200", "\302\201", "\302\202", "\302\203", "\302\204", "\302\205",
490 "\302\206", "\302\207", "\302\210", "\302\211", "\302\212", "\302\213",
491 "\302\214", "\302\215", undef, undef, "\302\220", "\302\221",
492 "\302\222", "\302\223", "\302\224", "\302\225", "\302\226", "\302\227",
493 "\302\230", "\302\231", "\302\232", "\302\233", "\302\234", "\302\235",
494 "\302\236", "\302\237", "\302\240", "\302\241", "\302\242", "\302\243",
495 "\302\244", "\302\245", "\302\246", "\302\247", "\302\250", "\302\251",
496 "\302\252", "\302\253", "\302\254", "\302\255", "\302\256", "\302\257",
497 "\302\260", "\302\261", "\302\262", "\302\263", "\302\264", "\302\265",
498 "\302\266", "\302\267", "\302\270", "\302\271", "\302\272", "\302\273",
499 "\302\274", "\302\275", "\302\276", "\302\277", "\303\200", "\303\201",
500 "\303\202", "\303\203", "\303\204", "\303\205", "\303\206", "\303\207",
501 "\303\210", "\303\211", "\303\212", "\303\213", "\303\214", "\303\215",
502 "\303\216", "\303\217", "\303\220", "\303\221", "\303\222", "\303\223",
503 "\303\224", "\303\225", "\303\226", "\303\227", "\303\230", "\303\231",
504 "\303\232", "\303\233", "\303\234", "\303\235", "\303\236", "\303\237",
505 "\303\240", "\303\241", "\303\242", "\303\243", "\303\244", "\303\245",
506 "\303\246", "\303\247", "\303\250", "\303\251", "\303\252", "\303\253",
507 "\303\254", "\303\255", "\303\256", "\303\257", "\303\260", "\303\261",
508 "\303\262", "\303\263", "\303\264", "\303\265", "\303\266", "\303\267",
509 "\303\270", "\303\271", "\303\272", "\303\273", "\303\274", "\303\275",
510 "\303\276", "\303\277" );
512 sub charset_convert ($$$) {
513 my($s, $charset_in, $charset_out) = @_;
514 if ($s !~ /[\200-\377]/s) { # FIXME: don't worry about iso2022 for now
516 } elsif ($charset_in eq 'ISO-8859-1' && $charset_out eq 'UTF-8') {
517 $s =~ s/[\200-\377]/ $latin1_utf8[ord($&) - 128] /egs;
518 } elsif ($charset_in ne $charset_out) {
519 VerboseWarnings::warn_normal "conversion from $charset_in to $charset_out is not supported\n", undef;
524 ###############################################################################
528 In addition to the basic scanning, this class will also perform
535 Emulation of c-format strings (see below)
539 Display of warnings for certain things that affects either the
540 ability of this class to yield correct output, or things that
541 are known to cause the original template to cause trouble.
545 Automatic correction of some of the things warned about
546 (e.g., SGML "closed start tag" notation).
550 =head2 c-format strings emulation
552 Because English word order is not universal, a simple extraction
553 of translatable strings may yield some strings like "Accounts for"
554 or ambiguous strings like "in". This makes the resulting strings
555 difficult to translate, but does not affect all languages alike.
556 For example, Chinese (with a somewhat different word order) would
557 be hit harder, but French would be relatively unaffected.
559 To overcome this problem, the scanner can be configured to detect
560 patterns with <TMPL_VAR> directives (as well as certain HTML tags),
561 and try to construct a larger pattern that will appear in the PO
562 file as c-format strings with %s placeholders. This additional
563 step allows the translator to deal with cases where word order
564 is different (replacing %s with %1$s, %2$s, etc.), or when certain
565 words will require certain inflectional suffixes in sentences.
567 Because this is an incompatible change, this mode must be explicitly
568 turned on using the set_allow_cformat(1) method call.
570 =head2 The flag characters
572 The character % is followed by zero or more of the following flags:
578 The value comes from HTML <INPUT> elements.
579 This abuse of the flag character is somewhat reasonable,
580 since TMPL_VAR and INPUT are both variables, but of different kinds.
584 =head2 The field width and precision
586 An optional 0.0 can be specified for %s to specify
587 that the <TMPL_VAR> should be suppressed.
589 =head2 The conversion specifier
595 Specifies any input field that is neither text nor hidden
596 (which currently mean radio buttons).
597 The p conversion specifier is chosen because this does not
598 evoke any certain sensible data type.
602 Specifies a text input field (<INPUT TYPE=TEXT>).
603 This use of the S conversion specifier is somewhat reasonable,
604 since text input fields contain values of undeterminable type,
605 which can be treated as strings.
609 Specifies a <TMPL_VAR>.
610 This use of the o conversion specifier is somewhat reasonable,
611 since <TMPL_VAR> denotes values of undeterminable type, which
612 can be treated as strings.
618 There is no code to save the tag name anywhere in the scanned token.
620 The use of <AI<i>> to stand for the I<i>th anchor
621 is not very well thought out.
622 Some abuse of c-format specifies might have been more appropriate.
626 This tokenizer is mostly based
627 on Ambrose's hideous Perl script known as subst.pl.