Bug 7929 - Duplicate id "availability" on XSLT result page
[koha.git] / misc / translator / TmplTokenizer.pm
blob6129f8d37e5a81407aed6fc5ffd2b1dd68fbdf14
1 package TmplTokenizer;
3 use strict;
4 #use warnings; FIXME - Bug 2505
5 use C4::TmplTokenType;
6 use C4::TmplToken;
7 use C4::TTParser;
8 use VerboseWarnings qw( pedantic_p error_normal warn_normal warn_pedantic );
9 require Exporter;
11 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
13 ###############################################################################
15 =head1 NAME
17 TmplTokenizer.pm - Simple-minded wrapper class for TTParser
19 =head1 DESCRIPTION
21 A wrapper for the functionality found in TTParser to allow an easier transition to Template Toolkit
23 =cut
25 ###############################################################################
27 $VERSION = 0.02;
29 @ISA = qw(Exporter);
30 @EXPORT_OK = qw();
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 ###############################################################################
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
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'}
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
65 sub ALLOW_CFORMAT_P () {'allow-cformat-p'}
67 sub new {
68 shift;
69 my ($filename) = @_;
70 #open my $handle,$filename or die "can't open $filename";
71 my $parser = C4::TTParser->new;
72 $parser->build_tokens( $filename );
73 bless {
74 filename => $filename,
75 _parser => $parser
76 # , handle => $handle
77 # , readahead => []
78 } , __PACKAGE__;
81 ###############################################################################
83 # Simple getters
85 sub filename {
86 my $this = shift;
87 return $this->{filename};
90 sub fatal_p {
91 my $this = shift;
92 return $this->{+FATAL_P};
95 # work around, currently not implemented
96 sub syntaxerror_p {
97 # my $this = shift;
98 # return $this->{+SYNTAXERROR_P};
99 return 0;
102 sub js_mode_p {
103 my $this = shift;
104 return $this->{+JS_MODE_P};
107 sub allow_cformat_p {
108 my $this = shift;
109 return $this->{+ALLOW_CFORMAT_P};
112 # Simple setters
114 sub _set_fatal {
115 my $this = shift;
116 $this->{+FATAL_P} = $_[0];
117 return $this;
120 sub _set_js_mode {
121 my $this = shift;
122 $this->{+JS_MODE_P} = $_[0];
123 return $this;
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;
133 ###############################################################################
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})};
140 sub parenleft () { '(' }
141 sub parenright () { ')' }
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 = $';
186 return @it;
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 (@) {
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) {
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;
216 } elsif ($state == STATE_STRING_LITERAL) {
217 if ($input[$i] eq parenright) {
218 $output[$j] = [1, $output[$j]->[1], $q, $s];
220 $state = 0;
221 } else {
222 die "identify_js_translatables internal error: Unknown state $state"
225 # use Data::Dumper;
226 # warn Dumper \@output;
227 return \@output;
230 ###############################################################################
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;
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;
249 sub _quote_cformat{
250 my $s = shift;
251 $s =~ s/%/%%/g;
252 return $s;
255 sub _formalize_string_cformat{
256 my $s = shift;
257 return _quote_cformat( string_canon_safe $s );
260 sub _formalize{
261 my $t = shift;
262 if( $t->type == C4::TmplTokenType::DIRECTIVE ){
263 return '%s';
264 } elsif( $t->type == C4::TmplTokenType::TEXT ){
265 return _formalize_string_cformat( $t->string );
266 } elsif( $t->type == C4::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';
275 } else{
276 return _quote_cformat $t->string;
278 } else{
279 return _quote_cformat $t->string;
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 C4::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 == C4::TmplTokenType::TEXT ){
291 # $s .= $item->string;
292 # } else {
293 # #must be a variable directive
294 # $s .= "%s";
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 = C4::TmplToken->new( $s, C4::TmplTokenType::TEXT_PARAMETRIZED, $parts[0]->line_number, $this->filename );
301 $t->set_children(@parts);
302 $t->set_form($s);
303 return $t;
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);
318 else {
319 return undef;
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 == C4::TmplTokenType::TEXT ){
325 push @parts, $next;
327 # elsif( $next->type == C4::TmplTokenType::DIRECTIVE && $next->string =~ m/\[%\s*\w+\s*%\]/ ){
328 elsif( $next->type == C4::TmplTokenType::DIRECTIVE ){
329 push @parts, $next;
331 elsif ( $next->type == C4::TmplTokenType::CDATA){
332 $self->_set_js_mode(1);
333 my $s0 = $next->string;
334 my @head = ();
335 my @tail = ();
337 if ($s0 =~ /^(\s*\[%\s*)(.*)(\s%=]\s*)$/s) {
338 push @head, $1;
339 push @tail, $3;
340 $s0 = $2;
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);
348 else {
349 # if there is nothing in parts, return this token
350 return $next unless @parts;
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);
360 ###############################################################################
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) {
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 != C4::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;
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 = $';
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 == C4::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';
416 } else {
417 warn_normal "$&: Expected an INPUT, but found a "
418 . $param->type->to_string . "\n", undef
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;
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
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
451 return $it;
455 # Other simple functions (These are not methods)
457 sub blank_p ($) {
458 my($s) = @_;
459 return $s =~ /^(?:\s|\&nbsp$re_end_entity|$re_tmpl_var|$re_xsl)*$/osi;
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;
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\"";
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;
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" );
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
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;
522 return $s;
525 ###############################################################################
527 =pod
529 In addition to the basic scanning, this class will also perform
530 the following:
532 =over
534 =item -
536 Emulation of c-format strings (see below)
538 =item -
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.
544 =item -
546 Automatic correction of some of the things warned about
547 (e.g., SGML "closed start tag" notation).
549 =back
551 =head2 c-format strings emulation
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.
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.
568 Because this is an incompatible change, this mode must be explicitly
569 turned on using the set_allow_cformat(1) method call.
571 =head2 The flag characters
573 The character % is followed by zero or more of the following flags:
575 =over
577 =item #
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.
583 =back
585 =head2 The field width and precision
587 An optional 0.0 can be specified for %s to specify
588 that the <TMPL_VAR> should be suppressed.
590 =head2 The conversion specifier
592 =over
594 =item p
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.
601 =item S
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.
608 =item s
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.
615 =back
617 =head1 BUGS
619 There is no code to save the tag name anywhere in the scanned token.
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.
625 =head1 HISTORY
627 This tokenizer is mostly based
628 on Ambrose's hideous Perl script known as subst.pl.
630 =cut