/*
 * Copyright (c) 2003 The Visigoth Software Society. All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowledgement:
 *       "This product includes software developed by the
 *        Visigoth Software Society (http://www.visigoths.org/)."
 *    Alternately, this acknowledgement may appear in the software itself,
 *    if and wherever such third-party acknowledgements normally appear.
 *
 * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the 
 *    project contributors may be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact visigoths@visigoths.org.
 *
 * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
 *    nor may "FreeMarker" or "Visigoth" appear in their names
 *    without prior written permission of the Visigoth Software Society.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Visigoth Software Society. For more
 * information on the Visigoth Software Society, please see
 * http://www.visigoths.org/
 */

options
{
   STATIC=false;
   UNICODE_INPUT=true;
   LEXER_CLASS="FMParserTokenManager";
   CONSTANTS_CLASS="FMParserConstants";
//   DEBUG_TOKEN_MANAGER=true;
//   DEBUG_PARSER=true;
}

PARSER_BEGIN(FMParser)

package freemarker.core;

import freemarker.template.*;
import freemarker.template.utility.StringUtil;
import freemarker.template.utility.DeepUnwrap;
import java.io.*;
import java.util.*;

/**
 * This class is generated by JavaCC from a grammar file.
 */
public class FMParser {

// Necessary for adding macros and setting location info.
  Template template;
  private String templateName;

// variables that keep track of whether we are in a loop or a switch.
  private int loopNesting, switchNesting;
  private boolean inMacro, inFunction, stripWhitespace, stripText;
  private LinkedList escapes = new LinkedList();
  private int contentNesting; // for stripText
  
  /**
   * Create an FM expression parser using a string.
   */
  static public FMParser createExpressionParser(String s) {
      SimpleCharStream scs = new SimpleCharStream(new StringReader(s), 1, 1, s.length());
      FMParserTokenManager token_source = new FMParserTokenManager(scs);
      token_source.SwitchTo(FMParserConstants.FM_EXPRESSION);
      return new FMParser(token_source);
  }
  
  /**
   * Constructs a new parser object.
   * @param template The template associated with this parser.
   * @param reader The character stream to use as input
   * @param strictEscapeSyntax Whether FreeMarker directives must start with a #
   */
  public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace) {
      this(reader);
      this.template = template;
      token_source.strictEscapeSyntax = strictEscapeSyntax;
      this.templateName = template != null ? template.getName() : "";
      token_source.templateName = templateName;
      this.stripWhitespace = stripWhitespace;
  }

  public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace, int tagSyntax) {
      this(template, reader, strictEscapeSyntax, stripWhitespace, tagSyntax, Configuration.PARSED_DEFAULT_INCOMPATIBLE_ENHANCEMENTS);
  }
  
  public FMParser(Template template, Reader reader, boolean strictEscapeSyntax, boolean stripWhitespace, int tagSyntax, int incompatibleChanges) {
      this(template, reader, strictEscapeSyntax, stripWhitespace);
      switch (tagSyntax) {
          case Configuration.AUTO_DETECT_TAG_SYNTAX : 
	     token_source.autodetectTagSyntax = true;
	     break;
	  case Configuration.ANGLE_BRACKET_TAG_SYNTAX :
	     token_source.altDirectiveSyntax = false;
	     break;
	  case Configuration.SQUARE_BRACKET_TAG_SYNTAX :
	     token_source.altDirectiveSyntax = true;
	     break;
	  default : throw new IllegalArgumentException("Illegal argument for tagSyntax");
      }
      token_source.incompatibleChanges = incompatibleChanges; 
  }
  
  public FMParser(String template) {
      this(null, new StringReader(template), true, true);
  }
  
  private String getErrorStart(Token t) {
      return "Error in template: " + template.getName()
            + "\non line " + t.beginLine + ", column " + t.beginColumn;
  }

  /**
   * Throw an exception if the expression passed in is a String
   * Literal
   */
  private void notStringLiteral(Expression exp, String expected) throws ParseException {
      if (exp instanceof StringLiteral) {
         String msg = "Error " + exp.getStartLocation()
                     + "\nFound string literal: " + exp
                     + "\nExpecting: " + expected;
         throw new ParseException(msg, exp);
      }
  }

  /**
   * Throw an exception if the expression passed in is a Number
   * Literal
   */
  private void notNumberLiteral(Expression exp, String expected) throws ParseException {
      if (exp instanceof NumberLiteral) {
         String msg = "Error " + exp.getStartLocation()
                     + "\nFound number literal: " + exp.getCanonicalForm()
                     + "\nExpecting " + expected;
         throw new ParseException(msg, exp);
      }
  }

  /**
   * Throw an exception if the expression passed in is a boolean
   * Literal
   */
  private void notBooleanLiteral(Expression exp, String expected) throws ParseException {
      if (exp instanceof BooleanLiteral) {
         String msg = "Error " + exp.getStartLocation()
                     + "\nFound: " + exp.getCanonicalForm()
                     + "\nExpecting " + expected;
         throw new ParseException(msg, exp);
      }
  }

  /**
   * Throw an exception if the expression passed in is a Hash
   * Literal
   */
  private void notHashLiteral(Expression exp, String expected) throws ParseException {
        if (exp instanceof HashLiteral) {
         String msg = "Error " + exp.getStartLocation()
                     + "\nFound hash literal: " + exp.getCanonicalForm()
                     + "\nExpecting " + expected;
         throw new ParseException(msg, exp);
      }
  }

  /**
   * Throw an exception if the expression passed in is a List
   * Literal
   */

  private void notListLiteral(Expression exp, String expected)
      throws ParseException
  {
        if (exp instanceof ListLiteral) {
         String msg = "Error " + exp.getStartLocation()
                     + "\nFound list literal: " + exp.getCanonicalForm()
                     + "\nExpecting " + expected;
         throw new ParseException(msg, exp);
      }
  }
  /**
   * Throw an exception if the expression passed in is a literal
   * other than of the numerical type
   */
  private void numberLiteralOnly(Expression 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(Expression exp) throws ParseException {
      notNumberLiteral(exp, "number");
      notListLiteral(exp, "number");
      notHashLiteral(exp, "number");
      notBooleanLiteral(exp, "number");
  }

  /**
   * Throw an exception if the expression passed in is a literal
   * other than of the boolean type
   */
  private void booleanLiteralOnly(Expression exp) throws ParseException {
      notStringLiteral(exp, "boolean (true/false)");
      notListLiteral(exp, "boolean (true/false)");
      notHashLiteral(exp, "boolean (true/false)");
      notNumberLiteral(exp, "boolean (true/false)");
  }

  private Expression escapedExpression(Expression exp) {
      if(!escapes.isEmpty()) {
          return ((EscapeBlock)escapes.getFirst()).doEscape(exp);
      }
      return exp;
  }
  
  private boolean getBoolean(Expression exp) throws ParseException {
      TemplateModel tm = null;
      try {
          tm = exp.getAsTemplateModel(null);
      } catch (Exception e) {
         throw new ParseException(e.getMessage() 
                                  + "\nCould not evaluate expression: "
                                  + exp.getCanonicalForm()
                                  + exp.getStartLocation(), exp);
      }
      if (tm instanceof TemplateBooleanModel) {
          try {
             return ((TemplateBooleanModel) tm).getAsBoolean();
          } catch (TemplateModelException tme) {
          }
      }
      if (tm instanceof TemplateScalarModel) {
          try {
              return StringUtil.getYesNo(((TemplateScalarModel) tm).getAsString());
          } catch (Exception e) {
              throw new ParseException(e.getMessage()
                                       + "\nExpecting yes/no, found: " + exp.getCanonicalForm()
                                       + exp.getStartLocation(), exp);
          }
      }
      throw new ParseException("Expecting boolean (yes/no) parameter" + exp.getStartLocation(), exp);
  }
}

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 :
{

/**
  The noparseTag is set when we enter
  a block of text that the parser more or less ignores.
  These are <noparse> and <comment>. This variable
  tells us what the closing tag should be, and when
  we hit that, we resume parsing. Note that with this
  scheme, <comment> and <noparse> tags cannot nest
  recursively, but it is not clear how important that is.
*/
    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 int hashLiteralNesting;
 private int parenthesisNesting;
 private int bracketNesting;
 private boolean inFTLHeader;
 boolean strictEscapeSyntax, 
         onlyTextOutput, 
         altDirectiveSyntax,
         autodetectTagSyntax,
         directiveSyntaxEstablished,
		 inInvocation;
 int incompatibleChanges;
 String templateName;

 
 // This method checks if we are in a strict mode where all
 // FreeMarker directives must start with <#. It also handled
 // tag syntax detection. If you update this logic, take a look
 // at the UNKNOWN_DIRECTIVE token too.
 private void strictSyntaxCheck(Token tok, int newLexState) {
     if (onlyTextOutput) {
        tok.kind = PRINTABLE_CHARS;
        return;
     }
     char firstChar = tok.image.charAt(0);
     if (autodetectTagSyntax && !directiveSyntaxEstablished) {
         altDirectiveSyntax = (firstChar == '[');
     }
     if ((firstChar == '[' && !altDirectiveSyntax) || (firstChar == '<' && altDirectiveSyntax)) {
        tok.kind = PRINTABLE_CHARS;
        return;
     }
     if (!strictEscapeSyntax) {
        SwitchTo(newLexState);
        return;
     }
     if (!altDirectiveSyntax) {
         if (!tok.image.startsWith("<#") && !tok.image.startsWith("</#")) {
            tok.kind = PRINTABLE_CHARS;
            return;
         }
     }
     directiveSyntaxEstablished = true;
     SwitchTo(newLexState);
 }
 
 private void unifiedCall(Token tok) {
     char firstChar = tok.image.charAt(0);
     if (autodetectTagSyntax && !directiveSyntaxEstablished) {
         altDirectiveSyntax = (firstChar == '[');
     }
     if (altDirectiveSyntax && firstChar == '<') {
        tok.kind = PRINTABLE_CHARS;
        return;
     } 
     if (!altDirectiveSyntax && firstChar == '[') {
        tok.kind = PRINTABLE_CHARS;
        return;
     }
     directiveSyntaxEstablished = true;
     SwitchTo(NO_SPACE_EXPRESSION);
 }
 
 private void unifiedCallEnd(Token tok) {
     char firstChar = tok.image.charAt(0);
     if (altDirectiveSyntax && firstChar == '<') {
        tok.kind = PRINTABLE_CHARS;
        return;
     } 
     if (!altDirectiveSyntax && firstChar == '[') {
        tok.kind = PRINTABLE_CHARS;
        return;
     }
 }
 
 private void closeBracket(Token tok) {
     if (bracketNesting >0) {
        --bracketNesting;
     } else {
        tok.kind=DIRECTIVE_END;
        if (inFTLHeader) {
           eatNewline(); 
           inFTLHeader = false; 
        }     
        SwitchTo(DEFAULT);     
     }
 }
 

 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) {
           altDirectiveSyntax = 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 == '[' && !altDirectiveSyntax) || (firstChar == '<' && altDirectiveSyntax)) {
          matchedToken.kind = PRINTABLE_CHARS;
       } 
       if (matchedToken.kind != PRINTABLE_CHARS) {
          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>)* ("/")? (">" | "]")>
    |
    <ATTEMPT : <START_TAG> "attempt" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <RECOVER : <START_TAG> "recover" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);} 
    |
    <IF : <START_TAG> "if" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <ELSE_IF : <START_TAG> "elseif" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <LIST : <START_TAG> "list" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <FOREACH : <START_TAG> "foreach" <BLANK>>  {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <SWITCH : <START_TAG> "switch" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <CASE : <START_TAG> "case" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <ASSIGN : <START_TAG> "assign" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <GLOBALASSIGN : <START_TAG> "global" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <LOCALASSIGN : <START_TAG> "local" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <_INCLUDE : <START_TAG> "include" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <IMPORT : <START_TAG> "import" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <FUNCTION : <START_TAG> "function" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <MACRO : <START_TAG> "macro" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <TRANSFORM : <START_TAG> "transform" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <VISIT : <START_TAG> "visit" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <STOP : <START_TAG> "stop" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <RETURN : <START_TAG> "return" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <CALL : <START_TAG> "call" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <SETTING : <START_TAG> "setting" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <COMPRESS : <START_TAG> "compress" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <COMMENT : <START_TAG> "comment" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, NO_PARSE); noparseTag="comment";}
    |
    <TERSE_COMMENT : ("<" | "[") "#--" > {noparseTag = "-->"; strictSyntaxCheck(matchedToken, NO_PARSE); }
    |
    <NOPARSE : <START_TAG> "noparse" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, NO_PARSE); noparseTag="noparse";}
    |
    <END_IF : <END_TAG> "if" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_LIST : <END_TAG> "list" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_RECOVER : <END_TAG> "recover" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_ATTEMPT : <END_TAG> "attempt" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_FOREACH : <END_TAG> "foreach" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_LOCAL : <END_TAG> "local" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_GLOBAL : <END_TAG> "global" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_ASSIGN : <END_TAG> "assign" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_FUNCTION : <END_TAG> "function" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_MACRO : <END_TAG> "macro" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_COMPRESS : <END_TAG> "compress" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_TRANSFORM : <END_TAG> "transform" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_SWITCH : <END_TAG> "switch" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <ELSE : <START_TAG> "else" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <BREAK : <START_TAG> "break" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <SIMPLE_RETURN : <START_TAG> "return" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <HALT : <START_TAG> "stop" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <FLUSH : <START_TAG> "flush" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <TRIM : <START_TAG> "t" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <LTRIM : <START_TAG> "lt" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <RTRIM : <START_TAG> "rt" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <NOTRIM : <START_TAG> "nt" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <DEFAUL : <START_TAG> "default" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <SIMPLE_NESTED : <START_TAG> "nested" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <NESTED : <START_TAG> "nested" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <SIMPLE_RECURSE : <START_TAG> "recurse" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <RECURSE : <START_TAG> "recurse" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <FALLBACK : <START_TAG> "fallback" <CLOSE_TAG2>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <ESCAPE : <START_TAG> "escape" <BLANK>> {strictSyntaxCheck(matchedToken, FM_EXPRESSION);}
    |
    <END_ESCAPE : <END_TAG> "escape" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <NOESCAPE : <START_TAG> "noescape" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, DEFAULT);}
    |
    <END_NOESCAPE : <END_TAG> "noescape" <CLOSE_TAG1>> {strictSyntaxCheck(matchedToken, 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);}
    |
    <UNKNOWN_DIRECTIVE : ("[#" | "[/#" | "<#" | "</#") (["a"-"z","A"-"Z", "_"])+>
    {
        if (!directiveSyntaxEstablished && incompatibleChanges < 2003019) {
            matchedToken.kind = PRINTABLE_CHARS;
        } else {
			char firstChar = matchedToken.image.charAt(0);
			
			if (!directiveSyntaxEstablished && autodetectTagSyntax) {
			    altDirectiveSyntax = (firstChar == '[');
			    directiveSyntaxEstablished = true;
			}
			
			if (firstChar == '<' && altDirectiveSyntax) {
			    matchedToken.kind = PRINTABLE_CHARS;
			} else if (firstChar == '[' && !altDirectiveSyntax) {
			    matchedToken.kind = PRINTABLE_CHARS;
			} else if (strictEscapeSyntax) {
			    String s = matchedToken.image;
			    int index = s.indexOf('#');
			    s = s.substring(index);
			    String msg = "Unknown directive: " 
			    + s
			    + " on line: " + matchedToken.beginLine
			    + ", column: " + matchedToken.beginColumn +1
			    + ", in template: " + templateName;
			    throw new TokenMgrError(msg, TokenMgrError.LEXICAL_ERROR);
			}
        }
    }
}

<DEFAULT, NODIRECTIVE> TOKEN :
{
    <WHITESPACE : (["\n", "\r", "\t", " "])+>
    |
    <PRINTABLE_CHARS : (~["$", "<", "#", "[", "{", "\n", "\r", "\t", " "])+>
    |
    <FALSE_ALERT : "$" | "#" | "<" | "[" | "{"> // to handle a lone dollar sign or "<" or "# or <@ with whitespace after"
    |
    <OUTPUT_ESCAPE : "${"> : FM_EXPRESSION
    |
    <NUMERICAL_ESCAPE : "#{"> : FM_EXPRESSION
}

<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 : "..">
  |
  <BUILT_IN : "?">
  |
  <EXISTS : "??">
  |
  <EQUALS : "=">
  |
  <DOUBLE_EQUALS : "==">
  |
  <NOT_EQUALS : "!=">
  |
  <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);
     }
  }
  |
  <OPEN_BRACE : "{" > {++hashLiteralNesting;}
  |
  <CLOSE_BRACE : "}" >
  {
     if (hashLiteralNesting == 0)
        SwitchTo(DEFAULT);
     else
        --hashLiteralNesting;
  }
  |
  < IN : "in">
  |
  < AS : "as">
  |
  < USING : "using">
  |
  < ID: <LETTER> (<LETTER>|<DIGIT>)* >
  |
  < #LETTER:
      [
       "\u0024",
       "\u0040"-"\u005a",
       "\u005f",
       "\u0061"-"\u007a",
       "\u00c0"-"\u00d6",
       "\u00d8"-"\u00f6",
       "\u00f8"-"\u00ff",
       "\u0100"-"\u1fff",
       "\u3040"-"\u318f",
       "\u3300"-"\u337f",
       "\u3400"-"\u3d2d",
       "\u4e00"-"\u9fff",
       "\uf900"-"\ufaff"
      ]
  >
  |
  < #DIGIT:
      [
       "\u0030"-"\u0039",
       "\u0660"-"\u0669",
       "\u06f0"-"\u06f9",
       "\u0966"-"\u096f",
       "\u09e6"-"\u09ef",
       "\u0a66"-"\u0a6f",
       "\u0ae6"-"\u0aef",
       "\u0b66"-"\u0b6f",
       "\u0be7"-"\u0bef",
       "\u0c66"-"\u0c6f",
       "\u0ce6"-"\u0cef",
       "\u0d66"-"\u0d6f",
       "\u0e50"-"\u0e59",
       "\u0ed0"-"\u0ed9",
       "\u1040"-"\u1049"
      ]
  >
}

<FM_EXPRESSION, NO_SPACE_EXPRESSION, NAMED_PARAMETER_EXPRESSION> TOKEN :
{
  <DIRECTIVE_END : ">">
    {
        if (inFTLHeader) eatNewline();
        inFTLHeader = false;
        if (altDirectiveSyntax) {
            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 ((altDirectiveSyntax && squareBracket) || (!altDirectiveSyntax && !squareBracket))
	      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)) {
            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 OrExpression, since
 * the OR is the operator with the lowest
 * precedence.
 */

Expression Expression() :
{
   Expression exp;
}
{
   exp=OrExpression()
   {
      return exp;
   }
}

/**
 * Lowest level expression, a literal, a variable,
 * or a possibly more complex expression bounded
 * by parentheses.
 */
Expression PrimaryExpression() :
{
   Expression exp;
}
{
   (
      exp=NumberLiteral()
      |
      exp=HashLiteral()
      |
      exp=StringLiteral(true)
      |
      exp=BooleanLiteral()
      |
      exp=ListLiteral()
      |
      exp=Identifier()
      |
      exp=Parenthesis()
      |
      exp=BuiltinVariable()
  )
  (
      LOOKAHEAD(<DOT>
                |<OPEN_BRACKET>
                |<OPEN_PAREN>
                |<BUILT_IN>
                |<EXCLAM>
		 |<TERMINATING_EXCLAM>
                |<EXISTS>)
      exp=AddSubExpression(exp)
  )*
  {
     return exp;
  }
}

Expression Parenthesis() :
{
   Expression exp, result;
   Token start, end;
}
{
    start=<OPEN_PAREN>
    exp=Expression()
    end=<CLOSE_PAREN>
    {
       result = new ParentheticalExpression(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.)
 */
Expression UnaryExpression() :
{
   Expression exp, result;
   boolean haveNot = false;
   Token t = null, start=null;
}
{
  (
   result=UnaryPlusMinusExpression()
   |
   result=NotExpression()
   |
   result=PrimaryExpression()
  )
  {
     return result;
  }
/*   
   (
     t=<EXCLAM>
     {
        haveNot = !haveNot;
        if (start == null)
            start = t;
     }
   )*
   exp=PrimaryExpression()
   {
      result = exp;
      if (haveNot) {
          booleanLiteralOnly(exp);
          result = new NotExpression(exp);
          result.setLocation(template, start, exp);
      }
      return result;
   }
*/   
}

Expression NotExpression() : 
{
   Token t;
   Expression exp, result=null;
   ArrayList nots = new ArrayList();
}
{
   (
      t=<EXCLAM> {nots.add(t);}
   )+
   exp=PrimaryExpression()
   {
      for (int i=0; i<nots.size(); i++) {
         result = new NotExpression(exp);
         Token tok = (Token) nots.get(nots.size() -i -1);
         result.setLocation(template, tok, exp);
         exp = result;
      }
      return result;
   }
}

Expression UnaryPlusMinusExpression() :
{
   Expression exp, result;
   boolean isMinus = false;
   Token t;
}
{
   (
      t=<PLUS>
      |
      t=<MINUS> {isMinus = true;}
   )
   exp=PrimaryExpression()
   {
      result = new UnaryPlusMinusExpression(exp, isMinus);  
      result.setLocation(template, t, exp);
      return result;
   }
}

Expression AdditiveExpression() :
{
   Expression 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 AddConcatExpression(lhs, rhs);
         }
         else {
             numberLiteralOnly(lhs);
             numberLiteralOnly(rhs);
             result = new ArithmeticExpression(lhs,
                                            rhs,
                                            ArithmeticExpression.SUBSTRACTION);
         }
         result.setLocation(template, lhs, rhs);
         lhs = result;
      }
   )*
   {
      return result;
   }
}

/**
 * A unary expression followed by zero or more
 * unary expressions with operators in between.
 */
Expression MultiplicativeExpression() :
{
   Expression lhs, rhs, result;
   int operation = ArithmeticExpression.MULTIPLICATION;
}
{
   lhs=UnaryExpression() {result = lhs;}
   (
      LOOKAHEAD(<TIMES>|<DIVIDE>|<PERCENT>)
      (
        (
         <TIMES> {operation = ArithmeticExpression.MULTIPLICATION;}
         |
         <DIVIDE> {operation = ArithmeticExpression.DIVISION;}
         |
         <PERCENT>{operation = ArithmeticExpression.MODULUS;}
        )
      )
      rhs=UnaryExpression()
      {
         numberLiteralOnly(lhs);
         numberLiteralOnly(rhs);
         result = new ArithmeticExpression(lhs, rhs, operation);
         result.setLocation(template, lhs, rhs);
         lhs = result;
      }
   )*
   {
      return result;
   }
}


Expression EqualityExpression() :
{
   Expression 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 ComparisonExpression(lhs, rhs, t.image);
        result.setLocation(template, lhs, rhs);
     }
   ]
   {
      return result;
   }
}

Expression RelationalExpression() :
{
   Expression 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 ComparisonExpression(lhs, rhs, t.image);
        result.setLocation(template, lhs, rhs);
     }
   ]
   {
      return result;
   }
}

Expression RangeExpression() :
{
   Expression lhs, rhs=null, result;
}
{
    lhs=AdditiveExpression() {result = lhs;}
    [
      LOOKAHEAD(<DOT_DOT>)
      <DOT_DOT>
       [
        LOOKAHEAD(Expression())
        rhs=AdditiveExpression()
       ]
       {
           numberLiteralOnly(lhs);
           if (rhs != null) {
               numberLiteralOnly(rhs);
           }
           Range range = new Range(lhs, rhs);
           if (rhs != null) {
               range.setLocation(template, lhs, rhs);
           } else {
              range.setLocation(template, lhs, lhs);
           }
           result = range;
        }
    ]
    {
        return result;
    }
}




Expression AndExpression() :
{
   Expression lhs, rhs, result;
}
{
   lhs=EqualityExpression() {result = lhs;}
   (
      LOOKAHEAD(<AND>)
      <AND>
      rhs=EqualityExpression()
      {
         booleanLiteralOnly(lhs);
         booleanLiteralOnly(rhs);
         result = new AndExpression(lhs, rhs);
         result.setLocation(template, lhs, rhs);
         lhs = result;
      }
   )*
   {
      return result;
   }
}

Expression OrExpression() :
{
   Expression lhs, rhs, result;
}
{
   lhs=AndExpression() {result = lhs;}
   (
     LOOKAHEAD(<OR>)
      <OR>
      rhs=AndExpression()
      {
         booleanLiteralOnly(lhs);
         booleanLiteralOnly(rhs);
         result = new OrExpression(lhs, rhs);
         result.setLocation(template, lhs, rhs);
         lhs = result;
      }
   )*
   {
      return result;
   }
}

ListLiteral ListLiteral() :
{
   ArrayList values = new ArrayList();
   Token begin, end;
}
{
    begin=<OPEN_BRACKET>
    values=PositionalArgs()
    end=<CLOSE_BRACKET>
    {
        ListLiteral result = new ListLiteral(values);
        result.setLocation(template, begin, end);
        return result;
    }
}

Expression NumberLiteral() :
{
   Token op = null, t;
}
{
   (
      t=<INTEGER>
      |
      t=<DECIMAL>
   )
   {
       String s = t.image;
       Expression result = new NumberLiteral(template.getArithmeticEngine().toNumber(s));
       Token startToken = (op != null) ? op : t;
       result.setLocation(template, startToken, t);
       return result;
   }
}

Identifier Identifier() :
{
    Token t;
}
{
    t=<ID>
    {
        Identifier id = new Identifier(t.image);
        id.setLocation(template, t, t);
        return id;
    }
}

Expression IdentifierOrStringLiteral() :
{
   Expression exp;
}
{
   (
      exp=Identifier()
      |
      exp=StringLiteral(false)
   )
   {
      return exp;
   }   
}

BuiltinVariable BuiltinVariable() :
{
   Token dot, name;
}
{
   dot=<DOT>
   name=<ID>
   {
      BuiltinVariable result = null;
      try {
          result = new BuiltinVariable(name.image);
      } catch (ParseException pe) {
          pe.lineNumber = dot.beginLine;
          pe.columnNumber = dot.beginColumn;
          throw pe;
      }
      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.
 */
Expression AddSubExpression(Expression exp) :
{
   Expression result = null;
}
{
     (
       result=DotVariable(exp)
       |
       result=DynamicKey(exp)
       |
       result=MethodArgs(exp)
       |
       result=BuiltIn(exp)
       |
       result=DefaultTo(exp)
       |
       result=Exists(exp)
     )
     {
        return result;
     }
}

Expression DefaultTo(Expression exp) :
{
   Expression rhs = null;
   Token t;
}
{
   (
      t=<TERMINATING_EXCLAM>
      |
      (
        t=<EXCLAM>
        [
          LOOKAHEAD(Expression())
            rhs=Expression()
        ]
      )
   )
   {
      DefaultToExpression result = new DefaultToExpression(exp, rhs);
      if (rhs ==null) {
          result.setLocation(template, exp, t);
      }
      else {
          result.setLocation(template, exp, rhs);
      }
      return result;
   }
}

Expression Exists(Expression exp) :
{
   Token t;
}
{
   t=<EXISTS>
   {
       ExistsExpression result = new ExistsExpression(exp);
       result.setLocation(template, exp, t);
       return result;
   }
}

Expression BuiltIn(Expression exp) :
{
   Token t=null;
}
{
   <BUILT_IN>
   t=<ID>
   {
       BuiltIn result = null;
       try {
           result = BuiltIn.newBuiltIn(exp, t.image, t, templateName);
       } catch (ParseException pe) {
           pe.lineNumber = t.beginLine;
           pe.columnNumber = t.beginColumn;
           throw pe;
       }
       result.setLocation(template, exp, t);
       return result;
   }
}


/**
 * production for when a key is specified by <DOT> + keyname
 */
Expression DotVariable(Expression 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))) {
                String msg = getErrorStart(t)
                            + "\n" + t.image + " is not a valid identifier.";
                throw new ParseException(msg, t.beginLine, t.beginColumn);
            }
        }
     )
     {
         notListLiteral(exp, "hash");
         notStringLiteral(exp, "hash");
         notBooleanLiteral(exp, "hash");
         Dot dot = new Dot(exp, t.image);
         dot.setLocation(template, exp, t);
         return dot;
     }
}

/**
 * production for when the key is specified
 * in brackets.
 */
Expression DynamicKey(Expression exp) :
{
   Expression arg;
   Token t;
}
{
   <OPEN_BRACKET>
   arg=Expression()
   t=<CLOSE_BRACKET>
   {
       notBooleanLiteral(exp, "list or hash");
       notNumberLiteral(exp, "list or hash");
       DynamicKeyName dkn = new DynamicKeyName(exp, arg);
       dkn.setLocation(template, exp, t);
       return dkn;
   }
}

/**
 * production for an arglist part of a method invocation.
 */
MethodCall MethodArgs(Expression exp) :
{
     ArrayList args = new ArrayList();
     Token end;
}
{
     <OPEN_PAREN>
     args=PositionalArgs()
     end=<CLOSE_PAREN>
     {
        args.trimToSize();
        MethodCall result = new MethodCall(exp, args);
        result.setLocation(template, exp, end);
        return result;
     }
}

StringLiteral StringLiteral(boolean interpolate) :
{
  Token t;
  boolean raw = false;
}
{
   (
     t=<STRING_LITERAL>
     |
     t=<RAW_STRING> {raw = true;}
   )
   {
       String s = t.image;
       // Get rid of the quotes.
       s = s.substring(1, s.length() -1);
       if (raw) {
           s=s.substring(1);
       }
       else try {
          s = StringUtil.FTLStringLiteralDec(s);
       } catch (ParseException pe) {
          pe.lineNumber = t.beginLine;
          pe.columnNumber = t.beginColumn;
          throw pe;
       }
       StringLiteral result = new StringLiteral(s);
       result.setLocation(template, t, t);
       if (interpolate && !raw) {
           if (t.image.indexOf("${") >=0 || t.image.indexOf("#{") >=0)
              result.checkInterpolation();
       }
       return result;
   }
}

Expression BooleanLiteral() :
{
   Token t;
   Expression result;
}
{
   (
       t=<FALSE> {result = new BooleanLiteral(false);}
       |
       t=<TRUE> {result = new BooleanLiteral(true);}
   )
   {
       result.setLocation(template, t, t);
       return result;
   }
}


HashLiteral HashLiteral() :
{
   Token begin, end;
   Expression key, value;
   ArrayList keys = new ArrayList();
   ArrayList values = new ArrayList();
}
{
   begin=<OPEN_BRACE>
   [
       key=Expression()
       (<COMMA>|<COLON>)
       value=Expression()
       {
          stringLiteralOnly(key);
          keys.add(key);
          values.add(value);
       }
       (
          <COMMA>
           key=Expression()
           (<COMMA>|<COLON>)
           value=Expression()
           {
               stringLiteralOnly(key);
               keys.add(key);
               values.add(value);
           }
       )*
   ]
   end=<CLOSE_BRACE>
   {
      HashLiteral result = new HashLiteral(keys, values);
      result.setLocation(template, begin, end);
      return result;
   }
}

/**
 * A production representing the ${...}
 * that outputs a variable.
 */
DollarVariable StringOutput() :
{
   Expression exp;
   Token begin, end;
}
{
   begin=<OUTPUT_ESCAPE>
   exp=Expression()
   {
      notHashLiteral(exp, "scalar");
      notListLiteral(exp, "scalar");
      notBooleanLiteral(exp, "scalar");
   }
   end=<CLOSE_BRACE>
   {
      DollarVariable result = new DollarVariable(exp, escapedExpression(exp));
      result.setLocation(template, begin, end);
      return result;
   }
}

NumericalOutput NumericalOutput() :
{
   Expression exp;
   Token fmt = null, begin, end;
}
{
   begin=<NUMERICAL_ESCAPE>
   exp=Expression() {numberLiteralOnly(exp);}
   [
      <SEMICOLON>
      fmt=<ID>
   ]
   end=<CLOSE_BRACE>
   {
      NumericalOutput 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", fmt.beginLine, fmt.beginColumn);
                       minFrac = Integer.parseInt(token);
                       break;
                    case 'M':
                       if (maxFrac != -1) throw new ParseException("invalid formatting string", fmt.beginLine, fmt.beginColumn);
                       maxFrac = Integer.parseInt(token);
                       break;
                    default:
                       throw new ParseException();
                    }
                    type = '-';
                } else if (token.equals("m")) {
                   type = 'm';
                } else if (token.equals("M")) {
                   type = 'M';
                } else {
                   throw new ParseException();
                }
             }
             catch (ParseException e) {
                String msg = getErrorStart(fmt)
                      + "\nInvalid format specifier "
                      + fmt.image;
                throw new ParseException(msg, fmt.beginLine, fmt.beginColumn);
             }
             catch (NumberFormatException e) {
                String msg = getErrorStart(fmt)
                      + "\nInvalid number in the format specifier "
                      + fmt.image;
                throw new ParseException(msg, fmt.beginLine, fmt.beginColumn);
             }
         }

         if (maxFrac == -1) {
            if (minFrac == -1) {
               String msg = getErrorStart(fmt)
                     + "\nInvalid format specification, at least one of m and M must be specified!";
               throw new ParseException(msg, fmt.beginLine, fmt.beginColumn);
            }
            maxFrac = minFrac;
         } else if (minFrac == -1) {
            minFrac = 0;
         }
         if (minFrac > maxFrac) {
            String msg = getErrorStart(fmt)
                  + "\nInvalid format specification, min cannot be greater than max!";
            throw new ParseException(msg, fmt.beginLine, fmt.beginColumn);
         }
         if (minFrac > 50 || maxFrac > 50) {// sanity check
               String msg = getErrorStart(fmt)
               + "\nCannot specify more than 50 fraction digits";
             throw new ParseException(msg, fmt.beginLine, fmt.beginColumn);
         }
         result = new NumericalOutput(exp, minFrac, maxFrac);
      } else {  // if format != null
         result = new NumericalOutput(exp);
      }
      result.setLocation(template, begin, end);
      return result;
   }
}

TemplateElement If() :
{
    Token start, end, t;
    Expression condition;
    TemplateElement block;
    IfBlock ifBlock;
    ConditionalBlock cblock;
}
{
   start=<IF>
   condition=Expression()
   <DIRECTIVE_END>
   block=OptionalBlock()
   {
      cblock = new ConditionalBlock(condition, block, true);
      cblock.setLocation(template, start, block);
      ifBlock = new IfBlock(cblock);
   }
   (
       t=<ELSE_IF>
       condition=Expression()
       LooseDirectiveEnd()
       block=OptionalBlock()
       {
          cblock = new ConditionalBlock(condition, block, false);
          cblock.setLocation(template, t, block);
          ifBlock.addBlock(cblock);
       }
   )*
   [
         t=<ELSE>
         block=OptionalBlock()
         {
            cblock = new ConditionalBlock(null, block, false);
            cblock.setLocation(template, t, block);
            ifBlock.addBlock(cblock);
         }
   ]
   end=<END_IF>
   {
       ifBlock.setLocation(template, start, end);
       return ifBlock;
   }
}

AttemptBlock Attempt() :
{
   Token start, end;
   TemplateElement block, recoveryBlock;
}
{
   start=<ATTEMPT>
   block=OptionalBlock()
   recoveryBlock=Recover()
   (
      end=<END_RECOVER>
      |
      end=<END_ATTEMPT>
   )
   {
      AttemptBlock result = new AttemptBlock(block, recoveryBlock);
      result.setLocation(template, start, end);
      return result;
   }
}

RecoveryBlock Recover() : 
{
   Token start;
   TemplateElement block;
}
{
   start=<RECOVER>
   block=OptionalBlock()
   {
      RecoveryBlock result = new RecoveryBlock(block);
      result.setLocation(template, start, block);
      return result;
   }
}

IteratorBlock List() :
{
    Expression exp;
    Token index, start, end;
    TemplateElement block;
}
{
   start=<LIST> {++loopNesting;}
   exp=Expression()
   <AS>
   index=<ID>
   <DIRECTIVE_END>
   block=OptionalBlock()
   end=<END_LIST>
   {
     --loopNesting;
     IteratorBlock result = new IteratorBlock(exp,
                                              index.image,
                                              block,
                                              false);
     result.setLocation(template, start, end);
     return result;
   }
}

IteratorBlock ForEach() :
{
     Expression exp;
     Token index, start, end;
     TemplateElement block;
}
{
   start=<FOREACH> {++loopNesting;}
   index=<ID>
   <IN>
   exp=Expression()
   <DIRECTIVE_END>
   block=OptionalBlock()
   end=<END_FOREACH>
   {
     --loopNesting;
     IteratorBlock result = new IteratorBlock(exp,
                                              index.image,
                                              block,
                                              true);
     result.setLocation(template, start, end);
     return result;
   }
}

VisitNode Visit() :
{
   Token start, end;
   Expression targetNode, namespaces=null;
}
{
   start=<VISIT>
   targetNode=Expression()
   [
       <USING>
       namespaces=Expression()
   ]
   end=LooseDirectiveEnd()
   {
       VisitNode result = new VisitNode(targetNode, namespaces);
       result.setLocation(template, start, end);
       return result;
   }
}

RecurseNode Recurse() :
{
   Token start, end = null;
   Expression node=null, namespaces=null;
}
{
   (
      start=<SIMPLE_RECURSE>
      |
      (
         start=<RECURSE>
         [
            node=Expression()
         ]
         [
            <USING>
            namespaces=Expression()
         ]
         end=LooseDirectiveEnd()
      )
   )
   {
       if (end == null) end = start;
       RecurseNode result = new RecurseNode(node, namespaces);
       result.setLocation(template, start, end);
       return result;
   }
}

FallbackInstruction FallBack() :
{
   Token tok;
}
{
   tok=<FALLBACK>
   {
      if (!inMacro) {
          throw new ParseException(getErrorStart(tok)
	                           + "\nCannot fall back "
				   + " outside a macro.",
                                   tok.beginLine, tok.beginColumn);
      
      }
      FallbackInstruction result = new FallbackInstruction();
      result.setLocation(template, tok, tok);
      return result;
   }
}

/**
 * Production used to break out of a loop or a switch block.
 */
BreakInstruction Break() :
{
   Token start;
}
{
    start=<BREAK>
    {
       if (loopNesting < 1 && switchNesting <1)
       {
          String msg = getErrorStart(start) + "\n"
                       + start.image
                       + " occurred outside a loop or a switch block.";
          throw new ParseException(msg, start.beginLine, start.beginColumn);
       }
       BreakInstruction result = new BreakInstruction();
       result.setLocation(template, start, start);
       return result;
    }
}

/**
 * Production used to jump out of a macro.
 * The stop instruction terminates the rendering of the template.
 */
ReturnInstruction Return() :
{
   Token start, end=null;
   Expression exp = null;
}
{
   (
      start=<SIMPLE_RETURN>{end = start;}
      |
      start=<RETURN> exp=Expression() end=LooseDirectiveEnd()
   )
   {
      if (inMacro) {
         if (exp != null) {
            String msg = getErrorStart(start) 
                         + "\nA macro cannot return a value";
            throw new ParseException(msg, start.beginLine, start.beginColumn);
         }
      }
      else if (inFunction) {
         if (exp == null) {
            String msg = getErrorStart(start) 
                         + "\nA function must return a value";
            throw new ParseException(msg, start.beginLine, start.beginColumn);
         }
      }
      else {
         if (exp == null) {
            String msg = getErrorStart(start) 
                         + "\nA return instruction can only occur inside a macro of function";
            throw new ParseException(msg, start.beginLine, start.beginColumn);
         }
      }
      ReturnInstruction result = new ReturnInstruction(exp);
      result.setLocation(template, start, end);
      return result;
   }
}

StopInstruction Stop() :
{
   Token start = null;
   Expression exp = null;
}
{
    (
      start=<HALT>
      |
      start=<STOP> exp=Expression() LooseDirectiveEnd()
    )
    {
       StopInstruction result = new StopInstruction(exp);
       result.setLocation(template, start, start);
       return result;
    }
}

TemplateElement Nested() :
{
  Token t, end;
  ArrayList bodyParameters;
  BodyInstruction result = null;
}
{
   (
      (
         t=<SIMPLE_NESTED>
         {
            result = new BodyInstruction(null);
            result.setLocation(template, t, t);
         }
      )
      |
      (
          t=<NESTED>
          bodyParameters=PositionalArgs()
          end=LooseDirectiveEnd()
          {
             result = new BodyInstruction(bodyParameters);
             result.setLocation(template, t, end);
          }
      )
   )
   {
       if (!inMacro) {
          throw new ParseException(getErrorStart(t)
	                           + "\nCannot use a "
				   + t.image
				   + " instruction outside a macro.",
                                   t.beginLine, t.beginColumn);
       }
       return result;
   }
}

TemplateElement Flush() :
{
  Token t;
}
{
   t=<FLUSH>
   {
       FlushInstruction result = new FlushInstruction();
       result.setLocation(template, t, t);
       return result;
   }
}

TemplateElement Trim() :
{
  Token t;
  TrimInstruction result=null;
}
{
   (
     t=<TRIM> {result = new TrimInstruction(true, true);}
     |
     t=<LTRIM> {result = new TrimInstruction(true, false);}
     |
     t=<RTRIM> {result = new TrimInstruction(false, true);}
     |
     t=<NOTRIM> {result = new TrimInstruction(false, false);}
   )
   {
       result.setLocation(template, t, t);
       return result;
   }
}


TemplateElement Assign() :
{
   Token start, end;
   int scope;
   Token id=null;
   Expression nameExp, exp, nsExp=null;
   String varName;
   ArrayList assignments = new ArrayList();
   Assignment ass;
   TemplateElement block;
}
{
    (
      start=<ASSIGN> {scope = Assignment.NAMESPACE;}
      |
      start=<GLOBALASSIGN>{scope = Assignment.GLOBAL;}
      |
      start=<LOCALASSIGN> {scope = Assignment.LOCAL;}
      {
          scope = Assignment.LOCAL;
          if (!inMacro && !inFunction) {
             String msg = getErrorStart(start)
                          + "\nLocal variable assigned outside a macro.";
             throw new ParseException(msg, start.beginLine, start.beginColumn);
          }
      }
    )
    nameExp=IdentifierOrStringLiteral()
    {
       varName = (nameExp instanceof StringLiteral) ? ((StringLiteral) nameExp).getAsString() : nameExp.toString();
    }
    ((
       <EQUALS>
       exp=Expression()
       {
          ass = new Assignment(varName, exp, scope);
          ass.setLocation(template, nameExp, exp);
          assignments.add(ass);
       }
       (
         LOOKAHEAD([<COMMA>](<ID>|<STRING_LITERAL>)<EQUALS>)
            [<COMMA>]
            nameExp=IdentifierOrStringLiteral()
            {
               varName = (nameExp instanceof StringLiteral) ? ((StringLiteral) nameExp).getAsString() : nameExp.toString();
            }
            <EQUALS>
            exp=Expression()
            {
               ass = new Assignment(varName, exp, scope);
               ass.setLocation(template, nameExp, exp);
               assignments.add(ass);
           } 
       )*
       [
          id=<IN>
          nsExp=Expression() {if (scope != Assignment.NAMESPACE) throw new ParseException(getErrorStart(id) + "\nCannot assign to namespace here.", id.beginLine, id.beginColumn);}
       ]
       end=LooseDirectiveEnd()
       {
           AssignmentInstruction ai = new AssignmentInstruction(scope);
           for (int i = 0; i< assignments.size(); i++) {
                ai.addAssignment((Assignment) assignments.get(i));
           }
           ai.setNamespaceExp(nsExp);
           ai.setLocation(template, start, end);
           return ai;
       }
    )
    |
    (
       [
          id=<IN>
          nsExp=Expression() {if (scope != Assignment.NAMESPACE) throw new ParseException(getErrorStart(id) + "\nCannot assign to namespace here.", id.beginLine, id.beginColumn);}
       ]
       <DIRECTIVE_END>
       block=OptionalBlock()
       (
          end=<END_LOCAL> {if (scope != Assignment.LOCAL) throw new ParseException(getErrorStart(end) + "\nMismatched assignment tags.", end.beginLine, end.beginColumn);}
          |
          end=<END_ASSIGN> {if (scope != Assignment.NAMESPACE) throw new ParseException(getErrorStart(end) + "\nMismatched assignment tags.", end.beginLine, end.beginColumn);}
          |
          end=<END_GLOBAL> {if (scope != Assignment.GLOBAL) throw new ParseException(getErrorStart(end) + "\nMismatched assignment tags", end.beginLine, end.beginColumn);}
       )
       {
          BlockAssignment ba = new BlockAssignment(block, varName, scope, nsExp);
          ba.setLocation(template, start, end);
          return ba;
       }
    ))
}

Include Include() :
{
   Expression nameExp;
   Token att, start, end;
   Expression exp, parseExp = null, encodingExp = null;
}
{
    start=<_INCLUDE>
    nameExp=Expression()
    [<SEMICOLON>]
    (
	  att=<ID>
	  <EQUALS>
	  exp=Expression()
	  {
	     String attString = att.image;
	     if (attString.equalsIgnoreCase("parse")) {
	        parseExp = exp;
	     }
	     else if (attString.equalsIgnoreCase("encoding")) {
	        encodingExp = exp;
	     }
	     else {
	         String msg = getErrorStart(att)
	               + "\nexpecting parse= or encoding= to be specified.";
	         throw new ParseException(msg, att.beginLine, att.beginColumn);
	     }
	  }
	)*
    end=LooseDirectiveEnd()
    {
       Include result = new Include(template, nameExp, encodingExp, parseExp);
       result.setLocation(template, start, end);
       return result;
    }
}

LibraryLoad Import() :
{
   Token start, end, ns;
   Expression nameExp;
}
{
   start=<IMPORT>
   nameExp=Expression()
   <AS>
   ns=<ID>
   end=LooseDirectiveEnd()
   {
       LibraryLoad result = new LibraryLoad(template, nameExp, ns.image);
       result.setLocation(template, start, end);
       template.addImport(result);
       return result;
   }
}

Macro Macro() :
{
   Token arg, start, end;
   Expression nameExp;
   String name;
   ArrayList argNames = new ArrayList();
   HashMap args = new HashMap();
   ArrayList defNames = new ArrayList();
   Expression defValue=null;
   TemplateElement block;
   boolean isFunction = false, hasDefaults=false;
   boolean isCatchAll = false;
   String catchAll = null;
}
{
    (
        start=<MACRO>
        |
        start=<FUNCTION> {isFunction = true;}
    )
    {
       if (inMacro || inFunction) {
         throw new ParseException(getErrorStart(start)
                     + "\nMacros cannot be nested.", start.beginLine, start.endLine);
       }
       if (isFunction) inFunction = true; else inMacro = true;
    }
    nameExp=IdentifierOrStringLiteral()
    {
       name = (nameExp instanceof StringLiteral) ? ((StringLiteral) nameExp).getAsString() : nameExp.toString();
    }
    [<OPEN_PAREN>]
    (
          arg=<ID> {defValue = null;}
          [<ELLIPSIS> { isCatchAll = true; }]
          [
            <EQUALS>
          	defValue=Expression()
            {
              defNames.add(arg.image);
	      hasDefaults = true;
            }
          ]
          [<COMMA>]
	  {
          if (catchAll != null) {
              throw new ParseException(getErrorStart(arg)
                + "\nThere may only be one \"catch-all\" parameter in a macro declaration, "
                + "and it must be the last parameter.", arg.beginLine, arg.endLine);
          }
          if (isCatchAll) {
              if (defValue != null) {
                  throw new ParseException(getErrorStart(arg)
                    + "\n\"Catch-all\" macro parameter may not have a default value.",
                    arg.beginLine, arg.endLine);
              }
              catchAll = arg.image;
          } else {
              argNames.add(arg.image);
              if (hasDefaults && defValue == null) {
                  throw new ParseException(getErrorStart(arg)
                    + "\nIn a macro declaration, parameters without a default value "
                    + "must all occur before the parameters with default values.",
                    arg.beginLine, arg.endLine);
              }
              args.put(arg.image, defValue);
          }
	  }
    )*
    [<CLOSE_PAREN>]
    <DIRECTIVE_END>
    block=OptionalBlock()
    (
        end=<END_MACRO> { if(isFunction) throw new ParseException(getErrorStart(start) + "\nExpected function end tag here.", start.beginLine, start.endLine); }
        |
        end=<END_FUNCTION> { if(!isFunction) throw new ParseException(getErrorStart(start) + "\nExpected macro end tag here.", start.beginLine, start.endLine); }
    )
    {
       inMacro = inFunction = false;
       Macro result = new Macro(name, argNames, args, block);
       result.setCatchAll(catchAll);
       result.isFunction = isFunction;
       result.setLocation(template, start, end);
       template.addMacro(result);
       return result;
    }
}


CompressedBlock Compress() :
{
   TemplateElement block;
   Token start, end;
}
{
    start=<COMPRESS>
    block=OptionalBlock()
    end=<END_COMPRESS>
    {
       CompressedBlock cb = new CompressedBlock(block);
       cb.setLocation(template, start, end);
       return cb;
    }
}

TemplateElement UnifiedMacroTransform() :
{
   Token start=null, end, t;
   HashMap namedArgs = null;
   ArrayList positionalArgs = null, bodyParameters = null;
   String directiveName = null;
   TemplateElement nestedBlock = null;
   Expression exp;
}
{
    start=<UNIFIED_CALL>
    exp=Expression()
    {
        if (exp instanceof Identifier || (exp instanceof Dot && ((Dot) exp).onlyHasIdentifiers())) {
	   directiveName = exp.getCanonicalForm();
	}
    }
    [<TERMINATING_WHITESPACE>]
    (
       LOOKAHEAD(<ID><EQUALS>)
       namedArgs = NamedArgs()
       |
       positionalArgs=PositionalArgs()
    )
    [
       <SEMICOLON>{bodyParameters = new ArrayList();}
       [
           [<TERMINATING_WHITESPACE>]t=<ID> {bodyParameters.add(t.image);}
           (
               [<TERMINATING_WHITESPACE>]<COMMA>
               [<TERMINATING_WHITESPACE>]t = <ID>{bodyParameters.add(t.image);}
           )*
       ]
    ]
    (
      end=<EMPTY_DIRECTIVE_END>
      |
      (
        <DIRECTIVE_END>
	nestedBlock=OptionalBlock()
	end=<UNIFIED_CALL_END>
	{
	   String s = end.image.substring(3);
	   s = s.substring(0, s.length() -1).trim();
	   if (s.length() >0 && !s.equals(directiveName)) {
              String msg = getErrorStart(end);
	      if (directiveName == null) {
                    throw new ParseException(msg + "\nExpecting </@>", end.beginLine, end.beginColumn);
	      }
	      else {
	          throw new ParseException(msg + "\nExpecting </@> or </@" + directiveName + ">", end.beginLine, end.beginColumn);
	      }
	   }
	}
      )
    )
    {
       TemplateElement result = (positionalArgs != null) ? new UnifiedCall(exp, positionalArgs, nestedBlock, bodyParameters)
                                         : new UnifiedCall(exp, namedArgs, nestedBlock, bodyParameters);
       result.setLocation(template, start, end);
       return result;
    }
}

TemplateElement Call() :
{
   Token start, end, id;
   HashMap namedArgs = null;
   ArrayList positionalArgs = null;
   String macroName= null;
}
{
    start=<CALL>
    id=<ID> {macroName = id.image;}
    (
       LOOKAHEAD(<ID><EQUALS>)
       namedArgs=NamedArgs()
       |
       (
          [
	    LOOKAHEAD(<OPEN_PAREN>)
	       <OPEN_PAREN>
	  ]
	  positionalArgs=PositionalArgs()
	  [<CLOSE_PAREN>]
       )
    )
    end=LooseDirectiveEnd()
    {
       UnifiedCall result = null;
       if (positionalArgs != null) {
          result = new UnifiedCall(new Identifier(macroName), positionalArgs, null, null);
       }
       else {
          result = new UnifiedCall(new Identifier(macroName), namedArgs, null, null);
       }
       result.legacySyntax = true;
       result.setLocation(template, start, end);
       return result;
    }
}

HashMap NamedArgs() :
{
    HashMap result = new HashMap();
    Token t;
    Expression exp;
}
{
  (
     t=<ID>
     <EQUALS>
     {
         token_source.SwitchTo(token_source.NAMED_PARAMETER_EXPRESSION);
	 token_source.inInvocation = true;
     }	     
     exp=Expression()
     {
        result.put(t.image, exp);
     }
  )+
  {
      token_source.inInvocation = false;
     return result;
  }
}

ArrayList PositionalArgs() :
{
  ArrayList result = new ArrayList();
  Expression arg;
}
{
  [
      arg=Expression() {result.add(arg);}
      (
         [<COMMA>]
         arg=Expression() {result.add(arg);}
      )*
  ]
  {
     return result;
  }
}


Comment Comment() :
{
   Token start, end;
   StringBuffer buf = new StringBuffer();
}
{
    (
       start=<COMMENT>
       |
       start=<TERSE_COMMENT>
    )
    end=UnparsedContent(buf)
    {
       Comment result = new Comment(buf.toString());
       result.setLocation(template, start, end);
       return result;
    }
}

TextBlock NoParse() :
{
   Token start, end;
   StringBuffer buf = new StringBuffer();
}
{
    start=<NOPARSE>
    end=UnparsedContent(buf)
    {
        TextBlock result = new TextBlock(buf.toString(), true);
        result.setLocation(template, start, end);
        return result;
    }
}


TransformBlock Transform() :
{
   Token start, end, argName;
   Expression exp, argExp;
   TemplateElement content = null;
   HashMap args = null;
}
{
    start=<TRANSFORM>
    exp=Expression()
    [<SEMICOLON>]
    (
       argName=<ID>
       <EQUALS>
       argExp = Expression()
       {
           if (args == null) args = new HashMap();
           args.put(argName.image, argExp);
       }
    )*
    (
       end=<EMPTY_DIRECTIVE_END>
       |
       (<DIRECTIVE_END>content=OptionalBlock()end=<END_TRANSFORM>)
    )
    {
       TransformBlock result = new TransformBlock(exp, args, content);
       result.setLocation(template, start, end);
       return result;
    }
}

SwitchBlock Switch() :
{
    SwitchBlock switchBlock;
    Case caseIns;
    Expression switchExp;
    Token start, end;
    boolean defaultFound = false;
}
{
   start=<SWITCH>
   switchExp=Expression()
   <DIRECTIVE_END>
   {
      ++switchNesting;
      switchBlock = new SwitchBlock(switchExp);
   }
   (
     LOOKAHEAD(2)
     caseIns=Case()
     {
        if (caseIns.isDefault) {
	   if (defaultFound) {
                String msg = getErrorStart(start)
                            + "\nYou can only have one default case in a switch statement";
                throw new ParseException(msg, start.beginLine, start.beginColumn);
	   }
	   defaultFound = true;
	}
        switchBlock.addCase(caseIns);
     }
   )*
   [<WHITESPACE>]
   end=<END_SWITCH>
   {
      --switchNesting;
      switchBlock.setLocation(template, start, end);
      return switchBlock;
   }
}

Case Case() :
{
    Expression exp = null;
    TemplateElement block;
    boolean isDefault = false;
    Token start;
}
{
   [<WHITESPACE>]
   (
      start=<CASE>exp=Expression()<DIRECTIVE_END>
      |
      start=<DEFAUL>{isDefault = true;}
   )
   block=OptionalBlock()
   {
       Case result = new Case(exp, block, isDefault);
       result.setLocation(template, start, block);
       return result;
   }
}

EscapeBlock Escape() :
{
    Token variable, start, end;
    Expression escapeExpr;
    TemplateElement content;
}
{
    start=<ESCAPE>
    variable=<ID>
    <AS>
    escapeExpr=Expression()
    <DIRECTIVE_END>
    {
        EscapeBlock result = new EscapeBlock(variable.image, escapeExpr, escapedExpression(escapeExpr));
        escapes.addFirst(result);
    }
    content=OptionalBlock()
    {
        result.setContent(content);
        escapes.removeFirst();
    }
    end=<END_ESCAPE>
    {
       result.setLocation(template, start, end);
       return result;
    }
}

NoEscapeBlock NoEscape() :
{
    Token start, end;
    TemplateElement content;
}
{
    start=<NOESCAPE>
    {
        if(escapes.isEmpty()) {
                String msg = getErrorStart(start)
                                  + "\nnoescape with no matching escape encountered.";
            throw new ParseException(msg, start.beginLine, start.beginColumn);
        }
        Object escape = escapes.removeFirst();
    }
    content=OptionalBlock()
    end=<END_NOESCAPE>
    {
       escapes.addFirst(escape);
       NoEscapeBlock result = new NoEscapeBlock(content);
       result.setLocation(template, start, end);
       return result;
    }
}

/**
 * Production to terminate potentially empty elements. Either a ">" or "/>"
 */

Token LooseDirectiveEnd() :
{
    Token t;
}
{
   (
      t=<DIRECTIVE_END>
      |
      t=<EMPTY_DIRECTIVE_END>
   )
   {
      return t;
   }
}

PropertySetting Setting() :
{
   Token start, end, key;
   Expression value;
}
{
   start=<SETTING>
   key=<ID>
   <EQUALS>
   value=Expression()
   end=LooseDirectiveEnd()
   {
      PropertySetting result = new PropertySetting(key.image, value);
      result.setLocation(template, start, end);
      return result;
   }
}

/**
 * A production for FreeMarker directives.
 */
TemplateElement FreemarkerDirective() :
{
   TemplateElement tp;
}
{
   (
     tp=If()
     |
     tp=List()
     |
     tp=ForEach()
     |
     tp=Assign()
     |
     tp=Include()
     |
     tp=Import()
     |
     tp=Macro()
     |
     tp=Compress()
     |
     tp=UnifiedMacroTransform()
     |
     tp=Call()
     |
     tp=Comment()
     |
     tp=NoParse()
     |
     tp=Transform()
     |
     tp=Switch()
     |
     tp=Setting()
     |
     tp=Break()
     |
     tp=Return()
     |
     tp=Stop()
     |
     tp=Flush()
     |
     tp=Trim()
     |
     tp=Nested()
     |
     tp=Escape()
     |
     tp=NoEscape()
     |
     tp=Visit()
     |
     tp=Recurse()
     |
     tp=FallBack()
     |
     tp=Attempt()
   )
   {
      return tp;
   }
}

/**
 * Production for a block of raw text
 * i.e. text that contains no
 * FreeMarker directives.
 */

TextBlock PCData() :
{
    StringBuffer buf = new StringBuffer();
    Token t=null, start=null, prevToken = null;
}
{
    (
      LOOKAHEAD(<WHITESPACE>|<PRINTABLE_CHARS>|<FALSE_ALERT>)
      (
         {prevToken = t;}         
         t=<WHITESPACE>
	 |
         t=<PRINTABLE_CHARS>
         |
         t=<FALSE_ALERT>
      )
      {
         buf.append(t.image);
         if (start == null) start = t;
         {if (prevToken != null) prevToken.next = null;}
      }
    )+
    {
         if (stripText && contentNesting == 1)
             return TextBlock.EMPTY_BLOCK;

         TextBlock result = new TextBlock(buf.toString(), false);
         result.setLocation(template, start, t);
         return result;
    }
}

/**
 * Production for dealing with unparsed content,
 * i.e. what is inside a comment or noparse tag.
 * It returns the ending token. The content
 * of the tag is put in buf.
 */

Token UnparsedContent(StringBuffer buf) :
{
   Token t;
}
{
   ((t=<KEEP_GOING> | t=<MAYBE_END> | t=<TERSE_COMMENT_END> | t=<LONE_LESS_THAN_OR_DASH>)
   {
       buf.append(t.image);
   })+
   {
      buf.setLength(buf.length() - t.image.length());
      return t;
   }
}

TemplateElement Content() :
{
    MixedContent nodes = new MixedContent();
    TemplateElement elem, begin=null;
    contentNesting++;
}
{
    (
      LOOKAHEAD(1) // Just tells javacc that we know what we're doing.
      (
         elem=PCData()
         |
         elem=StringOutput()
         |
         elem=NumericalOutput()
         |
         elem=FreemarkerDirective()
      )
      {
            if (begin == null) {
               begin = elem;
            }
            nodes.addElement(elem);
      }
    )+
    {
        contentNesting--;
        nodes.setLocation(template, begin, elem);
	return nodes;
    }
}

/**
 * A production freemarker text that may contain
 * ${...} and #{...} but no directives.
 */

TemplateElement FreeMarkerText() :
{
     MixedContent nodes = new MixedContent();
     TemplateElement elem, begin = null;
}
{
    (
      (
        elem=PCData()
        |
        elem=StringOutput()
        |
        elem=NumericalOutput()
      )
      {
         if (begin == null) {
            begin = elem;
         }
         nodes.addElement(elem);
      }
    )+
    {
       nodes.setLocation(template, begin, elem);
       return nodes;
    }
}

/**
 * A production for a block of optional content.
 * Returns an empty Text block if there is no
 * content.
 */

TemplateElement OptionalBlock() :
{
   TemplateElement tp = TextBlock.EMPTY_BLOCK;
}
{
   [
      LOOKAHEAD(1) // has no effect but to get rid of a spurious warning.
         tp=Content()
   ]
   {
      return tp;
   }
}

void HeaderElement() :
{
   Token key;
   Expression exp = null;
}
{
   [<WHITESPACE>]
   (
     <TRIVIAL_FTL_HEADER>
     |
     (
       <FTL_HEADER>
       (
          key=<ID>
          <EQUALS>
          exp=Expression()
          {
             String ks = key.image;
             TemplateModel value = null;
             try {
                value = exp.getAsTemplateModel(null);
             } catch (Exception e) {
                throw new ParseException("Could not evaluate expression: " 
                                         + exp.getCanonicalForm() + " " 
                                         + exp.getStartLocation()
                                         + "\nUnderlying cause: " + 
                                         e.getMessage(), exp);
             }
	     String vs = null;
             if (value instanceof TemplateScalarModel) {
                try {
                   vs = ((TemplateScalarModel) exp).getAsString();
                } catch (TemplateModelException tme) {}
             }
             if (template != null) {
                 if (ks.equalsIgnoreCase("encoding")) {
                     if (vs == null) {
                        throw new ParseException("expecting encoding string here: " 
                                                 + exp.getStartLocation(), exp);
                     }
                     String encoding = template.getEncoding();
                     if (encoding != null && !encoding.equals(vs)) {
                         throw new Template.WrongEncodingException(vs);
                     }
                 }
                 else if (ks.equalsIgnoreCase("STRIP_WHITESPACE")) {
                     this.stripWhitespace = getBoolean(exp);
                 }
                 else if (ks.equalsIgnoreCase("STRIP_TEXT")) {
                     this.stripText = getBoolean(exp);
                 }
                 else if (ks.equalsIgnoreCase("STRICT_SYNTAX")) {
                     this.token_source.strictEscapeSyntax = getBoolean(exp);
                 }
                 else if (ks.equalsIgnoreCase("ns_prefixes")) {
                     if (!(value instanceof TemplateHashModelEx)) {
                         throw new ParseException("Expecting a hash of prefixes to namespace URI's here. " + exp.getStartLocation(), exp);
                     }
                     TemplateHashModelEx prefixMap = (TemplateHashModelEx) value;
                     try {
                         TemplateCollectionModel keys = prefixMap.keys();
                         for (TemplateModelIterator it = keys.iterator(); it.hasNext();) {
                             String prefix = ((TemplateScalarModel) it.next()).getAsString();
                             TemplateModel valueModel = prefixMap.get(prefix);
                             if (!(valueModel instanceof TemplateScalarModel)) {
                                 throw new ParseException("Non-string value in prefix to namespace hash. " + exp.getStartLocation(), exp);
                             }
                             String nsURI = ((TemplateScalarModel) valueModel).getAsString();
                             try {
                                 template.addPrefixNSMapping(prefix, nsURI);
                             } catch (IllegalArgumentException iae) {
                                 throw new ParseException(iae.getMessage() + " " + exp.getStartLocation(), exp);
                             }
                         }
                     } catch (TemplateModelException tme) {
                     }
                 }
                 else if (ks.equalsIgnoreCase("attributes")) {
                     if (!(value instanceof TemplateHashModelEx)) {
                         throw new ParseException("Expecting a hash of attribute names to values here. " + exp.getStartLocation(), exp);
                     }
                     TemplateHashModelEx attributeMap = (TemplateHashModelEx) value;
                     try {
                         TemplateCollectionModel keys = attributeMap.keys();
                         for (TemplateModelIterator it = keys.iterator(); it.hasNext();) {
                             String attName = ((TemplateScalarModel) it.next()).getAsString();
                             Object attValue = DeepUnwrap.unwrap(attributeMap.get(attName));
                             template.setCustomAttribute(attName, attValue);
                         }
                     } catch (TemplateModelException tme) {
                     }
                 }
                 else {
                     throw new ParseException("Unknown FTL header parameter: " + key.image,
                                               key.beginLine, key.beginColumn);
                 }
             }
          }
       )*
     )
     LooseDirectiveEnd()
   )
}

Map ParamList() :
{
   Identifier id;
   Expression exp;
   Map result = new HashMap();
}
{
   (
      id=Identifier()
      <EQUALS>
      exp=Expression() {result.put(id.toString(), exp);}
      [<COMMA>]
   )+
   {
       return result;
   }
}


/**
 * Root production to be used when parsing
 * an entire file.
 */
TemplateElement Root() :
{
   TemplateElement doc;
}
{
   [
      LOOKAHEAD([<WHITESPACE>](<TRIVIAL_FTL_HEADER>|<FTL_HEADER>))
       HeaderElement()
   ]
   doc=OptionalBlock()
   <EOF>
   {
       doc.setParentRecursively(null);
       return doc.postParseCleanup(stripWhitespace);
   }
}
