Bug fixed (FREEMARKER-70): The usage of loop variable built-ins, like loopVar?index...
[incubator-freemarker.git] / src / main / javacc / FTL.jj
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  * 
10  *   http://www.apache.org/licenses/LICENSE-2.0
11  * 
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied.  See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19
20 options
21 {
22     STATIC = false;
23     UNICODE_INPUT = true;
24     // DEBUG_TOKEN_MANAGER = true;
25     // DEBUG_PARSER = true;
26 }
27
28 PARSER_BEGIN(FMParser)
29
30 package freemarker.core;
31
32 import freemarker.template.*;
33 import freemarker.template.utility.*;
34 import java.io.*;
35 import java.util.*;
36
37 /**
38  * This class is generated by JavaCC from a grammar file.
39  */
40 public class FMParser {
41
42     private static final int ITERATOR_BLOCK_KIND_LIST = 0; 
43     private static final int ITERATOR_BLOCK_KIND_FOREACH = 1; 
44     private static final int ITERATOR_BLOCK_KIND_ITEMS = 2; 
45     private static final int ITERATOR_BLOCK_KIND_USER_DIRECTIVE = 3; 
46
47     private static class ParserIteratorBlockContext {
48         /**
49          * loopVarName in <#list ... as loopVarName> or <#items as loopVarName>; null after we left the nested
50          * block of #list or #items, respectively.
51          */
52         private String loopVarName;
53         
54         /**
55          * loopVar1Name in <#list ... as k, loopVar2Name> or <#items as k, loopVar2Name>; null after we left the nested
56          * block of #list or #items, respectively.
57          */
58         private String loopVar2Name;
59         
60         /**
61          * See the ITERATOR_BLOCK_KIND_... costants.
62          */
63         private int kind;
64         
65         /**
66          * Is this a key-value pair listing? When there's a nested #items, it's only set there. 
67          */
68         private boolean hashListing;
69     }
70
71     private Template template;
72
73     private boolean stripWhitespace, stripText, preventStrippings;
74     private int incompatibleImprovements;
75     private OutputFormat outputFormat;
76     private int autoEscapingPolicy;
77     private boolean autoEscaping;
78     private ParserConfiguration pCfg;
79
80     /** Keeps track of #list and #foreach nesting. */
81     private List<ParserIteratorBlockContext> iteratorBlockContexts;
82     
83     /**
84      * Keeps track of the nesting depth of directives that support #break.
85      */
86     private int breakableDirectiveNesting;
87     
88     private boolean inMacro, inFunction;
89     private LinkedList escapes = new LinkedList();
90     private int mixedContentNesting; // for stripText
91
92     /**
93      * Create an FM expression parser using a string.
94      *
95      * @Deprecated This is an internal API of FreeMarker; can be removed any time.
96      */
97     static public FMParser createExpressionParser(String s) {
98         SimpleCharStream scs = new SimpleCharStream(new StringReader(s), 1, 1, s.length());
99         FMParserTokenManager token_source = new FMParserTokenManager(scs);
100         token_source.SwitchTo(FMParserConstants.FM_EXPRESSION);
101         FMParser parser = new FMParser(token_source);
102         token_source.setParser(parser);
103         return parser;
104     }
105
106     /**
107      * Constructs a new parser object.
108      * 
109      * @param template
110      *            The template associated with this parser.
111      * @param reader
112      *            The character stream to use as input
113      * @param strictEscapeSyntax
114      *            Whether FreeMarker directives must start with a #
115      *
116      * @Deprecated This is an internal API of FreeMarker; will be removed in 2.4.
117      */
118     public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace) {
119         this(template, reader, strictEscapeSyntax, stripWhitespace, Configuration.AUTO_DETECT_TAG_SYNTAX);
120     }
121
122     /**
123      * @Deprecated This is an internal API of FreeMarker; will be changed in 2.4.
124      */
125     public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace, int tagSyntax) {
126         this(template, reader, strictEscapeSyntax, stripWhitespace, tagSyntax,
127                 Configuration.PARSED_DEFAULT_INCOMPATIBLE_ENHANCEMENTS);
128     }
129
130     /**
131      * @Deprecated This is an internal API of FreeMarker; will be changed in 2.4.
132      */
133     public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace,
134             int tagSyntax, int incompatibleImprovements) {
135         this(template, reader, strictEscapeSyntax, stripWhitespace,
136                 tagSyntax, Configuration.AUTO_DETECT_NAMING_CONVENTION, incompatibleImprovements);
137     }
138
139     /**
140      * @Deprecated This is an internal API of FreeMarker; will be changed in 2.4.
141      */
142     public FMParser(String template) {
143         this(dummyTemplate(),
144                 new StringReader(template), true, true);
145     }
146
147     private static Template dummyTemplate() {
148         try {
149             return new Template(null, new StringReader(""), Configuration.getDefaultConfiguration());
150         } catch (IOException e) {
151             throw new RuntimeException("Failed to create dummy template", e);
152         }
153     }
154
155     /**
156      * @Deprecated This is an internal API of FreeMarker; will be changed in 2.4.
157      */
158     public FMParser(Template template, Reader reader, boolean strictSyntaxMode, boolean whitespaceStripping,
159             int tagSyntax, int namingConvention, int incompatibleImprovements) {
160         this(template, reader,
161                 new LegacyConstructorParserConfiguration(
162                         strictSyntaxMode, whitespaceStripping,
163                         tagSyntax, namingConvention,
164                         template != null ? template.getParserConfiguration().getAutoEscapingPolicy()
165                                 : Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY,
166                         template != null ? template.getParserConfiguration().getOutputFormat()
167                                 : null,
168                         template != null ? template.getParserConfiguration().getRecognizeStandardFileExtensions()
169                                 : null,
170                         template != null ? template.getParserConfiguration().getTabSize()
171                                 : null,
172                         new Version(incompatibleImprovements),
173                         template != null ? template.getArithmeticEngine() : null));
174     }
175
176     /**
177      * @Deprecated This is an internal API of FreeMarker; don't call it from outside FreeMarker.
178      * 
179      * @since 2.3.24
180      */
181     public FMParser(Template template, Reader reader, ParserConfiguration pCfg) {
182         this(template, true, readerToTokenManager(reader, pCfg), pCfg);
183     }
184
185     private static FMParserTokenManager readerToTokenManager(Reader reader, ParserConfiguration pCfg) {
186         SimpleCharStream simpleCharStream = new SimpleCharStream(reader, 1, 1);
187         simpleCharStream.setTabSize(pCfg.getTabSize());
188         return new FMParserTokenManager(simpleCharStream);
189     }
190
191     /**
192      * @Deprecated This is an internal API of FreeMarker; don't call it from outside FreeMarker.
193      * 
194      * @since 2.3.24
195      */
196     public FMParser(Template template, boolean newTemplate, FMParserTokenManager tkMan, ParserConfiguration pCfg) {
197         this(tkMan);
198
199         NullArgumentException.check(pCfg);
200         this.pCfg = pCfg;
201
202         NullArgumentException.check(template);
203         this.template = template;
204
205         // Hack due to legacy public constructors (removed in 2.4):
206         if (pCfg instanceof LegacyConstructorParserConfiguration) {
207             LegacyConstructorParserConfiguration lpCfg = (LegacyConstructorParserConfiguration) pCfg;
208             lpCfg.setArithmeticEngineIfNotSet(template.getArithmeticEngine());
209             lpCfg.setAutoEscapingPolicyIfNotSet(template.getConfiguration().getAutoEscapingPolicy());
210             lpCfg.setOutputFormatIfNotSet(template.getOutputFormat());
211             lpCfg.setRecognizeStandardFileExtensionsIfNotSet(
212                     template.getParserConfiguration().getRecognizeStandardFileExtensions());
213             lpCfg.setTabSizeIfNotSet(
214                     template.getParserConfiguration().getTabSize());
215         }
216
217         int incompatibleImprovements = pCfg.getIncompatibleImprovements().intValue();
218         token_source.incompatibleImprovements = incompatibleImprovements;
219         this.incompatibleImprovements = incompatibleImprovements;
220
221         {
222             OutputFormat outputFormatFromExt;
223             if (!pCfg.getRecognizeStandardFileExtensions()
224                     || (outputFormatFromExt = getFormatFromStdFileExt()) == null) {
225                 autoEscapingPolicy = pCfg.getAutoEscapingPolicy();
226                 outputFormat = pCfg.getOutputFormat();
227             } else {
228                 // Override it
229                 autoEscapingPolicy = Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY;
230                 outputFormat = outputFormatFromExt;
231             }
232         }
233         recalculateAutoEscapingField();
234
235         token_source.setParser(this);
236
237         token_source.strictEscapeSyntax = pCfg.getStrictSyntaxMode();
238
239         int tagSyntax = pCfg.getTagSyntax();
240         switch (tagSyntax) {
241         case Configuration.AUTO_DETECT_TAG_SYNTAX:
242             token_source.autodetectTagSyntax = true;
243             break;
244         case Configuration.ANGLE_BRACKET_TAG_SYNTAX:
245             token_source.squBracTagSyntax = false;
246             break;
247         case Configuration.SQUARE_BRACKET_TAG_SYNTAX:
248             token_source.squBracTagSyntax = true;
249             break;
250         default:
251             throw new IllegalArgumentException("Illegal argument for tagSyntax: " + tagSyntax);
252         }
253
254         int namingConvention = pCfg.getNamingConvention();
255         switch (namingConvention) {
256         case Configuration.AUTO_DETECT_NAMING_CONVENTION:
257         case Configuration.CAMEL_CASE_NAMING_CONVENTION:
258         case Configuration.LEGACY_NAMING_CONVENTION:
259             token_source.initialNamingConvention = namingConvention;
260             token_source.namingConvention = namingConvention;
261             break;
262         default:
263             throw new IllegalArgumentException("Illegal argument for namingConvention: " + namingConvention);
264         }
265
266         this.stripWhitespace = pCfg.getWhitespaceStripping();
267
268         // If this is a Template under construction, we do the below.
269         // If this is just the enclosing Template for ?eval or such, we must not modify it.
270         if (newTemplate) {
271             _TemplateAPI.setAutoEscaping(template, autoEscaping);
272             _TemplateAPI.setOutputFormat(template, outputFormat);
273         }
274     }
275     
276     void setupStringLiteralMode(FMParser parentParser, OutputFormat outputFormat) {
277         FMParserTokenManager parentTokenSource = parentParser.token_source;
278          
279         token_source.initialNamingConvention = parentTokenSource.initialNamingConvention;
280         token_source.namingConvention = parentTokenSource.namingConvention;
281         token_source.namingConventionEstabilisher = parentTokenSource.namingConventionEstabilisher;
282         token_source.SwitchTo(NODIRECTIVE);
283         
284         this.outputFormat = outputFormat;
285         recalculateAutoEscapingField();                                
286         if (incompatibleImprovements < _TemplateAPI.VERSION_INT_2_3_24) {
287             // Emulate bug, where the string literal parser haven't inherited the IcI:
288             incompatibleImprovements = _TemplateAPI.VERSION_INT_2_3_0;
289         }
290         
291         // So that loop variable built-ins, like ?index, works inside the interpolations in the string literal:
292         iteratorBlockContexts = parentParser.iteratorBlockContexts;
293     }
294
295     void tearDownStringLiteralMode(FMParser parentParser) {
296         // If the naming convention was established inside the string literal, it's inherited by the parent:
297         FMParserTokenManager parentTokenSource = parentParser.token_source; 
298         parentTokenSource.namingConvention = token_source.namingConvention;
299         parentTokenSource.namingConventionEstabilisher = token_source.namingConventionEstabilisher;
300     }
301     
302     /**
303      * Used when we need to recreate the source code from the AST (such as for the FM2 to FM3 converter).
304      */
305     void setPreventStrippings(boolean preventStrippings) {
306         this.preventStrippings = preventStrippings;
307     }
308
309     private OutputFormat getFormatFromStdFileExt() {
310         String sourceName = template.getSourceName();
311         if (sourceName == null) {
312             return null; // Not possible anyway...
313         }
314
315         int ln = sourceName.length();
316         if (ln < 5) return null;
317
318         char c = sourceName.charAt(ln - 5);
319         if (c != '.') return null;
320
321         c = sourceName.charAt(ln - 4);
322         if (c != 'f' && c != 'F') return null;
323
324         c = sourceName.charAt(ln - 3);
325         if (c != 't' && c != 'T') return null;
326
327         c = sourceName.charAt(ln - 2);
328         if (c != 'l' && c != 'L') return null;
329
330         c = sourceName.charAt(ln - 1);
331         try {
332             // Note: We get the output formats by name, so that custom overrides take effect.
333             if (c == 'h' || c == 'H') {
334                 return template.getConfiguration().getOutputFormat(HTMLOutputFormat.INSTANCE.getName());
335                 }
336             if (c == 'x' || c == 'X') {
337                 return template.getConfiguration().getOutputFormat(XMLOutputFormat.INSTANCE.getName());
338             }
339         } catch (UnregisteredOutputFormatException e) {
340             throw new BugException("Unregistered std format", e);
341         }
342         return null;
343     }
344     
345     /**
346      * Updates the {@link #autoEscaping} field based on the {@link #autoEscapingPolicy} and {@link #outputFormat} fields.
347      */
348     private void recalculateAutoEscapingField() {
349         if (outputFormat instanceof MarkupOutputFormat) {
350             if (autoEscapingPolicy == Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY) {
351                 autoEscaping = ((MarkupOutputFormat) outputFormat).isAutoEscapedByDefault();
352             } else if (autoEscapingPolicy == Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY) {
353                 autoEscaping = true;
354             } else if (autoEscapingPolicy == Configuration.DISABLE_AUTO_ESCAPING_POLICY) {
355                 autoEscaping = false;
356             } else {
357                 throw new IllegalStateException("Unhandled autoEscaping enum: " + autoEscapingPolicy);
358             }
359         } else {
360             autoEscaping = false;
361         }
362     }
363     
364     MarkupOutputFormat getMarkupOutputFormat() {
365         return outputFormat instanceof MarkupOutputFormat ? (MarkupOutputFormat) outputFormat : null;
366     }
367
368     /**
369      * Don't use it, unless you are developing FreeMarker itself.
370      */
371     public int _getLastTagSyntax() {
372         return token_source.squBracTagSyntax
373                 ? Configuration.SQUARE_BRACKET_TAG_SYNTAX
374                 : Configuration.ANGLE_BRACKET_TAG_SYNTAX;
375     }
376     
377     /**
378      * Don't use it, unless you are developing FreeMarker itself.
379      * The naming convention used by this template; if it couldn't be detected so far, it will be the most probable one.
380      * This could be used for formatting error messages, but not for anything serious.
381      */
382     public int _getLastNamingConvention() {
383         return token_source.namingConvention;
384     }
385
386     /**
387      * Throw an exception if the expression passed in is a String Literal
388      */
389     private void notStringLiteral(Expression exp, String expected) throws ParseException {
390         if (exp instanceof StringLiteral) {
391             throw new ParseException(
392                     "Found string literal: " + exp + ". Expecting: " + expected,
393                     exp);
394         }
395     }
396
397     /**
398      * Throw an exception if the expression passed in is a Number Literal
399      */
400     private void notNumberLiteral(Expression exp, String expected) throws ParseException {
401         if (exp instanceof NumberLiteral) {
402             throw new ParseException(
403                     "Found number literal: " + exp.getCanonicalForm() + ". Expecting " + expected,
404                     exp);
405         }
406     }
407
408     /**
409      * Throw an exception if the expression passed in is a boolean Literal
410      */
411     private void notBooleanLiteral(Expression exp, String expected) throws ParseException {
412         if (exp instanceof BooleanLiteral) {
413             throw new ParseException("Found: " + exp.getCanonicalForm() + ". Expecting " + expected, exp);
414         }
415     }
416
417     /**
418      * Throw an exception if the expression passed in is a Hash Literal
419      */
420     private void notHashLiteral(Expression exp, String expected) throws ParseException {
421         if (exp instanceof HashLiteral) {
422             throw new ParseException(
423                     "Found hash literal: " + exp.getCanonicalForm() + ". Expecting " + expected,
424                     exp);
425         }
426     }
427
428     /**
429      * Throw an exception if the expression passed in is a List Literal
430      */
431     private void notListLiteral(Expression exp, String expected)
432             throws ParseException
433     {
434         if (exp instanceof ListLiteral) {
435             throw new ParseException(
436                     "Found list literal: " + exp.getCanonicalForm() + ". Expecting " + expected,
437                     exp);
438         }
439     }
440
441     /**
442      * Throw an exception if the expression passed in is a literal other than of the numerical type
443      */
444     private void numberLiteralOnly(Expression exp) throws ParseException {
445         notStringLiteral(exp, "number");
446         notListLiteral(exp, "number");
447         notHashLiteral(exp, "number");
448         notBooleanLiteral(exp, "number");
449     }
450
451     /**
452      * Throw an exception if the expression passed in is not a string.
453      */
454     private void stringLiteralOnly(Expression exp) throws ParseException {
455         notNumberLiteral(exp, "string");
456         notListLiteral(exp, "string");
457         notHashLiteral(exp, "string");
458         notBooleanLiteral(exp, "string");
459     }
460
461     /**
462      * Throw an exception if the expression passed in is a literal other than of the boolean type
463      */
464     private void booleanLiteralOnly(Expression exp) throws ParseException {
465         notStringLiteral(exp, "boolean (true/false)");
466         notListLiteral(exp, "boolean (true/false)");
467         notHashLiteral(exp, "boolean (true/false)");
468         notNumberLiteral(exp, "boolean (true/false)");
469     }
470
471     private Expression escapedExpression(Expression exp) {
472         if (!escapes.isEmpty()) {
473             return ((EscapeBlock) escapes.getFirst()).doEscape(exp);
474         } else {
475             return exp;
476         }
477     }
478
479     private boolean getBoolean(Expression exp, boolean legacyCompat) throws ParseException {
480         TemplateModel tm = null;
481         try {
482             tm = exp.eval(null);
483         } catch (Exception e) {
484             throw new ParseException(e.getMessage()
485                     + "\nCould not evaluate expression: "
486                     + exp.getCanonicalForm(),
487                     exp,
488                     e);
489         }
490         if (tm instanceof TemplateBooleanModel) {
491             try {
492                 return ((TemplateBooleanModel) tm).getAsBoolean();
493             } catch (TemplateModelException tme) {
494             }
495         }
496         if (legacyCompat && tm instanceof TemplateScalarModel) {
497             try {
498                 return StringUtil.getYesNo(((TemplateScalarModel) tm).getAsString());
499             } catch (Exception e) {
500                 throw new ParseException(e.getMessage()
501                         + "\nExpecting boolean (true/false), found: " + exp.getCanonicalForm(),
502                         exp);
503             }
504         }
505         throw new ParseException("Expecting boolean (true/false) parameter", exp);
506     }
507     
508     void checkCurrentOutputFormatCanEscape(Token start) throws ParseException {
509         if (!(outputFormat instanceof MarkupOutputFormat)) {
510             throw new ParseException("The current output format can't do escaping: " + outputFormat,
511                     template, start);
512         }
513     }    
514     
515     private ParserIteratorBlockContext pushIteratorBlockContext() {
516         if (iteratorBlockContexts == null) {
517             iteratorBlockContexts = new ArrayList<ParserIteratorBlockContext>(4);
518         }
519         ParserIteratorBlockContext newCtx = new ParserIteratorBlockContext();
520         iteratorBlockContexts.add(newCtx);
521         return newCtx;
522     }
523     
524     private void popIteratorBlockContext() {
525         iteratorBlockContexts.remove(iteratorBlockContexts.size() - 1);
526     }
527     
528     private ParserIteratorBlockContext peekIteratorBlockContext() {
529         int size = iteratorBlockContexts != null ? iteratorBlockContexts.size() : 0;
530         return size != 0 ? (ParserIteratorBlockContext) iteratorBlockContexts.get(size - 1) : null; 
531     }
532     
533     private void checkLoopVariableBuiltInLHO(String loopVarName, Expression lhoExp, Token biName)
534             throws ParseException {
535         int size = iteratorBlockContexts != null ? iteratorBlockContexts.size() : 0;
536         for (int i = size - 1; i >= 0; i--) {
537             ParserIteratorBlockContext ctx = iteratorBlockContexts.get(i);
538             if (loopVarName.equals(ctx.loopVarName) || loopVarName.equals(ctx.loopVar2Name)) {
539                 if (ctx.kind == ITERATOR_BLOCK_KIND_USER_DIRECTIVE) {
540                                 throw new ParseException(
541                                         "The left hand operand of ?" + biName.image
542                                         + " can't be the loop variable of an user defined directive: "
543                                         +  loopVarName,
544                                         lhoExp);
545                 }
546                 return;  // success
547             }
548         }
549         throw new ParseException(
550                 "The left hand operand of ?" + biName.image + " must be a loop variable, "
551                 + "but there's no loop variable in scope with this name: " + loopVarName,
552                 lhoExp);
553     }
554     
555         private String forEachDirectiveSymbol() {    
556             // [2.4] Use camel case as the default
557             return token_source.namingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION ? "#forEach" : "#foreach";
558         }
559     
560 }
561
562 PARSER_END(FMParser)
563
564 /**
565  * The lexer portion defines 5 lexical states:
566  * DEFAULT, FM_EXPRESSION, IN_PAREN, NO_PARSE, and EXPRESSION_COMMENT.
567  * The DEFAULT state is when you are parsing
568  * text but are not inside a FreeMarker expression.
569  * FM_EXPRESSION is the state you are in
570  * when the parser wants a FreeMarker expression.
571  * IN_PAREN is almost identical really. The difference
572  * is that you are in this state when you are within
573  * FreeMarker expression and also within (...).
574  * This is a necessary subtlety because the
575  * ">" and ">=" symbols can only be used
576  * within parentheses because otherwise, it would
577  * be ambiguous with the end of a directive.
578  * So, for example, you enter the FM_EXPRESSION state
579  * right after a ${ and leave it after the matching }.
580  * Or, you enter the FM_EXPRESSION state right after
581  * an "<if" and then, when you hit the matching ">"
582  * that ends the if directive,
583  * you go back to DEFAULT lexical state.
584  * If, within the FM_EXPRESSION state, you enter a
585  * parenthetical expression, you enter the IN_PAREN
586  * state.
587  * Note that whitespace is ignored in the
588  * FM_EXPRESSION and IN_PAREN states
589  * but is passed through to the parser as PCDATA in the DEFAULT state.
590  * NO_PARSE and EXPRESSION_COMMENT are extremely simple
591  * lexical states. NO_PARSE is when you are in a comment
592  * block and EXPRESSION_COMMENT is when you are in a comment
593  * that is within an FTL expression.
594  */
595 TOKEN_MGR_DECLS:
596 {
597
598     private static final String PLANNED_DIRECTIVE_HINT
599             = "(If you have seen this directive in use elsewhere, this was a planned directive, "
600                 + "so maybe you need to upgrade FreeMarker.)";
601
602     /**
603      * The noparseTag is set when we enter a block of text that the parser more or less ignores. These are <noparse> and
604      * <comment>. This variable tells us what the closing tag should be, and when we hit that, we resume parsing. Note
605      * that with this scheme, <comment> and <noparse> tags cannot nest recursively, but it is not clear how important
606      * that is.
607      */
608     String noparseTag;
609
610     /**
611      * Keeps track of how deeply nested we have the hash literals. This is necessary since we need to be able to
612      * distinguish the } used to close a hash literal and the one used to close a ${
613      */
614     private FMParser parser;
615     private int postInterpolationLexState = -1;
616     private int hashLiteralNesting;
617     private int parenthesisNesting;
618     private int bracketNesting;
619     private boolean inFTLHeader;
620     boolean strictEscapeSyntax,
621             squBracTagSyntax,
622             autodetectTagSyntax,
623             directiveSyntaxEstablished,
624             inInvocation;
625     int initialNamingConvention;
626     int namingConvention;
627     Token namingConventionEstabilisher;
628     int incompatibleImprovements;
629
630     void setParser(FMParser parser) {
631         this.parser = parser;
632     }
633
634     // This method checks if we are in a strict mode where all
635     // FreeMarker directives must start with <#. It also handles
636     // tag syntax detection. If you update this logic, take a look
637     // at the UNKNOWN_DIRECTIVE token too.
638     private void strictSyntaxCheck(Token tok, int tokenNamingConvention, int newLexState) {
639         final String image = tok.image;
640         
641         // Non-strict syntax (deprecated) only supports legacy naming convention.
642         // We didn't push this on the tokenizer because it made it slow, so we filter here.
643         if (!strictEscapeSyntax
644                 && (tokenNamingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION)
645                 && !isStrictTag(image)) {
646             tok.kind = STATIC_TEXT_NON_WS;
647             return;
648         }
649         
650         char firstChar = image.charAt(0);
651         if (autodetectTagSyntax && !directiveSyntaxEstablished) {
652             squBracTagSyntax = (firstChar == '[');
653         }
654         if ((firstChar == '[' && !squBracTagSyntax) || (firstChar == '<' && squBracTagSyntax)) {
655             tok.kind = STATIC_TEXT_NON_WS;
656             return;
657         }
658         
659         if (!strictEscapeSyntax) {
660             // Legacy feature (or bug?): Tag syntax never gets estabilished in non-strict mode.
661             // We do establilish the naming convention though.
662             checkNamingConvention(tok, tokenNamingConvention);
663             SwitchTo(newLexState);
664             return;
665         }
666         
667         // For square bracket tags there's no non-strict token, so we are sure that it's an FTL tag.
668         // But if it's an angle bracket tag, we have to check if it's just static text or and FTL tag, because the
669         // tokenizer will emit the same kind of token for both.
670         if (!squBracTagSyntax && !isStrictTag(image)) {
671             tok.kind = STATIC_TEXT_NON_WS;
672             return;
673         }
674         
675         // We only get here if this is a strict FTL tag.
676         directiveSyntaxEstablished = true;
677         
678         checkNamingConvention(tok, tokenNamingConvention);
679         
680         SwitchTo(newLexState);
681     }
682
683     void checkNamingConvention(Token tok) {
684         checkNamingConvention(tok, _CoreStringUtils.getIdentifierNamingConvention(tok.image)); 
685     }
686     
687     void checkNamingConvention(Token tok, int tokenNamingConvention) {
688         if (tokenNamingConvention != Configuration.AUTO_DETECT_NAMING_CONVENTION) {
689                 if (namingConvention == Configuration.AUTO_DETECT_NAMING_CONVENTION) {
690                     namingConvention = tokenNamingConvention;
691                     namingConventionEstabilisher = tok;
692                 } else if (namingConvention != tokenNamingConvention) {
693                 throw newNameConventionMismatchException(tok);
694                 }
695         }
696     }
697     
698     private TokenMgrError newNameConventionMismatchException(Token tok) {
699         return new TokenMgrError(
700                 "Naming convention mismatch. "
701                 + "Identifiers that are part of the template language (not the user specified ones) "
702                 + (initialNamingConvention == Configuration.AUTO_DETECT_NAMING_CONVENTION
703                     ? "must consistently use the same naming convention within the same template. This template uses "
704                     : "must use the configured naming convention, which is the ")
705                 + (namingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION
706                             ? "camel case naming convention (like: exampleName) "
707                             : (namingConvention == Configuration.LEGACY_NAMING_CONVENTION
708                                     ? "legacy naming convention (directive (tag) names are like examplename, " 
709                                       + "everything else is like example_name) "
710                                     : "??? (internal error)"
711                                     ))
712                 + (namingConventionEstabilisher != null
713                         ? "estabilished by auto-detection at "
714                             + MessageUtil.formatPosition(
715                                     namingConventionEstabilisher.beginLine, namingConventionEstabilisher.beginColumn)
716                             + " by token " + StringUtil.jQuote(namingConventionEstabilisher.image.trim())
717                         : "")
718                 + ", but the problematic token, " + StringUtil.jQuote(tok.image.trim())
719                 + ", uses a different convention.",
720                 TokenMgrError.LEXICAL_ERROR,
721                 tok.beginLine, tok.beginColumn, tok.endLine, tok.endColumn);
722     }
723
724     /**
725      * Used for tags whose name isn't affected by naming convention.
726      */
727     private void strictSyntaxCheck(Token tok, int newLexState) {
728         strictSyntaxCheck(tok, Configuration.AUTO_DETECT_NAMING_CONVENTION, newLexState);
729     }
730     
731     private boolean isStrictTag(String image) {
732         return image.length() > 2 && (image.charAt(1) == '#' || image.charAt(2) == '#');
733     }
734     
735     /**
736      * Detects the naming convention used, both in start- and end-tag tokens.
737      *
738      * @param charIdxInName
739      *         The index of the deciding character relatively to the first letter of the name.
740      */
741     private static int getTagNamingConvention(Token tok, int charIdxInName) {
742         return _CoreStringUtils.isUpperUSASCII(getTagNameCharAt(tok, charIdxInName))
743                 ? Configuration.CAMEL_CASE_NAMING_CONVENTION : Configuration.LEGACY_NAMING_CONVENTION;
744     }
745
746     static char getTagNameCharAt(Token tok, int charIdxInName) {
747         final String image = tok.image;
748         
749         // Skip tag delimiter:
750         int idx = 0;
751         for (;;) {
752             final char c = image.charAt(idx);
753             if (c != '<' && c != '[' && c != '/' && c != '#') {
754                 break;
755             }
756             idx++;
757         }
758
759         return image.charAt(idx + charIdxInName);
760     }
761
762     private void unifiedCall(Token tok) {
763         char firstChar = tok.image.charAt(0);
764         if (autodetectTagSyntax && !directiveSyntaxEstablished) {
765             squBracTagSyntax = (firstChar == '[');
766         }
767         if (squBracTagSyntax && firstChar == '<') {
768             tok.kind = STATIC_TEXT_NON_WS;
769             return;
770         }
771         if (!squBracTagSyntax && firstChar == '[') {
772             tok.kind = STATIC_TEXT_NON_WS;
773             return;
774         }
775         directiveSyntaxEstablished = true;
776         SwitchTo(NO_SPACE_EXPRESSION);
777     }
778
779     private void unifiedCallEnd(Token tok) {
780         char firstChar = tok.image.charAt(0);
781         if (squBracTagSyntax && firstChar == '<') {
782             tok.kind = STATIC_TEXT_NON_WS;
783             return;
784         }
785         if (!squBracTagSyntax && firstChar == '[') {
786             tok.kind = STATIC_TEXT_NON_WS;
787             return;
788         }
789     }
790
791     private void closeBracket(Token tok) {
792         if (bracketNesting > 0) {
793             --bracketNesting;
794         } else {
795             tok.kind = DIRECTIVE_END;
796             if (inFTLHeader) {
797                 eatNewline();
798                 inFTLHeader = false;
799             }
800             SwitchTo(DEFAULT);
801         }
802     }
803     
804     private void startInterpolation(Token tok) {
805         if (postInterpolationLexState != -1) {
806             char c = tok.image.charAt(0);
807             throw new TokenMgrError(
808                     "You can't start an interpolation (" + c + "{...}) here "
809                     + "as you are inside another interpolation.)",
810                     TokenMgrError.LEXICAL_ERROR,
811                     tok.beginLine, tok.beginColumn,
812                     tok.endLine, tok.endColumn);
813         }
814         postInterpolationLexState = curLexState;
815         SwitchTo(FM_EXPRESSION);
816     }
817
818     /**
819      * @param tok
820      *         Assumed to be an '}', or something that is the closing pair of another "mirror image" character.
821      */
822     private void endInterpolation(Token tok) {
823         if (postInterpolationLexState == -1) {
824             char c = tok.image.charAt(0);
825             throw new TokenMgrError(
826                     "You can't have an \"" + c + "\" here, as there's nothing open that it could close.",
827                     TokenMgrError.LEXICAL_ERROR,
828                     tok.beginLine, tok.beginColumn,
829                     tok.endLine, tok.endColumn);
830         }
831         SwitchTo(postInterpolationLexState);
832         postInterpolationLexState = -1;
833     }
834
835     private void eatNewline() {
836         int charsRead = 0;
837         try {
838             while (true) {
839                 char c = input_stream.readChar();
840                 ++charsRead;
841                 if (!Character.isWhitespace(c)) {
842                     input_stream.backup(charsRead);
843                     return;
844                 } else if (c == '\r') {
845                     char next = input_stream.readChar();
846                     ++charsRead;
847                     if (next != '\n') {
848                         input_stream.backup(1);
849                     }
850                     return;
851                 } else if (c == '\n') {
852                     return;
853                 }
854             }
855         } catch (IOException ioe) {
856             input_stream.backup(charsRead);
857         }
858     }
859
860     private void ftlHeader(Token matchedToken) {
861         if (!directiveSyntaxEstablished) {
862             squBracTagSyntax = matchedToken.image.charAt(0) == '[';
863             directiveSyntaxEstablished = true;
864             autodetectTagSyntax = false;
865         }
866         String img = matchedToken.image;
867         char firstChar = img.charAt(0);
868         char lastChar = img.charAt(img.length() - 1);
869         if ((firstChar == '[' && !squBracTagSyntax) || (firstChar == '<' && squBracTagSyntax)) {
870             matchedToken.kind = STATIC_TEXT_NON_WS;
871         }
872         if (matchedToken.kind != STATIC_TEXT_NON_WS) {
873             if (lastChar != '>' && lastChar != ']') {
874                 SwitchTo(FM_EXPRESSION);
875                 inFTLHeader = true;
876             } else {
877                 eatNewline();
878             }
879         }
880     }
881 }
882
883 TOKEN:
884 {
885     <#BLANK : " " | "\t" | "\n" | "\r">
886     |
887     <#START_TAG : "<" | "<#" | "[#">
888     |
889     <#END_TAG : "</" | "</#" | "[/#">
890     |
891     <#CLOSE_TAG1 : (<BLANK>)* (">" | "]")>
892     |
893     <#CLOSE_TAG2 : (<BLANK>)* ("/")? (">" | "]")>
894     |
895     /*
896      * ATTENTION: Update _CoreAPI.*_BUILT_IN_DIRECTIVE_NAMES if you add new directives!
897      */
898     <ATTEMPT : <START_TAG> "attempt" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
899     |
900     <RECOVER : <START_TAG> "recover" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); } 
901     |
902     <IF : <START_TAG> "if" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
903     |
904     <ELSE_IF : <START_TAG> "else" ("i" | "I") "f" <BLANK>> {
905         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 4), FM_EXPRESSION);
906     }
907     |
908     <LIST : <START_TAG> "list" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
909     |
910     <ITEMS : <START_TAG> "items" (<BLANK>)+ <AS> <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
911     |
912     <SEP : <START_TAG> "sep" <CLOSE_TAG1>>
913     |
914     <FOREACH : <START_TAG> "for" ("e" | "E") "ach" <BLANK>> {
915         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 3), FM_EXPRESSION);
916     }
917     |
918     <SWITCH : <START_TAG> "switch" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
919     |
920     <CASE : <START_TAG> "case" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
921     |
922     <ASSIGN : <START_TAG> "assign" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
923     |
924     <GLOBALASSIGN : <START_TAG> "global" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
925     |
926     <LOCALASSIGN : <START_TAG> "local" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
927     |
928     <_INCLUDE : <START_TAG> "include" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
929     |
930     <IMPORT : <START_TAG> "import" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
931     |
932     <FUNCTION : <START_TAG> "function" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
933     |
934     <MACRO : <START_TAG> "macro" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
935     |
936     <TRANSFORM : <START_TAG> "transform" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
937     |
938     <VISIT : <START_TAG> "visit" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
939     |
940     <STOP : <START_TAG> "stop" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
941     |
942     <RETURN : <START_TAG> "return" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
943     |
944     <CALL : <START_TAG> "call" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
945     |
946     <SETTING : <START_TAG> "setting" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
947     |
948     <OUTPUTFORMAT : <START_TAG> "output" ("f"|"F") "ormat" <BLANK>> {
949         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 6), FM_EXPRESSION);
950     }
951     |
952     <AUTOESC : <START_TAG> "auto" ("e"|"E") "sc" <CLOSE_TAG1>> {
953         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 4), DEFAULT);
954     }
955     |
956     <NOAUTOESC : <START_TAG> "no" ("autoe"|"AutoE") "sc" <CLOSE_TAG1>> {
957         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
958     }
959     |
960     <COMPRESS : <START_TAG> "compress" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
961     |
962     <COMMENT : <START_TAG> "comment" <CLOSE_TAG1>> {
963         strictSyntaxCheck(matchedToken, NO_PARSE); noparseTag = "comment";
964     }
965     |
966     <TERSE_COMMENT : ("<" | "[") "#--" > { noparseTag = "-->"; strictSyntaxCheck(matchedToken, NO_PARSE); }
967     |
968     <NOPARSE: <START_TAG> "no" ("p" | "P") "arse" <CLOSE_TAG1>> {
969         int tagNamingConvention = getTagNamingConvention(matchedToken, 2);
970         strictSyntaxCheck(matchedToken, tagNamingConvention, NO_PARSE);
971         noparseTag = tagNamingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION ? "noParse" : "noparse";
972     }
973     |
974     <END_IF : <END_TAG> "if" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
975     |
976     <END_LIST : <END_TAG> "list" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
977     |
978     <END_ITEMS : <END_TAG> "items" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
979     |
980     <END_SEP : <END_TAG> "sep" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
981     |
982     <END_RECOVER : <END_TAG> "recover" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
983     |
984     <END_ATTEMPT : <END_TAG> "attempt" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
985     |
986     <END_FOREACH : <END_TAG> "for" ("e" | "E") "ach" <CLOSE_TAG1>> {
987         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 3), DEFAULT);
988     }
989     |
990     <END_LOCAL : <END_TAG> "local" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
991     |
992     <END_GLOBAL : <END_TAG> "global" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
993     |
994     <END_ASSIGN : <END_TAG> "assign" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
995     |
996     <END_FUNCTION : <END_TAG> "function" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
997     |
998     <END_MACRO : <END_TAG> "macro" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
999     |
1000     <END_OUTPUTFORMAT : <END_TAG> "output" ("f" | "F") "ormat" <CLOSE_TAG1>> {
1001         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 6), DEFAULT);
1002     }
1003     |
1004     <END_AUTOESC : <END_TAG> "auto" ("e" | "E") "sc" <CLOSE_TAG1>> {
1005         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 4), DEFAULT);
1006     }
1007     |
1008     <END_NOAUTOESC : <END_TAG> "no" ("autoe"|"AutoE") "sc" <CLOSE_TAG1>> {
1009         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
1010     }
1011     |
1012     <END_COMPRESS : <END_TAG> "compress" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1013     |
1014     <END_TRANSFORM : <END_TAG> "transform" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1015     |
1016     <END_SWITCH : <END_TAG> "switch" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1017     |
1018     <ELSE : <START_TAG> "else" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1019     |
1020     <BREAK : <START_TAG> "break" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1021     |
1022     <SIMPLE_RETURN : <START_TAG> "return" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1023     |
1024     <HALT : <START_TAG> "stop" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1025     |
1026     <FLUSH : <START_TAG> "flush" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1027     |
1028     <TRIM : <START_TAG> "t" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1029     |
1030     <LTRIM : <START_TAG> "lt" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1031     |
1032     <RTRIM : <START_TAG> "rt" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1033     |
1034     <NOTRIM : <START_TAG> "nt" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1035     |
1036     <DEFAUL : <START_TAG> "default" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1037     |
1038     <SIMPLE_NESTED : <START_TAG> "nested" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1039     |
1040     <NESTED : <START_TAG> "nested" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
1041     |
1042     <SIMPLE_RECURSE : <START_TAG> "recurse" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1043     |
1044     <RECURSE : <START_TAG> "recurse" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
1045     |
1046     <FALLBACK : <START_TAG> "fallback" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1047     |
1048     <ESCAPE : <START_TAG> "escape" <BLANK>> { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
1049     |
1050     <END_ESCAPE : <END_TAG> "escape" <CLOSE_TAG1>> { strictSyntaxCheck(matchedToken, DEFAULT); }
1051     |
1052     <NOESCAPE : <START_TAG> "no" ("e" | "E") "scape" <CLOSE_TAG1>> {
1053         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
1054     }
1055     |
1056     <END_NOESCAPE : <END_TAG> "no" ("e" | "E") "scape" <CLOSE_TAG1>> {
1057         strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
1058     }
1059     |
1060     <UNIFIED_CALL : "<@" | "[@" > { unifiedCall(matchedToken); }
1061     |
1062     <UNIFIED_CALL_END : ("<" | "[") "/@" ((<ID>) ("."<ID>)*)? <CLOSE_TAG1>> { unifiedCallEnd(matchedToken); }
1063     |
1064     <FTL_HEADER : ("<#ftl" | "[#ftl") <BLANK>> { ftlHeader(matchedToken); }
1065     |
1066     <TRIVIAL_FTL_HEADER : ("<#ftl" | "[#ftl") ("/")? (">" | "]")> { ftlHeader(matchedToken); }
1067     |
1068     /*
1069      * ATTENTION: Update _CoreAPI.*_BUILT_IN_DIRECTIVE_NAMES if you add new directives!
1070      */
1071     <UNKNOWN_DIRECTIVE : ("[#" | "[/#" | "<#" | "</#") (["a"-"z", "A"-"Z", "_"])+>
1072     {
1073         if (!directiveSyntaxEstablished && incompatibleImprovements < _TemplateAPI.VERSION_INT_2_3_19) {
1074             matchedToken.kind = STATIC_TEXT_NON_WS;
1075         } else {
1076             char firstChar = matchedToken.image.charAt(0);
1077
1078             if (!directiveSyntaxEstablished && autodetectTagSyntax) {
1079                 squBracTagSyntax = (firstChar == '[');
1080                 directiveSyntaxEstablished = true;
1081             }
1082
1083             if (firstChar == '<' && squBracTagSyntax) {
1084                 matchedToken.kind = STATIC_TEXT_NON_WS;
1085             } else if (firstChar == '[' && !squBracTagSyntax) {
1086                 matchedToken.kind = STATIC_TEXT_NON_WS;
1087             } else if (strictEscapeSyntax) {
1088                 String dn = matchedToken.image;
1089                 int index = dn.indexOf('#');
1090                 dn = dn.substring(index + 1);
1091
1092                 // Until the tokenizer/parser is reworked, we have this quirk where something like <#list>
1093                 // doesn't match any directive starter tokens, because that token requires whitespace after the
1094                 // name as it should be followed by parameters. For now we work this around so we don't report
1095                 // unknown directive:
1096                 if (_CoreAPI.ALL_BUILT_IN_DIRECTIVE_NAMES.contains(dn)) {
1097                     throw new TokenMgrError(
1098                             "#" + dn + " is an existing directive, but the tag is malformed. " 
1099                             + " (See FreeMarker Manual / Directive Reference.)",
1100                             TokenMgrError.LEXICAL_ERROR,
1101                             matchedToken.beginLine, matchedToken.beginColumn + 1,
1102                             matchedToken.endLine, matchedToken.endColumn);
1103                 }
1104
1105                 String tip = null;
1106                 if (dn.equals("set") || dn.equals("var")) {
1107                     tip = "Use #assign or #local or #global, depending on the intented scope "
1108                           + "(#assign is template-scope). " + PLANNED_DIRECTIVE_HINT;
1109                 } else if (dn.equals("else_if") || dn.equals("elif")) {
1110                         tip = "Use #elseif.";
1111                 } else if (dn.equals("no_escape")) {
1112                         tip = "Use #noescape instead.";
1113                 } else if (dn.equals("method")) {
1114                         tip = "Use #function instead.";
1115                 } else if (dn.equals("head") || dn.equals("template") || dn.equals("fm")) {
1116                         tip = "You may meant #ftl.";
1117                 } else if (dn.equals("try") || dn.equals("atempt")) {
1118                         tip = "You may meant #attempt.";
1119                 } else if (dn.equals("for") || dn.equals("each") || dn.equals("iterate") || dn.equals("iterator")) {
1120                     tip = "You may meant #list (http://freemarker.org/docs/ref_directive_list.html).";
1121                 } else if (dn.equals("prefix")) {
1122                     tip = "You may meant #import. " + PLANNED_DIRECTIVE_HINT;
1123                 } else if (dn.equals("item") || dn.equals("row") || dn.equals("rows")) {
1124                     tip = "You may meant #items.";
1125                 } else if (dn.equals("separator") || dn.equals("separate") || dn.equals("separ")) {
1126                     tip = "You may meant #sep.";
1127                 } else {
1128                     tip = "Help (latest version): http://freemarker.org/docs/ref_directive_alphaidx.html; "
1129                             + "you're using FreeMarker " + Configuration.getVersion() + ".";
1130                 }
1131                 throw new TokenMgrError(
1132                         "Unknown directive: #" + dn + (tip != null ? ". " + tip : ""),
1133                         TokenMgrError.LEXICAL_ERROR,
1134                         matchedToken.beginLine, matchedToken.beginColumn + 1,
1135                         matchedToken.endLine, matchedToken.endColumn);
1136             }
1137         }
1138     }
1139 }
1140
1141 <DEFAULT, NODIRECTIVE> TOKEN :
1142 {
1143     <STATIC_TEXT_WS : ("\n" | "\r" | "\t" | " ")+>
1144     |
1145     <STATIC_TEXT_NON_WS : (~["$", "<", "#", "[", "{", "\n", "\r", "\t", " "])+>
1146     |
1147     <STATIC_TEXT_FALSE_ALARM : "$" | "#" | "<" | "[" | "{"> // to handle a lone dollar sign or "<" or "# or <@ with whitespace after"
1148     |
1149     <DOLLAR_INTERPOLATION_OPENING : "${"> { startInterpolation(matchedToken); }
1150     |
1151     <HASH_INTERPOLATION_OPENING : "#{"> { startInterpolation(matchedToken); }
1152 }
1153
1154 <FM_EXPRESSION, IN_PAREN, NAMED_PARAMETER_EXPRESSION> SKIP :
1155 {
1156     < ( " " | "\t" | "\n" | "\r" )+ >
1157     |
1158     < ("<" | "[") ("#" | "!") "--"> : EXPRESSION_COMMENT
1159 }
1160
1161 <EXPRESSION_COMMENT> SKIP:
1162 {
1163     < (~["-", ">", "]"])+ >
1164     |
1165     < ">">
1166     |
1167     < "]">
1168     |
1169     < "-">
1170     |
1171     < "-->" | "--]">
1172     {
1173         if (parenthesisNesting > 0) SwitchTo(IN_PAREN);
1174         else if (inInvocation) SwitchTo(NAMED_PARAMETER_EXPRESSION);
1175         else SwitchTo(FM_EXPRESSION);
1176     }
1177 }
1178
1179 <FM_EXPRESSION, IN_PAREN, NO_SPACE_EXPRESSION, NAMED_PARAMETER_EXPRESSION> TOKEN :
1180 {
1181     <#ESCAPED_CHAR :
1182         "\\"
1183         (
1184             ("n" | "t" | "r" | "f" | "b" | "g" | "l" | "a" | "\\" | "'" | "\"" | "$" | "{")
1185             |
1186             ("x" ["0"-"9", "A"-"F", "a"-"f"])
1187         )
1188     >
1189     | 
1190     <STRING_LITERAL :
1191         (
1192             "\""
1193             ((~["\"", "\\"]) | <ESCAPED_CHAR>)*
1194             "\""
1195         )
1196         |
1197         (
1198             "'"
1199             ((~["'", "\\"]) | <ESCAPED_CHAR>)*
1200             "'"
1201         )
1202     >
1203     |
1204     <RAW_STRING : "r" (("\"" (~["\""])* "\"") | ("'" (~["'"])* "'"))>
1205     |
1206     <FALSE : "false">
1207     |
1208     <TRUE : "true">
1209     |
1210     <INTEGER : (["0"-"9"])+>
1211     |
1212     <DECIMAL : <INTEGER> "." <INTEGER>>
1213     |
1214     <DOT : ".">
1215     |
1216     <DOT_DOT : "..">
1217     |
1218     <DOT_DOT_LESS : "..<" | "..!" >
1219     |
1220     <DOT_DOT_ASTERISK : "..*" >
1221     |
1222     <BUILT_IN : "?">
1223     |
1224     <EXISTS : "??">
1225     |
1226     <EQUALS : "=">
1227     |
1228     <DOUBLE_EQUALS : "==">
1229     |
1230     <NOT_EQUALS : "!=">
1231     |
1232     <PLUS_EQUALS : "+=">
1233     |
1234     <MINUS_EQUALS : "-=">
1235     |
1236     <TIMES_EQUALS : "*=">
1237     |
1238     <DIV_EQUALS : "/=">
1239     |
1240     <MOD_EQUALS : "%=">
1241     |
1242     <PLUS_PLUS : "++">
1243     |
1244     <MINUS_MINUS : "--">
1245     |
1246     <LESS_THAN : "lt" | "\\lt" | "<" | "&lt;">
1247     |
1248     <LESS_THAN_EQUALS : "lte" | "\\lte" | "<=" | "&lt;=">
1249     |
1250     <ESCAPED_GT: "gt" | "\\gt" |  "&gt;">
1251     |
1252     <ESCAPED_GTE : "gte" | "\\gte" | "&gt;=">
1253     |
1254     <PLUS : "+">
1255     |
1256     <MINUS : "-">
1257     |
1258     <TIMES : "*">
1259     |
1260     <DOUBLE_STAR : "**">
1261     |
1262     <ELLIPSIS : "...">
1263     |
1264     <DIVIDE : "/">
1265     |
1266     <PERCENT : "%">
1267     |
1268     <AND : "&" | "&&" | "&amp;&amp;" | "\\and" >
1269     |
1270     <OR : "|" | "||">
1271     |
1272     <EXCLAM : "!">
1273     |
1274     <COMMA : ",">
1275     |
1276     <SEMICOLON : ";">
1277     |
1278     <COLON : ":">
1279     |
1280     <OPEN_BRACKET : "[">
1281     {
1282         ++bracketNesting;
1283     }
1284     |
1285     <CLOSE_BRACKET : "]">
1286     {
1287         closeBracket(matchedToken);
1288     }
1289     |
1290     <OPEN_PAREN : "(">
1291     {
1292         ++parenthesisNesting;
1293         if (parenthesisNesting == 1) SwitchTo(IN_PAREN);
1294     }
1295     |
1296     <CLOSE_PAREN : ")">
1297     {
1298         --parenthesisNesting;
1299         if (parenthesisNesting == 0) {
1300             if (inInvocation) SwitchTo(NAMED_PARAMETER_EXPRESSION);
1301             else SwitchTo(FM_EXPRESSION);
1302         }
1303     }
1304     |
1305     <OPENING_CURLY_BRACKET : "{">
1306     {
1307         ++hashLiteralNesting;
1308     }
1309     |
1310     <CLOSING_CURLY_BRACKET : "}">
1311     {
1312         if (hashLiteralNesting == 0) endInterpolation(matchedToken);
1313         else --hashLiteralNesting;
1314     }
1315     |
1316     <IN : "in">
1317     |
1318     <AS : "as">
1319     |
1320     <USING : "using">
1321     |
1322     <ID: <ID_START_CHAR> (<ID_START_CHAR>|<ASCII_DIGIT>)*> {
1323         // Remove backslashes from Token.image:
1324         final String s = matchedToken.image;
1325         if (s.indexOf('\\') != -1) {
1326             final int srcLn = s.length(); 
1327             final char[] newS = new char[srcLn - 1];
1328             int dstIdx = 0;
1329             for (int srcIdx = 0; srcIdx < srcLn; srcIdx++) {
1330                 final char c = s.charAt(srcIdx);
1331                 if (c != '\\') {
1332                     newS[dstIdx++] = c;
1333                 }
1334             }
1335             matchedToken.image = new String(newS, 0, dstIdx);
1336         }
1337     }
1338     |
1339     <OPEN_MISPLACED_INTERPOLATION : "${" | "#{">
1340     {
1341         if ("".length() == 0) {  // prevents unreachabe "break" compilation error in generated Java
1342             char c = matchedToken.image.charAt(0);
1343             throw new TokenMgrError(
1344                     "You can't use \"" + c + "{\" here as you are already in FreeMarker-expression-mode. Thus, instead "
1345                     + "of " + c + "{myExpression}, just write myExpression. "
1346                     + "(" + c + "{...} is only needed where otherwise static text is expected, i.e, outside " 
1347                     + "FreeMarker tags and ${...}-s.)",
1348                     TokenMgrError.LEXICAL_ERROR,
1349                     matchedToken.beginLine, matchedToken.beginColumn,
1350                     matchedToken.endLine, matchedToken.endColumn);
1351         }
1352     }
1353     |
1354     <#NON_ESCAPED_ID_START_CHAR:
1355         [
1356             // This was generated on JDK 1.8.0_20 Win64 with src/main/misc/identifierChars/IdentifierCharGenerator.java
1357                         "$", 
1358                         "@" - "Z", 
1359                         "_", 
1360                         "a" - "z", 
1361                         "\u00AA", 
1362                         "\u00B5", 
1363                         "\u00BA", 
1364                         "\u00C0" - "\u00D6", 
1365                         "\u00D8" - "\u00F6", 
1366                         "\u00F8" - "\u1FFF", 
1367                         "\u2071", 
1368                         "\u207F", 
1369                         "\u2090" - "\u209C", 
1370                         "\u2102", 
1371                         "\u2107", 
1372                         "\u210A" - "\u2113", 
1373                         "\u2115", 
1374                         "\u2119" - "\u211D", 
1375                         "\u2124", 
1376                         "\u2126", 
1377                         "\u2128", 
1378                         "\u212A" - "\u212D", 
1379                         "\u212F" - "\u2139", 
1380                         "\u213C" - "\u213F", 
1381                         "\u2145" - "\u2149", 
1382                         "\u214E", 
1383                         "\u2183" - "\u2184", 
1384                         "\u2C00" - "\u2C2E", 
1385                         "\u2C30" - "\u2C5E", 
1386                         "\u2C60" - "\u2CE4", 
1387                         "\u2CEB" - "\u2CEE", 
1388                         "\u2CF2" - "\u2CF3", 
1389                         "\u2D00" - "\u2D25", 
1390                         "\u2D27", 
1391                         "\u2D2D", 
1392                         "\u2D30" - "\u2D67", 
1393                         "\u2D6F", 
1394                         "\u2D80" - "\u2D96", 
1395                         "\u2DA0" - "\u2DA6", 
1396                         "\u2DA8" - "\u2DAE", 
1397                         "\u2DB0" - "\u2DB6", 
1398                         "\u2DB8" - "\u2DBE", 
1399                         "\u2DC0" - "\u2DC6", 
1400                         "\u2DC8" - "\u2DCE", 
1401                         "\u2DD0" - "\u2DD6", 
1402                         "\u2DD8" - "\u2DDE", 
1403                         "\u2E2F", 
1404                         "\u3005" - "\u3006", 
1405                         "\u3031" - "\u3035", 
1406                         "\u303B" - "\u303C", 
1407                         "\u3040" - "\u318F", 
1408                         "\u31A0" - "\u31BA", 
1409                         "\u31F0" - "\u31FF", 
1410                         "\u3300" - "\u337F", 
1411                         "\u3400" - "\u4DB5", 
1412                         "\u4E00" - "\uA48C", 
1413                         "\uA4D0" - "\uA4FD", 
1414                         "\uA500" - "\uA60C", 
1415                         "\uA610" - "\uA62B", 
1416                         "\uA640" - "\uA66E", 
1417                         "\uA67F" - "\uA697", 
1418                         "\uA6A0" - "\uA6E5", 
1419                         "\uA717" - "\uA71F", 
1420                         "\uA722" - "\uA788", 
1421                         "\uA78B" - "\uA78E", 
1422                         "\uA790" - "\uA793", 
1423                         "\uA7A0" - "\uA7AA", 
1424                         "\uA7F8" - "\uA801", 
1425                         "\uA803" - "\uA805", 
1426                         "\uA807" - "\uA80A", 
1427                         "\uA80C" - "\uA822", 
1428                         "\uA840" - "\uA873", 
1429                         "\uA882" - "\uA8B3", 
1430                         "\uA8D0" - "\uA8D9", 
1431                         "\uA8F2" - "\uA8F7", 
1432                         "\uA8FB", 
1433                         "\uA900" - "\uA925", 
1434                         "\uA930" - "\uA946", 
1435                         "\uA960" - "\uA97C", 
1436                         "\uA984" - "\uA9B2", 
1437                         "\uA9CF" - "\uA9D9", 
1438                         "\uAA00" - "\uAA28", 
1439                         "\uAA40" - "\uAA42", 
1440                         "\uAA44" - "\uAA4B", 
1441                         "\uAA50" - "\uAA59", 
1442                         "\uAA60" - "\uAA76", 
1443                         "\uAA7A", 
1444                         "\uAA80" - "\uAAAF", 
1445                         "\uAAB1", 
1446                         "\uAAB5" - "\uAAB6", 
1447                         "\uAAB9" - "\uAABD", 
1448                         "\uAAC0", 
1449                         "\uAAC2", 
1450                         "\uAADB" - "\uAADD", 
1451                         "\uAAE0" - "\uAAEA", 
1452                         "\uAAF2" - "\uAAF4", 
1453                         "\uAB01" - "\uAB06", 
1454                         "\uAB09" - "\uAB0E", 
1455                         "\uAB11" - "\uAB16", 
1456                         "\uAB20" - "\uAB26", 
1457                         "\uAB28" - "\uAB2E", 
1458                         "\uABC0" - "\uABE2", 
1459                         "\uABF0" - "\uABF9", 
1460                         "\uAC00" - "\uD7A3", 
1461                         "\uD7B0" - "\uD7C6", 
1462                         "\uD7CB" - "\uD7FB", 
1463                         "\uF900" - "\uFB06", 
1464                         "\uFB13" - "\uFB17", 
1465                         "\uFB1D", 
1466                         "\uFB1F" - "\uFB28", 
1467                         "\uFB2A" - "\uFB36", 
1468                         "\uFB38" - "\uFB3C", 
1469                         "\uFB3E", 
1470                         "\uFB40" - "\uFB41", 
1471                         "\uFB43" - "\uFB44", 
1472                         "\uFB46" - "\uFBB1", 
1473                         "\uFBD3" - "\uFD3D", 
1474                         "\uFD50" - "\uFD8F", 
1475                         "\uFD92" - "\uFDC7", 
1476                         "\uFDF0" - "\uFDFB", 
1477                         "\uFE70" - "\uFE74", 
1478                         "\uFE76" - "\uFEFC", 
1479                         "\uFF10" - "\uFF19", 
1480                         "\uFF21" - "\uFF3A", 
1481                         "\uFF41" - "\uFF5A", 
1482                         "\uFF66" - "\uFFBE", 
1483                         "\uFFC2" - "\uFFC7", 
1484                         "\uFFCA" - "\uFFCF", 
1485                         "\uFFD2" - "\uFFD7", 
1486                         "\uFFDA" - "\uFFDC" 
1487         ]
1488     >
1489     |
1490     <#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":")>
1491     |
1492     <#ID_START_CHAR: <NON_ESCAPED_ID_START_CHAR>|<ESCAPED_ID_CHAR>>
1493     |
1494     <#ASCII_DIGIT: ["0" - "9"]>
1495 }
1496
1497 <FM_EXPRESSION, NO_SPACE_EXPRESSION, NAMED_PARAMETER_EXPRESSION> TOKEN :
1498 {
1499     <DIRECTIVE_END : ">">
1500     {
1501         if (inFTLHeader) eatNewline();
1502         inFTLHeader = false;
1503         if (squBracTagSyntax) {
1504             matchedToken.kind = NATURAL_GT;
1505         } else {
1506             SwitchTo(DEFAULT);
1507         }
1508     }
1509     |
1510     <EMPTY_DIRECTIVE_END : "/>" | "/]">
1511     {
1512         if (inFTLHeader) eatNewline();
1513         inFTLHeader = false;
1514         SwitchTo(DEFAULT);
1515     }
1516 }
1517
1518 <IN_PAREN> TOKEN :
1519 {
1520     <NATURAL_GT : ">">
1521     |
1522     <NATURAL_GTE : ">=">
1523 }
1524
1525 <NO_SPACE_EXPRESSION> TOKEN :
1526 {
1527     <TERMINATING_WHITESPACE :  (["\n", "\r", "\t", " "])+> : FM_EXPRESSION
1528 }
1529
1530 <NAMED_PARAMETER_EXPRESSION> TOKEN :
1531 {
1532     <TERMINATING_EXCLAM : "!" (["\n", "\r", "\t", " "])+> : FM_EXPRESSION
1533 }
1534
1535 <NO_PARSE> TOKEN :
1536 {
1537     <TERSE_COMMENT_END : "-->" | "--]">
1538     {
1539         if (noparseTag.equals("-->")) {
1540             boolean squareBracket = matchedToken.image.endsWith("]");
1541             if ((squBracTagSyntax && squareBracket) || (!squBracTagSyntax && !squareBracket)) {
1542                 matchedToken.image = matchedToken.image + ";"; 
1543                 SwitchTo(DEFAULT);
1544             }
1545         }
1546     }
1547     |
1548     <MAYBE_END :
1549         ("<" | "[")
1550         "/"
1551         ("#")?
1552         (["a"-"z", "A"-"Z"])+
1553         ( " " | "\t" | "\n" | "\r" )*
1554         (">" | "]")
1555     >
1556     {
1557         StringTokenizer st = new StringTokenizer(image.toString(), " \t\n\r<>[]/#", false);
1558         if (st.nextToken().equals(noparseTag)) {
1559             matchedToken.image = matchedToken.image + ";"; 
1560             SwitchTo(DEFAULT);
1561         }
1562     }
1563     |
1564     <KEEP_GOING : (~["<", "[", "-"])+>
1565     |
1566     <LONE_LESS_THAN_OR_DASH : ["<", "[", "-"]>
1567 }
1568
1569 // Now the actual parsing code, starting
1570 // with the productions for FreeMarker's
1571 // expression syntax.
1572
1573 /**
1574  * This is the same as OrExpression, since
1575  * the OR is the operator with the lowest
1576  * precedence.
1577  */
1578 Expression Expression() :
1579 {
1580     Expression exp;
1581 }
1582 {
1583     exp = OrExpression()
1584     {
1585         return exp;
1586     }
1587 }
1588
1589 /**
1590  * Lowest level expression, a literal, a variable,
1591  * or a possibly more complex expression bounded
1592  * by parentheses.
1593  */
1594 Expression PrimaryExpression() :
1595 {
1596     Expression exp;
1597 }
1598 {
1599     (
1600         exp = NumberLiteral()
1601         |   
1602         exp = HashLiteral()
1603         |   
1604         exp = StringLiteral(true)
1605         |   
1606         exp = BooleanLiteral()
1607         |   
1608         exp = ListLiteral()
1609         |   
1610         exp = Identifier()
1611         |   
1612         exp = Parenthesis()
1613         |   
1614         exp = BuiltinVariable()
1615     )
1616     (
1617         LOOKAHEAD(<DOT> | <OPEN_BRACKET> |<OPEN_PAREN> | <BUILT_IN> | <EXCLAM> | <TERMINATING_EXCLAM> | <EXISTS>)
1618         exp = AddSubExpression(exp)
1619     )*
1620     {
1621         return exp;
1622     }
1623 }
1624
1625 Expression Parenthesis() :
1626 {
1627     Expression exp, result;
1628     Token start, end;
1629 }
1630 {
1631     start = <OPEN_PAREN>
1632     exp = Expression()
1633     end = <CLOSE_PAREN>
1634     {
1635         result = new ParentheticalExpression(exp);
1636         result.setLocation(template, start, end);
1637         return result;
1638     }
1639 }
1640
1641 /**
1642  * A primary expression preceded by zero or
1643  * more unary operators. (The only unary operator we
1644  * currently have is the NOT.)
1645  */
1646 Expression UnaryExpression() :
1647 {
1648     Expression exp, result;
1649     boolean haveNot = false;
1650     Token t = null, start = null;
1651 }
1652 {
1653     (
1654         result = UnaryPlusMinusExpression()
1655         |
1656         result = NotExpression()
1657         |
1658         result = PrimaryExpression()
1659     )
1660     {
1661         return result;
1662     }
1663 }
1664
1665 Expression NotExpression() : 
1666 {
1667     Token t;
1668     Expression exp, result = null;
1669     ArrayList nots = new ArrayList();
1670 }
1671 {
1672     (
1673         t = <EXCLAM> { nots.add(t); }
1674     )+
1675     exp = PrimaryExpression()
1676     {
1677         for (int i = 0; i < nots.size(); i++) {
1678             result = new NotExpression(exp);
1679             Token tok = (Token) nots.get(nots.size() -i -1);
1680             result.setLocation(template, tok, exp);
1681             exp = result;
1682         }
1683         return result;
1684     }
1685 }
1686
1687 Expression UnaryPlusMinusExpression() :
1688 {
1689     Expression exp, result;
1690     boolean isMinus = false;
1691     Token t;
1692 }
1693 {
1694     (
1695         t = <PLUS>
1696         |
1697         t = <MINUS> { isMinus = true; }
1698     )
1699     exp = PrimaryExpression()
1700     {
1701         result = new UnaryPlusMinusExpression(exp, isMinus);  
1702         result.setLocation(template, t, exp);
1703         return result;
1704     }
1705 }
1706
1707 Expression AdditiveExpression() :
1708 {
1709     Expression lhs, rhs, result;
1710     boolean plus;
1711 }
1712 {
1713     lhs = MultiplicativeExpression() { result = lhs; }
1714     (
1715         LOOKAHEAD(<PLUS>|<MINUS>)
1716         (
1717             (
1718                 <PLUS> { plus = true; }
1719                 |
1720                 <MINUS> { plus = false; }
1721             )
1722         )
1723         rhs = MultiplicativeExpression()
1724         {
1725             if (plus) {
1726                     // plus is treated separately, since it is also
1727                     // used for concatenation.
1728                 result = new AddConcatExpression(lhs, rhs);
1729             } else {
1730                 numberLiteralOnly(lhs);
1731                 numberLiteralOnly(rhs);
1732                 result = new ArithmeticExpression(lhs, rhs, ArithmeticExpression.TYPE_SUBSTRACTION);
1733             }
1734             result.setLocation(template, lhs, rhs);
1735             lhs = result;
1736         }
1737     )*
1738     {
1739         return result;
1740     }
1741 }
1742
1743 /**
1744  * A unary expression followed by zero or more
1745  * unary expressions with operators in between.
1746  */
1747 Expression MultiplicativeExpression() :
1748 {
1749     Expression lhs, rhs, result;
1750     int operation = ArithmeticExpression.TYPE_MULTIPLICATION;
1751 }
1752 {
1753     lhs = UnaryExpression() { result = lhs; }
1754     (
1755         LOOKAHEAD(<TIMES>|<DIVIDE>|<PERCENT>)
1756         (
1757             (
1758                 <TIMES> { operation = ArithmeticExpression.TYPE_MULTIPLICATION; }
1759                 |
1760                 <DIVIDE> { operation = ArithmeticExpression.TYPE_DIVISION; }
1761                 |
1762                 <PERCENT> {operation = ArithmeticExpression.TYPE_MODULO; }
1763             )
1764         )
1765         rhs = UnaryExpression()
1766         {
1767             numberLiteralOnly(lhs);
1768             numberLiteralOnly(rhs);
1769             result = new ArithmeticExpression(lhs, rhs, operation);
1770             result.setLocation(template, lhs, rhs);
1771             lhs = result;
1772         }
1773     )*
1774     {
1775         return result;
1776     }
1777 }
1778
1779
1780 Expression EqualityExpression() :
1781 {
1782     Expression lhs, rhs, result;
1783     Token t;
1784 }
1785 {
1786     lhs = RelationalExpression() { result = lhs; }
1787     [
1788         LOOKAHEAD(<NOT_EQUALS>|<EQUALS>|<DOUBLE_EQUALS>)
1789         (
1790             t = <NOT_EQUALS> 
1791             |
1792             t = <EQUALS> 
1793             |
1794             t = <DOUBLE_EQUALS>
1795         )
1796         rhs = RelationalExpression()
1797         {
1798                 notHashLiteral(lhs, "scalar");
1799                 notHashLiteral(rhs, "scalar");
1800                 notListLiteral(lhs, "scalar");
1801                 notListLiteral(rhs, "scalar");
1802                 result = new ComparisonExpression(lhs, rhs, t.image);
1803                 result.setLocation(template, lhs, rhs);
1804         }
1805     ]
1806     {
1807         return result;
1808     }
1809 }
1810
1811 Expression RelationalExpression() :
1812 {
1813     Expression lhs, rhs, result;
1814     Token t;
1815 }
1816 {
1817     lhs = RangeExpression() { result = lhs; }
1818     [
1819         LOOKAHEAD(<NATURAL_GTE>|<ESCAPED_GTE>|<NATURAL_GT>|<ESCAPED_GT>|<LESS_THAN_EQUALS>|<LESS_THAN_EQUALS>|<LESS_THAN>)
1820         (
1821             t = <NATURAL_GTE>
1822             |
1823             t = <ESCAPED_GTE>
1824             |
1825             t = <NATURAL_GT>
1826             |
1827             t = <ESCAPED_GT>
1828             |
1829             t = <LESS_THAN_EQUALS>
1830             |
1831             t = <LESS_THAN>
1832         )
1833         rhs = RangeExpression()
1834         {
1835             notHashLiteral(lhs, "scalar");
1836             notHashLiteral(rhs, "scalar");
1837             notListLiteral(lhs, "scalar");
1838             notListLiteral(rhs, "scalar");
1839             notStringLiteral(lhs, "number");
1840             notStringLiteral(rhs, "number");
1841             result = new ComparisonExpression(lhs, rhs, t.image);
1842             result.setLocation(template, lhs, rhs);
1843         }
1844     ]
1845     {
1846         return result;
1847     }
1848 }
1849
1850 Expression RangeExpression() :
1851 {
1852     Expression lhs, rhs = null, result;
1853     int endType;
1854     Token dotDot = null;
1855 }
1856 {
1857     lhs = AdditiveExpression() { result = lhs; }
1858     [
1859         LOOKAHEAD(1)  // To suppress warning
1860         (
1861             (
1862                 (
1863                     <DOT_DOT_LESS> { endType = Range.END_EXCLUSIVE; }
1864                     |
1865                     <DOT_DOT_ASTERISK> { endType = Range.END_SIZE_LIMITED; }
1866                 )
1867                 rhs = AdditiveExpression()
1868             )
1869             | 
1870             (
1871                 dotDot = <DOT_DOT> { endType = Range.END_UNBOUND; }
1872                 [
1873                     LOOKAHEAD(AdditiveExpression())
1874                     rhs = AdditiveExpression()
1875                     {
1876                         endType = Range.END_INCLUSIVE;
1877                     }
1878                 ]
1879             )
1880         )
1881         {
1882             numberLiteralOnly(lhs);
1883             if (rhs != null) {
1884                 numberLiteralOnly(rhs);
1885             }
1886            
1887             Range range = new Range(lhs, rhs, endType);
1888             if (rhs != null) {
1889                 range.setLocation(template, lhs, rhs);
1890             } else {
1891                 range.setLocation(template, lhs, dotDot);
1892             }
1893             result = range;
1894         }
1895     ]
1896     {
1897         return result;
1898     }
1899 }
1900
1901
1902
1903
1904 Expression AndExpression() :
1905 {
1906     Expression lhs, rhs, result;
1907 }
1908 {
1909     lhs = EqualityExpression() { result = lhs; }
1910     (
1911         LOOKAHEAD(<AND>)
1912         <AND>
1913         rhs = EqualityExpression()
1914         {
1915             booleanLiteralOnly(lhs);
1916             booleanLiteralOnly(rhs);
1917             result = new AndExpression(lhs, rhs);
1918             result.setLocation(template, lhs, rhs);
1919             lhs = result;
1920         }
1921     )*
1922     {
1923         return result;
1924     }
1925 }
1926
1927 Expression OrExpression() :
1928 {
1929     Expression lhs, rhs, result;
1930 }
1931 {
1932     lhs = AndExpression() { result = lhs; }
1933     (
1934         LOOKAHEAD(<OR>)
1935         <OR>
1936         rhs = AndExpression()
1937         {
1938             booleanLiteralOnly(lhs);
1939             booleanLiteralOnly(rhs);
1940             result = new OrExpression(lhs, rhs);
1941             result.setLocation(template, lhs, rhs);
1942             lhs = result;
1943         }
1944     )*
1945     {
1946         return result;
1947     }
1948 }
1949
1950 ListLiteral ListLiteral() :
1951 {
1952     ArrayList values = new ArrayList();
1953     Token begin, end;
1954 }
1955 {
1956     begin = <OPEN_BRACKET>
1957     values = PositionalArgs()
1958     end = <CLOSE_BRACKET>
1959     {
1960         ListLiteral result = new ListLiteral(values);
1961         result.setLocation(template, begin, end);
1962         return result;
1963     }
1964 }
1965
1966 Expression NumberLiteral() :
1967 {
1968     Token op = null, t;
1969 }
1970 {
1971     (
1972         t = <INTEGER>
1973         |
1974         t = <DECIMAL>
1975     )
1976     {
1977         String s = t.image;
1978         Expression result = new NumberLiteral(pCfg.getArithmeticEngine().toNumber(s));
1979         Token startToken = (op != null) ? op : t;
1980         result.setLocation(template, startToken, t);
1981         return result;
1982     }
1983 }
1984
1985 Identifier Identifier() :
1986 {
1987     Token t;
1988 }
1989 {
1990     t = <ID>
1991     {
1992         Identifier id = new Identifier(t.image);
1993         id.setLocation(template, t, t);
1994         return id;
1995     }
1996 }
1997
1998 Expression IdentifierOrStringLiteral() :
1999 {
2000     Expression exp;
2001 }
2002 {
2003     (
2004         exp = Identifier()
2005         |
2006         exp = StringLiteral(false)
2007     )
2008     {
2009         return exp;
2010     }   
2011 }
2012
2013 BuiltinVariable BuiltinVariable() :
2014 {
2015     Token dot, name;
2016 }
2017 {
2018     dot = <DOT>
2019     name = <ID>
2020     {
2021         BuiltinVariable result = null;
2022         token_source.checkNamingConvention(name);
2023
2024         TemplateModel parseTimeValue;
2025         String nameStr = name.image;
2026         if (nameStr.equals(BuiltinVariable.OUTPUT_FORMAT) || nameStr.equals(BuiltinVariable.OUTPUT_FORMAT_CC)) {
2027             parseTimeValue = new SimpleScalar(outputFormat.getName());
2028         } else if (nameStr.equals(BuiltinVariable.AUTO_ESC) || nameStr.equals(BuiltinVariable.AUTO_ESC_CC)) {
2029             parseTimeValue = autoEscaping ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
2030         } else {\r            parseTimeValue = null;
2031         }\r        
2032         result = new BuiltinVariable(name, token_source, parseTimeValue);
2033         
2034         result.setLocation(template, dot, name);
2035         return result;
2036     }
2037 }
2038
2039 /**
2040  * Production that builds up an expression
2041  * using the dot or dynamic key name
2042  * or the args list if this is a method invocation.
2043  */
2044 Expression AddSubExpression(Expression exp) :
2045 {
2046     Expression result = null;
2047 }
2048 {
2049     (
2050         result = DotVariable(exp)
2051         |
2052         result = DynamicKey(exp)
2053         |
2054         result = MethodArgs(exp)
2055         |
2056         result = BuiltIn(exp)
2057         |
2058         result = DefaultTo(exp)
2059         |
2060         result = Exists(exp)
2061     )
2062     {
2063         return result;
2064     }
2065 }
2066
2067 Expression DefaultTo(Expression exp) :
2068 {
2069     Expression rhs = null;
2070     Token t;
2071 }
2072 {
2073     (
2074         t = <TERMINATING_EXCLAM>
2075         |
2076         (
2077             t = <EXCLAM>
2078             [
2079                 LOOKAHEAD(Expression())
2080                 rhs = Expression()
2081             ]
2082         )
2083     )
2084     {
2085         DefaultToExpression result = new DefaultToExpression(exp, rhs);
2086         if (rhs == null) {
2087             result.setLocation(template, exp, t);
2088         } else {
2089             result.setLocation(template, exp, rhs);
2090         }
2091         return result;
2092     }
2093 }
2094
2095 Expression Exists(Expression exp) :
2096 {
2097     Token t;
2098 }
2099 {
2100     t = <EXISTS>
2101     {
2102         ExistsExpression result = new ExistsExpression(exp);
2103         result.setLocation(template, exp, t);
2104         return result;
2105     }
2106 }
2107
2108 Expression BuiltIn(Expression lhoExp) :
2109 {
2110     Token t = null;
2111     BuiltIn result;
2112     ArrayList/*<Expression>*/ args = null;
2113     Token openParen;
2114     Token closeParen;
2115 }
2116 {
2117     <BUILT_IN>
2118     t = <ID>
2119     {
2120         token_source.checkNamingConvention(t);
2121         result = BuiltIn.newBuiltIn(incompatibleImprovements, lhoExp, t, token_source);
2122         result.setLocation(template, lhoExp, t);
2123         
2124         if (!(result instanceof SpecialBuiltIn)) {
2125             return result;
2126         }
2127
2128         if (result instanceof BuiltInForLoopVariable) {
2129             if (!(lhoExp instanceof Identifier)) {
2130                 throw new ParseException(
2131                         "Expression used as the left hand operand of ?" + t.image
2132                         + " must be a simple loop variable name.", lhoExp);
2133             }
2134             String loopVarName = ((Identifier) lhoExp).getName();
2135             checkLoopVariableBuiltInLHO(loopVarName, lhoExp, t);
2136             ((BuiltInForLoopVariable) result).bindToLoopVariable(loopVarName);
2137             
2138             return result;
2139         }
2140         
2141         if (result instanceof BuiltInBannedWhenAutoEscaping) {
2142                 if (outputFormat instanceof MarkupOutputFormat && autoEscaping) {
2143                     throw new ParseException(
2144                             "Using ?" + t.image + " (legacy escaping) is not allowed when auto-escaping is on with "
2145                             + "a markup output format (" + outputFormat.getName() + "), to avoid double-escaping mistakes.",
2146                             template, t);
2147                 }
2148             
2149             return result;
2150         }
2151
2152         if (result instanceof MarkupOutputFormatBoundBuiltIn) {
2153             if (!(outputFormat instanceof MarkupOutputFormat)) {
2154                 throw new ParseException(
2155                         "?" + t.image + " can't be used here, as the current output format isn't a markup (escaping) "
2156                         + "format: " + outputFormat, template, t);
2157             }
2158             ((MarkupOutputFormatBoundBuiltIn) result).bindToMarkupOutputFormat((MarkupOutputFormat) outputFormat);
2159             
2160             return result;
2161         }
2162
2163         if (result instanceof OutputFormatBoundBuiltIn) {
2164             ((OutputFormatBoundBuiltIn) result).bindToOutputFormat(outputFormat, autoEscapingPolicy);
2165             
2166             return result;
2167         }
2168     }
2169     [
2170         LOOKAHEAD({ result instanceof BuiltInWithParseTimeParameters  })
2171         openParen = <OPEN_PAREN>
2172         args = PositionalArgs()
2173         closeParen = <CLOSE_PAREN> {
2174             result.setLocation(template, lhoExp, closeParen);
2175             ((BuiltInWithParseTimeParameters) result).bindToParameters(args, openParen, closeParen);
2176             
2177             return result;
2178         }
2179     ]
2180     {
2181         // Should have already return-ed
2182         throw new AssertionError("Unhandled " + SpecialBuiltIn.class.getName() + " subclass: " + result.getClass());
2183     }
2184 }
2185
2186
2187 /**
2188  * production for when a key is specified by <DOT> + keyname
2189  */
2190 Expression DotVariable(Expression exp) :
2191 {
2192     Token t;
2193 }
2194 {
2195         <DOT>
2196         (
2197             t = <ID> | t = <TIMES> | t = <DOUBLE_STAR> 
2198             |
2199             (
2200                 t = <LESS_THAN>
2201                 |
2202                 t = <LESS_THAN_EQUALS>
2203                 |
2204                 t = <ESCAPED_GT>
2205                 |
2206                 t = <ESCAPED_GTE>
2207                 |
2208                 t = <FALSE>
2209                 |
2210                 t = <TRUE>
2211                 |
2212                 t = <IN>
2213                 |
2214                 t = <AS>
2215                 |
2216                 t = <USING>
2217             )
2218             {
2219                 if (!Character.isLetter(t.image.charAt(0))) {
2220                     throw new ParseException(t.image + " is not a valid identifier.", template, t);
2221                 }
2222             }
2223         )
2224         {
2225             notListLiteral(exp, "hash");
2226             notStringLiteral(exp, "hash");
2227             notBooleanLiteral(exp, "hash");
2228             Dot dot = new Dot(exp, t.image);
2229             dot.setLocation(template, exp, t);
2230             return dot;
2231         }
2232 }
2233
2234 /**
2235  * production for when the key is specified
2236  * in brackets.
2237  */
2238 Expression DynamicKey(Expression exp) :
2239 {
2240     Expression arg;
2241     Token t;
2242 }
2243 {
2244     <OPEN_BRACKET>
2245     arg = Expression()
2246     t = <CLOSE_BRACKET>
2247     {
2248         notBooleanLiteral(exp, "list or hash");
2249         notNumberLiteral(exp, "list or hash");
2250         DynamicKeyName dkn = new DynamicKeyName(exp, arg);
2251         dkn.setLocation(template, exp, t);
2252         return dkn;
2253     }
2254 }
2255
2256 /**
2257  * production for an arglist part of a method invocation.
2258  */
2259 MethodCall MethodArgs(Expression exp) :
2260 {
2261         ArrayList args = new ArrayList();
2262         Token end;
2263 }
2264 {
2265         <OPEN_PAREN>
2266         args = PositionalArgs()
2267         end = <CLOSE_PAREN>
2268         {
2269             args.trimToSize();
2270             MethodCall result = new MethodCall(exp, args);
2271             result.setLocation(template, exp, end);
2272             return result;
2273         }
2274 }
2275
2276 StringLiteral StringLiteral(boolean interpolate) :
2277 {
2278     Token t;
2279     boolean raw = false;
2280 }
2281 {
2282     (
2283         t = <STRING_LITERAL>
2284         |
2285         t = <RAW_STRING> { raw = true; }
2286     )
2287     {
2288         String s;
2289         // Get rid of the quotes.
2290         if (raw) {
2291             s = t.image.substring(2, t.image.length() -1);
2292         } else {
2293                 try {
2294                     s = StringUtil.FTLStringLiteralDec(t.image.substring(1, t.image.length() -1));
2295                 } catch (ParseException pe) {
2296                     pe.lineNumber = t.beginLine;
2297                     pe.columnNumber = t.beginColumn;
2298                     pe.endLineNumber = t.endLine;
2299                     pe.endColumnNumber = t.endColumn;
2300                     throw pe;
2301                 }
2302         }
2303         StringLiteral result = new StringLiteral(s);
2304         result.setLocation(template, t, t);
2305         if (interpolate && !raw) {
2306             // TODO: This logic is broken. It can't handle literals that contains both ${...} and $\{...}. 
2307             if (t.image.indexOf("${") >= 0 || t.image.indexOf("#{") >= 0) result.parseValue(this, outputFormat);
2308         }
2309         return result;
2310     }
2311 }
2312
2313 Expression BooleanLiteral() :
2314 {
2315     Token t;
2316     Expression result;
2317 }
2318 {
2319     (
2320         t = <FALSE> { result = new BooleanLiteral(false); }
2321         |
2322         t = <TRUE> { result = new BooleanLiteral(true); }
2323     )
2324     {
2325         result.setLocation(template, t, t);
2326         return result;
2327     }
2328 }
2329
2330
2331 HashLiteral HashLiteral() :
2332 {
2333     Token begin, end;
2334     Expression key, value;
2335     ArrayList keys = new ArrayList();
2336     ArrayList values = new ArrayList();
2337 }
2338 {
2339     begin = <OPENING_CURLY_BRACKET>
2340     [
2341         key = Expression()
2342         (<COMMA>|<COLON>)
2343         value = Expression()
2344         {
2345             stringLiteralOnly(key);
2346             keys.add(key);
2347             values.add(value);
2348         }
2349         (
2350             <COMMA>
2351             key = Expression()
2352             (<COMMA>|<COLON>)
2353             value = Expression()
2354             {
2355                 stringLiteralOnly(key);
2356                 keys.add(key);
2357                 values.add(value);
2358             }
2359         )*
2360     ]
2361     end = <CLOSING_CURLY_BRACKET>
2362     {
2363         HashLiteral result = new HashLiteral(keys, values);
2364         result.setLocation(template, begin, end);
2365         return result;
2366     }
2367 }
2368
2369 /**
2370  * A production representing the ${...}
2371  * that outputs a variable.
2372  */
2373 DollarVariable StringOutput() :
2374 {
2375     Expression exp;
2376     Token begin, end;
2377 }
2378 {
2379     begin = <DOLLAR_INTERPOLATION_OPENING>
2380     exp = Expression()
2381     {
2382         notHashLiteral(exp, NonStringException.STRING_COERCABLE_TYPES_DESC);
2383         notListLiteral(exp, NonStringException.STRING_COERCABLE_TYPES_DESC);
2384     }
2385     end = <CLOSING_CURLY_BRACKET>
2386     {
2387         DollarVariable result = new DollarVariable(
2388                 exp, escapedExpression(exp),
2389                 outputFormat,
2390                 autoEscaping);
2391         result.setLocation(template, begin, end);
2392         return result;
2393     }
2394 }
2395
2396 NumericalOutput NumericalOutput() :
2397 {
2398     Expression exp;
2399     Token fmt = null, begin, end;
2400 }
2401 {
2402     begin = <HASH_INTERPOLATION_OPENING>
2403     exp = Expression() { numberLiteralOnly(exp); }
2404     [
2405         <SEMICOLON>
2406         fmt = <ID>
2407     ]
2408     end = <CLOSING_CURLY_BRACKET>
2409     {
2410         MarkupOutputFormat<?> autoEscOF = autoEscaping && outputFormat instanceof MarkupOutputFormat
2411                 ? (MarkupOutputFormat<?>) outputFormat : null;
2412     
2413         NumericalOutput result;
2414         if (fmt != null) {
2415             int minFrac = -1;  // -1 indicates that the value has not been set
2416             int maxFrac = -1;
2417
2418             StringTokenizer st = new StringTokenizer(fmt.image, "mM", true);
2419             char type = '-';
2420             while (st.hasMoreTokens()) {
2421                 String token = st.nextToken();
2422                 try {
2423                         if (type != '-') {
2424                             switch (type) {
2425                             case 'm':
2426                                 if (minFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
2427                                 minFrac = Integer.parseInt(token);
2428                                 break;
2429                             case 'M':
2430                                 if (maxFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
2431                                 maxFrac = Integer.parseInt(token);
2432                                 break;
2433                             default:
2434                                 throw new ParseException("Invalid formatting string", template, fmt);
2435                             }
2436                             type = '-';
2437                         } else if (token.equals("m")) {
2438                             type = 'm';
2439                         } else if (token.equals("M")) {
2440                             type = 'M';
2441                         } else {
2442                             throw new ParseException();
2443                         }
2444                 } catch (ParseException e) {
2445                         throw new ParseException("Invalid format specifier " + fmt.image, template, fmt);
2446                 } catch (NumberFormatException e) {
2447                         throw new ParseException("Invalid number in the format specifier " + fmt.image, template, fmt);
2448                 }
2449             }
2450
2451             if (maxFrac == -1) {
2452                     if (minFrac == -1) {
2453                         throw new ParseException(
2454                                         "Invalid format specification, at least one of m and M must be specified!", template, fmt);
2455                     }
2456                 maxFrac = minFrac;
2457             } else if (minFrac == -1) {
2458                 minFrac = 0;
2459             }
2460             if (minFrac > maxFrac) {
2461                 throw new ParseException(
2462                                 "Invalid format specification, min cannot be greater than max!", template, fmt);
2463             }
2464             if (minFrac > 50 || maxFrac > 50) {// sanity check
2465                 throw new ParseException("Cannot specify more than 50 fraction digits", template, fmt);
2466             }
2467             result = new NumericalOutput(exp, minFrac, maxFrac, autoEscOF);
2468         } else {  // if format != null
2469             result = new NumericalOutput(exp, autoEscOF);
2470         }
2471         result.setLocation(template, begin, end);
2472         return result;
2473     }
2474 }
2475
2476 TemplateElement If() :
2477 {
2478     Token start, end, t;
2479     Expression condition;
2480     TemplateElements children;
2481     IfBlock ifBlock;
2482     ConditionalBlock cblock;
2483 }
2484 {
2485     start = <IF>
2486     condition = Expression()
2487     end = <DIRECTIVE_END>
2488     children = MixedContentElements()
2489     {
2490         cblock = new ConditionalBlock(condition, children, ConditionalBlock.TYPE_IF);
2491         cblock.setLocation(template, start, end, children);
2492         ifBlock = new IfBlock(cblock);
2493     }
2494     (
2495         t = <ELSE_IF>
2496         condition = Expression()
2497         end = LooseDirectiveEnd()
2498         children = MixedContentElements()
2499         {
2500             cblock = new ConditionalBlock(condition, children, ConditionalBlock.TYPE_ELSE_IF);
2501             cblock.setLocation(template, t, end, children);
2502             ifBlock.addBlock(cblock);
2503         }
2504     )*
2505     [
2506             t = <ELSE>
2507             children = MixedContentElements()
2508             {
2509                 cblock = new ConditionalBlock(null, children, ConditionalBlock.TYPE_ELSE);
2510                 cblock.setLocation(template, t, t, children);
2511                 ifBlock.addBlock(cblock);
2512             }
2513     ]
2514     end = <END_IF>
2515     {
2516         ifBlock.setLocation(template, start, end);
2517         return ifBlock;
2518     }
2519 }
2520
2521 AttemptBlock Attempt() :
2522 {
2523     Token start, end;
2524     TemplateElements children;
2525     RecoveryBlock recoveryBlock;
2526 }
2527 {
2528     start = <ATTEMPT>
2529     children = MixedContentElements()
2530     recoveryBlock = Recover()
2531     (
2532         end = <END_RECOVER>
2533         |
2534         end = <END_ATTEMPT>
2535     )
2536     {
2537         AttemptBlock result = new AttemptBlock(children, recoveryBlock);
2538         result.setLocation(template, start, end);
2539         return result;
2540     }
2541 }
2542
2543 RecoveryBlock Recover() : 
2544 {
2545     Token start;
2546     TemplateElements children;
2547 }
2548 {
2549     start = <RECOVER>
2550     children = MixedContentElements()
2551     {
2552         RecoveryBlock result = new RecoveryBlock(children);
2553         result.setLocation(template, start, start, children);
2554         return result;
2555     }
2556 }
2557
2558 TemplateElement List() :
2559 {
2560     Expression exp;
2561     Token loopVar = null, loopVar2 = null, start, end;
2562     TemplateElements childrendBeforeElse;
2563     ElseOfList elseOfList = null;
2564     ParserIteratorBlockContext iterCtx;
2565 }
2566 {
2567     start = <LIST>
2568     exp = Expression()
2569     [
2570         <AS>
2571         loopVar = <ID>
2572         [
2573             <COMMA>
2574             loopVar2 = <ID>
2575         ]
2576     ]
2577     <DIRECTIVE_END>
2578     {
2579         iterCtx = pushIteratorBlockContext();
2580         if (loopVar != null) {
2581             iterCtx.loopVarName = loopVar.image;
2582             breakableDirectiveNesting++;
2583             if (loopVar2 != null) {
2584                 iterCtx.loopVar2Name = loopVar2.image;
2585                 iterCtx.hashListing = true;
2586                 if (iterCtx.loopVar2Name.equals(iterCtx.loopVarName)) {
2587                     throw new ParseException(
2588                             "The key and value loop variable names must differ, but both were: " + iterCtx.loopVarName,
2589                             template, start);
2590                 }
2591             }
2592         }
2593     }
2594     
2595     childrendBeforeElse = MixedContentElements()
2596     {
2597         if (loopVar != null) {
2598             breakableDirectiveNesting--;
2599         } else if (iterCtx.kind != ITERATOR_BLOCK_KIND_ITEMS) {
2600             throw new ParseException(
2601                     "#list must have either \"as loopVar\" parameter or nested #items that belongs to it.",
2602                     template, start);
2603         }
2604         popIteratorBlockContext();
2605     }
2606     
2607     [
2608         elseOfList = ElseOfList()
2609     ]
2610     
2611     end = <END_LIST>
2612     {
2613         IteratorBlock list = new IteratorBlock(
2614                 exp,
2615                 loopVar != null ? loopVar.image : null,  // null when we have a nested #items
2616                 loopVar2 != null ? loopVar2.image : null,
2617                 childrendBeforeElse, iterCtx.hashListing, false);
2618         list.setLocation(template, start, end);
2619
2620         TemplateElement result;
2621         if (elseOfList == null) {
2622             result = list;
2623         } else {
2624             result = new ListElseContainer(list, elseOfList);
2625             result.setLocation(template, start, end);
2626         }
2627         return result;
2628     }
2629 }
2630
2631 ElseOfList ElseOfList() :
2632 {
2633     Token start;
2634     TemplateElements children;
2635 }
2636 {
2637         start = <ELSE>
2638         children = MixedContentElements()
2639         {
2640             ElseOfList result = new ElseOfList(children);
2641                 result.setLocation(template, start, start, children);
2642                 return result;
2643         }
2644 }
2645
2646 IteratorBlock ForEach() :
2647 {
2648     Expression exp;
2649     Token loopVar, start, end;
2650     TemplateElements children;
2651 }
2652 {
2653     start = <FOREACH>
2654     loopVar = <ID>
2655     <IN>
2656     exp = Expression()
2657     <DIRECTIVE_END>
2658     {
2659         ParserIteratorBlockContext iterCtx = pushIteratorBlockContext();
2660         iterCtx.loopVarName = loopVar.image;
2661         iterCtx.kind = ITERATOR_BLOCK_KIND_FOREACH;
2662         breakableDirectiveNesting++;
2663     }
2664     
2665     children = MixedContentElements()
2666     
2667     end = <END_FOREACH>
2668     {
2669         breakableDirectiveNesting--;
2670         popIteratorBlockContext();
2671                 
2672         IteratorBlock result = new IteratorBlock(exp, loopVar.image, null, children, false, true);
2673         result.setLocation(template, start, end);
2674         return result;
2675     }
2676 }
2677
2678 Items Items() :
2679 {
2680     Token loopVar, loopVar2 = null, start, end;
2681     TemplateElements children;
2682     ParserIteratorBlockContext iterCtx;
2683 }
2684 {
2685     start = <ITEMS>
2686     loopVar = <ID>
2687     [
2688         <COMMA>
2689         loopVar2 = <ID>
2690     ]
2691     <DIRECTIVE_END>
2692     {
2693         iterCtx = peekIteratorBlockContext();
2694         if (iterCtx == null) {
2695             throw new ParseException("#items must be inside a #list block.", template, start);
2696         }
2697         if (iterCtx.loopVarName != null) {
2698             String msg;
2699                 if (iterCtx.kind == ITERATOR_BLOCK_KIND_FOREACH) {
2700                     msg = forEachDirectiveSymbol() + " doesn't support nested #items.";
2701                 } else if (iterCtx.kind == ITERATOR_BLOCK_KIND_ITEMS) {
2702                 msg = "Can't nest #items into each other when they belong to the same #list.";
2703                 } else {
2704                     msg = "The parent #list of the #items must not have \"as loopVar\" parameter.";
2705             }
2706             throw new ParseException(msg, template, start);
2707         }
2708         iterCtx.kind = ITERATOR_BLOCK_KIND_ITEMS;
2709         iterCtx.loopVarName = loopVar.image;
2710         if (loopVar2 != null) {
2711             iterCtx.loopVar2Name = loopVar2.image;
2712             iterCtx.hashListing = true;
2713             if (iterCtx.loopVar2Name.equals(iterCtx.loopVarName)) {
2714                 throw new ParseException(
2715                         "The key and value loop variable names must differ, but both were: " + iterCtx.loopVarName,
2716                         template, start);
2717             }
2718         }
2719     
2720         breakableDirectiveNesting++;
2721     }
2722     
2723     children = MixedContentElements()
2724     
2725     end = <END_ITEMS>
2726     {
2727         breakableDirectiveNesting--;
2728         iterCtx.loopVarName = null;
2729         iterCtx.loopVar2Name = null;
2730         
2731         Items result = new Items(loopVar.image, loopVar2 != null ? loopVar2.image : null, children);
2732         result.setLocation(template, start, end);
2733         return result;
2734     }
2735 }
2736
2737 Sep Sep() :
2738 {
2739     Token loopVar, start, end = null;
2740     TemplateElements children;
2741 }
2742 {
2743     start = <SEP>
2744     {
2745         if (peekIteratorBlockContext() == null) {
2746             throw new ParseException(
2747                     "#sep must be inside a #list (or " + forEachDirectiveSymbol() + ") block.",
2748                     template, start);
2749         }
2750     }
2751     children = MixedContentElements()
2752     [
2753         LOOKAHEAD(1)
2754         end = <END_SEP>
2755     ]
2756     {
2757         Sep result = new Sep(children);
2758         if (end != null) {
2759             result.setLocation(template, start, end);
2760         } else {
2761             result.setLocation(template, start, start, children);
2762         }
2763         return result;
2764     }
2765 }
2766
2767 VisitNode Visit() :
2768 {
2769     Token start, end;
2770     Expression targetNode, namespaces = null;
2771 }
2772 {
2773     start = <VISIT>
2774     targetNode = Expression()
2775     [
2776         <USING>
2777         namespaces = Expression()
2778     ]
2779     end = LooseDirectiveEnd()
2780     {
2781         VisitNode result = new VisitNode(targetNode, namespaces);
2782         result.setLocation(template, start, end);
2783         return result;
2784     }
2785 }
2786
2787 RecurseNode Recurse() :
2788 {
2789     Token start, end = null;
2790     Expression node = null, namespaces = null;
2791 }
2792 {
2793     (
2794         start = <SIMPLE_RECURSE>
2795         |
2796         (
2797             start = <RECURSE>
2798             [
2799                 node = Expression()
2800             ]
2801             [
2802                 <USING>
2803                 namespaces = Expression()
2804             ]
2805             end = LooseDirectiveEnd()
2806         )
2807     )
2808     {
2809         if (end == null) end = start;
2810         RecurseNode result = new RecurseNode(node, namespaces);
2811         result.setLocation(template, start, end);
2812         return result;
2813     }
2814 }
2815
2816 FallbackInstruction FallBack() :
2817 {
2818     Token tok;
2819 }
2820 {
2821     tok = <FALLBACK>
2822     {
2823         if (!inMacro) {
2824             throw new ParseException("Cannot fall back outside a macro.", template, tok);
2825         }
2826         FallbackInstruction result = new FallbackInstruction();
2827         result.setLocation(template, tok, tok);
2828         return result;
2829     }
2830 }
2831
2832 /**
2833  * Production used to break out of a loop or a switch block.
2834  */
2835 BreakInstruction Break() :
2836 {
2837     Token start;
2838 }
2839 {
2840     start = <BREAK>
2841     {
2842         if (breakableDirectiveNesting < 1) {
2843             throw new ParseException(start.image + " must be nested inside a directive that supports it: " 
2844                     + " #list with \"as\", #items, #switch (or the deprecated " + forEachDirectiveSymbol() + ")",
2845                     template, start);
2846         }
2847         BreakInstruction result = new BreakInstruction();
2848         result.setLocation(template, start, start);
2849         return result;
2850     }
2851 }
2852
2853 /**
2854  * Production used to jump out of a macro.
2855  * The stop instruction terminates the rendering of the template.
2856  */
2857 ReturnInstruction Return() :
2858 {
2859     Token start, end = null;
2860     Expression exp = null;
2861 }
2862 {
2863     (
2864         start = <SIMPLE_RETURN> { end = start; }
2865         |
2866         start = <RETURN> exp = Expression() end = LooseDirectiveEnd()
2867     )
2868     {
2869         if (inMacro) {
2870             if (exp != null) {
2871                 throw new ParseException("A macro cannot return a value", template, start);
2872             }
2873         } else if (inFunction) {
2874             if (exp == null) {
2875                 throw new ParseException("A function must return a value", template, start);
2876             }
2877         } else {
2878             if (exp == null) {
2879                 throw new ParseException(
2880                                 "A return instruction can only occur inside a macro or function", template, start);
2881             }
2882         }
2883         ReturnInstruction result = new ReturnInstruction(exp);
2884         result.setLocation(template, start, end);
2885         return result;
2886     }
2887 }
2888
2889 StopInstruction Stop() :
2890 {
2891     Token start = null;
2892     Expression exp = null;
2893 }
2894 {
2895     (
2896         start = <HALT>
2897         |
2898         start = <STOP> exp = Expression() LooseDirectiveEnd()
2899     )
2900     {
2901         StopInstruction result = new StopInstruction(exp);
2902         result.setLocation(template, start, start);
2903         return result;
2904     }
2905 }
2906
2907 TemplateElement Nested() :
2908 {
2909     Token t, end;
2910     ArrayList bodyParameters;
2911     BodyInstruction result = null;
2912 }
2913 {
2914     (
2915         (
2916             t = <SIMPLE_NESTED>
2917             {
2918                 result = new BodyInstruction(null);
2919                 result.setLocation(template, t, t);
2920             }
2921         )
2922         |
2923         (
2924             t = <NESTED>
2925             bodyParameters = PositionalArgs()
2926             end = LooseDirectiveEnd()
2927             {
2928                 result = new BodyInstruction(bodyParameters);
2929                 result.setLocation(template, t, end);
2930             }
2931         )
2932     )
2933     {
2934         if (!inMacro) {
2935             throw new ParseException("Cannot use a " + t.image + " instruction outside a macro.", template, t);
2936         }
2937         return result;
2938     }
2939 }
2940
2941 TemplateElement Flush() :
2942 {
2943     Token t;
2944 }
2945 {
2946     t = <FLUSH>
2947     {
2948         FlushInstruction result = new FlushInstruction();
2949         result.setLocation(template, t, t);
2950         return result;
2951     }
2952 }
2953
2954 TemplateElement Trim() :
2955 {
2956     Token t;
2957     TrimInstruction result = null;
2958 }
2959 {
2960     (
2961         t = <TRIM> { result = new TrimInstruction(true, true); }
2962         |
2963         t = <LTRIM> { result = new TrimInstruction(true, false); }
2964         |
2965         t = <RTRIM> { result = new TrimInstruction(false, true); }
2966         |
2967         t = <NOTRIM> { result = new TrimInstruction(false, false); }
2968     )
2969     {
2970         result.setLocation(template, t, t);
2971         return result;
2972     }
2973 }
2974
2975
2976 TemplateElement Assign() :
2977 {
2978     Token start, end;
2979     int scope;
2980     Token id = null;
2981     Token equalsOp;
2982     Expression nameExp, exp, nsExp = null;
2983     String varName;
2984     ArrayList assignments = new ArrayList();
2985     Assignment ass;
2986     TemplateElements children;
2987 }
2988 {
2989     (
2990         start = <ASSIGN> { scope = Assignment.NAMESPACE; }
2991         |
2992         start = <GLOBALASSIGN> { scope = Assignment.GLOBAL; }
2993         |
2994         start = <LOCALASSIGN> { scope = Assignment.LOCAL; }
2995         {
2996             scope = Assignment.LOCAL;
2997             if (!inMacro && !inFunction) {
2998                 throw new ParseException("Local variable assigned outside a macro.", template, start);
2999             }
3000         }
3001     )
3002     nameExp = IdentifierOrStringLiteral()
3003     {
3004         varName = (nameExp instanceof StringLiteral)
3005                 ? ((StringLiteral) nameExp).getAsString()
3006                 : ((Identifier) nameExp).getName();
3007     }
3008     (
3009         (
3010             (
3011                     (
3012                                 (<EQUALS>|<PLUS_EQUALS>|<MINUS_EQUALS>|<TIMES_EQUALS>|<DIV_EQUALS>|<MOD_EQUALS>)
3013                                 {
3014                                    equalsOp = token;
3015                                 }
3016                                 exp = Expression()
3017                         )
3018                         |
3019                         (
3020                         (<PLUS_PLUS>|<MINUS_MINUS>)
3021                         {
3022                            equalsOp = token;
3023                            exp = null;
3024                         }
3025                         )
3026                 )
3027                 {
3028                     ass = new Assignment(varName, equalsOp.kind, exp, scope);
3029                 if (exp != null) {
3030                    ass.setLocation(template, nameExp, exp);
3031                 } else {
3032                    ass.setLocation(template, nameExp, equalsOp);
3033                 }
3034                     assignments.add(ass);
3035                 }
3036                 (
3037                     LOOKAHEAD(
3038                        [<COMMA>]
3039                        (<ID>|<STRING_LITERAL>)
3040                        (<EQUALS>|<PLUS_EQUALS>|<MINUS_EQUALS>|<TIMES_EQUALS>|<DIV_EQUALS>|<MOD_EQUALS>
3041                                |<PLUS_PLUS>|<MINUS_MINUS>)
3042                     )
3043                     [<COMMA>]
3044                     nameExp = IdentifierOrStringLiteral()
3045                     {
3046                         varName = (nameExp instanceof StringLiteral)
3047                                         ? ((StringLiteral) nameExp).getAsString()
3048                                         : ((Identifier) nameExp).getName();
3049                     }
3050                     (
3051                         (
3052                             (<EQUALS>|<PLUS_EQUALS>|<MINUS_EQUALS>|<TIMES_EQUALS>|<DIV_EQUALS>|<MOD_EQUALS>)
3053                             {
3054                                equalsOp = token;
3055                             }
3056                             exp = Expression()
3057                         )
3058                         |
3059                         (
3060                             (<PLUS_PLUS>|<MINUS_MINUS>)
3061                             {
3062                                equalsOp = token;
3063                                exp = null;
3064                             }
3065                         )
3066                     )
3067                     {
3068                         ass = new Assignment(varName, equalsOp.kind, exp, scope);
3069                         if (exp != null) {
3070                            ass.setLocation(template, nameExp, exp);
3071                         } else {
3072                        ass.setLocation(template, nameExp, equalsOp);
3073                         }
3074                         assignments.add(ass);
3075                     } 
3076                 )*
3077                 [
3078                     id = <IN>
3079                     nsExp = Expression()
3080                     {
3081                         if (scope != Assignment.NAMESPACE) {
3082                                 throw new ParseException("Cannot assign to namespace here.", template, id);
3083                         }
3084                     }
3085                 ]
3086                 end = LooseDirectiveEnd()
3087                 {
3088                 if (assignments.size() == 1) {
3089                     Assignment a = (Assignment) assignments.get(0);
3090                     a.setNamespaceExp(nsExp);
3091                     a.setLocation(template, start, end);
3092                     return a;
3093                 } else {
3094                             AssignmentInstruction ai = new AssignmentInstruction(scope);
3095                             for (int i = 0; i< assignments.size(); i++) {
3096                                 ai.addAssignment((Assignment) assignments.get(i));
3097                             }
3098                             ai.setNamespaceExp(nsExp);
3099                             ai.setLocation(template, start, end);
3100                             return ai;
3101                     }
3102                 }
3103             )
3104             |
3105             (
3106                 [
3107                     id = <IN>
3108                     nsExp = Expression()
3109                     {
3110                         if (scope != Assignment.NAMESPACE) {
3111                                 throw new ParseException("Cannot assign to namespace here.", template, id);
3112                         }
3113                     }
3114                 ]
3115                 <DIRECTIVE_END>
3116                 children = MixedContentElements()
3117                 (
3118                     end = <END_LOCAL>
3119                     {
3120                         if (scope != Assignment.LOCAL) {
3121                                 throw new ParseException("Mismatched assignment tags.", template, end);
3122                                 }
3123                         }
3124                     |
3125                     end = <END_ASSIGN>
3126                     {
3127                         if (scope != Assignment.NAMESPACE) {
3128                                 throw new ParseException("Mismatched assignment tags.", template, end);
3129                                 }
3130                         }
3131                     |
3132                     end = <END_GLOBAL>
3133                     {
3134                         if (scope != Assignment.GLOBAL) throw new ParseException(
3135                                         "Mismatched assignment tags", template, end);
3136                 }
3137                 )
3138                 {
3139                     BlockAssignment ba = new BlockAssignment(
3140                            children, varName, scope, nsExp,
3141                            getMarkupOutputFormat());
3142                     ba.setLocation(template, start, end);
3143                     return ba;
3144                 }
3145             )
3146     )
3147 }
3148
3149 Include Include() :
3150 {
3151     Expression nameExp;
3152     Token att, start, end;
3153     Expression exp, parseExp = null, encodingExp = null, ignoreMissingExp = null;
3154 }
3155 {
3156     start = <_INCLUDE>
3157     nameExp = Expression()
3158     [<SEMICOLON>]
3159     (
3160         att = <ID>
3161         <EQUALS>
3162         exp = Expression()
3163         {
3164             String attString = att.image;
3165             if (attString.equalsIgnoreCase("parse")) {
3166                     parseExp = exp;
3167             } else if (attString.equalsIgnoreCase("encoding")) {
3168                 encodingExp = exp;
3169             } else if (attString.equalsIgnoreCase("ignore_missing") || attString.equals("ignoreMissing")) {
3170                 token_source.checkNamingConvention(att);
3171                 ignoreMissingExp = exp;
3172             } else {
3173                 String correctedName = attString.equals("ignoreMissing") ? "ignore_missing" : null;
3174                 throw new ParseException(
3175                                 "Unsupported named #include parameter: \"" + attString + "\". Supported parameters are: "
3176                                 + "\"parse\", \"encoding\", \"ignore_missing\"."
3177                                 + (correctedName == null
3178                                       ? ""
3179                                       : " Supporting camelCase parameter names is planned for FreeMarker 2.4.0; "
3180                                       + "check if an update is available, and if it indeed supports camel "
3181                                       + "case."),
3182                                 template, att);
3183             }
3184         }
3185     )*
3186     end = LooseDirectiveEnd()
3187     {
3188         Include result = new Include(template, nameExp, encodingExp, parseExp, ignoreMissingExp);
3189         result.setLocation(template, start, end);
3190         return result;
3191     }
3192 }
3193
3194 LibraryLoad Import() :
3195 {
3196     Token start, end, ns;
3197     Expression nameExp;
3198 }
3199 {
3200     start = <IMPORT>
3201     nameExp = Expression()
3202     <AS>
3203     ns = <ID>
3204     end = LooseDirectiveEnd()
3205     {
3206         LibraryLoad result = new LibraryLoad(template, nameExp, ns.image);
3207         result.setLocation(template, start, end);
3208         template.addImport(result);
3209         return result;
3210     }
3211 }
3212
3213 Macro Macro() :
3214 {
3215     Token arg, start, end;
3216     Expression nameExp;
3217     String name;
3218     ArrayList argNames = new ArrayList();
3219     HashMap args = new HashMap();
3220     ArrayList defNames = new ArrayList();
3221     Expression defValue = null;
3222     List lastIteratorBlockContexts;
3223     int lastBreakableDirectiveNesting;
3224     TemplateElements children;
3225     boolean isFunction = false, hasDefaults = false;
3226     boolean isCatchAll = false;
3227     String catchAll = null;
3228 }
3229 {
3230     (
3231         start = <MACRO>
3232         |
3233         start = <FUNCTION> { isFunction = true; }
3234     )
3235     {
3236         if (inMacro || inFunction) {
3237             throw new ParseException("Macro or function definitions can't be nested into each other.", template, start);
3238         }
3239         if (isFunction) inFunction = true; else inMacro = true;
3240     }
3241     nameExp = IdentifierOrStringLiteral()
3242     {
3243         name = (nameExp instanceof StringLiteral)
3244                 ? ((StringLiteral) nameExp).getAsString()
3245                 : ((Identifier) nameExp).getName();
3246     }
3247     [<OPEN_PAREN>]
3248     (
3249         arg = <ID> { defValue = null; }
3250         [
3251             <ELLIPSIS> { isCatchAll = true; }
3252         ]
3253         [
3254             <EQUALS>
3255             defValue = Expression()
3256             {
3257                 defNames.add(arg.image);
3258                 hasDefaults = true;
3259             }
3260         ]
3261         [<COMMA>]
3262         {
3263             if (catchAll != null) {
3264                 throw new ParseException(
3265                 "There may only be one \"catch-all\" parameter in a macro declaration, and it must be the last parameter.",
3266                 template, arg);
3267             }
3268             if (isCatchAll) {
3269                 if (defValue != null) {
3270                     throw new ParseException(
3271                     "\"Catch-all\" macro parameter may not have a default value.",
3272                     template, arg);
3273                 }
3274                 catchAll = arg.image;
3275             } else {
3276                 argNames.add(arg.image);
3277                 if (hasDefaults && defValue == null) {
3278                     throw new ParseException(
3279                                     "In a macro declaration, parameters without a default value "
3280                                     + "must all occur before the parameters with default values.",
3281                     template, arg);
3282                 }
3283                 args.put(arg.image, defValue);
3284             }
3285         }
3286     )*
3287     [<CLOSE_PAREN>]
3288     <DIRECTIVE_END>
3289     {
3290         // To prevent parser check loopholes like <#list ...><#macro ...><#break></#macro></#list>.
3291         lastIteratorBlockContexts = iteratorBlockContexts;
3292         iteratorBlockContexts = null;
3293         if (incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_23) {
3294                 lastBreakableDirectiveNesting = breakableDirectiveNesting;
3295                 breakableDirectiveNesting = 0; 
3296         } else {
3297             lastBreakableDirectiveNesting = 0; // Just to prevent uninitialized local variable error later
3298         }
3299     }
3300     children = MixedContentElements()
3301     (
3302         end = <END_MACRO>
3303         {
3304                 if (isFunction) throw new ParseException("Expected function end tag here.", template, end);
3305         }
3306         |
3307         end = <END_FUNCTION>
3308         {
3309                 if (!isFunction) throw new ParseException("Expected macro end tag here.", template, end);
3310         }
3311     )
3312     {
3313         iteratorBlockContexts = lastIteratorBlockContexts;
3314         if (incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_23) {
3315             breakableDirectiveNesting = lastBreakableDirectiveNesting;
3316         }
3317         
3318         inMacro = inFunction = false;
3319         Macro result = new Macro(name, argNames, args, catchAll, isFunction, children);
3320         result.setLocation(template, start, end);
3321         template.addMacro(result);
3322         return result;
3323     }
3324 }
3325
3326 CompressedBlock Compress() :
3327 {
3328     TemplateElements children;
3329     Token start, end;
3330 }
3331 {
3332     start = <COMPRESS>
3333     children = MixedContentElements()
3334     end = <END_COMPRESS>
3335     {
3336         CompressedBlock cb = new CompressedBlock(children);
3337         cb.setLocation(template, start, end);
3338         return cb;
3339     }
3340 }
3341
3342 TemplateElement UnifiedMacroTransform() :
3343 {
3344     Token start = null, end, t;
3345     HashMap namedArgs = null;
3346     ArrayList positionalArgs = null, bodyParameters = null;
3347     Expression startTagNameExp;
3348     TemplateElements children;
3349     Expression exp;
3350     int pushedCtxCount = 0;
3351 }
3352 {
3353     start = <UNIFIED_CALL>
3354     exp = Expression()
3355     {
3356         if (exp instanceof Identifier || (exp instanceof Dot && ((Dot) exp).onlyHasIdentifiers())) {
3357             startTagNameExp = exp;
3358         } else {
3359             startTagNameExp = null;
3360         }
3361     }
3362     [<TERMINATING_WHITESPACE>]
3363     (
3364         LOOKAHEAD(<ID><EQUALS>)
3365         namedArgs = NamedArgs()
3366         |
3367         positionalArgs = PositionalArgs()
3368     )
3369     [
3370         <SEMICOLON>
3371         { bodyParameters = new ArrayList(4); }
3372         [
3373             [<TERMINATING_WHITESPACE>] t = <ID> { bodyParameters.add(t.image); }
3374             (
3375                 [<TERMINATING_WHITESPACE>] <COMMA>
3376                 [<TERMINATING_WHITESPACE>] t = <ID> {bodyParameters.add(t.image); }
3377             )*
3378         ]
3379     ]
3380     (
3381         end = <EMPTY_DIRECTIVE_END> { children = TemplateElements.EMPTY; }
3382         |
3383         (
3384             <DIRECTIVE_END> {
3385                 if (bodyParameters != null && iteratorBlockContexts != null && !iteratorBlockContexts.isEmpty()) {
3386                     // It's possible that we shadow a #list/#items loop variable, in which case that must be noted.
3387                     int ctxsLen = iteratorBlockContexts.size();
3388                     int bodyParsLen = bodyParameters.size();
3389                         for (int bodyParIdx = 0; bodyParIdx < bodyParsLen; bodyParIdx++) {
3390                         String bodyParName = (String) bodyParameters.get(bodyParIdx);
3391                         walkCtxSack: for (int ctxIdx = ctxsLen - 1; ctxIdx >= 0; ctxIdx--) {
3392                             ParserIteratorBlockContext ctx
3393                                     = (ParserIteratorBlockContext) iteratorBlockContexts.get(ctxIdx);
3394                             if (ctx.loopVarName != null && ctx.loopVarName.equals(bodyParName)) {
3395                                 // If it wasn't already shadowed, shadow it:
3396                                 if (ctx.kind != ITERATOR_BLOCK_KIND_USER_DIRECTIVE) {
3397                                     ParserIteratorBlockContext shadowingCtx = pushIteratorBlockContext();
3398                                     shadowingCtx.loopVarName = bodyParName;
3399                                     shadowingCtx.kind = ITERATOR_BLOCK_KIND_USER_DIRECTIVE;
3400                                     pushedCtxCount++;
3401                                 }
3402                                 break walkCtxSack;
3403                             }
3404                         }
3405                    }
3406                 }
3407             }
3408             children = MixedContentElements()
3409             end = <UNIFIED_CALL_END>
3410             {
3411                 for (int i = 0; i < pushedCtxCount; i++) {
3412                     popIteratorBlockContext();
3413                 }
3414             
3415                 String endTagName = end.image.substring(3, end.image.length() - 1).trim();
3416                 if (endTagName.length() > 0) {
3417                     if (startTagNameExp == null) {
3418                         throw new ParseException("Expecting </@>", template, end);
3419                     } else {
3420                         String startTagName = startTagNameExp.getCanonicalForm();
3421                         if (!endTagName.equals(startTagName)) {
3422                             throw new ParseException("Expecting </@> or </@" + startTagName + ">", template, end);
3423                         }
3424                     }
3425                 }
3426             }
3427         )
3428     )
3429     {
3430         TemplateElement result = (positionalArgs != null)
3431                         ? new UnifiedCall(exp, positionalArgs, children, bodyParameters)
3432                     : new UnifiedCall(exp, namedArgs, children, bodyParameters);
3433         result.setLocation(template, start, end);
3434         return result;
3435     }
3436 }
3437
3438 TemplateElement Call() :
3439 {
3440     Token start, end, id;
3441     HashMap namedArgs = null;
3442     ArrayList positionalArgs = null;
3443     Identifier macroName= null;
3444 }
3445 {
3446     start = <CALL>
3447     id = <ID> {
3448         macroName = new Identifier(id.image);
3449         macroName.setLocation(template, id, id);
3450     }
3451     (
3452         LOOKAHEAD(<ID><EQUALS>)
3453         namedArgs = NamedArgs()
3454         |
3455         (
3456             [
3457             LOOKAHEAD(<OPEN_PAREN>)
3458                 <OPEN_PAREN>
3459             ]
3460             positionalArgs = PositionalArgs()
3461             [<CLOSE_PAREN>]
3462         )
3463     )
3464     end = LooseDirectiveEnd()
3465     {
3466         UnifiedCall result = null;
3467         if (positionalArgs != null) {
3468             result = new UnifiedCall(macroName, positionalArgs, TemplateElements.EMPTY, null);
3469         } else {
3470             result = new UnifiedCall(macroName, namedArgs, TemplateElements.EMPTY, null);
3471         }
3472         result.legacySyntax = true;
3473         result.setLocation(template, start, end);
3474         return result;
3475     }
3476 }
3477
3478 HashMap NamedArgs() :
3479 {
3480     HashMap result = new HashMap();
3481     Token t;
3482     Expression exp;
3483 }
3484 {
3485     (
3486         t = <ID>
3487         <EQUALS>
3488         {
3489             token_source.SwitchTo(token_source.NAMED_PARAMETER_EXPRESSION);
3490             token_source.inInvocation = true;
3491         }             
3492         exp = Expression()
3493         {
3494             result.put(t.image, exp);
3495         }
3496     )+
3497     {
3498         token_source.inInvocation = false;
3499         return result;
3500     }
3501 }
3502
3503 ArrayList PositionalArgs() :
3504 {
3505     ArrayList result = new ArrayList();
3506     Expression arg;
3507 }
3508 {
3509     [
3510         arg = Expression() { result.add(arg); }
3511         (
3512             [<COMMA>]
3513             arg = Expression() { result.add(arg); }
3514         )*
3515     ]
3516     {
3517         return result;
3518     }
3519 }
3520
3521
3522 Comment Comment() :
3523 {
3524     Token start, end;
3525     StringBuilder buf = new StringBuilder();
3526 }
3527 {
3528     (
3529         start = <COMMENT>
3530         |
3531         start = <TERSE_COMMENT>
3532     )
3533     end = UnparsedContent(start, buf)
3534     {
3535         Comment result = new Comment(buf.toString());
3536         result.setLocation(template, start, end);
3537         return result;
3538     }
3539 }
3540
3541 TextBlock NoParse() :
3542 {
3543     Token start, end;
3544     StringBuilder buf = new StringBuilder();
3545 }
3546 {
3547     start = <NOPARSE>
3548     end = UnparsedContent(start, buf)
3549     {
3550         TextBlock result = new TextBlock(buf.toString(), true);
3551         result.setLocation(template, start, end);
3552         return result;
3553     }
3554 }
3555
3556 TransformBlock Transform() :
3557 {
3558     Token start, end, argName;
3559     Expression exp, argExp;
3560     TemplateElements children = null;
3561     HashMap args = null;
3562 }
3563 {
3564     start = <TRANSFORM>
3565     exp = Expression()
3566     [<SEMICOLON>]
3567     (
3568         argName = <ID>
3569         <EQUALS>
3570         argExp = Expression()
3571         {
3572             if (args == null) args = new HashMap();
3573             args.put(argName.image, argExp);
3574         }
3575     )*
3576     (
3577         end = <EMPTY_DIRECTIVE_END>
3578         |
3579         (
3580             <DIRECTIVE_END>
3581             children = MixedContentElements()
3582             end = <END_TRANSFORM>
3583         )
3584     )
3585     {
3586         TransformBlock result = new TransformBlock(exp, args, children);
3587         result.setLocation(template, start, end);
3588         return result;
3589     }
3590 }
3591
3592 SwitchBlock Switch() :
3593 {
3594     SwitchBlock switchBlock;
3595     MixedContent ignoredSectionBeforeFirstCase = null;
3596     Case caseIns;
3597     Expression switchExp;
3598     Token start, end;
3599     boolean defaultFound = false;
3600 }
3601 {
3602     (
3603             start = <SWITCH>
3604             switchExp = Expression()
3605             <DIRECTIVE_END>
3606         [ ignoredSectionBeforeFirstCase = WhitespaceAndComments() ]
3607     )
3608     {
3609         breakableDirectiveNesting++;
3610         switchBlock = new SwitchBlock(switchExp, ignoredSectionBeforeFirstCase);
3611     }
3612     [
3613             (
3614                 caseIns = Case()
3615                 {
3616                     if (caseIns.condition == null) {
3617                         if (defaultFound) {
3618                             throw new ParseException(
3619                             "You can only have one default case in a switch statement", template, start);
3620                         }
3621                         defaultFound = true;
3622                     }
3623                     switchBlock.addCase(caseIns);
3624                 }
3625             )+
3626             [<STATIC_TEXT_WS>]
3627     ]
3628     end = <END_SWITCH>
3629     {
3630         breakableDirectiveNesting--;
3631         switchBlock.setLocation(template, start, end);
3632         return switchBlock;
3633     }
3634 }
3635
3636 Case Case() :
3637 {
3638     Expression exp;
3639     TemplateElements children;
3640     Token start;
3641 }
3642 {
3643     (
3644         start = <CASE> exp = Expression() <DIRECTIVE_END>
3645         |
3646         start = <DEFAUL> { exp = null; }
3647     )
3648     children = MixedContentElements()
3649     {
3650         Case result = new Case(exp, children);
3651         result.setLocation(template, start, start, children);
3652         return result;
3653     }
3654 }
3655
3656 EscapeBlock Escape() :
3657 {
3658     Token variable, start, end;
3659     Expression escapeExpr;
3660     TemplateElements children;
3661 }
3662 {
3663     start = <ESCAPE>
3664     {
3665         if (outputFormat instanceof MarkupOutputFormat && autoEscaping) {
3666             throw new ParseException(
3667                     "Using the \"escape\" directive (legacy escaping) is not allowed when auto-escaping is on with "
3668                     + "a markup output format (" + outputFormat.getName()
3669                     + "), to avoid confusion and double-escaping mistakes.",
3670                     template, start);
3671         }
3672     }
3673     variable = <ID>
3674     <AS>
3675     escapeExpr = Expression()
3676     <DIRECTIVE_END>
3677     {
3678         EscapeBlock result = new EscapeBlock(variable.image, escapeExpr, escapedExpression(escapeExpr));
3679         escapes.addFirst(result);
3680     }
3681     children = MixedContentElements()
3682     {
3683         result.setContent(children);
3684         escapes.removeFirst();
3685     }
3686     end = <END_ESCAPE>
3687     {
3688         result.setLocation(template, start, end);
3689         return result;
3690     }
3691 }
3692
3693 NoEscapeBlock NoEscape() :
3694 {
3695     Token start, end;
3696     TemplateElements children;
3697 }
3698 {
3699     start = <NOESCAPE>
3700     {
3701         if (escapes.isEmpty()) {
3702             throw new ParseException("#noescape with no matching #escape encountered.", template, start);
3703         }
3704         Object escape = escapes.removeFirst();
3705     }
3706     children = MixedContentElements()
3707     end = <END_NOESCAPE>
3708     {
3709         escapes.addFirst(escape);
3710         NoEscapeBlock result = new NoEscapeBlock(children);
3711         result.setLocation(template, start, end);
3712         return result;
3713     }
3714 }
3715
3716 OutputFormatBlock OutputFormat() :
3717 {
3718     Token start, end;
3719     Expression paramExp;
3720     TemplateElements children;
3721     OutputFormat lastOutputFormat;
3722 }
3723 {
3724     start = <OUTPUTFORMAT>
3725     paramExp = Expression()
3726     <DIRECTIVE_END>
3727     {
3728         if (!paramExp.isLiteral()) {
3729             throw new ParseException(
3730                     "Parameter expression must be parse-time evaluable (constant): "
3731                     + paramExp.getCanonicalForm(),
3732                     paramExp);
3733         }
3734     
3735         TemplateModel paramTM;
3736         try {
3737             paramTM = paramExp.eval(null);
3738         } catch (Exception e) {
3739             throw new ParseException(
3740                     "Could not evaluate expression (on parse-time): " + paramExp.getCanonicalForm()
3741                     + "\nUnderlying cause: " +  e,
3742                     paramExp, e);
3743         }
3744         String paramStr;
3745         if (paramTM instanceof TemplateScalarModel) {
3746             try {
3747                 paramStr = ((TemplateScalarModel) paramTM).getAsString();
3748             } catch (TemplateModelException e) {
3749                     throw new ParseException(
3750                             "Could not evaluate expression (on parse-time): " + paramExp.getCanonicalForm()
3751                             + "\nUnderlying cause: " +  e,
3752                             paramExp, e);
3753             }
3754         } else {
3755             throw new ParseException(
3756                     "Parameter must be a string, but was: " + ClassUtil.getFTLTypeDescription(paramTM),
3757                     paramExp);
3758         }
3759         
3760         lastOutputFormat = outputFormat;
3761         try { 
3762             if (paramStr.startsWith("{")) {
3763                 if (!paramStr.endsWith("}")) {
3764                     throw new ParseException("Output format name that starts with '{' must end with '}': " + paramStr,
3765                             template, start);
3766                 }
3767                 OutputFormat innerOutputFormat = template.getConfiguration().getOutputFormat(
3768                         paramStr.substring(1, paramStr.length() - 1));
3769                 if (!(innerOutputFormat instanceof MarkupOutputFormat)) {
3770                     throw new ParseException(
3771                             "The output format inside the {...} must be a markup format, but was: "
3772                             + innerOutputFormat,
3773                             template, start);
3774                 }
3775                 if (!(outputFormat instanceof MarkupOutputFormat)) {
3776                     throw new ParseException(
3777                             "The current output format must be a markup format when using {...}, but was: "
3778                             + outputFormat,
3779                             template, start);
3780                 }
3781                 outputFormat = new CombinedMarkupOutputFormat(
3782                         (MarkupOutputFormat) outputFormat, (MarkupOutputFormat) innerOutputFormat);
3783             } else {
3784                 outputFormat = template.getConfiguration().getOutputFormat(paramStr);
3785             }
3786             recalculateAutoEscapingField();
3787         } catch (IllegalArgumentException e) {
3788             throw new ParseException("Invalid format name: " + e.getMessage(), template, start, e.getCause());
3789         } catch (UnregisteredOutputFormatException e) {
3790             throw new ParseException(e.getMessage(), template, start, e.getCause());
3791         }
3792     }
3793     children = MixedContentElements()
3794     end = <END_OUTPUTFORMAT>
3795     {
3796         OutputFormatBlock result = new OutputFormatBlock(children, paramExp);
3797         result.setLocation(template, start, end);
3798         
3799         outputFormat = lastOutputFormat;
3800         recalculateAutoEscapingField();         
3801         return result;
3802     }
3803 }
3804
3805 AutoEscBlock AutoEsc() :
3806 {
3807     Token start, end;
3808     TemplateElements children;
3809     int lastAutoEscapingPolicy;
3810 }
3811 {
3812     start = <AUTOESC>
3813     {
3814         checkCurrentOutputFormatCanEscape(start);
3815         lastAutoEscapingPolicy = autoEscapingPolicy;
3816         autoEscapingPolicy = Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY;
3817         recalculateAutoEscapingField();
3818     }
3819     children = MixedContentElements()
3820     end = <END_AUTOESC>
3821     {
3822         AutoEscBlock result = new AutoEscBlock(children);
3823         result.setLocation(template, start, end);
3824         
3825         autoEscapingPolicy = lastAutoEscapingPolicy; 
3826         recalculateAutoEscapingField();
3827         return result;
3828     }
3829 }
3830
3831 NoAutoEscBlock NoAutoEsc() :
3832 {
3833     Token start, end;
3834     TemplateElements children;
3835     int lastAutoEscapingPolicy;
3836 }
3837 {
3838     start = <NOAUTOESC>
3839     {
3840         lastAutoEscapingPolicy = autoEscapingPolicy;
3841         autoEscapingPolicy = Configuration.DISABLE_AUTO_ESCAPING_POLICY;
3842         recalculateAutoEscapingField();
3843     }
3844     children = MixedContentElements()
3845     end = <END_NOAUTOESC>
3846     {
3847         NoAutoEscBlock result = new NoAutoEscBlock(children);
3848         result.setLocation(template, start, end);
3849         
3850         autoEscapingPolicy = lastAutoEscapingPolicy;
3851         recalculateAutoEscapingField(); 
3852         return result;
3853     }
3854 }
3855
3856 /**
3857  * Production to terminate potentially empty elements. Either a ">" or "/>"
3858  */
3859 Token LooseDirectiveEnd() :
3860 {
3861     Token t;
3862 }
3863 {
3864     (
3865         t = <DIRECTIVE_END>
3866         |
3867         t = <EMPTY_DIRECTIVE_END>
3868     )
3869     {
3870         return t;
3871     }
3872 }
3873
3874 PropertySetting Setting() :
3875 {
3876     Token start, end, key;
3877     Expression value;
3878 }
3879 {
3880     start = <SETTING>
3881     key = <ID>
3882     <EQUALS>
3883     value = Expression()
3884     end = LooseDirectiveEnd()
3885     {
3886         token_source.checkNamingConvention(key);
3887         PropertySetting result = new PropertySetting(key, token_source, value, template.getConfiguration());
3888         result.setLocation(template, start, end);
3889         return result;
3890     }
3891 }
3892
3893 /**
3894  * A production for FreeMarker directives.
3895  */
3896 TemplateElement FreemarkerDirective() :
3897 {
3898     TemplateElement tp;
3899 }
3900 {
3901     // Note that this doesn't include elements like "else", "recover", etc., because those indicate the end
3902     // of the MixedContentElements of "if", "attempt", etc.
3903     (
3904         tp = If()
3905         |
3906         tp = List()
3907         |
3908         tp = ForEach()
3909         |
3910         tp = Assign()
3911         |
3912         tp = Include()
3913         |
3914         tp = Import()
3915         |
3916         tp = Macro()
3917         |
3918         tp = Compress()
3919         |
3920         tp = UnifiedMacroTransform()
3921         |
3922         tp = Items()
3923         |
3924         tp = Sep()
3925         |
3926         tp = Call()
3927         |
3928         tp = Comment()
3929         |
3930         tp = NoParse()
3931         |
3932         tp = Transform()
3933         |
3934         tp = Switch()
3935         |
3936         tp = Setting()
3937         |
3938         tp = Break()
3939         |
3940         tp = Return()
3941         |
3942         tp = Stop()
3943         |
3944         tp = Flush()
3945         |
3946         tp = Trim()
3947         |
3948         tp = Nested()
3949         |
3950         tp = Escape()
3951         |
3952         tp = NoEscape()
3953         |
3954         tp = Visit()
3955         |
3956         tp = Recurse()
3957         |
3958         tp = FallBack()
3959         |
3960         tp = Attempt()
3961         |
3962         tp = OutputFormat()
3963         |
3964         tp = AutoEsc()
3965         |
3966         tp = NoAutoEsc()
3967     )
3968     {
3969         return tp;
3970     }
3971 }
3972
3973 /**
3974  * Production for a block of raw text
3975  * i.e. text that contains no
3976  * FreeMarker directives.
3977  */
3978 TextBlock PCData() :
3979 {
3980     StringBuilder buf = new StringBuilder();
3981     Token t = null, start = null, prevToken = null;
3982 }
3983 {
3984     (
3985         (
3986             t = <STATIC_TEXT_WS>
3987             |
3988             t = <STATIC_TEXT_NON_WS>
3989             |
3990             t = <STATIC_TEXT_FALSE_ALARM>
3991         )
3992         {
3993             buf.append(t.image);
3994             if (start == null) start = t;
3995             if (prevToken != null) prevToken.next = null;
3996             prevToken = t;
3997         }
3998     )+
3999     {
4000         if (stripText && mixedContentNesting == 1 && !preventStrippings) return null;
4001
4002         TextBlock result = new TextBlock(buf.toString(), false);
4003         result.setLocation(template, start, t);
4004         return result;
4005     }
4006 }
4007
4008 TextBlock WhitespaceText() :
4009 {
4010     Token t = null, start = null;
4011 }
4012 {
4013     t = <STATIC_TEXT_WS>
4014     {
4015         if (stripText && mixedContentNesting == 1 && !preventStrippings) return null;
4016
4017         TextBlock result = new TextBlock(t.image, false);
4018         result.setLocation(template, t, t);
4019         return result;
4020     }
4021 }
4022
4023 /**
4024  * Production for dealing with unparsed content,
4025  * i.e. what is inside a comment or noparse tag.
4026  * It returns the ending token. The content
4027  * of the tag is put in buf.
4028  */
4029 Token UnparsedContent(Token start, StringBuilder buf) :
4030 {
4031     Token t;
4032 }
4033 {
4034     (
4035         (t = <KEEP_GOING> | t = <MAYBE_END> | t = <TERSE_COMMENT_END> | t = <LONE_LESS_THAN_OR_DASH>)
4036         {
4037             buf.append(t.image);
4038         }
4039     )+
4040     {
4041         buf.setLength(buf.length() - t.image.length());
4042         if (!t.image.endsWith(";")
4043                 && _TemplateAPI.getTemplateLanguageVersionAsInt(template) >= _TemplateAPI.VERSION_INT_2_3_21) {
4044             throw new ParseException("Unclosed \"" + start.image + "\"", template, start);
4045         }
4046         return t;
4047     }
4048 }
4049
4050 TemplateElements MixedContentElements() :
4051 {
4052     TemplateElement[] childBuffer = null;
4053     int childCount = 0;
4054     TemplateElement elem;
4055     mixedContentNesting++;
4056 }
4057 {
4058     (
4059         LOOKAHEAD(1) // Just tells javacc that we know what we're doing.
4060         (
4061             elem = PCData()
4062             |
4063             elem = StringOutput()
4064             |
4065             elem = NumericalOutput()
4066             |
4067             elem = FreemarkerDirective()
4068         )
4069         {
4070             // Note: elem == null when it's was top-level PCData removed by stripText
4071             if (elem != null) {
4072                     childCount++;
4073                     if (childBuffer == null) {
4074                         childBuffer = new TemplateElement[16]; 
4075                     } else if (childBuffer.length < childCount) {
4076                         TemplateElement[] newChildBuffer = new TemplateElement[childCount * 2];
4077                         for (int i = 0; i < childBuffer.length; i++) {
4078                             newChildBuffer[i] = childBuffer[i];
4079                         }
4080                         childBuffer = newChildBuffer;
4081                     }
4082                     childBuffer[childCount - 1] = elem;
4083             }
4084         }
4085     )*
4086     {
4087         mixedContentNesting--;
4088         return childBuffer != null ? new TemplateElements(childBuffer, childCount) : TemplateElements.EMPTY;
4089     }
4090 }
4091
4092 /**
4093  * Not used anymore; kept for backward compatibility.
4094  *
4095  * @deprecated Use {@link #MixedContentElements} instead.
4096  */
4097 MixedContent MixedContent() :
4098 {
4099     MixedContent mixedContent = new MixedContent();
4100     TemplateElement elem, begin = null;
4101     mixedContentNesting++;
4102 }
4103 {
4104     (
4105         LOOKAHEAD(1) // Just tells javacc that we know what we're doing.
4106         (
4107             elem = PCData()
4108             |
4109             elem = StringOutput()
4110             |
4111             elem = NumericalOutput()
4112             |
4113             elem = FreemarkerDirective()
4114         )
4115         {
4116             if (begin == null) {
4117                 begin = elem;
4118             }
4119             mixedContent.addElement(elem);
4120         }
4121     )+
4122     {
4123         mixedContentNesting--;
4124         mixedContent.setLocation(template, begin, elem);
4125         return mixedContent;
4126     }
4127 }
4128
4129 /**
4130  * Not used anymore; kept for backward compatibility.
4131  *
4132  * <p>A production for a block of optional content.
4133  * Returns an empty Text block if there is no
4134  * content.
4135  *
4136  * @deprecated Use {@link #MixedContentElements} instead.
4137  */
4138 TemplateElement OptionalBlock() :
4139 {
4140     TemplateElement tp = null;
4141 }
4142 {
4143     [
4144         LOOKAHEAD(1) // has no effect but to get rid of a spurious warning.
4145         tp = MixedContent()
4146     ]
4147     {
4148         return tp != null ? tp : new TextBlock(CollectionUtils.EMPTY_CHAR_ARRAY, false);
4149     }
4150 }
4151
4152 /**
4153  * A production freemarker text that may contain
4154  * ${...} and #{...} but no directives.
4155  */
4156 TemplateElement FreeMarkerText() :
4157 {
4158     MixedContent nodes = new MixedContent();
4159     TemplateElement elem, begin = null;
4160 }
4161 {
4162     (
4163         (
4164             elem = PCData()
4165             |
4166             elem = StringOutput()
4167             |
4168             elem = NumericalOutput()
4169         )
4170         {
4171             if (begin == null) {
4172                 begin = elem;
4173             }
4174             nodes.addChild(elem);
4175         }
4176     )+
4177     {
4178         nodes.setLocation(template, begin, elem);
4179         return nodes;
4180     }
4181 }
4182
4183 /**
4184  * To be used between tags that in theory has nothing between, such between #switch and the first #case.
4185  */
4186 MixedContent WhitespaceAndComments() :
4187 {
4188     MixedContent nodes = new MixedContent();
4189     TemplateElement elem, begin = null;
4190 }
4191 {
4192     (
4193         (
4194             elem = WhitespaceText()
4195             |
4196             elem = Comment()
4197         )
4198         {
4199             if (elem != null) { // not removed by stripText
4200                     if (begin == null) {
4201                         begin = elem;
4202                     }
4203                     nodes.addChild(elem);
4204             }
4205         }
4206     )+
4207     {
4208         if (begin == null // Was is removed by stripText?
4209                 // Nodes here won't be ever executed anyway, but whitespace stripping should still remove the
4210                 // lonely TextBlock from the AST, as that's purely source code formatting. If it's not lonely, then
4211                 // there must be a comment, in which case the generic whitespace stripping algorithm will kick in.
4212                 || stripWhitespace && !preventStrippings
4213                         && nodes.getChildCount() == 1 && nodes.getChild(0) instanceof TextBlock) {
4214             return null;
4215         }
4216         nodes.setLocation(template, begin, elem);
4217         return nodes;
4218     }
4219 }
4220
4221 void HeaderElement() :
4222 {
4223     Token key;
4224     Expression exp = null;
4225     Token autoEscRequester = null;
4226 }
4227 {
4228     [<STATIC_TEXT_WS>]
4229     (
4230         <TRIVIAL_FTL_HEADER>
4231         |
4232         (
4233             <FTL_HEADER>
4234             (
4235                 key = <ID>
4236                 <EQUALS>
4237                 exp = Expression()
4238                 {
4239                     token_source.checkNamingConvention(key);
4240                 
4241                     String ks = key.image;
4242                     TemplateModel value = null;
4243                     try {
4244                         value = exp.eval(null);
4245                     } catch (Exception e) {
4246                         throw new ParseException(
4247                                         "Could not evaluate expression (on parse-time): " + exp.getCanonicalForm()
4248                                         + " \nUnderlying cause: " +  e,
4249                                 exp, e);
4250                     }
4251                     String vs = null;
4252                     if (value instanceof TemplateScalarModel) {
4253                         try {
4254                             vs = ((TemplateScalarModel) exp).getAsString();
4255                         } catch (TemplateModelException tme) {}
4256                     }
4257                     if (template != null) {
4258                         if (ks.equalsIgnoreCase("encoding")) {
4259                             if (vs == null) {
4260                                 throw new ParseException("Expected a string constant for \"" + ks + "\".", exp);
4261                             }
4262                             String encoding = template.getEncoding();
4263                             if (encoding != null && !encoding.equalsIgnoreCase(vs)) {
4264                                 throw new Template.WrongEncodingException(vs, encoding);
4265                             }
4266                         } else if (ks.equalsIgnoreCase("STRIP_WHITESPACE") || ks.equals("stripWhitespace")) {
4267                             this.stripWhitespace = getBoolean(exp, true);
4268                         } else if (ks.equalsIgnoreCase("STRIP_TEXT") || ks.equals("stripText")) {