freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [07/51] [partial] incubator-freemarker git commit: Migrated from Ant to Gradle, and modularized the project. This is an incomplete migration; there are some TODO-s in the build scripts, and release related tasks are still missing. What works: Building th
Date Sun, 14 May 2017 10:52:50 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/javacc/FTL.jj b/freemarker-core/src/main/javacc/FTL.jj
new file mode 100644
index 0000000..dc6079f
--- /dev/null
+++ b/freemarker-core/src/main/javacc/FTL.jj
@@ -0,0 +1,4132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+options
+{
+    STATIC = false;
+    UNICODE_INPUT = true;
+    // DEBUG_TOKEN_MANAGER = true;
+    // DEBUG_PARSER = true;
+}
+
+PARSER_BEGIN(FMParser)
+
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.*;
+import org.apache.freemarker.core.outputformat.*;
+import org.apache.freemarker.core.outputformat.impl.*;
+import org.apache.freemarker.core.model.*;
+import org.apache.freemarker.core.model.impl.*;
+import org.apache.freemarker.core.util.*;
+import java.io.*;
+import java.util.*;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+
+/**
+ * This class is generated by JavaCC from a grammar file.
+ */
+public class FMParser {
+
+    private static final int ITERATOR_BLOCK_KIND_LIST = 0; 
+    private static final int ITERATOR_BLOCK_KIND_ITEMS = 1;
+    private static final int ITERATOR_BLOCK_KIND_USER_DIRECTIVE = 2;
+
+    private static class ParserIteratorBlockContext {
+        /**
+         * loopVarName in <#list ... as loopVarName> or <#items as loopVarName>; null after we left the nested
+         * block of #list or #items, respectively.
+         */
+        private String loopVarName;
+        
+        /**
+         * loopVar1Name in <#list ... as k, loopVar2Name> or <#items as k, loopVar2Name>; null after we left the nested
+         * block of #list or #items, respectively.
+         */
+        private String loopVar2Name;
+        
+        /**
+         * See the ITERATOR_BLOCK_KIND_... costants.
+         */
+        private int kind;
+        
+        /**
+         * Is this a key-value pair listing? When there's a nested #items, it's only set there. 
+         */
+        private boolean hashListing;
+    }
+
+    private Template template;
+
+    private boolean stripWhitespace, stripText;
+    private int incompatibleImprovements;
+    private OutputFormat outputFormat;
+    private int autoEscapingPolicy;
+    private boolean autoEscaping;
+    private ParsingConfiguration pCfg;
+    private InputStream streamToUnmarkWhenEncEstabd;
+
+    /** Keeps track of #list nesting. */
+    private List/*<ParserIteratorBlockContext>*/ iteratorBlockContexts;
+    
+    /**
+     * Keeps track of the nesting depth of directives that support #break.
+     */
+    private int breakableDirectiveNesting;
+
+    private boolean inMacro, inFunction;
+    private LinkedList escapes = new LinkedList();
+    private int mixedContentNesting; // for stripText
+
+    FMParser(Template template, Reader reader,
+            ParsingConfiguration pCfg, OutputFormat outputFormat, Integer autoEscapingPolicy,
+            InputStream streamToUnmarkWhenEncEstabd) {
+        this(template, true, readerToTokenManager(reader, pCfg),
+                pCfg, outputFormat, autoEscapingPolicy,
+                streamToUnmarkWhenEncEstabd);
+    }
+
+    private static FMParserTokenManager readerToTokenManager(Reader reader, ParsingConfiguration pCfg) {
+        SimpleCharStream simpleCharStream = new SimpleCharStream(reader, 1, 1);
+        simpleCharStream.setTabSize(pCfg.getTabSize());
+        return new FMParserTokenManager(simpleCharStream);
+    }
+
+    FMParser(Template template, boolean newTemplate, FMParserTokenManager tkMan,
+            ParsingConfiguration pCfg, OutputFormat contextOutputFormat, Integer contextAutoEscapingPolicy,
+    		InputStream streamToUnmarkWhenEncEstabd) {
+        this(tkMan);
+
+        _NullArgumentException.check(pCfg);
+        this.pCfg = pCfg;
+        
+        this.streamToUnmarkWhenEncEstabd = streamToUnmarkWhenEncEstabd;
+
+        _NullArgumentException.check(template);
+        this.template = template;
+
+        int incompatibleImprovements = pCfg.getIncompatibleImprovements().intValue();
+        token_source.incompatibleImprovements = incompatibleImprovements;
+        this.incompatibleImprovements = incompatibleImprovements;
+
+        {
+            OutputFormat outputFormatFromExt = pCfg.getRecognizeStandardFileExtensions() ? getFormatFromStdFileExt()
+                    : null;
+            outputFormat = contextOutputFormat != null ? contextOutputFormat
+                    : outputFormatFromExt != null ? outputFormatFromExt
+                    : pCfg.getOutputFormat();
+            autoEscapingPolicy = contextAutoEscapingPolicy != null ? contextAutoEscapingPolicy
+                    : outputFormatFromExt != null ? Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY
+                    : pCfg.getAutoEscapingPolicy();
+        }
+        recalculateAutoEscapingField();
+
+        token_source.setParser(this);
+
+        int tagSyntax = pCfg.getTagSyntax();
+        switch (tagSyntax) {
+        case Configuration.AUTO_DETECT_TAG_SYNTAX:
+            token_source.autodetectTagSyntax = true;
+            break;
+        case Configuration.ANGLE_BRACKET_TAG_SYNTAX:
+            token_source.squBracTagSyntax = false;
+            break;
+        case Configuration.SQUARE_BRACKET_TAG_SYNTAX:
+            token_source.squBracTagSyntax = true;
+            break;
+        default:
+            throw new IllegalArgumentException("Illegal argument for tagSyntax: " + tagSyntax);
+        }
+
+        int namingConvention = pCfg.getNamingConvention();
+        switch (namingConvention) {
+        case Configuration.AUTO_DETECT_NAMING_CONVENTION:
+        case Configuration.CAMEL_CASE_NAMING_CONVENTION:
+        case Configuration.LEGACY_NAMING_CONVENTION:
+            token_source.initialNamingConvention = namingConvention;
+            token_source.namingConvention = namingConvention;
+            break;
+        default:
+            throw new IllegalArgumentException("Illegal argument for namingConvention: " + namingConvention);
+        }
+
+        this.stripWhitespace = pCfg.getWhitespaceStripping();
+
+        // If this is a Template under construction, we do the below.
+        // If this is just the enclosing Template for ?eval or such, we must not modify it.
+        if (newTemplate) {
+            template.setAutoEscapingPolicy(autoEscapingPolicy);
+            template.setOutputFormat(outputFormat);
+        }
+    }
+    
+    void setupStringLiteralMode(FMParserTokenManager parentTokenSource, OutputFormat outputFormat) {
+        token_source.initialNamingConvention = parentTokenSource.initialNamingConvention;
+        token_source.namingConvention = parentTokenSource.namingConvention;
+        token_source.namingConventionEstabilisher = parentTokenSource.namingConventionEstabilisher;
+        token_source.SwitchTo(NODIRECTIVE);
+        
+        this.outputFormat = outputFormat;
+        recalculateAutoEscapingField();                                
+    }
+
+    void tearDownStringLiteralMode(FMParserTokenManager parentTokenSource) {
+        parentTokenSource.namingConvention = token_source.namingConvention;
+        parentTokenSource.namingConventionEstabilisher = token_source.namingConventionEstabilisher;
+    }
+
+    private OutputFormat getFormatFromStdFileExt() {
+        String name = template.getSourceOrLookupName();
+        if (name == null) {
+            return null;
+        }
+
+        int ln = name.length();
+        if (ln < 5) return null;
+
+        char c = name.charAt(ln - 5);
+        if (c != '.') return null;
+
+        c = name.charAt(ln - 4);
+        if (c != 'f' && c != 'F') return null;
+
+        c = name.charAt(ln - 3);
+        if (c != 't' && c != 'T') return null;
+
+        c = name.charAt(ln - 2);
+        if (c != 'l' && c != 'L') return null;
+
+        c = name.charAt(ln - 1);
+        try {
+            // Note: We get the output formats by name, so that custom overrides take effect.
+            if (c == 'h' || c == 'H') {
+                return template.getConfiguration().getOutputFormat(HTMLOutputFormat.INSTANCE.getName());
+                }
+            if (c == 'x' || c == 'X') {
+                return template.getConfiguration().getOutputFormat(XMLOutputFormat.INSTANCE.getName());
+            }
+        } catch (UnregisteredOutputFormatException e) {
+            throw new BugException("Unregistered std format", e);
+        }
+        return null;
+    }
+    
+    /**
+     * Updates the {@link #autoEscaping} field based on the {@link #autoEscapingPolicy} and {@link #outputFormat} fields.
+     */
+    private void recalculateAutoEscapingField() {
+        if (outputFormat instanceof MarkupOutputFormat) {
+            if (autoEscapingPolicy == Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY) {
+                autoEscaping = ((MarkupOutputFormat) outputFormat).isAutoEscapedByDefault();
+            } else if (autoEscapingPolicy == Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY) {
+                autoEscaping = true;
+            } else if (autoEscapingPolicy == Configuration.DISABLE_AUTO_ESCAPING_POLICY) {
+                autoEscaping = false;
+            } else {
+                throw new IllegalStateException("Unhandled autoEscaping enum: " + autoEscapingPolicy);
+            }
+        } else {
+            autoEscaping = false;
+        }
+    }
+    
+    MarkupOutputFormat getMarkupOutputFormat() {
+        return outputFormat instanceof MarkupOutputFormat ? (MarkupOutputFormat) outputFormat : null;
+    }
+
+    /**
+     * Don't use it, unless you are developing FreeMarker itself.
+     */
+    public int _getLastTagSyntax() {
+        return token_source.squBracTagSyntax
+                ? Configuration.SQUARE_BRACKET_TAG_SYNTAX
+                : Configuration.ANGLE_BRACKET_TAG_SYNTAX;
+    }
+    
+    /**
+     * Don't use it, unless you are developing FreeMarker itself.
+     * The naming convention used by this template; if it couldn't be detected so far, it will be the most probable one.
+     * This could be used for formatting error messages, but not for anything serious.
+     */
+    public int _getLastNamingConvention() {
+        return token_source.namingConvention;
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a String Literal
+     */
+    private void notStringLiteral(ASTExpression exp, String expected) throws ParseException {
+        if (exp instanceof ASTExpStringLiteral) {
+            throw new ParseException(
+                    "Found string literal: " + exp + ". Expecting: " + expected,
+                    exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a Number Literal
+     */
+    private void notNumberLiteral(ASTExpression exp, String expected) throws ParseException {
+        if (exp instanceof ASTExpNumberLiteral) {
+            throw new ParseException(
+                    "Found number literal: " + exp.getCanonicalForm() + ". Expecting " + expected,
+                    exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a boolean Literal
+     */
+    private void notBooleanLiteral(ASTExpression exp, String expected) throws ParseException {
+        if (exp instanceof ASTExpBooleanLiteral) {
+            throw new ParseException("Found: " + exp.getCanonicalForm() + ". Expecting " + expected, exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a Hash Literal
+     */
+    private void notHashLiteral(ASTExpression exp, String expected) throws ParseException {
+        if (exp instanceof ASTExpHashLiteral) {
+            throw new ParseException(
+                    "Found hash literal: " + exp.getCanonicalForm() + ". Expecting " + expected,
+                    exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a List Literal
+     */
+    private void notListLiteral(ASTExpression exp, String expected)
+            throws ParseException
+    {
+        if (exp instanceof ASTExpListLiteral) {
+            throw new ParseException(
+                    "Found list literal: " + exp.getCanonicalForm() + ". Expecting " + expected,
+                    exp);
+        }
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a literal other than of the numerical type
+     */
+    private void numberLiteralOnly(ASTExpression exp) throws ParseException {
+        notStringLiteral(exp, "number");
+        notListLiteral(exp, "number");
+        notHashLiteral(exp, "number");
+        notBooleanLiteral(exp, "number");
+    }
+
+    /**
+     * Throw an exception if the expression passed in is not a string.
+     */
+    private void stringLiteralOnly(ASTExpression exp) throws ParseException {
+        notNumberLiteral(exp, "string");
+        notListLiteral(exp, "string");
+        notHashLiteral(exp, "string");
+        notBooleanLiteral(exp, "string");
+    }
+
+    /**
+     * Throw an exception if the expression passed in is a literal other than of the boolean type
+     */
+    private void booleanLiteralOnly(ASTExpression exp) throws ParseException {
+        notStringLiteral(exp, "boolean (true/false)");
+        notListLiteral(exp, "boolean (true/false)");
+        notHashLiteral(exp, "boolean (true/false)");
+        notNumberLiteral(exp, "boolean (true/false)");
+    }
+
+    private ASTExpression escapedExpression(ASTExpression exp) {
+        if (!escapes.isEmpty()) {
+            return ((ASTDirEscape) escapes.getFirst()).doEscape(exp);
+        } else {
+            return exp;
+        }
+    }
+
+    private boolean getBoolean(ASTExpression exp, boolean legacyCompat) throws ParseException {
+        TemplateModel tm = null;
+        try {
+            tm = exp.eval(null);
+        } catch (Exception e) {
+            throw new ParseException(e.getMessage()
+                    + "\nCould not evaluate expression: "
+                    + exp.getCanonicalForm(),
+                    exp,
+                    e);
+        }
+        if (tm instanceof TemplateBooleanModel) {
+            try {
+                return ((TemplateBooleanModel) tm).getAsBoolean();
+            } catch (TemplateModelException tme) {
+            }
+        }
+        if (legacyCompat && tm instanceof TemplateScalarModel) {
+            try {
+                return _StringUtil.getYesNo(((TemplateScalarModel) tm).getAsString());
+            } catch (Exception e) {
+                throw new ParseException(e.getMessage()
+                        + "\nExpecting boolean (true/false), found: " + exp.getCanonicalForm(),
+                        exp);
+            }
+        }
+        throw new ParseException("Expecting boolean (true/false) parameter", exp);
+    }
+    
+    void checkCurrentOutputFormatCanEscape(Token start) throws ParseException {
+        if (!(outputFormat instanceof MarkupOutputFormat)) {
+            throw new ParseException("The current output format can't do escaping: " + outputFormat,
+                    template, start);
+        }
+    }    
+    
+    private ParserIteratorBlockContext pushIteratorBlockContext() {
+        if (iteratorBlockContexts == null) {
+            iteratorBlockContexts = new ArrayList(4);
+        }
+        ParserIteratorBlockContext newCtx = new ParserIteratorBlockContext();
+        iteratorBlockContexts.add(newCtx);
+        return newCtx;
+    }
+    
+    private void popIteratorBlockContext() {
+        iteratorBlockContexts.remove(iteratorBlockContexts.size() - 1);
+    }
+    
+    private ParserIteratorBlockContext peekIteratorBlockContext() {
+        int size = iteratorBlockContexts != null ? iteratorBlockContexts.size() : 0;
+        return size != 0 ? (ParserIteratorBlockContext) iteratorBlockContexts.get(size - 1) : null; 
+    }
+    
+    private void checkLoopVariableBuiltInLHO(String loopVarName, ASTExpression lhoExp, Token biName)
+            throws ParseException {
+        int size = iteratorBlockContexts != null ? iteratorBlockContexts.size() : 0;
+        for (int i = size - 1; i >= 0; i--) {
+            ParserIteratorBlockContext ctx = (ParserIteratorBlockContext) iteratorBlockContexts.get(i);
+            if (loopVarName.equals(ctx.loopVarName) || loopVarName.equals(ctx.loopVar2Name)) {
+                if (ctx.kind == ITERATOR_BLOCK_KIND_USER_DIRECTIVE) {
+			        throw new ParseException(
+			                "The left hand operand of ?" + biName.image
+			                + " can't be the loop variable of an user defined directive: "
+			                +  loopVarName,
+			                lhoExp);
+                }
+                return;  // success
+            }
+        }
+        throw new ParseException(
+                "The left hand operand of ?" + biName.image + " must be a loop variable, "
+                + "but there's no loop variable in scope with this name: " + loopVarName,
+                lhoExp);
+    }
+
+}
+
+PARSER_END(FMParser)
+
+/**
+ * The lexer portion defines 5 lexical states:
+ * DEFAULT, FM_EXPRESSION, IN_PAREN, NO_PARSE, and EXPRESSION_COMMENT.
+ * The DEFAULT state is when you are parsing
+ * text but are not inside a FreeMarker expression.
+ * FM_EXPRESSION is the state you are in
+ * when the parser wants a FreeMarker expression.
+ * IN_PAREN is almost identical really. The difference
+ * is that you are in this state when you are within
+ * FreeMarker expression and also within (...).
+ * This is a necessary subtlety because the
+ * ">" and ">=" symbols can only be used
+ * within parentheses because otherwise, it would
+ * be ambiguous with the end of a directive.
+ * So, for example, you enter the FM_EXPRESSION state
+ * right after a ${ and leave it after the matching }.
+ * Or, you enter the FM_EXPRESSION state right after
+ * an "<if" and then, when you hit the matching ">"
+ * that ends the if directive,
+ * you go back to DEFAULT lexical state.
+ * If, within the FM_EXPRESSION state, you enter a
+ * parenthetical expression, you enter the IN_PAREN
+ * state.
+ * Note that whitespace is ignored in the
+ * FM_EXPRESSION and IN_PAREN states
+ * but is passed through to the parser as PCDATA in the DEFAULT state.
+ * NO_PARSE and EXPRESSION_COMMENT are extremely simple
+ * lexical states. NO_PARSE is when you are in a comment
+ * block and EXPRESSION_COMMENT is when you are in a comment
+ * that is within an FTL expression.
+ */
+TOKEN_MGR_DECLS:
+{
+
+    private static final String PLANNED_DIRECTIVE_HINT
+            = "(If you have seen this directive in use elsewhere, this was a planned directive, "
+                + "so maybe you need to upgrade FreeMarker.)";
+
+    /**
+     * The noparseTag is set when we enter a block of text that the parser more or less ignores. These are <noparse> and
+     * <#-- ... --->. This variable tells us what the closing tag should be, and when we hit that, we resume parsing.
+     * Note that with this scheme, <noparse> tags and comments cannot nest recursively.
+     */
+    String noparseTag;
+
+    /**
+     * Keeps track of how deeply nested we have the hash literals. This is necessary since we need to be able to
+     * distinguish the } used to close a hash literal and the one used to close a ${
+     */
+    private FMParser parser;
+    private int postInterpolationLexState = -1;
+    private int hashLiteralNesting;
+    private int parenthesisNesting;
+    private int bracketNesting;
+    private boolean inFTLHeader;
+    boolean squBracTagSyntax,
+            autodetectTagSyntax,
+            directiveSyntaxEstablished,
+            inInvocation;
+    int initialNamingConvention;
+    int namingConvention;
+    Token namingConventionEstabilisher;
+    int incompatibleImprovements;
+
+    void setParser(FMParser parser) {
+        this.parser = parser;
+    }
+
+    /**
+     * This method handles tag syntax ('<' VS '['), and also participates in naming convention detection.
+     * If you update this logic, take a look at the UNKNOWN_DIRECTIVE token too. 
+     */
+    private void handleTagSyntaxAndSwitch(Token tok, int tokenNamingConvention, int newLexState) {
+        final String image = tok.image;
+        
+        char firstChar = image.charAt(0);
+        if (autodetectTagSyntax && !directiveSyntaxEstablished) {
+            squBracTagSyntax = (firstChar == '[');
+        }
+        if ((firstChar == '[' && !squBracTagSyntax) || (firstChar == '<' && squBracTagSyntax)) {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+        
+        directiveSyntaxEstablished = true;
+        
+        checkNamingConvention(tok, tokenNamingConvention);
+        
+        SwitchTo(newLexState);
+    }
+
+    /**
+     * Used for tags whose name isn't affected by naming convention.
+     */
+    private void handleTagSyntaxAndSwitch(Token tok, int newLexState) {
+        handleTagSyntaxAndSwitch(tok, Configuration.AUTO_DETECT_NAMING_CONVENTION, newLexState);
+    }
+
+    void checkNamingConvention(Token tok) {
+        checkNamingConvention(tok, _StringUtil.getIdentifierNamingConvention(tok.image)); 
+    }
+    
+    void checkNamingConvention(Token tok, int tokenNamingConvention) {
+        if (tokenNamingConvention != Configuration.AUTO_DETECT_NAMING_CONVENTION) {
+	        if (namingConvention == Configuration.AUTO_DETECT_NAMING_CONVENTION) {
+	            namingConvention = tokenNamingConvention;
+	            namingConventionEstabilisher = tok;
+	        } else if (namingConvention != tokenNamingConvention) {
+                throw newNameConventionMismatchException(tok);
+	        }
+        }
+    }
+    
+    private TokenMgrError newNameConventionMismatchException(Token tok) {
+        return new TokenMgrError(
+                "Naming convention mismatch. "
+                + "Identifiers that are part of the template language (not the user specified ones) "
+                + (initialNamingConvention == Configuration.AUTO_DETECT_NAMING_CONVENTION
+                    ? "must consistently use the same naming convention within the same template. This template uses "
+                    : "must use the configured naming convention, which is the ")
+                + (namingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION
+                            ? "camel case naming convention (like: exampleName) "
+                            : (namingConvention == Configuration.LEGACY_NAMING_CONVENTION
+                                    ? "legacy naming convention (directive (tag) names are like examplename, " 
+                                      + "everything else is like example_name) "
+                                    : "??? (internal error)"
+                                    ))
+                + (namingConventionEstabilisher != null
+                        ? "estabilished by auto-detection at "
+                            + MessageUtil.formatPosition(
+                                    namingConventionEstabilisher.beginLine, namingConventionEstabilisher.beginColumn)
+                            + " by token " + _StringUtil.jQuote(namingConventionEstabilisher.image.trim())
+                        : "")
+                + ", but the problematic token, " + _StringUtil.jQuote(tok.image.trim())
+                + ", uses a different convention.",
+                TokenMgrError.LEXICAL_ERROR,
+                tok.beginLine, tok.beginColumn, tok.endLine, tok.endColumn);
+    }
+
+    /**
+     * Detects the naming convention used, both in start- and end-tag tokens.
+     *
+     * @param charIdxInName
+     *         The index of the deciding character relatively to the first letter of the name.
+     */
+    private static int getTagNamingConvention(Token tok, int charIdxInName) {
+        return _StringUtil.isUpperUSASCII(getTagNameCharAt(tok, charIdxInName))
+                ? Configuration.CAMEL_CASE_NAMING_CONVENTION : Configuration.LEGACY_NAMING_CONVENTION;
+    }
+
+    static char getTagNameCharAt(Token tok, int charIdxInName) {
+        final String image = tok.image;
+        
+        // Skip tag delimiter:
+        int idx = 0;
+        for (;;) {
+            final char c = image.charAt(idx);
+            if (c != '<' && c != '[' && c != '/' && c != '#') {
+                break;
+            }
+            idx++;
+        }
+
+        return image.charAt(idx + charIdxInName);
+    }
+
+    private void unifiedCall(Token tok) {
+        char firstChar = tok.image.charAt(0);
+        if (autodetectTagSyntax && !directiveSyntaxEstablished) {
+            squBracTagSyntax = (firstChar == '[');
+        }
+        if (squBracTagSyntax && firstChar == '<') {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+        if (!squBracTagSyntax && firstChar == '[') {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+        directiveSyntaxEstablished = true;
+        SwitchTo(NO_SPACE_EXPRESSION);
+    }
+
+    private void unifiedCallEnd(Token tok) {
+        char firstChar = tok.image.charAt(0);
+        if (squBracTagSyntax && firstChar == '<') {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+        if (!squBracTagSyntax && firstChar == '[') {
+            tok.kind = STATIC_TEXT_NON_WS;
+            return;
+        }
+    }
+
+    private void closeBracket(Token tok) {
+        if (bracketNesting > 0) {
+            --bracketNesting;
+        } else {
+            tok.kind = DIRECTIVE_END;
+            if (inFTLHeader) {
+                eatNewline();
+                inFTLHeader = false;
+            }
+            SwitchTo(DEFAULT);
+        }
+    }
+    
+    private void startInterpolation(Token tok) {
+        if (postInterpolationLexState != -1) {
+            char c = tok.image.charAt(0);
+            throw new TokenMgrError(
+                    "You can't start an interpolation (" + c + "{...}) here "
+                    + "as you are inside another interpolation.)",
+                    TokenMgrError.LEXICAL_ERROR,
+                    tok.beginLine, tok.beginColumn,
+                    tok.endLine, tok.endColumn);
+        }
+        postInterpolationLexState = curLexState;
+        SwitchTo(FM_EXPRESSION);
+    }
+
+    /**
+     * @param tok
+     *         Assumed to be an '}', or something that is the closing pair of another "mirror image" character.
+     */
+    private void endInterpolation(Token tok) {
+        if (postInterpolationLexState == -1) {
+            char c = tok.image.charAt(0);
+            throw new TokenMgrError(
+                    "You can't have an \"" + c + "\" here, as there's nothing open that it could close.",
+                    TokenMgrError.LEXICAL_ERROR,
+                    tok.beginLine, tok.beginColumn,
+                    tok.endLine, tok.endColumn);
+        }
+        SwitchTo(postInterpolationLexState);
+        postInterpolationLexState = -1;
+    }
+
+    private void eatNewline() {
+        int charsRead = 0;
+        try {
+            while (true) {
+                char c = input_stream.readChar();
+                ++charsRead;
+                if (!Character.isWhitespace(c)) {
+                    input_stream.backup(charsRead);
+                    return;
+                } else if (c == '\r') {
+                    char next = input_stream.readChar();
+                    ++charsRead;
+                    if (next != '\n') {
+                        input_stream.backup(1);
+                    }
+                    return;
+                } else if (c == '\n') {
+                    return;
+                }
+            }
+        } catch (IOException ioe) {
+            input_stream.backup(charsRead);
+        }
+    }
+
+    private void ftlHeader(Token matchedToken) {
+        if (!directiveSyntaxEstablished) {
+            squBracTagSyntax = matchedToken.image.charAt(0) == '[';
+            directiveSyntaxEstablished = true;
+            autodetectTagSyntax = false;
+        }
+        String img = matchedToken.image;
+        char firstChar = img.charAt(0);
+        char lastChar = img.charAt(img.length() - 1);
+        if ((firstChar == '[' && !squBracTagSyntax) || (firstChar == '<' && squBracTagSyntax)) {
+            matchedToken.kind = STATIC_TEXT_NON_WS;
+        }
+        if (matchedToken.kind != STATIC_TEXT_NON_WS) {
+            if (lastChar != '>' && lastChar != ']') {
+                SwitchTo(FM_EXPRESSION);
+                inFTLHeader = true;
+            } else {
+                eatNewline();
+            }
+        }
+    }
+}
+
+TOKEN:
+{
+    <#BLANK : " " | "\t" | "\n" | "\r">
+    |
+    <#START_TAG : "<#" | "[#">
+    |
+    <#END_TAG : "</#" | "[/#">
+    |
+    <#CLOSE_TAG1 : (<BLANK>)* (">" | "]")>
+    |
+    <#CLOSE_TAG2 : (<BLANK>)* ("/")? (">" | "]")>
+    |
+    /*
+     * ATTENTION: Update _CoreAPI.*_BUILT_IN_DIRECTIVE_NAMES if you add new directives!
+     */
+    <ATTEMPT : <START_TAG> "attempt" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <RECOVER : <START_TAG> "recover" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); } 
+    |
+    <IF : <START_TAG> "if" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <ELSE_IF : <START_TAG> "else" ("i" | "I") "f" <BLANK>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 4), FM_EXPRESSION);
+    }
+    |
+    <LIST : <START_TAG> "list" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <ITEMS : <START_TAG> "items" (<BLANK>)+ <AS> <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <SEP : <START_TAG> "sep" <CLOSE_TAG1>>
+    |
+    <SWITCH : <START_TAG> "switch" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <CASE : <START_TAG> "case" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <ASSIGN : <START_TAG> "assign" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <GLOBALASSIGN : <START_TAG> "global" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <LOCALASSIGN : <START_TAG> "local" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <_INCLUDE : <START_TAG> "include" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <IMPORT : <START_TAG> "import" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <FUNCTION : <START_TAG> "function" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <MACRO : <START_TAG> "macro" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <VISIT : <START_TAG> "visit" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <STOP : <START_TAG> "stop" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <RETURN : <START_TAG> "return" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <SETTING : <START_TAG> "setting" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <OUTPUTFORMAT : <START_TAG> "output" ("f"|"F") "ormat" <BLANK>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 6), FM_EXPRESSION);
+    }
+    |
+    <AUTOESC : <START_TAG> "auto" ("e"|"E") "sc" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 4), DEFAULT);
+    }
+    |
+    <NOAUTOESC : <START_TAG> "no" ("autoe"|"AutoE") "sc" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
+    }
+    |
+    <COMPRESS : <START_TAG> "compress" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <TERSE_COMMENT : ("<" | "[") "#--" > { noparseTag = "-->"; handleTagSyntaxAndSwitch(matchedToken, NO_PARSE); }
+    |
+    <NOPARSE: <START_TAG> "no" ("p" | "P") "arse" <CLOSE_TAG1>> {
+        int tagNamingConvention = getTagNamingConvention(matchedToken, 2);
+        handleTagSyntaxAndSwitch(matchedToken, tagNamingConvention, NO_PARSE);
+        noparseTag = tagNamingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION ? "noParse" : "noparse";
+    }
+    |
+    <END_IF : <END_TAG> "if" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_LIST : <END_TAG> "list" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_ITEMS : <END_TAG> "items" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_SEP : <END_TAG> "sep" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_RECOVER : <END_TAG> "recover" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_ATTEMPT : <END_TAG> "attempt" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_LOCAL : <END_TAG> "local" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_GLOBAL : <END_TAG> "global" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_ASSIGN : <END_TAG> "assign" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_FUNCTION : <END_TAG> "function" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_MACRO : <END_TAG> "macro" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_OUTPUTFORMAT : <END_TAG> "output" ("f" | "F") "ormat" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 6), DEFAULT);
+    }
+    |
+    <END_AUTOESC : <END_TAG> "auto" ("e" | "E") "sc" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 4), DEFAULT);
+    }
+    |
+    <END_NOAUTOESC : <END_TAG> "no" ("autoe"|"AutoE") "sc" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
+    }
+    |
+    <END_COMPRESS : <END_TAG> "compress" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <END_SWITCH : <END_TAG> "switch" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <ELSE : <START_TAG> "else" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <BREAK : <START_TAG> "break" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <SIMPLE_RETURN : <START_TAG> "return" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <HALT : <START_TAG> "stop" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <FLUSH : <START_TAG> "flush" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <TRIM : <START_TAG> "t" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <LTRIM : <START_TAG> "lt" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <RTRIM : <START_TAG> "rt" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <NOTRIM : <START_TAG> "nt" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <DEFAUL : <START_TAG> "default" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <SIMPLE_NESTED : <START_TAG> "nested" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <NESTED : <START_TAG> "nested" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <SIMPLE_RECURSE : <START_TAG> "recurse" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <RECURSE : <START_TAG> "recurse" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <FALLBACK : <START_TAG> "fallback" <CLOSE_TAG2>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <ESCAPE : <START_TAG> "escape" <BLANK>> { handleTagSyntaxAndSwitch(matchedToken, FM_EXPRESSION); }
+    |
+    <END_ESCAPE : <END_TAG> "escape" <CLOSE_TAG1>> { handleTagSyntaxAndSwitch(matchedToken, DEFAULT); }
+    |
+    <NOESCAPE : <START_TAG> "no" ("e" | "E") "scape" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
+    }
+    |
+    <END_NOESCAPE : <END_TAG> "no" ("e" | "E") "scape" <CLOSE_TAG1>> {
+        handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
+    }
+    |
+    <UNIFIED_CALL : "<@" | "[@" > { unifiedCall(matchedToken); }
+    |
+    <UNIFIED_CALL_END : ("<" | "[") "/@" ((<ID>) ("."<ID>)*)? <CLOSE_TAG1>> { unifiedCallEnd(matchedToken); }
+    |
+    <FTL_HEADER : ("<#ftl" | "[#ftl") <BLANK>> { ftlHeader(matchedToken); }
+    |
+    <TRIVIAL_FTL_HEADER : ("<#ftl" | "[#ftl") ("/")? (">" | "]")> { ftlHeader(matchedToken); }
+    |
+    /*
+     * ATTENTION: Update _CoreAPI.*_BUILT_IN_DIRECTIVE_NAMES if you add new directives!
+     */
+    <UNKNOWN_DIRECTIVE : ("[#" | "[/#" | "<#" | "</#") (["a"-"z", "A"-"Z", "_"])+>
+    {
+        char firstChar = matchedToken.image.charAt(0);
+
+        if (!directiveSyntaxEstablished && autodetectTagSyntax) {
+            squBracTagSyntax = (firstChar == '[');
+            directiveSyntaxEstablished = true;
+        }
+
+        if (firstChar == '<' && squBracTagSyntax) {
+            matchedToken.kind = STATIC_TEXT_NON_WS;
+        } else if (firstChar == '[' && !squBracTagSyntax) {
+            matchedToken.kind = STATIC_TEXT_NON_WS;
+        } else {
+            String dn = matchedToken.image;
+            int index = dn.indexOf('#');
+            dn = dn.substring(index + 1);
+
+            // Until the tokenizer/parser is reworked, we have this quirk where something like <#list>
+            // doesn't match any directive starter tokens, because that token requires whitespace after the
+            // name as it should be followed by parameters. For now we work this around so we don't report
+            // unknown directive:
+            if (ASTDirective.ALL_BUILT_IN_DIRECTIVE_NAMES.contains(dn)) {
+                throw new TokenMgrError(
+                        "#" + dn + " is an existing directive, but the tag is malformed. " 
+                        + " (See FreeMarker Manual / Directive Reference.)",
+                        TokenMgrError.LEXICAL_ERROR,
+                        matchedToken.beginLine, matchedToken.beginColumn + 1,
+                        matchedToken.endLine, matchedToken.endColumn);
+            }
+
+            String tip = null;
+            if (dn.equals("set") || dn.equals("var")) {
+                tip = "Use #assign or #local or #global, depending on the intented scope "
+                      + "(#assign is template-scope). " + PLANNED_DIRECTIVE_HINT;
+            } else if (dn.equals("else_if") || dn.equals("elif")) {
+            	tip = "Use #elseif.";
+            } else if (dn.equals("no_escape")) {
+            	tip = "Use #noescape instead.";
+            } else if (dn.equals("method")) {
+            	tip = "Use #function instead.";
+            } else if (dn.equals("head") || dn.equals("template") || dn.equals("fm")) {
+            	tip = "You may meant #ftl.";
+            } else if (dn.equals("try") || dn.equals("atempt")) {
+            	tip = "You may meant #attempt.";
+            } else if (dn.equals("for") || dn.equals("each") || dn.equals("iterate") || dn.equals("iterator")) {
+                tip = "You may meant #list (http://freemarker.org/docs/ref_directive_list.html).";
+            } else if (dn.equals("prefix")) {
+                tip = "You may meant #import. " + PLANNED_DIRECTIVE_HINT;
+            } else if (dn.equals("item") || dn.equals("row") || dn.equals("rows")) {
+                tip = "You may meant #items.";
+            } else if (dn.equals("separator") || dn.equals("separate") || dn.equals("separ")) {
+                tip = "You may meant #sep.";
+            } else {
+                tip = "Help (latest version): http://freemarker.org/docs/ref_directive_alphaidx.html; "
+                        + "you're using FreeMarker " + Configuration.getVersion() + ".";
+            }
+            throw new TokenMgrError(
+                    "Unknown directive: #" + dn + (tip != null ? ". " + tip : ""),
+                    TokenMgrError.LEXICAL_ERROR,
+                    matchedToken.beginLine, matchedToken.beginColumn + 1,
+                    matchedToken.endLine, matchedToken.endColumn);
+        }
+    }
+}
+
+<DEFAULT, NODIRECTIVE> TOKEN :
+{
+    <STATIC_TEXT_WS : ("\n" | "\r" | "\t" | " ")+>
+    |
+    <STATIC_TEXT_NON_WS : (~["$", "<", "#", "[", "{", "\n", "\r", "\t", " "])+>
+    |
+    <STATIC_TEXT_FALSE_ALARM : "$" | "#" | "<" | "[" | "{"> // to handle a lone dollar sign or "<" or "# or <@ with whitespace after"
+    |
+    <DOLLAR_INTERPOLATION_OPENING : "${"> { startInterpolation(matchedToken); }
+    |
+    <HASH_INTERPOLATION_OPENING : "#{"> { startInterpolation(matchedToken); }
+}
+
+<FM_EXPRESSION, IN_PAREN, NAMED_PARAMETER_EXPRESSION> SKIP :
+{
+    < ( " " | "\t" | "\n" | "\r" )+ >
+    |
+    < ("<" | "[") ("#" | "!") "--"> : EXPRESSION_COMMENT
+}
+
+<EXPRESSION_COMMENT> SKIP:
+{
+    < (~["-", ">", "]"])+ >
+    |
+    < ">">
+    |
+    < "]">
+    |
+    < "-">
+    |
+    < "-->" | "--]">
+    {
+        if (parenthesisNesting > 0) SwitchTo(IN_PAREN);
+        else if (inInvocation) SwitchTo(NAMED_PARAMETER_EXPRESSION);
+        else SwitchTo(FM_EXPRESSION);
+    }
+}
+
+<FM_EXPRESSION, IN_PAREN, NO_SPACE_EXPRESSION, NAMED_PARAMETER_EXPRESSION> TOKEN :
+{
+    <#ESCAPED_CHAR :
+        "\\"
+        (
+            ("n" | "t" | "r" | "f" | "b" | "g" | "l" | "a" | "\\" | "'" | "\"" | "$" | "{")
+            |
+            ("x" ["0"-"9", "A"-"F", "a"-"f"])
+        )
+    >
+    | 
+    <STRING_LITERAL :
+        (
+            "\""
+            ((~["\"", "\\"]) | <ESCAPED_CHAR>)*
+            "\""
+        )
+        |
+        (
+            "'"
+            ((~["'", "\\"]) | <ESCAPED_CHAR>)*
+            "'"
+        )
+    >
+    |
+    <RAW_STRING : "r" (("\"" (~["\""])* "\"") | ("'" (~["'"])* "'"))>
+    |
+    <FALSE : "false">
+    |
+    <TRUE : "true">
+    |
+    <INTEGER : (["0"-"9"])+>
+    |
+    <DECIMAL : <INTEGER> "." <INTEGER>>
+    |
+    <DOT : ".">
+    |
+    <DOT_DOT : "..">
+    |
+    <DOT_DOT_LESS : "..<" | "..!" >
+    |
+    <DOT_DOT_ASTERISK : "..*" >
+    |
+    <BUILT_IN : "?">
+    |
+    <EXISTS : "??">
+    |
+    <EQUALS : "=">
+    |
+    <DOUBLE_EQUALS : "==">
+    |
+    <NOT_EQUALS : "!=">
+    |
+    <PLUS_EQUALS : "+=">
+    |
+    <MINUS_EQUALS : "-=">
+    |
+    <TIMES_EQUALS : "*=">
+    |
+    <DIV_EQUALS : "/=">
+    |
+    <MOD_EQUALS : "%=">
+    |
+    <PLUS_PLUS : "++">
+    |
+    <MINUS_MINUS : "--">
+    |
+    <LESS_THAN : "lt" | "\\lt" | "<" | "&lt;">
+    |
+    <LESS_THAN_EQUALS : "lte" | "\\lte" | "<=" | "&lt;=">
+    |
+    <ESCAPED_GT: "gt" | "\\gt" |  "&gt;">
+    |
+    <ESCAPED_GTE : "gte" | "\\gte" | "&gt;=">
+    |
+    <PLUS : "+">
+    |
+    <MINUS : "-">
+    |
+    <TIMES : "*">
+    |
+    <DOUBLE_STAR : "**">
+    |
+    <ELLIPSIS : "...">
+    |
+    <DIVIDE : "/">
+    |
+    <PERCENT : "%">
+    |
+    <AND : "&" | "&&" >
+    |
+    <OR : "|" | "||">
+    |
+    <EXCLAM : "!">
+    |
+    <COMMA : ",">
+    |
+    <SEMICOLON : ";">
+    |
+    <COLON : ":">
+    |
+    <OPEN_BRACKET : "[">
+    {
+        ++bracketNesting;
+    }
+    |
+    <CLOSE_BRACKET : "]">
+    {
+        closeBracket(matchedToken);
+    }
+    |
+    <OPEN_PAREN : "(">
+    {
+        ++parenthesisNesting;
+        if (parenthesisNesting == 1) SwitchTo(IN_PAREN);
+    }
+    |
+    <CLOSE_PAREN : ")">
+    {
+        --parenthesisNesting;
+        if (parenthesisNesting == 0) {
+            if (inInvocation) SwitchTo(NAMED_PARAMETER_EXPRESSION);
+            else SwitchTo(FM_EXPRESSION);
+        }
+    }
+    |
+    <OPENING_CURLY_BRACKET : "{">
+    {
+        ++hashLiteralNesting;
+    }
+    |
+    <CLOSING_CURLY_BRACKET : "}">
+    {
+        if (hashLiteralNesting == 0) endInterpolation(matchedToken);
+        else --hashLiteralNesting;
+    }
+    |
+    <IN : "in">
+    |
+    <AS : "as">
+    |
+    <USING : "using">
+    |
+    <ID: <ID_START_CHAR> (<ID_START_CHAR>|<ASCII_DIGIT>)*> {
+        // Remove backslashes from Token.image:
+        final String s = matchedToken.image;
+        if (s.indexOf('\\') != -1) {
+            final int srcLn = s.length(); 
+            final char[] newS = new char[srcLn - 1];
+            int dstIdx = 0;
+            for (int srcIdx = 0; srcIdx < srcLn; srcIdx++) {
+                final char c = s.charAt(srcIdx);
+                if (c != '\\') {
+                    newS[dstIdx++] = c;
+                }
+            }
+            matchedToken.image = new String(newS, 0, dstIdx);
+        }
+    }
+    |
+    <OPEN_MISPLACED_INTERPOLATION : "${" | "#{">
+    {
+        if ("".length() == 0) {  // prevents unreachabe "break" compilation error in generated Java
+            char c = matchedToken.image.charAt(0);
+            throw new TokenMgrError(
+                    "You can't use \"" + c + "{\" here as you are already in FreeMarker-expression-mode. Thus, instead "
+                    + "of " + c + "{myExpression}, just write myExpression. "
+                    + "(" + c + "{...} is only needed where otherwise static text is expected, i.e, outside " 
+                    + "FreeMarker tags and ${...}-s.)",
+                    TokenMgrError.LEXICAL_ERROR,
+                    matchedToken.beginLine, matchedToken.beginColumn,
+                    matchedToken.endLine, matchedToken.endColumn);
+        }
+    }
+    |
+    <#NON_ESCAPED_ID_START_CHAR:
+        [
+            // This was generated on JDK 1.8.0_20 Win64 with src/main/misc/identifierChars/IdentifierCharGenerator.java
+			"$", 
+			"@" - "Z", 
+			"_", 
+			"a" - "z", 
+			"\u00AA", 
+			"\u00B5", 
+			"\u00BA", 
+			"\u00C0" - "\u00D6", 
+			"\u00D8" - "\u00F6", 
+			"\u00F8" - "\u1FFF", 
+			"\u2071", 
+			"\u207F", 
+			"\u2090" - "\u209C", 
+			"\u2102", 
+			"\u2107", 
+			"\u210A" - "\u2113", 
+			"\u2115", 
+			"\u2119" - "\u211D", 
+			"\u2124", 
+			"\u2126", 
+			"\u2128", 
+			"\u212A" - "\u212D", 
+			"\u212F" - "\u2139", 
+			"\u213C" - "\u213F", 
+			"\u2145" - "\u2149", 
+			"\u214E", 
+			"\u2183" - "\u2184", 
+			"\u2C00" - "\u2C2E", 
+			"\u2C30" - "\u2C5E", 
+			"\u2C60" - "\u2CE4", 
+			"\u2CEB" - "\u2CEE", 
+			"\u2CF2" - "\u2CF3", 
+			"\u2D00" - "\u2D25", 
+			"\u2D27", 
+			"\u2D2D", 
+			"\u2D30" - "\u2D67", 
+			"\u2D6F", 
+			"\u2D80" - "\u2D96", 
+			"\u2DA0" - "\u2DA6", 
+			"\u2DA8" - "\u2DAE", 
+			"\u2DB0" - "\u2DB6", 
+			"\u2DB8" - "\u2DBE", 
+			"\u2DC0" - "\u2DC6", 
+			"\u2DC8" - "\u2DCE", 
+			"\u2DD0" - "\u2DD6", 
+			"\u2DD8" - "\u2DDE", 
+			"\u2E2F", 
+			"\u3005" - "\u3006", 
+			"\u3031" - "\u3035", 
+			"\u303B" - "\u303C", 
+			"\u3040" - "\u318F", 
+			"\u31A0" - "\u31BA", 
+			"\u31F0" - "\u31FF", 
+			"\u3300" - "\u337F", 
+			"\u3400" - "\u4DB5", 
+			"\u4E00" - "\uA48C", 
+			"\uA4D0" - "\uA4FD", 
+			"\uA500" - "\uA60C", 
+			"\uA610" - "\uA62B", 
+			"\uA640" - "\uA66E", 
+			"\uA67F" - "\uA697", 
+			"\uA6A0" - "\uA6E5", 
+			"\uA717" - "\uA71F", 
+			"\uA722" - "\uA788", 
+			"\uA78B" - "\uA78E", 
+			"\uA790" - "\uA793", 
+			"\uA7A0" - "\uA7AA", 
+			"\uA7F8" - "\uA801", 
+			"\uA803" - "\uA805", 
+			"\uA807" - "\uA80A", 
+			"\uA80C" - "\uA822", 
+			"\uA840" - "\uA873", 
+			"\uA882" - "\uA8B3", 
+			"\uA8D0" - "\uA8D9", 
+			"\uA8F2" - "\uA8F7", 
+			"\uA8FB", 
+			"\uA900" - "\uA925", 
+			"\uA930" - "\uA946", 
+			"\uA960" - "\uA97C", 
+			"\uA984" - "\uA9B2", 
+			"\uA9CF" - "\uA9D9", 
+			"\uAA00" - "\uAA28", 
+			"\uAA40" - "\uAA42", 
+			"\uAA44" - "\uAA4B", 
+			"\uAA50" - "\uAA59", 
+			"\uAA60" - "\uAA76", 
+			"\uAA7A", 
+			"\uAA80" - "\uAAAF", 
+			"\uAAB1", 
+			"\uAAB5" - "\uAAB6", 
+			"\uAAB9" - "\uAABD", 
+			"\uAAC0", 
+			"\uAAC2", 
+			"\uAADB" - "\uAADD", 
+			"\uAAE0" - "\uAAEA", 
+			"\uAAF2" - "\uAAF4", 
+			"\uAB01" - "\uAB06", 
+			"\uAB09" - "\uAB0E", 
+			"\uAB11" - "\uAB16", 
+			"\uAB20" - "\uAB26", 
+			"\uAB28" - "\uAB2E", 
+			"\uABC0" - "\uABE2", 
+			"\uABF0" - "\uABF9", 
+			"\uAC00" - "\uD7A3", 
+			"\uD7B0" - "\uD7C6", 
+			"\uD7CB" - "\uD7FB", 
+			"\uF900" - "\uFB06", 
+			"\uFB13" - "\uFB17", 
+			"\uFB1D", 
+			"\uFB1F" - "\uFB28", 
+			"\uFB2A" - "\uFB36", 
+			"\uFB38" - "\uFB3C", 
+			"\uFB3E", 
+			"\uFB40" - "\uFB41", 
+			"\uFB43" - "\uFB44", 
+			"\uFB46" - "\uFBB1", 
+			"\uFBD3" - "\uFD3D", 
+			"\uFD50" - "\uFD8F", 
+			"\uFD92" - "\uFDC7", 
+			"\uFDF0" - "\uFDFB", 
+			"\uFE70" - "\uFE74", 
+			"\uFE76" - "\uFEFC", 
+			"\uFF10" - "\uFF19", 
+			"\uFF21" - "\uFF3A", 
+			"\uFF41" - "\uFF5A", 
+			"\uFF66" - "\uFFBE", 
+			"\uFFC2" - "\uFFC7", 
+			"\uFFCA" - "\uFFCF", 
+			"\uFFD2" - "\uFFD7", 
+			"\uFFDA" - "\uFFDC" 
+        ]
+    >
+    |
+    <#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":")>
+    |
+    <#ID_START_CHAR: <NON_ESCAPED_ID_START_CHAR>|<ESCAPED_ID_CHAR>>
+    |
+    <#ASCII_DIGIT: ["0" - "9"]>
+}
+
+<FM_EXPRESSION, NO_SPACE_EXPRESSION, NAMED_PARAMETER_EXPRESSION> TOKEN :
+{
+    <DIRECTIVE_END : ">">
+    {
+        if (inFTLHeader) eatNewline();
+        inFTLHeader = false;
+        if (squBracTagSyntax) {
+            matchedToken.kind = NATURAL_GT;
+        } else {
+            SwitchTo(DEFAULT);
+        }
+    }
+    |
+    <EMPTY_DIRECTIVE_END : "/>" | "/]">
+    {
+        if (inFTLHeader) eatNewline();
+        inFTLHeader = false;
+        SwitchTo(DEFAULT);
+    }
+}
+
+<IN_PAREN> TOKEN :
+{
+    <NATURAL_GT : ">">
+    |
+    <NATURAL_GTE : ">=">
+}
+
+<NO_SPACE_EXPRESSION> TOKEN :
+{
+    <TERMINATING_WHITESPACE :  (["\n", "\r", "\t", " "])+> : FM_EXPRESSION
+}
+
+<NAMED_PARAMETER_EXPRESSION> TOKEN :
+{
+    <TERMINATING_EXCLAM : "!" (["\n", "\r", "\t", " "])+> : FM_EXPRESSION
+}
+
+<NO_PARSE> TOKEN :
+{
+    <TERSE_COMMENT_END : "-->" | "--]">
+    {
+        if (noparseTag.equals("-->")) {
+            boolean squareBracket = matchedToken.image.endsWith("]");
+            if ((squBracTagSyntax && squareBracket) || (!squBracTagSyntax && !squareBracket)) {
+                matchedToken.image = matchedToken.image + ";"; 
+                SwitchTo(DEFAULT);
+            }
+        }
+    }
+    |
+    <MAYBE_END :
+        ("<" | "[")
+        "/#"
+        (["a"-"z", "A"-"Z"])+
+        ( " " | "\t" | "\n" | "\r" )*
+        (">" | "]")
+    >
+    {
+        StringTokenizer st = new StringTokenizer(image.toString(), " \t\n\r<>[]/#", false);
+        if (st.nextToken().equals(noparseTag)) {
+            matchedToken.image = matchedToken.image + ";"; 
+            SwitchTo(DEFAULT);
+        }
+    }
+    |
+    <KEEP_GOING : (~["<", "[", "-"])+>
+    |
+    <LONE_LESS_THAN_OR_DASH : ["<", "[", "-"]>
+}
+
+// Now the actual parsing code, starting
+// with the productions for FreeMarker's
+// expression syntax.
+
+/**
+ * This is the same as ASTExpOr, since
+ * OR is the operator with the lowest
+ * precedence.
+ */
+ASTExpression ASTExpression() :
+{
+    ASTExpression exp;
+}
+{
+    exp = ASTExpOr()
+    {
+        return exp;
+    }
+}
+
+/**
+ * Lowest level expression, a literal, a variable,
+ * or a possibly more complex expression bounded
+ * by parentheses.
+ */
+ASTExpression PrimaryExpression() :
+{
+    ASTExpression exp;
+}
+{
+    (
+        exp = ASTExpNumberLiteral()
+        |   
+        exp = ASTExpHashLiteral()
+        |   
+        exp = ASTExpStringLiteral(true)
+        |   
+        exp = ASTExpBooleanLiteral()
+        |   
+        exp = ASTExpListLiteral()
+        |   
+        exp = ASTExpVariable()
+        |   
+        exp = Parenthesis()
+        |   
+        exp = ASTExpBuiltInVariable()
+    )
+    (
+        LOOKAHEAD(<DOT> | <OPEN_BRACKET> |<OPEN_PAREN> | <BUILT_IN> | <EXCLAM> | <TERMINATING_EXCLAM> | <EXISTS>)
+        exp = AddSubExpression(exp)
+    )*
+    {
+        return exp;
+    }
+}
+
+ASTExpression Parenthesis() :
+{
+    ASTExpression exp, result;
+    Token start, end;
+}
+{
+    start = <OPEN_PAREN>
+    exp = ASTExpression()
+    end = <CLOSE_PAREN>
+    {
+        result = new ASTExpParenthesis(exp);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+/**
+ * A primary expression preceded by zero or
+ * more unary operators. (The only unary operator we
+ * currently have is the NOT.)
+ */
+ASTExpression UnaryExpression() :
+{
+    ASTExpression exp, result;
+    boolean haveNot = false;
+    Token t = null, start = null;
+}
+{
+    (
+        result = ASTExpNegateOrPlus()
+        |
+        result = ASTExpNot()
+        |
+        result = PrimaryExpression()
+    )
+    {
+        return result;
+    }
+}
+
+ASTExpression ASTExpNot() : 
+{
+    Token t;
+    ASTExpression exp, result = null;
+    ArrayList nots = new ArrayList();
+}
+{
+    (
+        t = <EXCLAM> { nots.add(t); }
+    )+
+    exp = PrimaryExpression()
+    {
+        for (int i = 0; i < nots.size(); i++) {
+            result = new ASTExpNot(exp);
+            Token tok = (Token) nots.get(nots.size() -i -1);
+            result.setLocation(template, tok, exp);
+            exp = result;
+        }
+        return result;
+    }
+}
+
+ASTExpression ASTExpNegateOrPlus() :
+{
+    ASTExpression exp, result;
+    boolean isMinus = false;
+    Token t;
+}
+{
+    (
+        t = <PLUS>
+        |
+        t = <MINUS> { isMinus = true; }
+    )
+    exp = PrimaryExpression()
+    {
+        result = new ASTExpNegateOrPlus(exp, isMinus);  
+        result.setLocation(template, t, exp);
+        return result;
+    }
+}
+
+ASTExpression AdditiveExpression() :
+{
+    ASTExpression lhs, rhs, result;
+    boolean plus;
+}
+{
+    lhs = MultiplicativeExpression() { result = lhs; }
+    (
+        LOOKAHEAD(<PLUS>|<MINUS>)
+        (
+            (
+                <PLUS> { plus = true; }
+                |
+                <MINUS> { plus = false; }
+            )
+        )
+        rhs = MultiplicativeExpression()
+        {
+            if (plus) {
+	            // plus is treated separately, since it is also
+	            // used for concatenation.
+                result = new ASTExpAddOrConcat(lhs, rhs);
+            } else {
+                numberLiteralOnly(lhs);
+                numberLiteralOnly(rhs);
+                result = new ArithmeticExpression(lhs, rhs, ArithmeticExpression.TYPE_SUBSTRACTION);
+            }
+            result.setLocation(template, lhs, rhs);
+            lhs = result;
+        }
+    )*
+    {
+        return result;
+    }
+}
+
+/**
+ * A unary expression followed by zero or more
+ * unary expressions with operators in between.
+ */
+ASTExpression MultiplicativeExpression() :
+{
+    ASTExpression lhs, rhs, result;
+    int operation = ArithmeticExpression.TYPE_MULTIPLICATION;
+}
+{
+    lhs = UnaryExpression() { result = lhs; }
+    (
+        LOOKAHEAD(<TIMES>|<DIVIDE>|<PERCENT>)
+        (
+            (
+                <TIMES> { operation = ArithmeticExpression.TYPE_MULTIPLICATION; }
+                |
+                <DIVIDE> { operation = ArithmeticExpression.TYPE_DIVISION; }
+                |
+                <PERCENT> {operation = ArithmeticExpression.TYPE_MODULO; }
+            )
+        )
+        rhs = UnaryExpression()
+        {
+            numberLiteralOnly(lhs);
+            numberLiteralOnly(rhs);
+            result = new ArithmeticExpression(lhs, rhs, operation);
+            result.setLocation(template, lhs, rhs);
+            lhs = result;
+        }
+    )*
+    {
+        return result;
+    }
+}
+
+
+ASTExpression EqualityExpression() :
+{
+    ASTExpression lhs, rhs, result;
+    Token t;
+}
+{
+    lhs = RelationalExpression() { result = lhs; }
+    [
+        LOOKAHEAD(<NOT_EQUALS>|<EQUALS>|<DOUBLE_EQUALS>)
+        (
+            t = <NOT_EQUALS> 
+            |
+            t = <EQUALS> 
+            |
+            t = <DOUBLE_EQUALS>
+        )
+        rhs = RelationalExpression()
+        {
+	        notHashLiteral(lhs, "scalar");
+	        notHashLiteral(rhs, "scalar");
+	        notListLiteral(lhs, "scalar");
+	        notListLiteral(rhs, "scalar");
+	        result = new ASTExpComparison(lhs, rhs, t.image);
+	        result.setLocation(template, lhs, rhs);
+        }
+    ]
+    {
+        return result;
+    }
+}
+
+ASTExpression RelationalExpression() :
+{
+    ASTExpression lhs, rhs, result;
+    Token t;
+}
+{
+    lhs = RangeExpression() { result = lhs; }
+    [
+        LOOKAHEAD(<NATURAL_GTE>|<ESCAPED_GTE>|<NATURAL_GT>|<ESCAPED_GT>|<LESS_THAN_EQUALS>|<LESS_THAN_EQUALS>|<LESS_THAN>)
+        (
+            t = <NATURAL_GTE>
+            |
+            t = <ESCAPED_GTE>
+            |
+            t = <NATURAL_GT>
+            |
+            t = <ESCAPED_GT>
+            |
+            t = <LESS_THAN_EQUALS>
+            |
+            t = <LESS_THAN>
+        )
+        rhs = RangeExpression()
+        {
+            notHashLiteral(lhs, "scalar");
+            notHashLiteral(rhs, "scalar");
+            notListLiteral(lhs, "scalar");
+            notListLiteral(rhs, "scalar");
+            notStringLiteral(lhs, "number");
+            notStringLiteral(rhs, "number");
+            result = new ASTExpComparison(lhs, rhs, t.image);
+            result.setLocation(template, lhs, rhs);
+        }
+    ]
+    {
+        return result;
+    }
+}
+
+ASTExpression RangeExpression() :
+{
+    ASTExpression lhs, rhs = null, result;
+    int endType;
+    Token dotDot = null;
+}
+{
+    lhs = AdditiveExpression() { result = lhs; }
+    [
+        LOOKAHEAD(1)  // To suppress warning
+        (
+            (
+                (
+                    <DOT_DOT_LESS> { endType = ASTExpRange.END_EXCLUSIVE; }
+                    |
+                    <DOT_DOT_ASTERISK> { endType = ASTExpRange.END_SIZE_LIMITED; }
+                )
+                rhs = AdditiveExpression()
+            )
+            | 
+            (
+                dotDot = <DOT_DOT> { endType = ASTExpRange.END_UNBOUND; }
+                [
+                    LOOKAHEAD(AdditiveExpression())
+                    rhs = AdditiveExpression()
+                    {
+                        endType = ASTExpRange.END_INCLUSIVE;
+                    }
+                ]
+            )
+        )
+        {
+            numberLiteralOnly(lhs);
+            if (rhs != null) {
+                numberLiteralOnly(rhs);
+            }
+           
+            ASTExpRange range = new ASTExpRange(lhs, rhs, endType);
+            if (rhs != null) {
+                range.setLocation(template, lhs, rhs);
+            } else {
+                range.setLocation(template, lhs, dotDot);
+            }
+            result = range;
+        }
+    ]
+    {
+        return result;
+    }
+}
+
+
+
+
+ASTExpression ASTExpAnd() :
+{
+    ASTExpression lhs, rhs, result;
+}
+{
+    lhs = EqualityExpression() { result = lhs; }
+    (
+        LOOKAHEAD(<AND>)
+        <AND>
+        rhs = EqualityExpression()
+        {
+            booleanLiteralOnly(lhs);
+            booleanLiteralOnly(rhs);
+            result = new ASTExpAnd(lhs, rhs);
+            result.setLocation(template, lhs, rhs);
+            lhs = result;
+        }
+    )*
+    {
+        return result;
+    }
+}
+
+ASTExpression ASTExpOr() :
+{
+    ASTExpression lhs, rhs, result;
+}
+{
+    lhs = ASTExpAnd() { result = lhs; }
+    (
+        LOOKAHEAD(<OR>)
+        <OR>
+        rhs = ASTExpAnd()
+        {
+            booleanLiteralOnly(lhs);
+            booleanLiteralOnly(rhs);
+            result = new ASTExpOr(lhs, rhs);
+            result.setLocation(template, lhs, rhs);
+            lhs = result;
+        }
+    )*
+    {
+        return result;
+    }
+}
+
+ASTExpListLiteral ASTExpListLiteral() :
+{
+    ArrayList values = new ArrayList();
+    Token begin, end;
+}
+{
+    begin = <OPEN_BRACKET>
+    values = PositionalArgs()
+    end = <CLOSE_BRACKET>
+    {
+        ASTExpListLiteral result = new ASTExpListLiteral(values);
+        result.setLocation(template, begin, end);
+        return result;
+    }
+}
+
+ASTExpression ASTExpNumberLiteral() :
+{
+    Token op = null, t;
+}
+{
+    (
+        t = <INTEGER>
+        |
+        t = <DECIMAL>
+    )
+    {
+        String s = t.image;
+        ASTExpression result = new ASTExpNumberLiteral(pCfg.getArithmeticEngine().toNumber(s));
+        Token startToken = (op != null) ? op : t;
+        result.setLocation(template, startToken, t);
+        return result;
+    }
+}
+
+ASTExpVariable ASTExpVariable() :
+{
+    Token t;
+}
+{
+    t = <ID>
+    {
+        ASTExpVariable id = new ASTExpVariable(t.image);
+        id.setLocation(template, t, t);
+        return id;
+    }
+}
+
+ASTExpression IdentifierOrStringLiteral() :
+{
+    ASTExpression exp;
+}
+{
+    (
+        exp = ASTExpVariable()
+        |
+        exp = ASTExpStringLiteral(false)
+    )
+    {
+        return exp;
+    }   
+}
+
+ASTExpBuiltInVariable ASTExpBuiltInVariable() :
+{
+    Token dot, name;
+}
+{
+    dot = <DOT>
+    name = <ID>
+    {
+        ASTExpBuiltInVariable result = null;
+        token_source.checkNamingConvention(name);
+
+        TemplateModel parseTimeValue;
+        String nameStr = name.image;
+        if (nameStr.equals(ASTExpBuiltInVariable.OUTPUT_FORMAT) || nameStr.equals(ASTExpBuiltInVariable.OUTPUT_FORMAT_CC)) {
+            parseTimeValue = new SimpleScalar(outputFormat.getName());
+        } else if (nameStr.equals(ASTExpBuiltInVariable.AUTO_ESC) || nameStr.equals(ASTExpBuiltInVariable.AUTO_ESC_CC)) {
+            parseTimeValue = autoEscaping ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
+        } else {
+            parseTimeValue = null;
+        }
+        
+        result = new ASTExpBuiltInVariable(name, token_source, parseTimeValue);
+        
+        result.setLocation(template, dot, name);
+        return result;
+    }
+}
+
+/**
+ * Production that builds up an expression
+ * using the dot or dynamic key name
+ * or the args list if this is a method invocation.
+ */
+ASTExpression AddSubExpression(ASTExpression exp) :
+{
+    ASTExpression result = null;
+}
+{
+    (
+        result = DotVariable(exp)
+        |
+        result = DynamicKey(exp)
+        |
+        result = MethodArgs(exp)
+        |
+        result = ASTExpBuiltIn(exp)
+        |
+        result = DefaultTo(exp)
+        |
+        result = Exists(exp)
+    )
+    {
+        return result;
+    }
+}
+
+ASTExpression DefaultTo(ASTExpression exp) :
+{
+    ASTExpression rhs = null;
+    Token t;
+}
+{
+    (
+        t = <TERMINATING_EXCLAM>
+        |
+        (
+            t = <EXCLAM>
+            [
+                LOOKAHEAD(ASTExpression())
+                rhs = ASTExpression()
+            ]
+        )
+    )
+    {
+        ASTExpDefault result = new ASTExpDefault(exp, rhs);
+        if (rhs == null) {
+            result.setLocation(template, exp, t);
+        } else {
+            result.setLocation(template, exp, rhs);
+        }
+        return result;
+    }
+}
+
+ASTExpression Exists(ASTExpression exp) :
+{
+    Token t;
+}
+{
+    t = <EXISTS>
+    {
+        ASTExpExists result = new ASTExpExists(exp);
+        result.setLocation(template, exp, t);
+        return result;
+    }
+}
+
+ASTExpression ASTExpBuiltIn(ASTExpression lhoExp) :
+{
+    Token t = null;
+    ASTExpBuiltIn result;
+    ArrayList/*<ASTExpression>*/ args = null;
+    Token openParen;
+    Token closeParen;
+}
+{
+    <BUILT_IN>
+    t = <ID>
+    {
+        token_source.checkNamingConvention(t);
+        result = ASTExpBuiltIn.newBuiltIn(incompatibleImprovements, lhoExp, t, token_source);
+        result.setLocation(template, lhoExp, t);
+        
+        if (!(result instanceof SpecialBuiltIn)) {
+            return result;
+        }
+
+        if (result instanceof BuiltInForLoopVariable) {
+            if (!(lhoExp instanceof ASTExpVariable)) {
+                throw new ParseException(
+                        "Expression used as the left hand operand of ?" + t.image
+                        + " must be a simple loop variable name.", lhoExp);
+            }
+            String loopVarName = ((ASTExpVariable) lhoExp).getName();
+            checkLoopVariableBuiltInLHO(loopVarName, lhoExp, t);
+            ((BuiltInForLoopVariable) result).bindToLoopVariable(loopVarName);
+            
+            return result;
+        }
+        
+        if (result instanceof BuiltInBannedWhenAutoEscaping) {
+	        if (outputFormat instanceof MarkupOutputFormat && autoEscaping) {
+	            throw new ParseException(
+	                    "Using ?" + t.image + " (legacy escaping) is not allowed when auto-escaping is on with "
+	                    + "a markup output format (" + outputFormat.getName() + "), to avoid double-escaping mistakes.",
+	                    template, t);
+	        }
+            
+            return result;
+        }
+
+        if (result instanceof MarkupOutputFormatBoundBuiltIn) {
+            if (!(outputFormat instanceof MarkupOutputFormat)) {
+                throw new ParseException(
+                        "?" + t.image + " can't be used here, as the current output format isn't a markup (escaping) "
+                        + "format: " + outputFormat, template, t);
+            }
+            ((MarkupOutputFormatBoundBuiltIn) result).bindToMarkupOutputFormat((MarkupOutputFormat) outputFormat);
+            
+            return result;
+        }
+
+        if (result instanceof OutputFormatBoundBuiltIn) {
+            ((OutputFormatBoundBuiltIn) result).bindToOutputFormat(outputFormat, autoEscapingPolicy);
+            
+            return result;
+        }
+    }
+    [
+        LOOKAHEAD({ result instanceof BuiltInWithParseTimeParameters  })
+        openParen = <OPEN_PAREN>
+        args = PositionalArgs()
+        closeParen = <CLOSE_PAREN> {
+            result.setLocation(template, lhoExp, closeParen);
+            ((BuiltInWithParseTimeParameters) result).bindToParameters(args, openParen, closeParen);
+            
+            return result;
+        }
+    ]
+    {
+        // Should have already return-ed
+        throw new AssertionError("Unhandled " + SpecialBuiltIn.class.getName() + " subclass: " + result.getClass());
+    }
+}
+
+
+/**
+ * production for when a key is specified by <DOT> + keyname
+ */
+ASTExpression DotVariable(ASTExpression exp) :
+{
+    Token t;
+}
+{
+        <DOT>
+        (
+            t = <ID> | t = <TIMES> | t = <DOUBLE_STAR> 
+            |
+            (
+                t = <LESS_THAN>
+                |
+                t = <LESS_THAN_EQUALS>
+                |
+                t = <ESCAPED_GT>
+                |
+                t = <ESCAPED_GTE>
+                |
+                t = <FALSE>
+                |
+                t = <TRUE>
+                |
+                t = <IN>
+                |
+                t = <AS>
+                |
+                t = <USING>
+            )
+            {
+                if (!Character.isLetter(t.image.charAt(0))) {
+                    throw new ParseException(t.image + " is not a valid identifier.", template, t);
+                }
+            }
+        )
+        {
+            notListLiteral(exp, "hash");
+            notStringLiteral(exp, "hash");
+            notBooleanLiteral(exp, "hash");
+            ASTExpDot dot = new ASTExpDot(exp, t.image);
+            dot.setLocation(template, exp, t);
+            return dot;
+        }
+}
+
+/**
+ * production for when the key is specified
+ * in brackets.
+ */
+ASTExpression DynamicKey(ASTExpression exp) :
+{
+    ASTExpression arg;
+    Token t;
+}
+{
+    <OPEN_BRACKET>
+    arg = ASTExpression()
+    t = <CLOSE_BRACKET>
+    {
+        notBooleanLiteral(exp, "list or hash");
+        notNumberLiteral(exp, "list or hash");
+        ASTExpDynamicKeyName dkn = new ASTExpDynamicKeyName(exp, arg);
+        dkn.setLocation(template, exp, t);
+        return dkn;
+    }
+}
+
+/**
+ * production for an arglist part of a method invocation.
+ */
+ASTExpMethodCall MethodArgs(ASTExpression exp) :
+{
+        ArrayList args = new ArrayList();
+        Token end;
+}
+{
+        <OPEN_PAREN>
+        args = PositionalArgs()
+        end = <CLOSE_PAREN>
+        {
+            args.trimToSize();
+            ASTExpMethodCall result = new ASTExpMethodCall(exp, args);
+            result.setLocation(template, exp, end);
+            return result;
+        }
+}
+
+ASTExpStringLiteral ASTExpStringLiteral(boolean interpolate) :
+{
+    Token t;
+    boolean raw = false;
+}
+{
+    (
+        t = <STRING_LITERAL>
+        |
+        t = <RAW_STRING> { raw = true; }
+    )
+    {
+        String s;
+        // Get rid of the quotes.
+        if (raw) {
+            s = t.image.substring(2, t.image.length() -1);
+        } else {
+	        try {
+	            s = FTLUtil.unescapeStringLiteralPart(t.image.substring(1, t.image.length() -1));
+            } catch (GenericParseException e) {
+                throw new ParseException(e.getMessage(), template, t);
+            }
+        }
+        ASTExpStringLiteral result = new ASTExpStringLiteral(s);
+        result.setLocation(template, t, t);
+        if (interpolate && !raw) {
+            // TODO: This logic is broken. It can't handle literals that contains both ${...} and $\{...}. 
+            if (t.image.indexOf("${") >= 0 || t.image.indexOf("#{") >= 0) result.parseValue(token_source, outputFormat);
+        }
+        return result;
+    }
+}
+
+ASTExpression ASTExpBooleanLiteral() :
+{
+    Token t;
+    ASTExpression result;
+}
+{
+    (
+        t = <FALSE> { result = new ASTExpBooleanLiteral(false); }
+        |
+        t = <TRUE> { result = new ASTExpBooleanLiteral(true); }
+    )
+    {
+        result.setLocation(template, t, t);
+        return result;
+    }
+}
+
+
+ASTExpHashLiteral ASTExpHashLiteral() :
+{
+    Token begin, end;
+    ASTExpression key, value;
+    ArrayList keys = new ArrayList();
+    ArrayList values = new ArrayList();
+}
+{
+    begin = <OPENING_CURLY_BRACKET>
+    [
+        key = ASTExpression()
+        (<COMMA>|<COLON>)
+        value = ASTExpression()
+        {
+            stringLiteralOnly(key);
+            keys.add(key);
+            values.add(value);
+        }
+        (
+            <COMMA>
+            key = ASTExpression()
+            (<COMMA>|<COLON>)
+            value = ASTExpression()
+            {
+                stringLiteralOnly(key);
+                keys.add(key);
+                values.add(value);
+            }
+        )*
+    ]
+    end = <CLOSING_CURLY_BRACKET>
+    {
+        ASTExpHashLiteral result = new ASTExpHashLiteral(keys, values);
+        result.setLocation(template, begin, end);
+        return result;
+    }
+}
+
+/**
+ * A production representing the ${...}
+ * that outputs a variable.
+ */
+ASTDollarInterpolation StringOutput() :
+{
+    ASTExpression exp;
+    Token begin, end;
+}
+{
+    begin = <DOLLAR_INTERPOLATION_OPENING>
+    exp = ASTExpression()
+    {
+        notHashLiteral(exp, NonStringException.STRING_COERCABLE_TYPES_DESC);
+        notListLiteral(exp, NonStringException.STRING_COERCABLE_TYPES_DESC);
+    }
+    end = <CLOSING_CURLY_BRACKET>
+    {
+        ASTDollarInterpolation result = new ASTDollarInterpolation(
+                exp, escapedExpression(exp),
+                outputFormat,
+                autoEscaping);
+        result.setLocation(template, begin, end);
+        return result;
+    }
+}
+
+ASTHashInterpolation ASTHashInterpolation() :
+{
+    ASTExpression exp;
+    Token fmt = null, begin, end;
+}
+{
+    begin = <HASH_INTERPOLATION_OPENING>
+    exp = ASTExpression() { numberLiteralOnly(exp); }
+    [
+        <SEMICOLON>
+        fmt = <ID>
+    ]
+    end = <CLOSING_CURLY_BRACKET>
+    {
+        MarkupOutputFormat<?> autoEscOF = autoEscaping && outputFormat instanceof MarkupOutputFormat
+                ? (MarkupOutputFormat<?>) outputFormat : null;
+    
+        ASTHashInterpolation result;
+        if (fmt != null) {
+            int minFrac = -1;  // -1 indicates that the value has not been set
+            int maxFrac = -1;
+
+            StringTokenizer st = new StringTokenizer(fmt.image, "mM", true);
+            char type = '-';
+            while (st.hasMoreTokens()) {
+                String token = st.nextToken();
+                try {
+	                if (type != '-') {
+	                    switch (type) {
+	                    case 'm':
+	                        if (minFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
+	                        minFrac = Integer.parseInt(token);
+	                        break;
+	                    case 'M':
+	                        if (maxFrac != -1) throw new ParseException("Invalid formatting string", template, fmt);
+	                        maxFrac = Integer.parseInt(token);
+	                        break;
+	                    default:
+	                        throw new ParseException("Invalid formatting string", template, fmt);
+	                    }
+	                    type = '-';
+	                } else if (token.equals("m")) {
+	                    type = 'm';
+	                } else if (token.equals("M")) {
+	                    type = 'M';
+	                } else {
+	                    throw new ParseException();
+	                }
+                } catch (ParseException e) {
+                	throw new ParseException("Invalid format specifier " + fmt.image, template, fmt);
+                } catch (NumberFormatException e) {
+                	throw new ParseException("Invalid number in the format specifier " + fmt.image, template, fmt);
+                }
+            }
+
+            if (maxFrac == -1) {
+	            if (minFrac == -1) {
+	                throw new ParseException(
+	                		"Invalid format specification, at least one of m and M must be specified!", template, fmt);
+	            }
+            	maxFrac = minFrac;
+            } else if (minFrac == -1) {
+            	minFrac = 0;
+            }
+            if (minFrac > maxFrac) {
+            	throw new ParseException(
+            			"Invalid format specification, min cannot be greater than max!", template, fmt);
+            }
+            if (minFrac > 50 || maxFrac > 50) {// sanity check
+                throw new ParseException("Cannot specify more than 50 fraction digits", template, fmt);
+            }
+            result = new ASTHashInterpolation(exp, minFrac, maxFrac, autoEscOF);
+        } else {  // if format != null
+            result = new ASTHashInterpolation(exp, autoEscOF);
+        }
+        result.setLocation(template, begin, end);
+        return result;
+    }
+}
+
+ASTElement If() :
+{
+    Token start, end, t;
+    ASTExpression condition;
+    TemplateElements children;
+    ASTDirIfElseIfElseContainer ifBlock;
+    ASTDirIfOrElseOrElseIf cblock;
+}
+{
+    start = <IF>
+    condition = ASTExpression()
+    end = <DIRECTIVE_END>
+    children = MixedContentElements()
+    {
+        cblock = new ASTDirIfOrElseOrElseIf(condition, children, ASTDirIfOrElseOrElseIf.TYPE_IF);
+        cblock.setLocation(template, start, end, children);
+        ifBlock = new ASTDirIfElseIfElseContainer(cblock);
+    }
+    (
+        t = <ELSE_IF>
+        condition = ASTExpression()
+        end = LooseDirectiveEnd()
+        children = MixedContentElements()
+        {
+            cblock = new ASTDirIfOrElseOrElseIf(condition, children, ASTDirIfOrElseOrElseIf.TYPE_ELSE_IF);
+            cblock.setLocation(template, t, end, children);
+            ifBlock.addBlock(cblock);
+        }
+    )*
+    [
+            t = <ELSE>
+            children = MixedContentElements()
+            {
+                cblock = new ASTDirIfOrElseOrElseIf(null, children, ASTDirIfOrElseOrElseIf.TYPE_ELSE);
+                cblock.setLocation(template, t, t, children);
+                ifBlock.addBlock(cblock);
+            }
+    ]
+    end = <END_IF>
+    {
+        ifBlock.setLocation(template, start, end);
+        return ifBlock;
+    }
+}
+
+ASTDirAttemptRecoverContainer Attempt() :
+{
+    Token start, end;
+    TemplateElements children;
+    ASTDirRecover recoveryBlock;
+}
+{
+    start = <ATTEMPT>
+    children = MixedContentElements()
+    recoveryBlock = Recover()
+    (
+        end = <END_RECOVER>
+        |
+        end = <END_ATTEMPT>
+    )
+    {
+        ASTDirAttemptRecoverContainer result = new ASTDirAttemptRecoverContainer(children, recoveryBlock);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirRecover Recover() : 
+{
+    Token start;
+    TemplateElements children;
+}
+{
+    start = <RECOVER>
+    children = MixedContentElements()
+    {
+        ASTDirRecover result = new ASTDirRecover(children);
+        result.setLocation(template, start, start, children);
+        return result;
+    }
+}
+
+ASTElement List() :
+{
+    ASTExpression exp;
+    Token loopVar = null, loopVar2 = null, start, end;
+    TemplateElements childrendBeforeElse;
+    ASTDirElseOfList elseOfList = null;
+    ParserIteratorBlockContext iterCtx;
+}
+{
+    start = <LIST>
+    exp = ASTExpression()
+    [
+        <AS>
+        loopVar = <ID>
+        [
+            <COMMA>
+            loopVar2 = <ID>
+        ]
+    ]
+    <DIRECTIVE_END>
+    {
+        iterCtx = pushIteratorBlockContext();
+        if (loopVar != null) {
+            iterCtx.loopVarName = loopVar.image;
+            breakableDirectiveNesting++;
+            if (loopVar2 != null) {
+                iterCtx.loopVar2Name = loopVar2.image;
+                iterCtx.hashListing = true;
+                if (iterCtx.loopVar2Name.equals(iterCtx.loopVarName)) {
+                    throw new ParseException(
+                            "The key and value loop variable names must differ, but both were: " + iterCtx.loopVarName,
+                            template, start);
+                }
+            }
+        }
+    }
+    
+    childrendBeforeElse = MixedContentElements()
+    {
+        if (loopVar != null) {
+            breakableDirectiveNesting--;
+        } else if (iterCtx.kind != ITERATOR_BLOCK_KIND_ITEMS) {
+            throw new ParseException(
+                    "#list must have either \"as loopVar\" parameter or nested #items that belongs to it.",
+                    template, start);
+        }
+        popIteratorBlockContext();
+    }
+    
+    [
+        elseOfList = ASTDirElseOfList()
+    ]
+    
+    end = <END_LIST>
+    {
+        ASTDirList list = new ASTDirList(
+                exp,
+                loopVar != null ? loopVar.image : null,  // null when we have a nested #items
+                loopVar2 != null ? loopVar2.image : null,
+                childrendBeforeElse, iterCtx.hashListing);
+        list.setLocation(template, start, end);
+
+        ASTElement result;
+        if (elseOfList == null) {
+            result = list;
+        } else {
+            result = new ASTDirListElseContainer(list, elseOfList);
+            result.setLocation(template, start, end);
+        }
+        return result;
+    }
+}
+
+ASTDirElseOfList ASTDirElseOfList() :
+{
+    Token start;
+    TemplateElements children;
+}
+{
+        start = <ELSE>
+        children = MixedContentElements()
+        {
+            ASTDirElseOfList result = new ASTDirElseOfList(children);
+	        result.setLocation(template, start, start, children);
+	        return result;
+        }
+}
+
+ASTDirItems Items() :
+{
+    Token loopVar, loopVar2 = null, start, end;
+    TemplateElements children;
+    ParserIteratorBlockContext iterCtx;
+}
+{
+    start = <ITEMS>
+    loopVar = <ID>
+    [
+        <COMMA>
+        loopVar2 = <ID>
+    ]
+    <DIRECTIVE_END>
+    {
+        iterCtx = peekIteratorBlockContext();
+        if (iterCtx == null) {
+            throw new ParseException("#items must be inside a #list block.", template, start);
+        }
+        if (iterCtx.loopVarName != null) {
+            String msg;
+	        if (iterCtx.kind == ITERATOR_BLOCK_KIND_ITEMS) {
+                msg = "Can't nest #items into each other when they belong to the same #list.";
+	        } else {
+	            msg = "The parent #list of the #items must not have \"as loopVar\" parameter.";
+            }
+            throw new ParseException(msg, template, start);
+        }
+        iterCtx.kind = ITERATOR_BLOCK_KIND_ITEMS;
+        iterCtx.loopVarName = loopVar.image;
+        if (loopVar2 != null) {
+            iterCtx.loopVar2Name = loopVar2.image;
+            iterCtx.hashListing = true;
+            if (iterCtx.loopVar2Name.equals(iterCtx.loopVarName)) {
+                throw new ParseException(
+                        "The key and value loop variable names must differ, but both were: " + iterCtx.loopVarName,
+                        template, start);
+            }
+        }
+    
+        breakableDirectiveNesting++;
+    }
+    
+    children = MixedContentElements()
+    
+    end = <END_ITEMS>
+    {
+        breakableDirectiveNesting--;
+        iterCtx.loopVarName = null;
+        iterCtx.loopVar2Name = null;
+        
+        ASTDirItems result = new ASTDirItems(loopVar.image, loopVar2 != null ? loopVar2.image : null, children);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirSep Sep() :
+{
+    Token loopVar, start, end = null;
+    TemplateElements children;
+}
+{
+    start = <SEP>
+    {
+        if (peekIteratorBlockContext() == null) {
+            throw new ParseException(
+                    "#sep must be inside a #list block.",
+                    template, start);
+        }
+    }
+    children = MixedContentElements()
+    [
+        LOOKAHEAD(1)
+        end = <END_SEP>
+    ]
+    {
+        ASTDirSep result = new ASTDirSep(children);
+        if (end != null) {
+            result.setLocation(template, start, end);
+        } else {
+            result.setLocation(template, start, start, children);
+        }
+        return result;
+    }
+}
+
+ASTDirVisit Visit() :
+{
+    Token start, end;
+    ASTExpression targetNode, namespaces = null;
+}
+{
+    start = <VISIT>
+    targetNode = ASTExpression()
+    [
+        <USING>
+        namespaces = ASTExpression()
+    ]
+    end = LooseDirectiveEnd()
+    {
+        ASTDirVisit result = new ASTDirVisit(targetNode, namespaces);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirRecurse Recurse() :
+{
+    Token start, end = null;
+    ASTExpression node = null, namespaces = null;
+}
+{
+    (
+        start = <SIMPLE_RECURSE>
+        |
+        (
+            start = <RECURSE>
+            [
+                node = ASTExpression()
+            ]
+            [
+                <USING>
+                namespaces = ASTExpression()
+            ]
+            end = LooseDirectiveEnd()
+        )
+    )
+    {
+        if (end == null) end = start;
+        ASTDirRecurse result = new ASTDirRecurse(node, namespaces);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirFallback FallBack() :
+{
+    Token tok;
+}
+{
+    tok = <FALLBACK>
+    {
+        if (!inMacro) {
+            throw new ParseException("Cannot fall back outside a macro.", template, tok);
+        }
+        ASTDirFallback result = new ASTDirFallback();
+        result.setLocation(template, tok, tok);
+        return result;
+    }
+}
+
+/**
+ * Production used to break out of a loop or a switch block.
+ */
+ASTDirBreak Break() :
+{
+    Token start;
+}
+{
+    start = <BREAK>
+    {
+        if (breakableDirectiveNesting < 1) {
+            throw new ParseException(start.image + " must be nested inside a directive that supports it: " 
+                    + " #list with \"as\", #items, #switch",
+                    template, start);
+        }
+        ASTDirBreak result = new ASTDirBreak();
+        result.setLocation(template, start, start);
+        return result;
+    }
+}
+
+/**
+ * Production used to jump out of a macro.
+ * The stop instruction terminates the rendering of the template.
+ */
+ASTDirReturn Return() :
+{
+    Token start, end = null;
+    ASTExpression exp = null;
+}
+{
+    (
+        start = <SIMPLE_RETURN> { end = start; }
+        |
+        start = <RETURN> exp = ASTExpression() end = LooseDirectiveEnd()
+    )
+    {
+        if (inMacro) {
+            if (exp != null) {
+            	throw new ParseException("A macro cannot return a value", template, start);
+            }
+        } else if (inFunction) {
+            if (exp == null) {
+            	throw new ParseException("A function must return a value", template, start);
+            }
+        } else {
+            if (exp == null) {
+            	throw new ParseException(
+            			"A return instruction can only occur inside a macro or function", template, start);
+            }
+        }
+        ASTDirReturn result = new ASTDirReturn(exp);
+        result.setLocation(template, start, end);
+        return result;
+    }
+}
+
+ASTDirStop Stop() :
+{
+    Token start = null;
+    ASTExpression exp = null;
+}
+{
+    (
+        start = <HALT>
+        |
+        start = <STOP> exp = ASTExpression() LooseDirectiveEnd()
+    )
+    {
+        ASTDirStop result = new ASTDirStop(exp);
+        result.setLocation(template, start, start);
+        return result;
+    }
+}
+
+ASTElement Nested() :
+{
+    Token t, end;
+    ArrayList bodyParameters;
+    ASTDirNested result = null;
+}
+{
+    (
+        (
+            t = <SIMPLE_NESTED>
+            {
+                result = new ASTDirNested(null);
+                result.setLocation(template, t, t);
+            }
+        )
+        |
+        (
+            t = <NESTED>
+            bodyParameters = PositionalArgs()
+            end = LooseDirectiveEnd()
+            {
+                result = new ASTDirNested(bodyParameters);
+                result.setLocation(template, t, end);
+            }
+        )
+    )
+    {
+        if (!inMacro) {
+            throw new ParseException("Cannot use a " + t.image + " instruction outside a macro.", template, t);
+        }
+        return result;
+    }
+}
+
+ASTElement Flush() :
+{
+    Token t;
+}
+{
+    t = <FLUSH>
+    {
+        ASTDirFlush result = new ASTDirFlush();
+        result.setLocation(template, t, t);
+        return result;
+    }
+}
+
+ASTElement Trim() :
+{
+    Token t;
+    ASTDirTOrTrOrTl result = null;
+}
+{
+    (
+        t = <TRIM> { result = new ASTDirTOrTrOrTl(true, true); }
+        |
+        t = <LTRIM> { result = new ASTDirTOrTrOrTl(true, false); }
+        |
+        t = <RTRIM> { result = new ASTDirTOrTrOrTl(false, true); }
+        |
+        t = <NOTRIM> { result = new ASTDirTOrTrOrTl(false, false); }
+    )
+    {
+        result.setLocation(template, t, t);
+        return result;
+    }
+}
+
+
+ASTElement Assign() :
+{
+    Token start, end;
+    int scope;
+    Token id = null;
+    Token equalsOp;
+    ASTExpression nameExp, exp, nsExp = null;
+    String varName;
+    ArrayList assignments = new ArrayList();
+    ASTDirAssignment ass;
+    TemplateElements children;
+}
+{
+    (
+        start = <ASSIGN> { scope = ASTDirAssignment.NAMESPACE; }
+        |
+        start = <GLOBALASSIGN> { scope = ASTDirAssignment.GLOBAL; }
+        |
+        start = <LOCALASSIGN> { scope = ASTDirAssignment.LOCAL; }
+        {
+            scope = ASTDirAssignment.LOCAL;
+            if (!inMacro && !inFunction) {
+                throw new ParseException("Local variable assigned outside a macro.", template, start);
+            }
+        }
+    )
+    nameExp = IdentifierOrStringLiteral()
+    {
+        varName = (nameExp instanceof ASTExpStringLiteral)
+                ? ((ASTExpStringLiteral) nameExp).getAsString()
+                : ((ASTExpVariable) nameExp).getName();
+    }
+    (
+    	(
+            (
+	    	    (
+			        (<EQUALS>|<PLUS_EQUALS>|<MINUS_EQUALS>|<TIMES_EQUALS>|<DIV_EQUALS>|<MOD_EQUALS>)
+			        {
+			           equalsOp = token;
+

<TRUNCATED>


Mime
View raw message