File Coverage

lib/Jinja2/TT2/Tokenizer.pm
Criterion Covered Total %
statement 145 149 97.3
branch 54 60 90.0
condition 11 18 61.1
subroutine 7 7 100.0
pod 0 2 0.0
total 217 236 91.9


line stmt bran cond sub pod time code
1             package Jinja2::TT2::Tokenizer;
2              
3 3     3   254906 use strict;
  3         9  
  3         137  
4 3     3   40 use warnings;
  3         5  
  3         230  
5 3     3   48 use v5.20;
  3         11  
6              
7             # Token types
8             use constant {
9 3         8465 TOK_TEXT => 'TEXT',
10             TOK_VAR_START => 'VAR_START',
11             TOK_VAR_END => 'VAR_END',
12             TOK_STMT_START => 'STMT_START',
13             TOK_STMT_END => 'STMT_END',
14             TOK_COMMENT => 'COMMENT',
15             TOK_EXPR => 'EXPR',
16             TOK_NAME => 'NAME',
17             TOK_STRING => 'STRING',
18             TOK_NUMBER => 'NUMBER',
19             TOK_OPERATOR => 'OPERATOR',
20             TOK_PIPE => 'PIPE',
21             TOK_DOT => 'DOT',
22             TOK_COMMA => 'COMMA',
23             TOK_COLON => 'COLON',
24             TOK_LPAREN => 'LPAREN',
25             TOK_RPAREN => 'RPAREN',
26             TOK_LBRACKET => 'LBRACKET',
27             TOK_RBRACKET => 'RBRACKET',
28             TOK_LBRACE => 'LBRACE',
29             TOK_RBRACE => 'RBRACE',
30             TOK_ASSIGN => 'ASSIGN',
31             TOK_TILDE => 'TILDE',
32             TOK_EOF => 'EOF',
33 3     3   34 };
  3         8  
34              
35             # Jinja2 keywords
36             my %KEYWORDS = map { $_ => 1 } qw(
37             if elif else endif
38             for in endfor
39             block endblock
40             extends include import from
41             macro endmacro call endcall
42             filter endfilter
43             set
44             raw endraw
45             with endwith
46             autoescape endautoescape
47             and or not
48             is
49             true false True False none None
50             recursive
51             as
52             ignore missing
53             without context
54             scoped
55             );
56              
57             sub new {
58 3     3 0 369789 my ($class, %opts) = @_;
59             return bless {
60             # Jinja2 delimiters (can be customized)
61             block_start => $opts{block_start} // '{%',
62             block_end => $opts{block_end} // '%}',
63             variable_start => $opts{variable_start} // '{{',
64             variable_end => $opts{variable_end} // '}}',
65             comment_start => $opts{comment_start} // '{#',
66 3   50     101 comment_end => $opts{comment_end} // '#}',
      50        
      50        
      50        
      50        
      50        
67             }, $class;
68             }
69              
70             sub tokenize {
71 77     77 0 194960 my ($self, $template) = @_;
72              
73 77         141 my @tokens;
74 77         176 my $pos = 0;
75 77         150 my $len = length($template);
76              
77 77         264 while ($pos < $len) {
78 142         353 my $remaining = substr($template, $pos);
79              
80             # Check for comment {# ... #}
81 142 100       565 if ($remaining =~ /^\{#-?\s*(.*?)\s*-?#\}/s) {
82 3         15 my $comment = $1;
83 3         16 push @tokens, { type => TOK_COMMENT, value => $comment, pos => $pos };
84 3         12 $pos += length($&);
85 3         12 next;
86             }
87              
88             # Check for variable {{ ... }}
89 139 100       587 if ($remaining =~ /^\{\{-?\s*/) {
90 54         168 my $match_start = $&;
91 54 100       172 my $strip_before = ($match_start =~ /-/) ? 1 : 0;
92 54         111 $pos += length($match_start);
93              
94 54         297 push @tokens, {
95             type => TOK_VAR_START,
96             value => '{{',
97             strip_before => $strip_before,
98             pos => $pos - length($match_start)
99             };
100              
101             # Tokenize the expression inside
102 54         198 my ($expr_tokens, $new_pos) = $self->_tokenize_expression(
103             $template, $pos, '}}'
104             );
105 54         124 push @tokens, @$expr_tokens;
106 54         96 $pos = $new_pos;
107              
108             # Match closing }}
109 54         119 $remaining = substr($template, $pos);
110 54 50       279 if ($remaining =~ /^\s*(-?)\}\}/) {
111 54 100       167 my $strip_after = $1 ? 1 : 0;
112 54         251 push @tokens, {
113             type => TOK_VAR_END,
114             value => '}}',
115             strip_after => $strip_after,
116             pos => $pos
117             };
118 54         128 $pos += length($&);
119             } else {
120 0         0 die "Unclosed variable tag at position $pos";
121             }
122 54         227 next;
123             }
124              
125             # Check for statement {% ... %}
126 85 100       391 if ($remaining =~ /^\{%-?\s*/) {
127 55         146 my $match_start = $&;
128 55 50       179 my $strip_before = ($match_start =~ /-/) ? 1 : 0;
129 55         103 $pos += length($match_start);
130              
131 55         409 push @tokens, {
132             type => TOK_STMT_START,
133             value => '{%',
134             strip_before => $strip_before,
135             pos => $pos - length($match_start)
136             };
137              
138             # Tokenize the statement inside
139 55         195 my ($stmt_tokens, $new_pos) = $self->_tokenize_expression(
140             $template, $pos, '%}'
141             );
142 55         131 push @tokens, @$stmt_tokens;
143 55         94 $pos = $new_pos;
144              
145             # Match closing %}
146 55         129 $remaining = substr($template, $pos);
147 55 50       244 if ($remaining =~ /^\s*(-?)%\}/) {
148 55 50       176 my $strip_after = $1 ? 1 : 0;
149 55         218 push @tokens, {
150             type => TOK_STMT_END,
151             value => '%}',
152             strip_after => $strip_after,
153             pos => $pos
154             };
155 55         127 $pos += length($&);
156             } else {
157 0         0 die "Unclosed statement tag at position $pos";
158             }
159 55         220 next;
160             }
161              
162             # Plain text - find the next tag or end
163 30         66 my $text_end = length($remaining);
164              
165             # Find position of next tag
166 30         87 for my $pattern ('{{', '{%', '{#') {
167 90         187 my $idx = index($remaining, $pattern);
168 90 100 100     302 if ($idx >= 0 && $idx < $text_end) {
169 29         62 $text_end = $idx;
170             }
171             }
172              
173 30 50       77 if ($text_end > 0) {
174 30         68 my $text = substr($remaining, 0, $text_end);
175 30         120 push @tokens, { type => TOK_TEXT, value => $text, pos => $pos };
176 30         80 $pos += $text_end;
177 30         82 next;
178             }
179              
180             # Fallback - shouldn't happen normally
181 0         0 $pos++;
182             }
183              
184 77         248 push @tokens, { type => TOK_EOF, value => '', pos => $pos };
185 77         452 return @tokens;
186             }
187              
188             sub _tokenize_expression {
189 109     109   269 my ($self, $template, $pos, $end_marker) = @_;
190              
191 109         170 my @tokens;
192 109         179 my $len = length($template);
193 109         213 my $end_re = quotemeta($end_marker);
194 109         449 $end_re =~ s/\\\}/-?\\\}/; # Allow whitespace control -
195              
196 109         328 while ($pos < $len) {
197 468         1099 my $remaining = substr($template, $pos);
198              
199             # Check for end marker (with optional whitespace control)
200 468 100 66     6231 last if $remaining =~ /^\s*-?$end_re/ || $remaining =~ /^\s*$end_re/;
201              
202             # Skip whitespace
203 359 100       1195 if ($remaining =~ /^(\s+)/) {
204 84         225 $pos += length($1);
205 84         402 next;
206             }
207              
208             # String literals (single or double quoted)
209 275 100       847 if ($remaining =~ /^("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/) {
210 7         71 push @tokens, { type => TOK_STRING, value => $1, pos => $pos };
211 7         21 $pos += length($1);
212 7         23 next;
213             }
214              
215             # Numbers (integer or float)
216 268 100       760 if ($remaining =~ /^(\d+(?:_\d+)*(?:\.\d+)?(?:[eE][+-]?\d+)?)/) {
217 14         108 push @tokens, { type => TOK_NUMBER, value => $1, pos => $pos };
218 14         33 $pos += length($1);
219 14         33 next;
220             }
221              
222             # Multi-character operators
223 254 100       671 if ($remaining =~ /^(==|!=|<=|>=|<|>|\*\*|\/\/|and\b|or\b|not\b|in\b|is\b)/) {
224 16         89 push @tokens, { type => TOK_OPERATOR, value => $1, pos => $pos };
225 16         39 $pos += length($1);
226 16         74 next;
227             }
228              
229             # Single character operators
230 238 100       587 if ($remaining =~ /^([+\-*\/%])/) {
231 1         5 push @tokens, { type => TOK_OPERATOR, value => $1, pos => $pos };
232 1         3 $pos += length($1);
233 1         3 next;
234             }
235              
236             # Pipe (filter separator)
237 237 100       669 if ($remaining =~ /^\|/) {
238 15         55 push @tokens, { type => TOK_PIPE, value => '|', pos => $pos };
239 15         1237 $pos++;
240 15         45 next;
241             }
242              
243             # Tilde (string concatenation)
244 222 100       524 if ($remaining =~ /^~/) {
245 1         5 push @tokens, { type => TOK_TILDE, value => '~', pos => $pos };
246 1         2 $pos++;
247 1         4 next;
248             }
249              
250             # Dot
251 221 100       847 if ($remaining =~ /^\./) {
252 9         37 push @tokens, { type => TOK_DOT, value => '.', pos => $pos };
253 9         19 $pos++;
254 9         24 next;
255             }
256              
257             # Comma
258 212 100       492 if ($remaining =~ /^,/) {
259 6         29 push @tokens, { type => TOK_COMMA, value => ',', pos => $pos };
260 6         11 $pos++;
261 6         18 next;
262             }
263              
264             # Colon
265 206 100       544 if ($remaining =~ /^:/) {
266 2         10 push @tokens, { type => TOK_COLON, value => ':', pos => $pos };
267 2         6 $pos++;
268 2         7 next;
269             }
270              
271             # Assignment
272 204 100       464 if ($remaining =~ /^=(?!=)/) {
273 2         9 push @tokens, { type => TOK_ASSIGN, value => '=', pos => $pos };
274 2         4 $pos++;
275 2         5 next;
276             }
277              
278             # Parentheses
279 202 100       474 if ($remaining =~ /^\(/) {
280 5         25 push @tokens, { type => TOK_LPAREN, value => '(', pos => $pos };
281 5         12 $pos++;
282 5         55 next;
283             }
284 197 100       433 if ($remaining =~ /^\)/) {
285 5         43 push @tokens, { type => TOK_RPAREN, value => ')', pos => $pos };
286 5         13 $pos++;
287 5         15 next;
288             }
289              
290             # Brackets
291 192 100       453 if ($remaining =~ /^\[/) {
292 3         18 push @tokens, { type => TOK_LBRACKET, value => '[', pos => $pos };
293 3         8 $pos++;
294 3         11 next;
295             }
296 189 100       495 if ($remaining =~ /^\]/) {
297 3         19 push @tokens, { type => TOK_RBRACKET, value => ']', pos => $pos };
298 3         8 $pos++;
299 3         10 next;
300             }
301              
302             # Braces (for dict literals)
303 186 100       459 if ($remaining =~ /^\{/) {
304 2         12 push @tokens, { type => TOK_LBRACE, value => '{', pos => $pos };
305 2         6 $pos++;
306 2         8 next;
307             }
308 184 100       399 if ($remaining =~ /^\}/) {
309 2         12 push @tokens, { type => TOK_RBRACE, value => '}', pos => $pos };
310 2         5 $pos++;
311 2         6 next;
312             }
313              
314             # Names/identifiers/keywords
315 182 50       692 if ($remaining =~ /^([a-zA-Z_][a-zA-Z0-9_]*)/) {
316 182         452 my $name = $1;
317 182         835 push @tokens, { type => TOK_NAME, value => $name, pos => $pos };
318 182         345 $pos += length($name);
319 182         1123 next;
320             }
321              
322             # Unknown character - skip
323 0         0 $pos++;
324             }
325              
326 109         505 return (\@tokens, $pos);
327             }
328              
329             1;
330              
331             __END__