/* * 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. */ /* * NOTE : please see documentation at bottom of this file. (It was placed there its tiring * to always have to page past it... :) */ options { /** The default package for this parser kit. This is now done from Maven. NODE_PACKAGE="org.apache.velocity.runtime.parser"; */ /** A source file will be generated for each non-terminal */ MULTI=true; /** * Each node will have access to the parser, I did this so * some global information can be shared via the parser. I * think this will come in handly keeping track of * context, and being able to push changes back into * the context when nodes make modifications to the * context by setting properties, variables and * what not. */ NODE_USES_PARSER=true; /** * The parser must be non-static in order for the * above option to work, otherwise the parser value * is passed in as null, which isn't all the useful ;) */ STATIC=false; /** * Enables the use of a visitor that each of nodes * will accept. This way we can separate the logic * of node processing in a visitor and out of the * nodes themselves. If processing changes then * the nothing has to change in the node code. */ VISITOR=true; /** * Declare that we are accepting unicode input and * that we are using a custom character stream class * Note that the char stream class is really a slightly * modified ASCII_CharStream, as it appears we are safe * because we only deal with pre-encoding-converted * Readers rather than raw input streams. */ UNICODE_INPUT=true; USER_CHAR_STREAM=true; /** * for debugging purposes. Those are now handled from within javacc-maven-plugin debugging flags in pom.xml DEBUG_PARSER = true; DEBUG_LOOKAHEAD = true; DEBUG_TOKEN_MANAGER = true; */ } PARSER_BEGIN(${parser.basename}Parser) package ${parser.package}; import java.io.*; import java.util.*; import org.apache.velocity.Template; import org.apache.velocity.exception.VelocityException; import org.apache.velocity.runtime.RuntimeServices; import org.apache.velocity.runtime.parser.*; import org.apache.velocity.runtime.parser.node.*; import org.apache.velocity.runtime.directive.*; import org.apache.velocity.runtime.directive.MacroParseException; import org.apache.velocity.runtime.RuntimeConstants; import static org.apache.velocity.runtime.RuntimeConstants.SpaceGobbling; import org.slf4j.Logger; /** * This class is responsible for parsing a Velocity * template. This class was generated by JavaCC using * the JJTree extension to produce an Abstract * Syntax Tree (AST) of the template. * * Please look at the Parser.jjt file which is * what controls the generation of this class. * * @author Jason van Zyl * @author Geir Magnusson Jr. * @author Henning P. Schmiedehausen * @version $Id$ */ public class ${parser.basename}Parser implements Parser { /** * Parser debugging flag. * When debug is active, javacc Parser will contain (among other things) * a trace_call() method. So we use the presence of this method to * initialize our flag. */ private static boolean debugParser; static { try { ${parser.basename}Parser.class.getDeclaredMethod("trace_call", String.class); debugParser = true; } catch(NoSuchMethodException nsfe) { debugParser = false; } } /** * Our own trace method. Use sparsingly in production, since each * and every call will introduce an execution branch and slow down parsing. */ public static void trace(String message) { if (debugParser) System.out.println(message); } /** * Keep track of defined macros, used for escape processing */ private Map macroNames = new HashMap(); /** * Current template we are parsing. Passed to us in parse() */ public Template currentTemplate = null; /** * Set to true if the property * RuntimeConstants.RUNTIME_REFERENCES_STRICT_ESCAPE is set to true */ public boolean strictEscape = false; /** * Set to true if the propoerty * RuntimeConstants.PARSER_HYPHEN_ALLOWED is set to true */ public boolean hyphenAllowedInIdentifiers = false; VelocityCharStream velcharstream = null; private RuntimeServices rsvc = null; @Override public RuntimeServices getRuntimeServices() { return rsvc; } private Logger log = null; /** * This constructor was added to allow the re-use of parsers. * The normal constructor takes a single argument which * an InputStream. This simply creates a re-usable parser * object, we satisfy the requirement of an InputStream * by using a newline character as an input stream. */ public ${parser.basename}Parser( RuntimeServices rs) { /* * need to call the CTOR first thing. */ this( new VelocityCharStream( new ByteArrayInputStream("\n".getBytes()), 1, 1 )); /* * then initialize logger */ log = rs.getLog("parser"); /* * now setup a VCS for later use */ velcharstream = new VelocityCharStream( new ByteArrayInputStream("\n".getBytes()), 1, 1 ); strictEscape = rs.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT_ESCAPE, false); hyphenAllowedInIdentifiers = rs.getBoolean(RuntimeConstants.PARSER_HYPHEN_ALLOWED, false); /* * and save the RuntimeServices */ rsvc = rs; /* * then initialize customizable characters */ dollar = '${parser.char.dollar}'; hash = '${parser.char.hash}'; at = '${parser.char.at}'; asterisk = '${parser.char.asterisk}'; } /** * This was also added to allow parsers to be * re-usable. Normal JavaCC use entails passing an * input stream to the constructor and the parsing * process is carried out once. We want to be able * to re-use parsers: we do this by adding this * method and re-initializing the lexer with * the new stream that we want parsed. */ @Override public SimpleNode parse( Reader reader, Template template ) throws ParseException { SimpleNode sn = null; currentTemplate = template; try { token_source.clearStateVars(); /* * reinitialize the VelocityCharStream * with the new reader */ velcharstream.ReInit( reader, 1, 1 ); /* * now reinit the Parser with this CharStream */ ReInit( velcharstream ); /* * do that voodoo... */ sn = process(); } catch (MacroParseException mee) { /* * thrown by the Macro class when something is amiss in the * Macro specification */ log.error("{}: {}", template.getName(), mee.getMessage(), mee); throw mee; } catch (ParseException pe) { log.error("{}: {}", currentTemplate.getName(), pe.getMessage()); throw new TemplateParseException (pe.currentToken, pe.expectedTokenSequences, pe.tokenImage, currentTemplate.getName()); } catch (TokenMgrError tme) { throw new ParseException("Lexical error: " + tme.toString()); } catch (Exception e) { String msg = template.getName() + ": " + e.getMessage(); log.error(msg, e); throw new VelocityException(msg, e, getRuntimeServices().getLogContext().getStackTrace()); } currentTemplate = null; return sn; } /** * This method gets a Directive from the directives Hashtable */ @Override public Directive getDirective(String directive) { return (Directive) rsvc.getDirective(directive); } /** * This method finds out of the directive exists in the directives Map. */ @Override public boolean isDirective(String directive) { return rsvc.getDirective(directive) != null; } /** * Produces a processed output for an escaped control or * pluggable directive */ private String escapedDirective( String strImage ) { int iLast = strImage.lastIndexOf("\\"); String strDirective = strImage.substring(iLast + 1); boolean bRecognizedDirective = false; // we don't have to call substring method all the time in this method String dirTag = strDirective.substring(1); if (dirTag.charAt(0) == '{') { dirTag = dirTag.substring(1, dirTag.length() - 1); } /* * If this is a predefined derective or if we detect * a macro definition (this is aproximate at best) then * we absorb the forward slash. If in strict reference * mode then we always absord the forward slash regardless * if the derective is defined or not. */ if (strictEscape || isDirective(dirTag) || macroNames.containsKey(dirTag) || rsvc.isVelocimacro(dirTag, currentTemplate)) { bRecognizedDirective = true; } else { /* order for speed? */ if ( dirTag.equals("if") || dirTag.equals("end") || dirTag.equals("set") || dirTag.equals("else") || dirTag.equals("elseif") ) { bRecognizedDirective = true; } } /* * if so, make the proper prefix string (let the escapes do their thing..) * otherwise, just return what it is.. */ if (bRecognizedDirective) return ( strImage.substring(0,iLast/2) + strDirective); else return ( strImage ); } /** * Check whether there is a left parenthesis with leading optional * whitespaces. This method is used in the semantic look ahead of * Directive method. This is done in code instead of as a production * for simplicity and efficiency. */ private boolean isLeftParenthesis() { char c; int no = 0; try { while(true) { /** * Read a character */ c = velcharstream.readChar(); no++; if (c == '(') { return true; } /** * if not a white space return */ else if (c != ' ' && c != '\n' && c != '\r' && c != '\t') { return false; } } } catch(IOException e) { } finally { /** * Backup the stream to the initial state */ velcharstream.backup(no); } return false; } /** * Check whether there is a right parenthesis with leading optional * whitespaces. This method is used in the semantic look ahead of * Directive method. This is done in code instead of as a production * for simplicity and efficiency. */ private boolean isRightParenthesis() { char c; int no = -1; try { while(true) { /** * Read a character */ if (no == -1) { switch (getToken(1).kind) { case RPAREN: return true; case WHITESPACE: case NEWLINE: no = 0; break; default: return false; } } c = velcharstream.readChar(); no++; if (c == ')') { return true; } /** * if not a white space return */ else if (c != ' ' && c != '\n' && c != '\r' && c != '\t') { return false; } } } catch(IOException e) { } finally { /** * Backup the stream to the initial state */ if (no > 0) velcharstream.backup(no); } return false; } /** * We use this method in a lookahead to determine if we are in a macro * default value assignment. The standard lookahead is not smart enough. * here we look for the equals after the reference. */ private boolean isAssignment() { // Basically if the last character read was not '$' then false if (token_source.getCurrentLexicalState() != REFERENCE) return false; char c = ' '; int backup = 0; try { // Read through any white space while(Character.isWhitespace(c)) { c = velcharstream.readChar(); backup++; } // This is what we are ultimately looking for if (c != '=') return false; } catch (IOException e) { } finally { velcharstream.backup(backup); } return true; } @Override public Template getCurrentTemplate() { return currentTemplate; } @Override public void resetCurrentTemplate() { currentTemplate = null; } @Override public char dollar() { return dollar; } @Override public char hash() { return hash; } @Override public char at() { return at; } @Override public char asterisk() { return asterisk; } private char dollar = '$'; private char hash = '#'; private char at = '@'; private char asterisk = '*'; } PARSER_END(${parser.basename}Parser) TOKEN_MGR_DECLS: { private int fileDepth = 0; private int lparen = 0; private int rparen = 0; private int curlyLevel = 0; List stateStack = new ArrayList(50); private boolean inComment; private boolean inSet; /** * Our own trace method. Use sparsingly in production, since each * and every call will introduce an execution branch and slow down parsing. */ public static void trace(String message) { ${parser.basename}Parser.trace(message); } /** * Switches to a new state (add some log to the default method) */ public void switchTo(int lexState) { trace(" switch to " + lexStateNames[lexState]); SwitchTo(lexState); } public int getCurrentLexicalState() { return curLexState; } /** * pops a state off the stack, and restores paren counts * * @return boolean : success of operation */ public boolean stateStackPop() { ParserState s; try { s = (ParserState) stateStack.remove(stateStack.size() - 1); // stack.pop } catch(IndexOutOfBoundsException e) { // empty stack lparen=0; switchTo(DEFAULT); return false; } trace(" stack pop (" + stateStack.size() + ")"); lparen = s.lparen; rparen = s.rparen; curlyLevel = s.curlyLevel; switchTo(s.lexstate); return true; } /** * pushes the current state onto the 'state stack', * and maintains the parens counts * public because we need it in PD & VM handling * * @return boolean : success. It can fail if the state machine * gets messed up (do don't mess it up :) */ public boolean stateStackPush() { trace(" (" + stateStack.size() + ") pushing cur state : " + lexStateNames[curLexState] ); ParserState s = new ParserState(); s.lparen = lparen; s.rparen = rparen; s.curlyLevel = curlyLevel; s.lexstate = curLexState; stateStack.add(s); // stack.push lparen = 0; curlyLevel = 0; return true; } /** * Clears all state variables, resets to * start values, clears stateStack. Call * before parsing. */ public void clearStateVars() { stateStack.clear(); lparen = 0; rparen = 0; curlyLevel = 0; inComment = false; inSet = false; return; } public void setInSet(boolean value) { inSet = value; } public boolean isInSet() { return inSet; } /** * Holds the state of the parsing process. */ private static class ParserState { int lparen; int rparen; int curlyLevel; int lexstate; } /** * handles the dropdown logic when encountering a RPAREN */ private void RPARENHandler() { /* * Ultimately, we want to drop down to the state below * the one that has an open (if we hit bottom (DEFAULT), * that's fine. It's just text schmoo. */ boolean closed = false; if (inComment) closed = true; while( !closed ) { /* * look at current state. If we haven't seen a lparen * in this state then we drop a state, because this * lparen clearly closes our state */ if( lparen > 0) { /* * if rparen + 1 == lparen, then this state is closed. * Otherwise, increment and keep parsing */ if( lparen == rparen + 1) { stateStackPop(); } else { rparen++; } closed = true; } else { /* * now, drop a state */ if(!stateStackPop()) break; } } } } /* ------------------------------------------------------------------------ * * Tokens * * ------------------------------------------------------------------------- */ /* The VelocityCharStream will send a zero-width whitespace just before EOF to let us accept a terminal $ or # */ TOKEN : { { stateStackPop(); } } /* In all other states, keep the zero-width whitespace for now */ TOKEN : { } TOKEN: { { stateStackPush(); switchTo(REFINDEX); } | /* we need to give precedence to the logical 'or' here, it's a hack to avoid multiplying parsing modes */ { stateStackPop(); } | { if (curlyLevel == 1) { switchTo(ALT_VAL); } else { stateStackPop(); } } } TOKEN: { { stateStackPop(); } } TOKEN: { | | } TOKEN: { } TOKEN: { } TOKEN : { { ++curlyLevel; } | { --curlyLevel; if (curLexState == ALT_VAL && curlyLevel == 0) { stateStackPop(); } } } TOKEN: { { if (!inComment) lparen++; /* * If in REFERENCE and we have seen the dot, then move * to REFMOD2 -> Modifier() */ if (curLexState == REFMODIFIER || curLexState == OLD_REFMODIFIER ) switchTo( REFMOD2 ); } } /* * we never will see a ')' in anything but DIRECTIVE and REFMOD2. * Each have their own */ TOKEN: { { RPARENHandler(); } } TOKEN: { /* * in REFMOD2, we don't want to bind the whitespace and \n like we * do when closing a directive. */ { /* * need to simply switch back to REFERENCE, not drop down the stack * because we can (infinitely) chain, ala * $foo.bar().blargh().woogie().doogie() */ switchTo( REFMOD3 ); } } /*---------------------------------------------- * * escape "\\" handling for the built-in directives * *--------------------------------------------- */ TOKEN: { /* * We have to do this, because we want these to be a Text node, and * whatever follows to be peer to this text in the tree. * * We need to touch the ASTs for these, because we want an even # of \'s * to render properly in front of the block * * This is really simplistic. I actually would prefer to find them in * grammatical context, but I am neither smart nor rested, a receipe * for disaster, another long night with Mr. Parser, or both. */ )* "\\${parser.char.hash}" ( | ) > } /* * We added the lexical states REFERENCE, REFMODIFIER, REFMOD2 to * address JIRA issue VELOCITY-631. With SET_DIRECTIVE only in the * DEFAULT lexical state the following VTL fails "$a#set($b = 1)" * because the Reference token uses LOOKAHEAD(2) combined with the * fact that we explicity set the lex state to REFERENCE with the $ * token, which means we would never evaluate this token during the * look ahead. This general issue is disscussed here: * * http://www.engr.mun.ca/~theo/JavaCC-FAQ/javacc-faq-ie.htm#tth_sEc3.12 * */ TOKEN: { { if (! inComment) { trace(" #set : going to DIRECTIVE" ); stateStackPush(); setInSet(true); switchTo(DIRECTIVE); } /* * need the LPAREN action */ if (!inComment) { lparen++; /* * If in REFERENCE and we have seen the dot, then move * to REFMOD2 -> Modifier() */ if (curLexState == REFMODIFIER || curLexState == OLD_REFMODIFIER ) switchTo( REFMOD2 ); } } } <*> MORE : { /* * Note : DOLLARBANG is a duplicate of DOLLAR. They must be identical. */ { if (! inComment) { /* * if we find ourselves in REFERENCE or PRE_REFERENCE, we need to pop down * to end the previous ref */ if (curLexState == REFERENCE || curLexState == PRE_REFERENCE || curLexState == PRE_OLD_REFERENCE) { stateStackPop(); } int preReferenceState = parser.hyphenAllowedInIdentifiers ? PRE_OLD_REFERENCE : PRE_REFERENCE; trace( " $ : going to " + lexStateNames[preReferenceState]); /* do not push PRE states */ if (curLexState != PRE_REFERENCE && curLexState != PRE_DIRECTIVE && curLexState != PRE_OLD_REFERENCE) { stateStackPush(); } switchTo(preReferenceState); } } | { if (! inComment) { /* * if we find ourselves in REFERENCE or PRE_REFERENCE, we need to pop down * to end the previous ref */ if (curLexState == REFERENCE || curLexState == PRE_REFERENCE || curLexState == PRE_OLD_REFERENCE) { stateStackPop(); } int preReferenceState = parser.hyphenAllowedInIdentifiers ? PRE_OLD_REFERENCE : PRE_REFERENCE; trace( " $ : going to " + lexStateNames[preReferenceState]); /* do not push PRE states */ if (curLexState != PRE_REFERENCE && curLexState != PRE_DIRECTIVE && curLexState != PRE_OLD_REFERENCE) { stateStackPush(); } switchTo(preReferenceState); } } | "${parser.char.hash}[[" { if (!inComment) { inComment = true; /* do not push PRE states */ if (curLexState != PRE_REFERENCE && curLexState != PRE_DIRECTIVE && curLexState != PRE_OLD_REFERENCE) { stateStackPush(); } switchTo( IN_TEXTBLOCK ); } } | <"${parser.char.hash}${parser.char.asterisk}${parser.char.asterisk}" ~["${parser.char.hash}","\u001C"]> { if (!inComment) { input_stream.backup(1); inComment = true; /* do not push PRE states */ if (curLexState != PRE_REFERENCE && curLexState != PRE_DIRECTIVE && curLexState != PRE_OLD_REFERENCE) { stateStackPush(); } switchTo( IN_FORMAL_COMMENT); } } | "${parser.char.hash}${parser.char.asterisk}" { if (!inComment) { inComment=true; /* do not push PRE states */ if (curLexState != PRE_REFERENCE && curLexState != PRE_DIRECTIVE && curLexState != PRE_OLD_REFERENCE) { stateStackPush(); } switchTo( IN_MULTI_LINE_COMMENT ); } } | { if (! inComment) { /* * We can have the situation where #if($foo)$foo#end. * We need to transition out of REFERENCE before going to DIRECTIVE. * I don't really like this, but I can't think of a legal way * you are going into DIRECTIVE while in REFERENCE. -gmj */ if (curLexState == REFERENCE || curLexState == PRE_REFERENCE || curLexState == PRE_OLD_REFERENCE || curLexState == REFMODIFIER || curLexState == OLD_REFMODIFIER ) { stateStackPop(); } trace(" # : going to PRE_DIRECTIVE" ); /* do not push PRE states */ if (curLexState != PRE_REFERENCE && curLexState != PRE_DIRECTIVE && curLexState != PRE_OLD_REFERENCE) { stateStackPush(); } switchTo(PRE_DIRECTIVE); } } } // treat the single line comment case separately // to avoid ## errors TOKEN : { { if (!inComment) { if (curLexState == REFERENCE || curLexState == PRE_REFERENCE || curLexState == PRE_OLD_REFERENCE) { stateStackPop(); } inComment = true; stateStackPush(); switchTo(IN_SINGLE_LINE_COMMENT); } } } /* ----------------------------------------------------------------------- * * *_COMMENT Lexical tokens * *-----------------------------------------------------------------------*/ TOKEN : { { inComment = false; stateStackPop(); if (curLexState == REFERENCE || curLexState == REFMOD3) { // end of reference: pop again stateStackPop(); } } } TOKEN : { { inComment = false; stateStackPop(); if (curLexState == REFERENCE || curLexState == REFMOD3) { // end of reference: pop again stateStackPop(); } } } TOKEN : { { inComment = false; stateStackPop(); if (curLexState == REFERENCE || curLexState == REFMOD3) { // end of reference: pop again stateStackPop(); } } } TOKEN : { { inComment = false; stateStackPop(); } } SKIP : { < ~[] > } MORE : { < ~["\u001C"] > } /* ----------------------------------------------------------------------- * * DIRECTIVE Lexical State (some of it, anyway) * * ---------------------------------------------------------------------- */ TOKEN: { | { trace(" NEWLINE :"); /* if (isInSet()) */ setInSet(false); } } /* needed for stuff like #foo() followed by ( '$' | '#' )* followed by ( | ) so that directive postfix doesn't eat the '$'s and '#'s */ TOKEN: { { stateStackPop(); } } TOKEN : { // < STRING_LITERAL: ("\"" ( (~["\"","\u001C"]) | ("\\" ( ["n","t","b","r","f"] | ["0"-"7"] ( ["0"-"7"] )? | ["0"-"3"] ["0"-"7"] ["0"-"7"] | "u" ["0"-"9", "a"-"f", "A"-"F"] ["0"-"9", "a"-"f", "A"-"F"] ["0"-"9", "a"-"f", "A"-"F"] ["0"-"9", "a"-"f", "A"-"F"] ) ) | ("\"\"") | ( "\\" (" ")* "\n") )* "\"" ) | ("\'" ( (~["\'","\u001C"]) | ("''") | ( "\\" (" ")* "\n") )* "\'" ) > { /* * - if we are in DIRECTIVE and haven't seen ( yet, then also drop out. * don't forget to account for the beloved yet wierd #set * - finally, if we are in REFMOD2 (remember : $foo.bar( ) then " is ok! */ if( curLexState == DIRECTIVE && !isInSet() && lparen == 0) stateStackPop(); } } TOKEN: { | } TOKEN : { | | | | | | | | | " | "gt" > | =" | "ge" > | | | | { stateStackPop(); } | { switchTo(DIRECTIVE); } | { switchTo(DIRECTIVE); } | { stateStackPop(); } } TOKEN: { <#DIGIT: [ "0"-"9" ] > /* * treat FLOATING_POINT_LITERAL and INTEGER_LITERAL differently as a range can only handle integers. */ /** * Note -- we also define an integer as ending with a double period, * in order to avoid 1..3 being defined as floating point (1.) then a period, then a integer */ | )+ ("..")? > { /* * Remove the double period if it is there */ if (matchedToken.image.endsWith("..")) { input_stream.backup(2); matchedToken.image = matchedToken.image.substring(0,matchedToken.image.length()-2); } /* * check to see if we are in set * ex. #set($foo = $foo + 3) * because we want to handle the \n after */ if ( lparen == 0 && !isInSet() && curLexState != REFMOD2 && curLexState != REFINDEX && curLexState != ALT_VAL) { stateStackPop(); } } | )+ "." ()* ()? | ("-")? "." ()+ ()? | ("-")? ()+ > { /* * check to see if we are in set * ex. #set $foo = $foo + 3 * because we want to handle the \n after */ if ( lparen == 0 && !isInSet() && curLexState != REFMOD2 && curLexState != ALT_VAL) { stateStackPop(); } } | <#EXPONENT: ["e","E"] (["+","-"])? (["0"-"9"])+ > } /** * TODO, the "@" symbol for block macros to be correct really should prefix WORD * and BRACKETED_WORD, e.g., TOKEN: { <#LETTER: [ "a"-"z", "A"-"Z" ] > | <#DIRECTIVE_CHAR: [ "a"-"z", "A"-"Z", "0"-"9", "_" ] > | | ["_"] | ["${parser.char.at}"]) ()* > | | ["_"] | ["${parser.char.at}"]) ()* "}" > } /* ----------------------------------------------------------------------- * * REFERENCE Lexical States * * This is more than a single state, because of the structure of * the VTL references. We use three states because the set of tokens * for each state can be different. * * $foo.bar( "arg" ) * ^ ^ ^ ^ ^ * | | | | | * |_________________ > PRE_REFERENCE : state initiated by the '$' character. * | | | | (or PRE_OLD_REFERENCE if '-' is allowed in identifiers) * |________________> REFERENCE : state initiated by the identifier. Continues * | | | until end of the reference, or the . character. * |_____________ > REFMODIFIER : state switched to when the is encountered. * | | (or OLD_REFMODIFIER if '-' is allowed in identifiers) * | | note that this is a switch, not a push. See notes at bottom. * |_________ > REFMOD2 : state switch to when the LPAREN is encountered. * | again, this is a switch, not a push. * |_ > REFMOD3 : state only checking for a possible '.' or '[' continuation. * * During the REFERENCE, REFMODIFIER or REFMOD3 lex states we will switch to: * - REFINDEX if a bracket '[' is encountered: $foo[1], $foo.bar[1], $foo.bar( "arg" )[1] * - ALT_VAL if a pipe '|' is encountered (only for formal references): ${foo|'foo'} * ---------------------------------------------------------------------------- */ TOKEN : { <#ALPHA_CHAR: ["a"-"z", "A"-"Z", "_"] > | <#IDENTIFIER_CHAR: [ "a"-"z", "A"-"Z", "0"-"9", "_" ] > | ) ()* > { if (curLexState == PRE_REFERENCE) { switchTo(REFERENCE); } } } TOKEN : { <#OLD_ALPHA_CHAR: ["a"-"z", "A"-"Z", "_"] > | <#OLD_IDENTIFIER_CHAR: [ "a"-"z", "A"-"Z", "0"-"9", "_", "-" ] > | ) ()* > { if (curLexState == PRE_OLD_REFERENCE) { switchTo(REFERENCE); } } } TOKEN: { > { /* * push the alpha char back into the stream so the following identifier * is complete */ input_stream.backup(1); /* * and munge the so we just get a . when we have normal text that * looks like a ref.ident */ matchedToken.image = "."; int refModifierState = parser.hyphenAllowedInIdentifiers ? OLD_REFMODIFIER : REFMODIFIER; trace("DOT : switching to " + lexStateNames[refModifierState]); switchTo(refModifierState); } } TOKEN : { { ++curlyLevel; } | { /* maybe it wasn't for our state */ while (curlyLevel == 0 && curLexState != DEFAULT) { stateStackPop(); } /* At this point, here are all the possible states: * - DEFAULT, which means the '}' is schmoo * - DIRECTIVE or REFMOD2, which means the '}' is a closing map curly * - one of the other REFERENCE states or ALT_VAL, which means the '}' ends the reference * If we're in the last case, pop up state. */ if (curLexState != DEFAULT && curLexState != DIRECTIVE && curLexState != REFMOD2) { stateStackPop(); } } } SPECIAL_TOKEN : { { /* * push every terminator character back into the stream */ input_stream.backup(1); trace("REF_TERM :"); stateStackPop(); } } SPECIAL_TOKEN : { { trace("DIRECTIVE_TERM :"); input_stream.backup(1); stateStackPop(); } } /* TEXT must end with a newline, and contain at least one non-whitespace character in the first line, so that the sequence is not read as a TEXT (needed for space gobbling) */ TOKEN : { | | ((~["${parser.char.dollar}", "${parser.char.hash}", "\\", "\r", "\n","\u001C"])* )* > } TOKEN : { } /** * This method is what starts the whole parsing * process. After the parsing is complete and * the template has been turned into an AST, * this method returns the root of AST which * can subsequently be traversed by a visitor * which implements the ParserVisitor interface * which is generated automatically by JavaCC */ SimpleNode process() : { boolean afterNewline = true; } { ( LOOKAHEAD({ getToken(1).kind != EOF }) afterNewline = Statement(afterNewline) )* { return jjtThis; } } /** * These are the types of statements that * are acceptable in Velocity templates. */ boolean Statement(boolean afterNewline) #void : { } { LOOKAHEAD( { getToken(1).kind == IF_DIRECTIVE || afterNewline && getToken(1).kind == WHITESPACE && getToken(2).kind == IF_DIRECTIVE } ) afterNewline = IfStatement(afterNewline) { return afterNewline; } | LOOKAHEAD(2) Reference() { return false; } | LOOKAHEAD(2) afterNewline = Comment() { return afterNewline; } | Textblock() { return false; } | LOOKAHEAD( { getToken(1).kind == SET_DIRECTIVE || afterNewline && getToken(1).kind == WHITESPACE && getToken(2).kind == SET_DIRECTIVE } ) afterNewline = SetDirective(afterNewline) { return afterNewline; } | EscapedDirective() { return false; } | Escape() { return false; } | LOOKAHEAD( { getToken(1).kind == WORD || getToken(1).kind == BRACKETED_WORD || afterNewline && getToken(1).kind == WHITESPACE && ( getToken(2).kind == WORD || getToken(2).kind == BRACKETED_WORD ) } ) afterNewline = Directive(afterNewline) { return afterNewline; } | afterNewline = Text() { return afterNewline; } | () #Text { return true; } | ((() { afterNewline = false; } ) (() { afterNewline = true; })? ) #Text { return afterNewline; } | () #Text { return false; } | () #Text { return true; } | LOOKAHEAD(2) EndingZeroWidthWhitespace() { return afterNewline; } | () #Text { return afterNewline; } // needed here since it can be triggered in mode out of any boolean evaluation | () #Text { afterNewline = !afterNewline; return false; } } void EndingZeroWidthWhitespace() #void : {} { { } } /** * used to separate the notion of a valid directive that has been * escaped, versus something that looks like a directive and * is just schmoo. This is important to do as a separate production * that creates a node, because we want this, in either case, to stop * the further parsing of the Directive() tree. */ void EscapedDirective() : {} { { Token t = null; } t = { /* * churn and burn.. */ t.image = escapedDirective( t.image ); } } /** * Used to catch and process escape sequences in grammatical constructs * as escapes outside of VTL are just characters. Right now we have both * this and the EscapeDirective() construction because in the EscapeDirective() * case, we want to suck in the #<directive> and here we don't. We just want * the escapes to render correctly */ void Escape() : {} { { Token t = null; int count = 0; boolean control = false; } ( LOOKAHEAD(2) t = { count++; } )+ { /* * first, check to see if we have a control directive */ switch(t.next.kind ) { case IF_DIRECTIVE : case ELSE : case ELSEIF : case END : control = true; break; } /* * if that failed, lets lookahead to see if we matched a PD or a VM */ String nTag = t.next.image.substring(1); if (strictEscape || isDirective(nTag) || macroNames.containsKey(nTag) || rsvc.isVelocimacro(nTag, currentTemplate)) { control = true; } jjtThis.val = ""; for( int i = 0; i < count; i++) jjtThis.val += ( control ? "\\" : "\\\\"); } } boolean Comment() : {} { ( ) ? { return true; } | { return false; } | { return false; } } void Textblock() : {} { } void FloatingPointLiteral() : {} { } void IntegerLiteral() : {} { } void StringLiteral() : {} { } /** * This method corresponds to variable * references in Velocity templates. * The following are examples of variable * references that may be found in a * template: * * $foo * $bar * */ void Identifier() : {} { | } void Word() : {} { } /** * Supports the arguments for the Pluggable Directives */ int DirectiveArg() #void : {} { Reference() { return ParserTreeConstants.JJTREFERENCE; } | Word() { return ParserTreeConstants.JJTWORD; } | StringLiteral() { return ParserTreeConstants.JJTSTRINGLITERAL; } | IntegerLiteral() { return ParserTreeConstants.JJTINTEGERLITERAL; } /* * Need to put this before the floating point expansion */ | LOOKAHEAD( ( | )* ( Reference() | IntegerLiteral()) ( | )* ) IntegerRange() { return ParserTreeConstants.JJTINTEGERRANGE; } | FloatingPointLiteral() { return ParserTreeConstants.JJTFLOATINGPOINTLITERAL; } | Map() { return ParserTreeConstants.JJTMAP; } | ObjectArray() { return ParserTreeConstants.JJTOBJECTARRAY; } | True() { return ParserTreeConstants.JJTTRUE; } | False() { return ParserTreeConstants.JJTFALSE; } } void DirectiveAssign() : {} { Reference() } /** * Supports the Pluggable Directives * #foo( arg+ ) * @return true if ends with a newline */ boolean Directive(boolean afterNewline) : { Token id = null, t = null, u = null, end = null, _else = null; int argType; int argPos = 0; Directive d; int directiveType; boolean isVM = false; boolean isMacro = false; ArrayList argtypes = new ArrayList(4); String blockPrefix = ""; ASTBlock block = null, elseBlock = null; boolean hasParentheses = false; boolean newlineAtStart = afterNewline; } { [ (t = ) { // only possible if not after new line jjtThis.setPrefix(t.image); t = null; } ] /* * note that if we were escaped, that is now handled by * EscapedDirective() */ ((id = ) | (id = )) { String directiveName; int p = id.image.lastIndexOf(hash); if (id.kind == StandardParserConstants.BRACKETED_WORD) { directiveName = id.image.substring(p + 2, id.image.length() - 1); } else { directiveName = id.image.substring(p + 1); } d = getDirective(directiveName); /* * Velocimacro support : if the directive is macro directive * then set the flag so after the block parsing, we add the VM * right then. (So available if used w/in the current template ) */ if (directiveName.equals("macro")) { isMacro = true; } /* * set the directive name from here. No reason for the thing to know * about parser tokens */ jjtThis.setDirectiveName(directiveName); if ( d == null) { if( directiveName.charAt(0) == at ) { // block macro call of type: #@foobar($arg1 $arg2) astBody #end directiveType = Directive.BLOCK; } else { /* * if null, then not a real directive, but maybe a Velocimacro */ isVM = rsvc.isVelocimacro(directiveName, currentTemplate); directiveType = Directive.LINE; } } else { directiveType = d.getType(); } /* * now, switch us out of PRE_DIRECTIVE */ token_source.switchTo(DIRECTIVE); argPos = 0; } /** * Look for the pattern [WHITESPACE] */ ( LOOKAHEAD( { isLeftParenthesis() } ) /* * if this is indeed a token, match the #foo ( arg, arg... ) pattern */ ( ( | )* ( LOOKAHEAD({ !isRightParenthesis() }) ( | )* [ ( | )*] ( [ LOOKAHEAD( { isMacro && isAssignment() }) DirectiveAssign() ( | )* ( | )* { argtypes.add(ParserTreeConstants.JJTDIRECTIVEASSIGN); } ] LOOKAHEAD( { !isRightParenthesis() } ) ( argType = DirectiveArg() { argtypes.add(argType); if (d == null && argType == ParserTreeConstants.JJTWORD) { if (isVM) { throw new MacroParseException("Invalid argument " + (argPos+1) + " in macro call " + id.image, currentTemplate.getName(), id); } } argPos++; } ) | { if (!isMacro) { // We only allow line comments in macro definitions for now throw new MacroParseException("A Line comment is not allowed in " + id.image + " arguments", currentTemplate.getName(), id); } } [] ) )* ( | )* { hasParentheses = true; } ) | { token_source.stateStackPop(); } ) { afterNewline = false; } [ // Conditions where whitespace and newline postfix is eaten by space gobbling at this point: // - block directive // - new line before directive without backward compatibility mode // - backward compatibility mode *with parentheses* // - #include() or #parse() LOOKAHEAD(2, { directiveType != Directive.LINE || newlineAtStart && rsvc.getSpaceGobbling() != SpaceGobbling.BC || rsvc.getSpaceGobbling() == SpaceGobbling.BC && hasParentheses || d != null && (d instanceof Include || d instanceof Parse) }) ( [ ( t = ) ] ( u = ) ) { afterNewline = true; if (directiveType == Directive.LINE) { jjtThis.setPostfix(t == null ? u.image : t.image + u.image); } else { blockPrefix = (t == null ? u.image : t.image + u.image); } t = u = null; } ] { if (d != null) { d.checkArgs(argtypes, id, currentTemplate.getName()); } if (directiveType == Directive.LINE) { return afterNewline; } } /* * and the following block if the PD needs it */ ( ( ( LOOKAHEAD( { getToken(1).kind != END && getToken(1).kind != ELSE && ( !afterNewline || getToken(1).kind != WHITESPACE || getToken(2).kind != END && getToken(2).kind != ELSE ) }) afterNewline = Statement(afterNewline) )* { block = jjtThis; block.setPrefix(blockPrefix); blockPrefix = ""; } ) #Block ) [ LOOKAHEAD( 1, { afterNewline }) (t = ) { block.setPostfix(t.image); t = null; } ] /* * then an optional #else for the #foreach directive */ ( [ LOOKAHEAD( { d != null && (d instanceof Foreach) && getToken(1).kind == ELSE } ) ( (_else = ) ( [ LOOKAHEAD(2) ( [ ( t = ) ] ( u = ) ) { jjtThis.setPrefix(t == null ? u.image : t.image + u.image); t = u = null; afterNewline = true; } ] ( LOOKAHEAD( { getToken(1).kind != END && (!afterNewline || getToken(1).kind != WHITESPACE || getToken(2).kind != END) }) afterNewline = Statement(afterNewline) )* { elseBlock = jjtThis; } ) #Block { int pos = _else.image.lastIndexOf(hash); if (pos > 0) { block.setMorePostfix(_else.image.substring(0, pos)); } block = elseBlock; } ) ] ) [ LOOKAHEAD( 1, { afterNewline }) (t = ) { block.setPostfix(t.image); t = null; afterNewline = false; } ] ( (end = ) { afterNewline = false; } [ LOOKAHEAD(2, { newlineAtStart || rsvc.getSpaceGobbling() == SpaceGobbling.BC }) ( [ ( t = ) ] ( u = ) ) { jjtThis.setPostfix(t == null ? u.image : t.image + u.image); t = u = null; afterNewline = true; } ] { int pos = end.image.lastIndexOf(hash); if (pos > 0) { block.setMorePostfix(end.image.substring(0, pos)); } } ) { /* * VM : if we are processing a #macro directive, we need to * process the block. In truth, I can just register the name * and do the work later when init-ing. That would work * as long as things were always defined before use. This way * we don't have to worry about forward references and such... */ if (isMacro) { // Add the macro name so that we can peform escape processing // on defined macros String macroName = jjtThis.jjtGetChild(0).getFirstToken().image; macroNames.put(macroName, macroName); } if (d != null) { d.checkArgs(argtypes, id, currentTemplate.getName()); } /* * VM : end */ return afterNewline; } } /** * for creating a map in a #set * * #set($foo = {$foo : $bar, $blargh : $thingy}) */ void Map() : {} { ( LOOKAHEAD(2) Parameter() Parameter() ( Parameter() Parameter() )* | ( | )* ) /** note: need both tokens as they are generated in different states **/ ( | ) } void ObjectArray() : {} { [ Parameter() ( Parameter() )* ] } /** * supports the [n..m] vector generator for use in * the #foreach() to generate measured ranges w/o * needing explicit support from the app/servlet */ void IntegerRange() : {} { ( | )* ( Reference() | IntegerLiteral()) (|)* (|)* (Reference() | IntegerLiteral()) (|)* } /** * A Simplified parameter more suitable for an index position: $foo[$index] */ void IndexParameter() #void: {} { (|)* ( Expression() ) (|)* } /** * This method has yet to be fully implemented * but will allow arbitrarily nested method * calls */ void Parameter() #void: {} { (|)* ( StringLiteral() | IntegerLiteral() | LOOKAHEAD( ( | )* ( Reference() | IntegerLiteral()) ( | )* ) IntegerRange() | Map() | ObjectArray() | True() | False() | Reference() | FloatingPointLiteral() ) (|)* } /** * This method has yet to be fully implemented * but will allow arbitrarily nested method * calls */ void Method() : {} { Identifier() [ Expression() ( Expression() )* ] } void Index() : {} { IndexParameter() } void Reference() : {} { /* * A reference is either $ or ${} or ${'|') */ ( ( | ) (Index())* (LOOKAHEAD(2) (LOOKAHEAD(3) Method() | Identifier() ) (Index())* )* ) | ( ( | ) (Index())* (LOOKAHEAD(2) (LOOKAHEAD(3) Method() | Identifier() ) (Index())* )* [ Expression() ] ( | ) ) } void True() : {} { } void False() : {} { } /** * This is somewhat of a kludge, the problem is that the parser picks * up on '$[' , or '$![' as being a Reference, and does not dismiss it even though * there is no between $ and [, This has something to do * with the LOOKAHEAD in Reference, but I never found a way to resolve * it in a more fashionable way.. */ TOKEN : { } /** * This method is responsible for allowing * all non-grammar text to pass through * unscathed. * @return true if last read token was a newline */ boolean Text() : { Token t = null; } { { return true; } | { return false; } | { return false; } | { return false; } | { return false; } | { return false; } | { return false; } | { return false; } | { return false; } | { return false; } | { return false; } | { return false; } | t= { /* Drop the ending zero-width whitespace */ t.image = t.image.substring(0, t.image.length() - 1); return false; } } /* ----------------------------------------------------------------------- * * Defined Directive Syntax * * ----------------------------------------------------------------------*/ boolean IfStatement(boolean afterNewline) : { Token t = null, u = null, end = null; ASTBlock lastBlock = null; boolean newlineAtStart = afterNewline; } { [ ( t = ) { // only possible if not after new line jjtThis.setPrefix(t.image); t = null; } ] ( | )* Expression() ( [ LOOKAHEAD(2) ( [ ( t = ) ] ( u = ) ) { jjtThis.setPrefix(t == null ? u.image : t.image + u.image); t = u = null; afterNewline = true; } ] ( LOOKAHEAD( { (getToken(1).kind != ELSEIF && getToken(1).kind != ELSE && getToken(1).kind != END) && (!afterNewline || getToken(1).kind != WHITESPACE || (getToken(2).kind != ELSEIF && getToken(2).kind != ELSE && getToken(2).kind != END)) }) afterNewline = Statement(afterNewline) )* { lastBlock = jjtThis; } ) #Block [ LOOKAHEAD( { getToken(1).kind == ELSEIF || (afterNewline && getToken(1).kind == WHITESPACE && getToken(2).kind == ELSEIF) }) ( LOOKAHEAD( { getToken(1).kind == ELSEIF || (afterNewline && getToken(1).kind == WHITESPACE && getToken(2).kind == ELSEIF) }) ( lastBlock = ElseIfStatement(lastBlock, afterNewline) { afterNewline = lastBlock.endsWithNewline; } ))+ ] [ LOOKAHEAD( { getToken(1).kind == ELSE || (afterNewline && getToken(1).kind == WHITESPACE && getToken(2).kind == ELSE) } ) lastBlock = ElseStatement(lastBlock, afterNewline) { afterNewline = lastBlock.endsWithNewline; } ] [ LOOKAHEAD( 1, { afterNewline } ) ( t = ) { lastBlock.setPostfix(t.image); t = null; } ] (end = ) { afterNewline = false; } [ LOOKAHEAD(2, { newlineAtStart || rsvc.getSpaceGobbling() == SpaceGobbling.BC } ) ( [ ( t = ) ] ( u = ) ) { jjtThis.setPostfix(t == null ? u.image : t.image + u.image); afterNewline = true; } ] { int pos = end.image.lastIndexOf(hash); if (pos > 0) { lastBlock.setMorePostfix(end.image.substring(0, pos)); } return afterNewline; } } ASTBlock ElseStatement(ASTBlock previousBlock, boolean afterNewline) : { Token t = null, u = null, _else = null; ASTBlock block = null; } { [ ( t = ) { previousBlock.setPostfix(t.image); t = null; } ] (_else = ) ( [ LOOKAHEAD(2) ( [ ( t = ) ] ( u = ) ) { jjtThis.setPrefix(t == null ? u.image : t.image + u.image); t = u = null; afterNewline = true; } ] ( LOOKAHEAD( { getToken(1).kind != END && (!afterNewline || getToken(1).kind != WHITESPACE || getToken(2).kind != END) }) afterNewline = Statement(afterNewline) )* { block = jjtThis; block.endsWithNewline = afterNewline; } ) #Block { int pos = _else.image.lastIndexOf(hash); if (pos > 0) { previousBlock.setMorePostfix(_else.image.substring(0, pos)); } return block; } } ASTBlock ElseIfStatement(ASTBlock previousBlock, boolean afterNewline) : { Token t = null, u = null, elseif = null; ASTBlock block = null; } { [ ( t = ) { previousBlock.setPostfix(t.image); t = null; } ] (elseif = ) ( | )* Expression() ( [ LOOKAHEAD(2) ( [ ( t = ) ] ( u = ) ) { jjtThis.setPrefix(t == null ? u.image : t.image + u.image); t = u = null; afterNewline = true; } ] ( LOOKAHEAD( { (getToken(1).kind != ELSEIF && getToken(1).kind != ELSE && getToken(1).kind != END) && (!afterNewline || getToken(1).kind != WHITESPACE || (getToken(2).kind != ELSEIF && getToken(2).kind != ELSE && getToken(2).kind != END)) }) afterNewline = Statement(afterNewline) )* { block = jjtThis; block.endsWithNewline = afterNewline; } ) #Block { int pos = elseif.image.lastIndexOf(hash); if (pos > 0) { previousBlock.setMorePostfix(elseif.image.substring(0, pos)); } return block; } } /** * Currently support both types of set : * #set( expr ) * #set expr */ boolean SetDirective(boolean afterNewline) : { Token t = null, u = null; boolean endsWithNewline = false; } { [ ( t = ) { // only possible after new line jjtThis.setPrefix(t.image); t = null; } ] (( | )* Reference() ( | )* Expression() { /* * ensure that inSet is false. Leads to some amusing bugs... */ token_source.setInSet(false); } [ LOOKAHEAD(2, { afterNewline || rsvc.getSpaceGobbling() == SpaceGobbling.BC } ) ( [ ( t = ) ] ( u = ) ) { jjtThis.setPostfix(t == null ? u.image : t.image + u.image); endsWithNewline = true; } ] ) { return endsWithNewline; } } /* ----------------------------------------------------------------------- * * Expression Syntax * * ----------------------------------------------------------------------*/ void Expression() : {} { // LOOKAHEAD( PrimaryExpression() ) Assignment() //| ConditionalOrExpression() } void Assignment() #Assignment(2) : {} { PrimaryExpression() Expression() } void ConditionalOrExpression() #void : {} { ConditionalAndExpression() ( ( | ) ConditionalAndExpression() #OrNode(2) )* } void ConditionalAndExpression() #void : {} { EqualityExpression() ( EqualityExpression() #AndNode(2) )* } void EqualityExpression() #void : {} { RelationalExpression() ( RelationalExpression() #EQNode(2) | RelationalExpression() #NENode(2) )* } void RelationalExpression() #void : {} { AdditiveExpression() ( AdditiveExpression() #LTNode(2) | AdditiveExpression() #GTNode(2) | AdditiveExpression() #LENode(2) | AdditiveExpression() #GENode(2) )* } void AdditiveExpression() #void : {} { MultiplicativeExpression() ( MultiplicativeExpression() #AddNode(2) | MultiplicativeExpression() #SubtractNode(2) )* } void MultiplicativeExpression() #void : {} { UnaryExpression() ( UnaryExpression() #MulNode(2) | UnaryExpression() #DivNode(2) | UnaryExpression() #ModNode(2) )* } void UnaryExpression() #void : {} { ( | )* ( UnaryExpression() #NotNode(1) | PrimaryExpression() #NegateNode(1) | PrimaryExpression() ) } void PrimaryExpression() #void : {} { ( | )* ( StringLiteral() | Reference() | IntegerLiteral() | LOOKAHEAD( ( | )* ( Reference() | IntegerLiteral()) ( | )* ) IntegerRange() | FloatingPointLiteral() | Map() | ObjectArray() | True() | False() | Expression() ) ( | )* } /* ====================================================================== Notes ----- template == the input stream for this parser, contains 'VTL' mixed in with 'schmoo' VTL == Velocity Template Language : the references, directives, etc schmoo == the non-VTL component of a template reference == VTL entity that represents data within the context. ex. $foo directive == VTL entity that denotes 'action' (#set, #foreach, #if ) defined directive (DD) == VTL directive entity that is expressed explicitly w/in this grammar pluggable directive (PD) == VTL directive entity that is defined outside of the grammar. PD's allow VTL to be easily expandable w/o parser modification. The problem with parsing VTL is that an input stream consists generally of little bits of VTL mixed in with 'other stuff, referred to as 'schmoo'. Unlike other languages, like C or Java, where the parser can punt whenever it encounters input that doesn't conform to the grammar, the VTL parser can't do that. It must simply output the schmoo and keep going. There are a few things that we do here : - define a set of parser states (DEFAULT, DIRECTIVE, REFERENCE, etc) - define for each parser state a set of tokens for each state - define the VTL grammar, expressed (mostly) in the productions such as Text(), SetStatement(), etc. It is clear that this expression of the VTL grammar (the contents of this .jjt file) is maturing and evolving as we learn more about how to parse VTL ( and as I learn about parsing...), so in the event this documentation is in disagreement w/ the source, the source takes precedence. :) Parser States ------------- DEFAULT : This is the base or starting state, and strangely enough, the default state. PRE_DIRECTIVE : State immediately following '#' before we figure out which defined or pluggable directive (or neither) we are working with. DIRECTIVE : This state is triggered by the a match of a DD or a PD. PRE_REFERENCE : Triggered by '$'. Analagous to PRE_DIRECTIVE. When '-' is allowed in identifiers, this state is called PRE_OLD_REFERENCE. REFERENCE : Triggered by the REFMODIFIER : Triggered by . when in REFERENCE, REFMODIFIER or REFMOD3. When '-' is allowed in identifiers, this state is called OLD_REFMODIFIER. REFMOD2 : Triggered by '(' when in REFMODIFIER REFMOD3 : Triggered by the corresponding ')' REFINDEX : Array index. Triggered by '[' in REFERENCE, REFMODIFIER, REFMOD3. ALT_VAL : Alternate value. Triggered by '|' in REFERENCE, REFMODIFIER, REFMOD3. (cont) Escape Sequences ---------------- The escape processing in VTL is very simple. The '\' character acts only as an escape when : 1) On or more touch a VTL element. A VTL element is either : 1) It preceeds a reference that is in the context. 2) It preceeds a defined directive (#set, #if, #end, etc) or a valid pluggable directive, such as #foreach In all other cases the '\' is just another piece of text. The purpose of this is to allow the non-VTL parts of a template (the 'schmoo') to not have to be altered for processing by Velocity. So if in the context $foo and $bar were defined and $woogie was not \$foo \$bar \$woogie would output $foo $bar \$woogie Further, you can stack them and they affect left to right, just like convention escape characters in other languages. \$foo = $foo \\$foo = \ \\\$foo = \$foo What You Expect --------------- The recent versions of the parser are trying to support precise output to support general template use. The directives do not render trailing whitespace and newlines if followed by a newline. They will render preceeding whitespace. The only exception is #set, which also eats preceeding whitespace. So, with a template : ------ #set($foo="foo") #if($foo) \$foo = $foo #end ------ it will render precisely : ------ $foo = foo ------ */