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