| /* |
| * 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 freemarker.core; |
| |
| import freemarker.core.LocalLambdaExpression.LambdaParameterList; |
| import freemarker.template.*; |
| import freemarker.template.utility.*; |
| import java.io.*; |
| import java.util.*; |
| import static freemarker.template.Configuration.*; |
| |
| /** |
| * 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_FOREACH = 1; |
| private static final int ITERATOR_BLOCK_KIND_ITEMS = 2; |
| private static final int ITERATOR_BLOCK_KIND_USER_DIRECTIVE = 3; |
| |
| 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, preventStrippings; |
| private int incompatibleImprovements; |
| private OutputFormat outputFormat; |
| private int autoEscapingPolicy; |
| private boolean autoEscaping; |
| private ParserConfiguration pCfg; |
| |
| /** Keeps track of #list and #foreach nesting. */ |
| private List<ParserIteratorBlockContext> iteratorBlockContexts; |
| |
| /** |
| * Keeps track of the nesting depth of directives that support #break. |
| */ |
| private int breakableDirectiveNesting; |
| |
| /** |
| * Keeps track of the nesting depth of directives that support #continue. |
| */ |
| private int continuableDirectiveNesting; |
| |
| private boolean inMacro, inFunction, requireArgsSpecialVariable; |
| private LinkedList escapes = new LinkedList(); |
| private int mixedContentNesting; // for stripText |
| |
| /** |
| * Create an FM expression parser using a string. |
| * |
| * @Deprecated This is an internal API of FreeMarker; can be removed any time. |
| */ |
| static public FMParser createExpressionParser(String s) { |
| SimpleCharStream scs = new SimpleCharStream(new StringReader(s), 1, 1, s.length()); |
| FMParserTokenManager token_source = new FMParserTokenManager(scs); |
| token_source.SwitchTo(FMParserConstants.FM_EXPRESSION); |
| FMParser parser = new FMParser(token_source); |
| token_source.setParser(parser); |
| return parser; |
| } |
| |
| /** |
| * Constructs a new parser object. |
| * |
| * @param template |
| * The template associated with this parser. |
| * @param reader |
| * The character stream to use as input |
| * @param strictSyntaxMode |
| * Whether FreeMarker directives must start with a # |
| * |
| * @Deprecated This is an internal API of FreeMarker; will be removed in 2.4. |
| */ |
| public FMParser(Template template, Reader reader, boolean strictSyntaxMode, boolean stripWhitespace) { |
| this(template, reader, strictSyntaxMode, stripWhitespace, Configuration.AUTO_DETECT_TAG_SYNTAX); |
| } |
| |
| /** |
| * @Deprecated This is an internal API of FreeMarker; will be changed in 2.4. |
| */ |
| public FMParser(Template template, Reader reader, boolean strictSyntaxMode, boolean stripWhitespace, int tagSyntax) { |
| this(template, reader, strictSyntaxMode, stripWhitespace, tagSyntax, |
| Configuration.PARSED_DEFAULT_INCOMPATIBLE_ENHANCEMENTS); |
| } |
| |
| /** |
| * @Deprecated This is an internal API of FreeMarker; will be changed in 2.4. |
| */ |
| public FMParser(Template template, Reader reader, boolean strictSyntaxMode, boolean stripWhitespace, |
| int tagSyntax, int incompatibleImprovements) { |
| this(template, reader, strictSyntaxMode, stripWhitespace, |
| tagSyntax, Configuration.AUTO_DETECT_NAMING_CONVENTION, incompatibleImprovements); |
| } |
| |
| /** |
| * @Deprecated This is an internal API of FreeMarker; will be changed in 2.4. |
| */ |
| public FMParser(String template) { |
| this(dummyTemplate(), |
| new StringReader(template), true, true); |
| } |
| |
| private static Template dummyTemplate() { |
| try { |
| return new Template(null, new StringReader(""), Configuration.getDefaultConfiguration()); |
| } catch (IOException e) { |
| throw new RuntimeException("Failed to create dummy template", e); |
| } |
| } |
| |
| /** |
| * @Deprecated This is an internal API of FreeMarker; will be changed in 2.4. |
| */ |
| public FMParser(Template template, Reader reader, boolean strictSyntaxMode, boolean whitespaceStripping, |
| int tagSyntax, int namingConvention, int incompatibleImprovements) { |
| this(template, reader, |
| new LegacyConstructorParserConfiguration( |
| strictSyntaxMode, whitespaceStripping, |
| tagSyntax, LEGACY_INTERPOLATION_SYNTAX, namingConvention, |
| template != null ? template.getParserConfiguration().getAutoEscapingPolicy() |
| : Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, |
| template != null ? template.getParserConfiguration().getOutputFormat() |
| : null, |
| template != null ? template.getParserConfiguration().getRecognizeStandardFileExtensions() |
| : null, |
| template != null ? template.getParserConfiguration().getTabSize() |
| : null, |
| new Version(incompatibleImprovements), |
| template != null ? template.getArithmeticEngine() : null)); |
| } |
| |
| /** |
| * @Deprecated This is an internal API of FreeMarker; don't call it from outside FreeMarker. |
| * |
| * @since 2.3.24 |
| */ |
| public FMParser(Template template, Reader reader, ParserConfiguration pCfg) { |
| this(template, true, readerToTokenManager(reader, pCfg), pCfg); |
| } |
| |
| private static FMParserTokenManager readerToTokenManager(Reader reader, ParserConfiguration pCfg) { |
| SimpleCharStream simpleCharStream = new SimpleCharStream(reader, 1, 1); |
| simpleCharStream.setTabSize(pCfg.getTabSize()); |
| return new FMParserTokenManager(simpleCharStream); |
| } |
| |
| /** |
| * @Deprecated This is an internal API of FreeMarker; don't call it from outside FreeMarker. |
| * |
| * @since 2.3.24 |
| */ |
| public FMParser(Template template, boolean newTemplate, FMParserTokenManager tkMan, ParserConfiguration pCfg) { |
| this(tkMan); |
| |
| NullArgumentException.check(pCfg); |
| this.pCfg = pCfg; |
| |
| NullArgumentException.check(template); |
| this.template = template; |
| |
| // Hack due to legacy public constructors (removed in 2.4): |
| if (pCfg instanceof LegacyConstructorParserConfiguration) { |
| LegacyConstructorParserConfiguration lpCfg = (LegacyConstructorParserConfiguration) pCfg; |
| lpCfg.setArithmeticEngineIfNotSet(template.getArithmeticEngine()); |
| lpCfg.setAutoEscapingPolicyIfNotSet(template.getConfiguration().getAutoEscapingPolicy()); |
| lpCfg.setOutputFormatIfNotSet(template.getOutputFormat()); |
| lpCfg.setRecognizeStandardFileExtensionsIfNotSet( |
| template.getParserConfiguration().getRecognizeStandardFileExtensions()); |
| lpCfg.setTabSizeIfNotSet( |
| template.getParserConfiguration().getTabSize()); |
| } |
| |
| int incompatibleImprovements = pCfg.getIncompatibleImprovements().intValue(); |
| token_source.incompatibleImprovements = incompatibleImprovements; |
| this.incompatibleImprovements = incompatibleImprovements; |
| |
| { |
| OutputFormat outputFormatFromExt; |
| if (!pCfg.getRecognizeStandardFileExtensions() |
| || (outputFormatFromExt = getFormatFromStdFileExt()) == null) { |
| autoEscapingPolicy = pCfg.getAutoEscapingPolicy(); |
| outputFormat = pCfg.getOutputFormat(); |
| } else { |
| // Override it |
| autoEscapingPolicy = Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY; |
| outputFormat = outputFormatFromExt; |
| } |
| } |
| recalculateAutoEscapingField(); |
| |
| token_source.setParser(this); |
| |
| token_source.strictSyntaxMode = pCfg.getStrictSyntaxMode(); |
| |
| 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); |
| } |
| |
| token_source.interpolationSyntax = pCfg.getInterpolationSyntax(); |
| |
| 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) { |
| _TemplateAPI.setAutoEscaping(template, autoEscaping); |
| _TemplateAPI.setOutputFormat(template, outputFormat); |
| } |
| } |
| |
| void setupStringLiteralMode(FMParser parentParser, OutputFormat outputFormat) { |
| FMParserTokenManager parentTokenSource = parentParser.token_source; |
| |
| token_source.initialNamingConvention = parentTokenSource.initialNamingConvention; |
| token_source.namingConvention = parentTokenSource.namingConvention; |
| token_source.namingConventionEstabilisher = parentTokenSource.namingConventionEstabilisher; |
| token_source.SwitchTo(NO_DIRECTIVE); |
| |
| this.outputFormat = outputFormat; |
| recalculateAutoEscapingField(); |
| if (incompatibleImprovements < _VersionInts.V_2_3_24) { |
| // Emulate bug, where the string literal parser haven't inherited the IcI: |
| incompatibleImprovements = _VersionInts.V_2_3_0; |
| } |
| |
| // So that loop variable built-ins, like ?index, works inside the interpolations in the string literal: |
| iteratorBlockContexts = parentParser.iteratorBlockContexts; |
| } |
| |
| void tearDownStringLiteralMode(FMParser parentParser) { |
| // If the naming convention was established inside the string literal, it's inherited by the parent: |
| FMParserTokenManager parentTokenSource = parentParser.token_source; |
| parentTokenSource.namingConvention = token_source.namingConvention; |
| parentTokenSource.namingConventionEstabilisher = token_source.namingConventionEstabilisher; |
| } |
| |
| /** |
| * Used when we need to recreate the source code from the AST (such as for the FM2 to FM3 converter). |
| */ |
| void setPreventStrippings(boolean preventStrippings) { |
| this.preventStrippings = preventStrippings; |
| } |
| |
| private OutputFormat getFormatFromStdFileExt() { |
| String sourceName = template.getSourceName(); |
| if (sourceName == null) { |
| return null; // Not possible anyway... |
| } |
| |
| int ln = sourceName.length(); |
| if (ln < 5) return null; |
| |
| char c = sourceName.charAt(ln - 5); |
| if (c != '.') return null; |
| |
| c = sourceName.charAt(ln - 4); |
| if (c != 'f' && c != 'F') return null; |
| |
| c = sourceName.charAt(ln - 3); |
| if (c != 't' && c != 'T') return null; |
| |
| c = sourceName.charAt(ln - 2); |
| if (c != 'l' && c != 'L') return null; |
| |
| c = sourceName.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(Expression exp, String expected) throws ParseException { |
| if (exp instanceof StringLiteral) { |
| 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(Expression exp, String expected) throws ParseException { |
| if (exp instanceof NumberLiteral) { |
| 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(Expression exp, String expected) throws ParseException { |
| if (exp instanceof BooleanLiteral) { |
| throw new ParseException("Found: " + exp.getCanonicalForm() + " literal. Expecting " + expected , exp); |
| } |
| } |
| |
| /** |
| * Throw an exception if the expression passed in is a Hash Literal |
| */ |
| private void notHashLiteral(Expression exp, String expected) throws ParseException { |
| if (exp instanceof HashLiteral) { |
| 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(Expression exp, String expected) |
| throws ParseException |
| { |
| if (exp instanceof ListLiteral) { |
| 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(Expression exp) throws ParseException { |
| notStringLiteral(exp, "number"); |
| notListLiteral(exp, "number"); |
| notHashLiteral(exp, "number"); |
| notBooleanLiteral(exp, "number"); |
| } |
| |
| /** |
| * Throw an exception if the expression passed in is not a string. |
| */ |
| private void stringLiteralOnly(Expression exp) throws ParseException { |
| notNumberLiteral(exp, "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(Expression exp) throws ParseException { |
| notStringLiteral(exp, "boolean (true/false)"); |
| notListLiteral(exp, "boolean (true/false)"); |
| notHashLiteral(exp, "boolean (true/false)"); |
| notNumberLiteral(exp, "boolean (true/false)"); |
| } |
| |
| private Expression escapedExpression(Expression exp) throws ParseException { |
| if (!escapes.isEmpty()) { |
| return ((EscapeBlock) escapes.getFirst()).doEscape(exp); |
| } else { |
| return exp; |
| } |
| } |
| |
| private boolean getBoolean(Expression 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<ParserIteratorBlockContext>(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, Expression lhoExp, Token biName) |
| throws ParseException { |
| int size = iteratorBlockContexts != null ? iteratorBlockContexts.size() : 0; |
| for (int i = size - 1; i >= 0; i--) { |
| ParserIteratorBlockContext ctx = 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); |
| } |
| |
| private String forEachDirectiveSymbol() { |
| // [2.4] Use camel case as the default |
| return token_source.namingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION ? "#forEach" : "#foreach"; |
| } |
| |
| } |
| |
| 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 |
| * <comment>. This variable tells us what the closing tag should be, and when we hit that, we resume parsing. Note |
| * that with this scheme, <comment> and <noparse> tags cannot nest recursively, but it is not clear how important |
| * that is. |
| */ |
| String noparseTag; |
| |
| private FMParser parser; |
| private int postInterpolationLexState = -1; |
| /** |
| * Keeps track of how deeply nested we have the hash literals. This is necessary since we need to be able to |
| * distinguish the } used to close a hash literal and the one used to close a ${ |
| */ |
| private int curlyBracketNesting; |
| private int parenthesisNesting; |
| private int bracketNesting; |
| private boolean inFTLHeader; |
| boolean strictSyntaxMode, |
| squBracTagSyntax, |
| autodetectTagSyntax, |
| tagSyntaxEstablished, |
| inInvocation; |
| int interpolationSyntax; |
| int initialNamingConvention; |
| int namingConvention; |
| Token namingConventionEstabilisher; |
| int incompatibleImprovements; |
| |
| void setParser(FMParser parser) { |
| this.parser = parser; |
| } |
| |
| // This method checks if we are in a strict mode where all |
| // FreeMarker directives must start with <#. It also handles |
| // tag syntax 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; |
| |
| // Non-strict syntax (deprecated) only supports legacy naming convention. |
| // We didn't push this on the tokenizer because it made it slow, so we filter here. |
| if (!strictSyntaxMode |
| && (tokenNamingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION) |
| && !isStrictTag(image)) { |
| tok.kind = STATIC_TEXT_NON_WS; |
| return; |
| } |
| |
| char firstChar = image.charAt(0); |
| if (autodetectTagSyntax && !tagSyntaxEstablished) { |
| squBracTagSyntax = (firstChar == '['); |
| } |
| if ((firstChar == '[' && !squBracTagSyntax) || (firstChar == '<' && squBracTagSyntax)) { |
| tok.kind = STATIC_TEXT_NON_WS; |
| return; |
| } |
| |
| if (!strictSyntaxMode) { |
| // Legacy feature (or bug?): Tag syntax never gets estabilished in non-strict mode. |
| // We do establish the naming convention though. |
| checkNamingConvention(tok, tokenNamingConvention); |
| SwitchTo(newLexState); |
| return; |
| } |
| |
| // For square bracket tags there's no non-strict token, so we are sure that it's an FTL tag. |
| // But if it's an angle bracket tag, we have to check if it's just static text or and FTL tag, because the |
| // tokenizer will emit the same kind of token for both. |
| if (!squBracTagSyntax && !isStrictTag(image)) { |
| tok.kind = STATIC_TEXT_NON_WS; |
| return; |
| } |
| |
| // We only get here if this is a strict FTL tag. |
| tagSyntaxEstablished = true; |
| |
| if (incompatibleImprovements >= _VersionInts.V_2_3_28 |
| || interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX) { |
| // For END_xxx tags, as they can't contain expressions, the whole tag is a single token. So this is the only |
| // chance to check if we got something inconsistent like `</#if]`. (We can't do this at the #CLOSE_TAG1 or |
| // such, as at that point it's possible that the tag syntax is not yet established.) |
| char lastChar = image.charAt(image.length() - 1); |
| // Is it an end tag? |
| if (lastChar == ']' || lastChar == '>') { |
| if (!squBracTagSyntax && lastChar != '>' || squBracTagSyntax && lastChar != ']') { |
| throw new TokenMgrError( |
| "The tag shouldn't end with \""+ lastChar + "\".", |
| TokenMgrError.LEXICAL_ERROR, |
| tok.beginLine, tok.beginColumn, |
| tok.endLine, tok.endColumn); |
| } |
| } // if end-tag |
| } |
| |
| checkNamingConvention(tok, tokenNamingConvention); |
| |
| SwitchTo(newLexState); |
| } |
| |
| void checkNamingConvention(Token tok) { |
| checkNamingConvention(tok, _CoreStringUtils.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); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| private boolean isStrictTag(String image) { |
| return image.length() > 2 && (image.charAt(1) == '#' || image.charAt(2) == '#'); |
| } |
| |
| /** |
| * 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 _CoreStringUtils.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 && !tagSyntaxEstablished) { |
| squBracTagSyntax = (firstChar == '['); |
| } |
| if (squBracTagSyntax && firstChar == '<') { |
| tok.kind = STATIC_TEXT_NON_WS; |
| return; |
| } |
| if (!squBracTagSyntax && firstChar == '[') { |
| tok.kind = STATIC_TEXT_NON_WS; |
| return; |
| } |
| tagSyntaxEstablished = 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 startInterpolation(Token tok) { |
| if ( |
| interpolationSyntax == LEGACY_INTERPOLATION_SYNTAX |
| && tok.kind == SQUARE_BRACKET_INTERPOLATION_OPENING |
| || interpolationSyntax == DOLLAR_INTERPOLATION_SYNTAX |
| && tok.kind != DOLLAR_INTERPOLATION_OPENING |
| || interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX |
| && tok.kind != SQUARE_BRACKET_INTERPOLATION_OPENING) { |
| tok.kind = STATIC_TEXT_NON_WS; |
| return; |
| } |
| |
| if (postInterpolationLexState != -1) { |
| // This certainly never occurs, as starting an interpolation in expression mode fails earlier. |
| char c = tok.image.charAt(0); |
| throw new TokenMgrError( |
| "You can't start an interpolation (" + tok.image + "..." |
| + (interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX ? "]" : "}") |
| + ") here as you are inside another interpolation.)", |
| TokenMgrError.LEXICAL_ERROR, |
| tok.beginLine, tok.beginColumn, |
| tok.endLine, tok.endColumn); |
| } |
| postInterpolationLexState = curLexState; |
| SwitchTo(FM_EXPRESSION); |
| } |
| |
| private void endInterpolation(Token closingTk) { |
| SwitchTo(postInterpolationLexState); |
| postInterpolationLexState = -1; |
| } |
| |
| private TokenMgrError newUnexpectedClosingTokenException(Token closingTk) { |
| return new TokenMgrError( |
| "You can't have an \"" + closingTk.image + "\" here, as there's nothing open that it could close.", |
| TokenMgrError.LEXICAL_ERROR, |
| closingTk.beginLine, closingTk.beginColumn, |
| closingTk.endLine, closingTk.endColumn); |
| } |
| |
| 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 (!tagSyntaxEstablished) { |
| squBracTagSyntax = matchedToken.image.charAt(0) == '['; |
| tagSyntaxEstablished = 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>> |
| | |
| <FOREACH : <START_TAG> "for" ("e" | "E") "ach" <BLANK>> { |
| handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 3), FM_EXPRESSION); |
| } |
| | |
| <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); } |
| | |
| <TRANSFORM : <START_TAG> "transform" <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); } |
| | |
| <CALL : <START_TAG> "call" <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); } |
| | |
| <COMMENT : <START_TAG> "comment" <CLOSE_TAG1>> { |
| handleTagSyntaxAndSwitch(matchedToken, NO_PARSE); noparseTag = "comment"; |
| } |
| | |
| <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_FOREACH : <END_TAG> "for" ("e" | "E") "ach" <CLOSE_TAG1>> { |
| handleTagSyntaxAndSwitch(matchedToken, getTagNamingConvention(matchedToken, 3), 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_TRANSFORM : <END_TAG> "transform" <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); } |
| | |
| <CONTINUE : <START_TAG> "continue" <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", "_"])+> |
| { |
| if (!tagSyntaxEstablished && incompatibleImprovements < _VersionInts.V_2_3_19) { |
| matchedToken.kind = STATIC_TEXT_NON_WS; |
| } else { |
| char firstChar = matchedToken.image.charAt(0); |
| |
| if (!tagSyntaxEstablished && autodetectTagSyntax) { |
| squBracTagSyntax = (firstChar == '['); |
| tagSyntaxEstablished = true; |
| } |
| |
| if (firstChar == '<' && squBracTagSyntax) { |
| matchedToken.kind = STATIC_TEXT_NON_WS; |
| } else if (firstChar == '[' && !squBracTagSyntax) { |
| matchedToken.kind = STATIC_TEXT_NON_WS; |
| } else if (strictSyntaxMode) { |
| 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 (_CoreAPI.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, NO_DIRECTIVE> 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); } |
| | |
| <SQUARE_BRACKET_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" | ">="> |
| | |
| <LAMBDA_ARROW : "->" | "->"> |
| | |
| <PLUS : "+"> |
| | |
| <MINUS : "-"> |
| | |
| <TIMES : "*"> |
| | |
| <DOUBLE_STAR : "**"> |
| | |
| <ELLIPSIS : "..."> |
| | |
| <DIVIDE : "/"> |
| | |
| <PERCENT : "%"> |
| | |
| <AND : "&" | "&&" | "&&" | "\\and" > |
| | |
| <OR : "|" | "||"> |
| | |
| <EXCLAM : "!"> |
| | |
| <COMMA : ","> |
| | |
| <SEMICOLON : ";"> |
| | |
| <COLON : ":"> |
| | |
| <OPEN_BRACKET : "["> |
| { |
| ++bracketNesting; |
| } |
| | |
| <CLOSE_BRACKET : "]"> |
| { |
| if (bracketNesting > 0) { |
| --bracketNesting; |
| } else if (interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX && postInterpolationLexState != -1) { |
| endInterpolation(matchedToken); |
| } else { |
| // There's a legacy glitch where you can always close a tag with `]`, like `<#if x]`. We have to keep that |
| // working for backward compatibility, hence we don't always throw at !squBracTagSyntax: |
| if (!squBracTagSyntax |
| && (incompatibleImprovements >= _VersionInts.V_2_3_28 |
| || interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX) |
| || postInterpolationLexState != -1 /* We're in an interpolation => We aren't in a tag */) { |
| throw newUnexpectedClosingTokenException(matchedToken); |
| } |
| |
| // Close tag, either legally or to emulate legacy glitch: |
| matchedToken.kind = DIRECTIVE_END; |
| if (inFTLHeader) { |
| eatNewline(); |
| inFTLHeader = false; |
| } |
| SwitchTo(DEFAULT); |
| } |
| } |
| | |
| <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 : "{"> |
| { |
| ++curlyBracketNesting; |
| } |
| | |
| <CLOSING_CURLY_BRACKET : "}"> |
| { |
| if (curlyBracketNesting > 0) { |
| --curlyBracketNesting; |
| } else if (interpolationSyntax != SQUARE_BRACKET_INTERPOLATION_SYNTAX && postInterpolationLexState != -1) { |
| endInterpolation(matchedToken); |
| } else { |
| throw newUnexpectedClosingTokenException(matchedToken); |
| } |
| } |
| | |
| <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 closerC = matchedToken.image.charAt(0) != '[' ? '}' : ']'; |
| throw new TokenMgrError( |
| "You can't use " + matchedToken.image + "..." + closerC + " (an interpolation) here as you are " |
| + "already in FreeMarker-expression-mode. Thus, instead of " + matchedToken.image + "myExpression" |
| + closerC + ", just write myExpression. (" + matchedToken.image + "..." + closerC + " is only " |
| + "used where otherwise static text is expected, i.e., outside FreeMarker tags and " |
| + "interpolations, or inside string literals.)", |
| 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" |
| ] |
| > |
| | |
| // Keep this in sync with StringUtil.isBackslashEscapedFTLIdentifierCharacter |
| <#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 || postInterpolationLexState != -1 /* We are in an interpolation */) { |
| matchedToken.kind = NATURAL_GT; |
| } else { |
| SwitchTo(DEFAULT); |
| } |
| } |
| | |
| <EMPTY_DIRECTIVE_END : "/>" | "/]"> |
| { |
| if (tagSyntaxEstablished && (incompatibleImprovements >= _VersionInts.V_2_3_28 |
| || interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX)) { |
| String image = matchedToken.image; |
| char lastChar = image.charAt(image.length() - 1); |
| if (!squBracTagSyntax && lastChar != '>' || squBracTagSyntax && lastChar != ']') { |
| throw new TokenMgrError( |
| "The tag shouldn't end with \"" + lastChar + "\".", |
| TokenMgrError.LEXICAL_ERROR, |
| matchedToken.beginLine, matchedToken.beginColumn, |
| matchedToken.endLine, matchedToken.endColumn); |
| } |
| } |
| |
| 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 OrExpression, since |
| * the OR is the operator with the lowest |
| * precedence. |
| */ |
| Expression Expression() : |
| { |
| Expression exp; |
| } |
| { |
| exp = OrExpression() |
| { |
| return exp; |
| } |
| } |
| |
| /** |
| * Should be called HighestPrecedenceExpression. |
| * Deals with the operators that have the highest precedence. Also deals with `exp!default` and `exp!`, due to parser |
| * tricks needed because of the last. |
| */ |
| Expression PrimaryExpression() : |
| { |
| Expression exp; |
| } |
| { |
| exp = AtomicExpression() |
| ( |
| exp = DotVariable(exp) |
| | |
| exp = DynamicKey(exp) |
| | |
| exp = MethodArgs(exp) |
| | |
| exp = BuiltIn(exp) |
| | |
| exp = DefaultTo(exp) |
| | |
| exp = Exists(exp) |
| )* |
| { |
| return exp; |
| } |
| } |
| |
| /** |
| * Lowest level expression, a literal, a variable, |
| * or a possibly more complex expression bounded |
| * by parentheses. |
| */ |
| Expression AtomicExpression() : |
| { |
| Expression exp; |
| } |
| { |
| ( |
| exp = NumberLiteral() |
| | |
| exp = HashLiteral() |
| | |
| exp = StringLiteral(true) |
| | |
| exp = BooleanLiteral() |
| | |
| exp = ListLiteral() |
| | |
| exp = Identifier() |
| | |
| exp = Parenthesis() |
| | |
| exp = BuiltinVariable() |
| ) |
| { |
| return exp; |
| } |
| } |
| |
| Expression Parenthesis() : |
| { |
| Expression exp, result; |
| Token start, end; |
| } |
| { |
| start = <OPEN_PAREN> |
| exp = Expression() |
| end = <CLOSE_PAREN> |
| { |
| result = new ParentheticalExpression(exp); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| /** |
| * Should be called UnaryPrefixExpression. |
| * A primary expression preceded by zero or more unary prefix operators. |
| */ |
| Expression UnaryExpression() : |
| { |
| Expression exp, result; |
| boolean haveNot = false; |
| Token t = null, start = null; |
| } |
| { |
| ( |
| result = UnaryPlusMinusExpression() |
| | |
| result = NotExpression() |
| | |
| result = PrimaryExpression() |
| ) |
| { |
| return result; |
| } |
| } |
| |
| Expression NotExpression() : |
| { |
| Token t; |
| Expression exp, result = null; |
| ArrayList nots = new ArrayList(); |
| } |
| { |
| ( |
| t = <EXCLAM> { nots.add(t); } |
| )+ |
| exp = PrimaryExpression() |
| { |
| for (int i = 0; i < nots.size(); i++) { |
| result = new NotExpression(exp); |
| Token tok = (Token) nots.get(nots.size() -i -1); |
| result.setLocation(template, tok, exp); |
| exp = result; |
| } |
| return result; |
| } |
| } |
| |
| Expression UnaryPlusMinusExpression() : |
| { |
| Expression exp, result; |
| boolean isMinus = false; |
| Token t; |
| } |
| { |
| ( |
| t = <PLUS> |
| | |
| t = <MINUS> { isMinus = true; } |
| ) |
| exp = PrimaryExpression() |
| { |
| result = new UnaryPlusMinusExpression(exp, isMinus); |
| result.setLocation(template, t, exp); |
| return result; |
| } |
| } |
| |
| Expression AdditiveExpression() : |
| { |
| Expression lhs, rhs, result; |
| boolean plus; |
| } |
| { |
| lhs = MultiplicativeExpression() { result = lhs; } |
| ( |
| LOOKAHEAD(<PLUS>|<MINUS>) |
| ( |
| ( |
| <PLUS> { plus = true; } |
| | |
| <MINUS> { plus = false; } |
| ) |
| ) |
| rhs = MultiplicativeExpression() |
| { |
| if (plus) { |
| // plus is treated separately, since it is also |
| // used for concatenation. |
| result = new AddConcatExpression(lhs, rhs); |
| } else { |
| numberLiteralOnly(lhs); |
| numberLiteralOnly(rhs); |
| result = new ArithmeticExpression(lhs, rhs, ArithmeticExpression.TYPE_SUBSTRACTION); |
| } |
| result.setLocation(template, lhs, rhs); |
| lhs = result; |
| } |
| )* |
| { |
| return result; |
| } |
| } |
| |
| /** |
| * A unary prefix expression followed by zero or more |
| * unary prefix expressions with operators in between. |
| */ |
| Expression MultiplicativeExpression() : |
| { |
| Expression 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; |
| } |
| } |
| |
| |
| Expression EqualityExpression() : |
| { |
| Expression lhs, rhs, result; |
| Token t; |
| } |
| { |
| lhs = RelationalExpression() { result = lhs; } |
| [ |
| LOOKAHEAD(<NOT_EQUALS>|<EQUALS>|<DOUBLE_EQUALS>) |
| ( |
| t = <NOT_EQUALS> |
| | |
| t = <EQUALS> |
| | |
| t = <DOUBLE_EQUALS> |
| ) |
| rhs = RelationalExpression() |
| { |
| notHashLiteral(lhs, "different type for equality check"); |
| notHashLiteral(rhs, "different type for equality check"); |
| notListLiteral(lhs, "different type for equality check"); |
| notListLiteral(rhs, "different type for equality check"); |
| result = new ComparisonExpression(lhs, rhs, t.image); |
| result.setLocation(template, lhs, rhs); |
| } |
| ] |
| { |
| return result; |
| } |
| } |
| |
| Expression RelationalExpression() : |
| { |
| Expression lhs, rhs, result; |
| Token t; |
| } |
| { |
| lhs = RangeExpression() { result = lhs; } |
| [ |
| LOOKAHEAD(<NATURAL_GTE>|<ESCAPED_GTE>|<NATURAL_GT>|<ESCAPED_GT>|<LESS_THAN_EQUALS>|<LESS_THAN_EQUALS>|<LESS_THAN>) |
| ( |
| t = <NATURAL_GTE> |
| | |
| t = <ESCAPED_GTE> |
| | |
| t = <NATURAL_GT> |
| | |
| t = <ESCAPED_GT> |
| | |
| t = <LESS_THAN_EQUALS> |
| | |
| t = <LESS_THAN> |
| ) |
| rhs = RangeExpression() |
| { |
| numberLiteralOnly(lhs); |
| numberLiteralOnly(rhs); |
| result = new ComparisonExpression(lhs, rhs, t.image); |
| result.setLocation(template, lhs, rhs); |
| } |
| ] |
| { |
| return result; |
| } |
| } |
| |
| Expression RangeExpression() : |
| { |
| Expression lhs, rhs = null, result; |
| int endType; |
| Token dotDot = null; |
| } |
| { |
| lhs = AdditiveExpression() { result = lhs; } |
| [ |
| LOOKAHEAD(1) // To suppress warning |
| ( |
| ( |
| ( |
| <DOT_DOT_LESS> { endType = Range.END_EXCLUSIVE; } |
| | |
| <DOT_DOT_ASTERISK> { endType = Range.END_SIZE_LIMITED; } |
| ) |
| rhs = AdditiveExpression() |
| ) |
| | |
| ( |
| dotDot = <DOT_DOT> { endType = Range.END_UNBOUND; } |
| [ |
| LOOKAHEAD(AdditiveExpression()) |
| rhs = AdditiveExpression() |
| { |
| endType = Range.END_INCLUSIVE; |
| } |
| ] |
| ) |
| ) |
| { |
| numberLiteralOnly(lhs); |
| if (rhs != null) { |
| numberLiteralOnly(rhs); |
| } |
| |
| Range range = new Range(lhs, rhs, endType); |
| if (rhs != null) { |
| range.setLocation(template, lhs, rhs); |
| } else { |
| range.setLocation(template, lhs, dotDot); |
| } |
| result = range; |
| } |
| ] |
| { |
| return result; |
| } |
| } |
| |
| Expression AndExpression() : |
| { |
| Expression lhs, rhs, result; |
| } |
| { |
| lhs = EqualityExpression() { result = lhs; } |
| ( |
| LOOKAHEAD(<AND>) |
| <AND> |
| rhs = EqualityExpression() |
| { |
| booleanLiteralOnly(lhs); |
| booleanLiteralOnly(rhs); |
| result = new AndExpression(lhs, rhs); |
| result.setLocation(template, lhs, rhs); |
| lhs = result; |
| } |
| )* |
| { |
| return result; |
| } |
| } |
| |
| Expression OrExpression() : |
| { |
| Expression lhs, rhs, result; |
| } |
| { |
| lhs = AndExpression() { result = lhs; } |
| ( |
| LOOKAHEAD(<OR>) |
| <OR> |
| rhs = AndExpression() |
| { |
| booleanLiteralOnly(lhs); |
| booleanLiteralOnly(rhs); |
| result = new OrExpression(lhs, rhs); |
| result.setLocation(template, lhs, rhs); |
| lhs = result; |
| } |
| )* |
| { |
| return result; |
| } |
| } |
| |
| ListLiteral ListLiteral() : |
| { |
| ArrayList values = new ArrayList(); |
| Token begin, end; |
| } |
| { |
| begin = <OPEN_BRACKET> |
| values = PositionalArgs() |
| end = <CLOSE_BRACKET> |
| { |
| ListLiteral result = new ListLiteral(values); |
| result.setLocation(template, begin, end); |
| return result; |
| } |
| } |
| |
| Expression NumberLiteral() : |
| { |
| Token op = null, t; |
| } |
| { |
| ( |
| t = <INTEGER> |
| | |
| t = <DECIMAL> |
| ) |
| { |
| String s = t.image; |
| Expression result = new NumberLiteral(pCfg.getArithmeticEngine().toNumber(s)); |
| Token startToken = (op != null) ? op : t; |
| result.setLocation(template, startToken, t); |
| return result; |
| } |
| } |
| |
| Identifier Identifier() : |
| { |
| Token t; |
| } |
| { |
| t = <ID> |
| { |
| Identifier id = new Identifier(t.image); |
| id.setLocation(template, t, t); |
| return id; |
| } |
| } |
| |
| Expression IdentifierOrStringLiteral() : |
| { |
| Expression exp; |
| } |
| { |
| ( |
| exp = Identifier() |
| | |
| exp = StringLiteral(false) |
| ) |
| { |
| return exp; |
| } |
| } |
| |
| BuiltinVariable BuiltinVariable() : |
| { |
| Token dot, name; |
| } |
| { |
| dot = <DOT> |
| name = <ID> |
| { |
| BuiltinVariable result = null; |
| token_source.checkNamingConvention(name); |
| |
| TemplateModel parseTimeValue; |
| String nameStr = name.image; |
| if (nameStr.equals(BuiltinVariable.OUTPUT_FORMAT) || nameStr.equals(BuiltinVariable.OUTPUT_FORMAT_CC)) { |
| parseTimeValue = new SimpleScalar(outputFormat.getName()); |
| } else if (nameStr.equals(BuiltinVariable.AUTO_ESC) || nameStr.equals(BuiltinVariable.AUTO_ESC_CC)) { |
| parseTimeValue = autoEscaping ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; |
| } else if (nameStr.equals(BuiltinVariable.ARGS)) { |
| if (!inMacro && !inFunction) { |
| throw new ParseException("The \"" + BuiltinVariable.ARGS + "\" special variable must be " |
| + "inside a macro or function in the template source code.", template, name); |
| } |
| requireArgsSpecialVariable = true; |
| parseTimeValue = null; |
| } else { |
| parseTimeValue = null; |
| } |
| |
| result = new BuiltinVariable(name, token_source, parseTimeValue); |
| |
| result.setLocation(template, dot, name); |
| return result; |
| } |
| } |
| |
| Expression DefaultTo(Expression exp) : |
| { |
| Expression rhs = null; |
| Token t; |
| } |
| { |
| ( |
| t = <TERMINATING_EXCLAM> |
| | |
| ( |
| t = <EXCLAM> |
| [ |
| LOOKAHEAD(Expression()) |
| rhs = Expression() |
| ] |
| ) |
| ) |
| { |
| DefaultToExpression result = new DefaultToExpression(exp, rhs); |
| if (rhs == null) { |
| // <TERMINATING_EXCLAM> contains the whitespace after the `!`, so we have to use the t.beginXxx: |
| result.setLocation(template, exp.beginColumn, exp.beginLine, t.beginColumn, t.beginLine); |
| } else { |
| result.setLocation(template, exp, rhs); |
| } |
| return result; |
| } |
| } |
| |
| Expression Exists(Expression exp) : |
| { |
| Token t; |
| } |
| { |
| t = <EXISTS> |
| { |
| ExistsExpression result = new ExistsExpression(exp); |
| result.setLocation(template, exp, t); |
| return result; |
| } |
| } |
| |
| Expression BuiltIn(Expression lhoExp) : |
| { |
| Token t = null; |
| BuiltIn result; |
| ArrayList<Expression> args = null; |
| Token openParen; |
| Token closeParen; |
| MethodCall methodCall; |
| } |
| { |
| <BUILT_IN> |
| t = <ID> |
| { |
| token_source.checkNamingConvention(t); |
| result = BuiltIn.newBuiltIn(incompatibleImprovements, lhoExp, t, token_source); |
| result.setLocation(template, lhoExp, t); |
| |
| if (!(result instanceof SpecialBuiltIn)) { |
| return result; |
| } |
| |
| if (result instanceof BuiltInForLoopVariable) { |
| if (!(lhoExp instanceof Identifier)) { |
| throw new ParseException( |
| "Expression used as the left hand operand of ?" + t.image |
| + " must be a simple loop variable name.", lhoExp); |
| } |
| String loopVarName = ((Identifier) 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 |
| && !((BuiltInWithParseTimeParameters) result).isLocalLambdaParameterSupported() }) |
| openParen = <OPEN_PAREN> |
| args = PositionalArgs() |
| closeParen = <CLOSE_PAREN> { |
| result.setLocation(template, lhoExp, closeParen); |
| ((BuiltInWithParseTimeParameters) result).bindToParameters(args, openParen, closeParen); |
| |
| return result; |
| } |
| ] |
| // In principle we should embed the BuiltInWithParseTimeParameters LOOKAHEAD into the |
| // isLocalLambdaParameterSupported LOOKAHEAD, but then the `result` variable was out of scope in the code |
| // generated for the nested LOOKAHEAD. So we had to flatten this by checking isLocalLambdaParameterSupported twice. |
| [ |
| LOOKAHEAD({ |
| result instanceof BuiltInWithParseTimeParameters |
| && ((BuiltInWithParseTimeParameters) result).isLocalLambdaParameterSupported() }) |
| openParen = <OPEN_PAREN> |
| args = PositionalMaybeLambdaArgs() |
| closeParen = <CLOSE_PAREN> { |
| result.setLocation(template, lhoExp, closeParen); |
| ((BuiltInWithParseTimeParameters) result).bindToParameters(args, openParen, closeParen); |
| |
| return result; |
| } |
| ] |
| |
| [ |
| LOOKAHEAD(<OPEN_PAREN>, { result instanceof BuiltInWithDirectCallOptimization }) |
| methodCall = MethodArgs(result) |
| { |
| ((BuiltInWithDirectCallOptimization) result).setDirectlyCalled(); |
| return methodCall; |
| } |
| ] |
| |
| { |
| if (result instanceof BuiltInWithDirectCallOptimization) { |
| // We had no (...) |
| return result; |
| } |
| |
| // Should have already return-ed |
| throw new AssertionError("Unhandled " + SpecialBuiltIn.class.getName() + " subclass: " + result.getClass()); |
| } |
| |
| } |
| |
| // Only supported as the argument of certain built-ins, so it's not called inside Expression. |
| Expression LocalLambdaExpression() : |
| { |
| LambdaParameterList lhs; |
| Expression rhs, result; |
| } |
| { |
| ( |
| LOOKAHEAD(LambdaExpressionParameterList() <LAMBDA_ARROW>) |
| ( |
| lhs = LambdaExpressionParameterList() |
| <LAMBDA_ARROW> |
| rhs = OrExpression() |
| { |
| result = new LocalLambdaExpression(lhs, rhs); |
| if (lhs.getOpeningParenthesis() != null) { |
| // (args) -> exp |
| result.setLocation(template, lhs.getOpeningParenthesis(), rhs); |
| } else { |
| // singleArg -> exp |
| result.setLocation(template, lhs.getParameters().get(0), rhs); |
| } |
| } |
| ) |
| | |
| result = OrExpression() |
| ) |
| { |
| return result; |
| } |
| } |
| |
| LambdaParameterList LambdaExpressionParameterList() : |
| { |
| Token openParen = null; |
| Token closeParen = null; |
| List<Identifier> params = null; |
| Identifier param; |
| } |
| { |
| ( |
| ( |
| openParen = <OPEN_PAREN> |
| [ |
| param = Identifier() |
| { |
| params = new ArrayList<Identifier>(4); |
| params.add(param); |
| } |
| ( |
| <COMMA> |
| param = Identifier() |
| { |
| params.add(param); |
| } |
| )* |
| ] |
| closeParen = <CLOSE_PAREN> |
| ) |
| | |
| param = Identifier() |
| { |
| params = Collections.<Identifier>singletonList(param); |
| } |
| ) |
| { |
| return new LambdaParameterList( |
| openParen, |
| params != null ? params : Collections.<Identifier>emptyList(), |
| closeParen); |
| } |
| } |
| |
| /** |
| * production for when a key is specified by <DOT> + keyname |
| */ |
| Expression DotVariable(Expression exp) : |
| { |
| Token t; |
| } |
| { |
| <DOT> |
| ( |
| t = <ID> | t = <TIMES> | t = <DOUBLE_STAR> |
| | |
| ( |
| t = <LESS_THAN> |
| | |
| t = <LESS_THAN_EQUALS> |
| | |
| t = <ESCAPED_GT> |
| | |
| t = <ESCAPED_GTE> |
| | |
| t = <FALSE> |
| | |
| t = <TRUE> |
| | |
| t = <IN> |
| | |
| t = <AS> |
| | |
| t = <USING> |
| ) |
| { |
| if (!Character.isLetter(t.image.charAt(0))) { |
| throw new ParseException(t.image + " is not a valid identifier.", template, t); |
| } |
| } |
| ) |
| { |
| notListLiteral(exp, "hash"); |
| notStringLiteral(exp, "hash"); |
| notBooleanLiteral(exp, "hash"); |
| Dot dot = new Dot(exp, t.image); |
| dot.setLocation(template, exp, t); |
| return dot; |
| } |
| } |
| |
| /** |
| * production for when the key is specified |
| * in brackets. |
| */ |
| Expression DynamicKey(Expression exp) : |
| { |
| Expression arg; |
| Token t; |
| } |
| { |
| <OPEN_BRACKET> |
| arg = Expression() |
| t = <CLOSE_BRACKET> |
| { |
| notBooleanLiteral(exp, "list or hash"); |
| notNumberLiteral(exp, "list or hash"); |
| DynamicKeyName dkn = new DynamicKeyName(exp, arg); |
| dkn.setLocation(template, exp, t); |
| return dkn; |
| } |
| } |
| |
| /** |
| * production for an arglist part of a method invocation. |
| */ |
| MethodCall MethodArgs(Expression exp) : |
| { |
| ArrayList args = new ArrayList(); |
| Token end; |
| } |
| { |
| <OPEN_PAREN> |
| args = PositionalArgs() |
| end = <CLOSE_PAREN> |
| { |
| args.trimToSize(); |
| MethodCall result = new MethodCall(exp, args); |
| result.setLocation(template, exp, end); |
| return result; |
| } |
| } |
| |
| StringLiteral StringLiteral(boolean interpolate) : |
| { |
| Token t; |
| boolean raw = false; |
| } |
| { |
| ( |
| t = <STRING_LITERAL> |
| | |
| t = <RAW_STRING> { raw = true; } |
| ) |
| { |
| String s; |
| // Get rid of the quotes. |
| if (raw) { |
| s = t.image.substring(2, t.image.length() -1); |
| } else { |
| try { |
| s = StringUtil.FTLStringLiteralDec(t.image.substring(1, t.image.length() -1)); |
| } catch (ParseException pe) { |
| pe.lineNumber = t.beginLine; |
| pe.columnNumber = t.beginColumn; |
| pe.endLineNumber = t.endLine; |
| pe.endColumnNumber = t.endColumn; |
| throw pe; |
| } |
| } |
| StringLiteral result = new StringLiteral(s); |
| result.setLocation(template, t, t); |
| if (interpolate && !raw) { |
| // TODO: This logic is broken. It can't handle literals that contains both ${...} and $\{...}. |
| int interpolationSyntax = pCfg.getInterpolationSyntax(); |
| if ((interpolationSyntax == LEGACY_INTERPOLATION_SYNTAX |
| || interpolationSyntax == DOLLAR_INTERPOLATION_SYNTAX) |
| && t.image.indexOf("${") != -1 |
| || interpolationSyntax == LEGACY_INTERPOLATION_SYNTAX |
| && t.image.indexOf("#{") != -1 |
| || interpolationSyntax == SQUARE_BRACKET_INTERPOLATION_SYNTAX |
| && t.image.indexOf("[=") != -1) { |
| result.parseValue(this, outputFormat); |
| } |
| } |
| return result; |
| } |
| } |
| |
| Expression BooleanLiteral() : |
| { |
| Token t; |
| Expression result; |
| } |
| { |
| ( |
| t = <FALSE> { result = new BooleanLiteral(false); } |
| | |
| t = <TRUE> { result = new BooleanLiteral(true); } |
| ) |
| { |
| result.setLocation(template, t, t); |
| return result; |
| } |
| } |
| |
| |
| HashLiteral HashLiteral() : |
| { |
| Token begin, end; |
| Expression key, value; |
| ArrayList<Expression> keys = new ArrayList<Expression>(); |
| ArrayList<Expression> values = new ArrayList<Expression>(); |
| } |
| { |
| begin = <OPENING_CURLY_BRACKET> |
| [ |
| key = Expression() |
| (<COMMA>|<COLON>) |
| value = Expression() |
| { |
| stringLiteralOnly(key); |
| keys.add(key); |
| values.add(value); |
| } |
| ( |
| <COMMA> |
| key = Expression() |
| (<COMMA>|<COLON>) |
| value = Expression() |
| { |
| stringLiteralOnly(key); |
| keys.add(key); |
| values.add(value); |
| } |
| )* |
| ] |
| end = <CLOSING_CURLY_BRACKET> |
| { |
| keys.trimToSize(); |
| values.trimToSize(); |
| HashLiteral result = new HashLiteral(keys, values); |
| result.setLocation(template, begin, end); |
| return result; |
| } |
| } |
| |
| /** |
| * A production representing the ${...} or [=...] that outputs a variable; should be called NormalInterpolation. |
| */ |
| DollarVariable StringOutput() : |
| { |
| Expression exp; |
| Token begin, end; |
| } |
| { |
| ( |
| ( |
| begin = <DOLLAR_INTERPOLATION_OPENING> |
| exp = Expression() |
| end = <CLOSING_CURLY_BRACKET> |
| ) |
| | |
| ( |
| begin = <SQUARE_BRACKET_INTERPOLATION_OPENING> |
| exp = Expression() |
| end = <CLOSE_BRACKET> |
| ) |
| ) |
| { |
| notHashLiteral(exp, NonStringException.STRING_COERCABLE_TYPES_DESC); |
| notListLiteral(exp, NonStringException.STRING_COERCABLE_TYPES_DESC); |
| |
| DollarVariable result = new DollarVariable( |
| exp, escapedExpression(exp), |
| outputFormat, |
| autoEscaping); |
| result.setLocation(template, begin, end); |
| return result; |
| } |
| } |
| |
| /** Should be called NumericalInterpolation */ |
| NumericalOutput NumericalOutput() : |
| { |
| Expression exp; |
| Token fmt = null, begin, end; |
| } |
| { |
| begin = <HASH_INTERPOLATION_OPENING> |
| exp = Expression() { numberLiteralOnly(exp); } |
| [ |
| <SEMICOLON> |
| fmt = <ID> |
| ] |
| end = <CLOSING_CURLY_BRACKET> |
| { |
| MarkupOutputFormat<?> autoEscOF = autoEscaping && outputFormat instanceof MarkupOutputFormat |
| ? (MarkupOutputFormat<?>) outputFormat : null; |
| |
| NumericalOutput result; |
| if (fmt != null) { |
| int minFrac = -1; // -1 indicates that the value has not been set |
| int maxFrac = -1; |
| |
| StringTokenizer st = new StringTokenizer(fmt.image, "mM", true); |
| char type = '-'; |
| while (st.hasMoreTokens()) { |
| String token = st.nextToken(); |
| try { |
| if (type != '-') { |
| switch (type) { |
| case 'm': |
| if (minFrac != -1) throw new ParseException("Invalid formatting string", 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 NumericalOutput(exp, minFrac, maxFrac, autoEscOF); |
| } else { // if format != null |
| result = new NumericalOutput(exp, autoEscOF); |
| } |
| result.setLocation(template, begin, end); |
| return result; |
| } |
| } |
| |
| TemplateElement If() : |
| { |
| Token start, end, t; |
| Expression condition; |
| TemplateElements children; |
| IfBlock ifBlock; |
| ConditionalBlock cblock; |
| } |
| { |
| start = <IF> |
| condition = Expression() |
| end = <DIRECTIVE_END> |
| children = MixedContentElements() |
| { |
| cblock = new ConditionalBlock(condition, children, ConditionalBlock.TYPE_IF); |
| cblock.setLocation(template, start, end, children); |
| ifBlock = new IfBlock(cblock); |
| } |
| ( |
| t = <ELSE_IF> |
| condition = Expression() |
| end = LooseDirectiveEnd() |
| children = MixedContentElements() |
| { |
| cblock = new ConditionalBlock(condition, children, ConditionalBlock.TYPE_ELSE_IF); |
| cblock.setLocation(template, t, end, children); |
| ifBlock.addBlock(cblock); |
| } |
| )* |
| [ |
| t = <ELSE> |
| children = MixedContentElements() |
| { |
| cblock = new ConditionalBlock(null, children, ConditionalBlock.TYPE_ELSE); |
| cblock.setLocation(template, t, t, children); |
| ifBlock.addBlock(cblock); |
| } |
| ] |
| end = <END_IF> |
| { |
| ifBlock.setLocation(template, start, end); |
| return ifBlock; |
| } |
| } |
| |
| AttemptBlock Attempt() : |
| { |
| Token start, end; |
| TemplateElements children; |
| RecoveryBlock recoveryBlock; |
| } |
| { |
| start = <ATTEMPT> |
| children = MixedContentElements() |
| recoveryBlock = Recover() |
| ( |
| end = <END_RECOVER> |
| | |
| end = <END_ATTEMPT> |
| ) |
| { |
| AttemptBlock result = new AttemptBlock(children, recoveryBlock); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| RecoveryBlock Recover() : |
| { |
| Token start; |
| TemplateElements children; |
| } |
| { |
| start = <RECOVER> |
| children = MixedContentElements() |
| { |
| RecoveryBlock result = new RecoveryBlock(children); |
| result.setLocation(template, start, start, children); |
| return result; |
| } |
| } |
| |
| TemplateElement List() : |
| { |
| Expression exp; |
| Token loopVar = null, loopVar2 = null, start, end; |
| TemplateElements childrendBeforeElse; |
| ElseOfList elseOfList = null; |
| ParserIteratorBlockContext iterCtx; |
| } |
| { |
| start = <LIST> |
| exp = Expression() |
| [ |
| <AS> |
| loopVar = <ID> |
| [ |
| <COMMA> |
| loopVar2 = <ID> |
| ] |
| ] |
| <DIRECTIVE_END> |
| { |
| iterCtx = pushIteratorBlockContext(); |
| if (loopVar != null) { |
| iterCtx.loopVarName = loopVar.image; |
| breakableDirectiveNesting++; |
| continuableDirectiveNesting++; |
| 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--; |
| continuableDirectiveNesting--; |
| } 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 = ElseOfList() |
| ] |
| |
| end = <END_LIST> |
| { |
| IteratorBlock list = new IteratorBlock( |
| exp, |
| loopVar != null ? loopVar.image : null, // null when we have a nested #items |
| loopVar2 != null ? loopVar2.image : null, |
| childrendBeforeElse, iterCtx.hashListing, false); |
| list.setLocation(template, start, end); |
| |
| TemplateElement result; |
| if (elseOfList == null) { |
| result = list; |
| } else { |
| result = new ListElseContainer(list, elseOfList); |
| result.setLocation(template, start, end); |
| } |
| return result; |
| } |
| } |
| |
| ElseOfList ElseOfList() : |
| { |
| Token start; |
| TemplateElements children; |
| } |
| { |
| start = <ELSE> |
| children = MixedContentElements() |
| { |
| ElseOfList result = new ElseOfList(children); |
| result.setLocation(template, start, start, children); |
| return result; |
| } |
| } |
| |
| IteratorBlock ForEach() : |
| { |
| Expression exp; |
| Token loopVar, start, end; |
| TemplateElements children; |
| } |
| { |
| start = <FOREACH> |
| loopVar = <ID> |
| <IN> |
| exp = Expression() |
| <DIRECTIVE_END> |
| { |
| ParserIteratorBlockContext iterCtx = pushIteratorBlockContext(); |
| iterCtx.loopVarName = loopVar.image; |
| iterCtx.kind = ITERATOR_BLOCK_KIND_FOREACH; |
| breakableDirectiveNesting++; |
| continuableDirectiveNesting++; |
| } |
| |
| children = MixedContentElements() |
| |
| end = <END_FOREACH> |
| { |
| breakableDirectiveNesting--; |
| continuableDirectiveNesting--; |
| popIteratorBlockContext(); |
| |
| IteratorBlock result = new IteratorBlock(exp, loopVar.image, null, children, false, true); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| Items 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_FOREACH) { |
| msg = forEachDirectiveSymbol() + " doesn't support nested #items."; |
| } else 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++; |
| continuableDirectiveNesting++; |
| } |
| |
| children = MixedContentElements() |
| |
| end = <END_ITEMS> |
| { |
| breakableDirectiveNesting--; |
| continuableDirectiveNesting--; |
| iterCtx.loopVarName = null; |
| iterCtx.loopVar2Name = null; |
| |
| Items result = new Items(loopVar.image, loopVar2 != null ? loopVar2.image : null, children); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| Sep Sep() : |
| { |
| Token loopVar, start, end = null; |
| TemplateElements children; |
| } |
| { |
| start = <SEP> |
| { |
| if (peekIteratorBlockContext() == null) { |
| throw new ParseException( |
| "#sep must be inside a #list (or " + forEachDirectiveSymbol() + ") block.", |
| template, start); |
| } |
| } |
| children = MixedContentElements() |
| [ |
| LOOKAHEAD(1) |
| end = <END_SEP> |
| ] |
| { |
| Sep result = new Sep(children); |
| if (end != null) { |
| result.setLocation(template, start, end); |
| } else { |
| result.setLocation(template, start, start, children); |
| } |
| return result; |
| } |
| } |
| |
| VisitNode Visit() : |
| { |
| Token start, end; |
| Expression targetNode, namespaces = null; |
| } |
| { |
| start = <VISIT> |
| targetNode = Expression() |
| [ |
| <USING> |
| namespaces = Expression() |
| ] |
| end = LooseDirectiveEnd() |
| { |
| VisitNode result = new VisitNode(targetNode, namespaces); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| RecurseNode Recurse() : |
| { |
| Token start, end = null; |
| Expression node = null, namespaces = null; |
| } |
| { |
| ( |
| start = <SIMPLE_RECURSE> |
| | |
| ( |
| start = <RECURSE> |
| [ |
| node = Expression() |
| ] |
| [ |
| <USING> |
| namespaces = Expression() |
| ] |
| end = LooseDirectiveEnd() |
| ) |
| ) |
| { |
| if (end == null) end = start; |
| RecurseNode result = new RecurseNode(node, namespaces); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| FallbackInstruction FallBack() : |
| { |
| Token tok; |
| } |
| { |
| tok = <FALLBACK> |
| { |
| if (!inMacro) { |
| throw new ParseException("Cannot fall back outside a macro.", template, tok); |
| } |
| FallbackInstruction result = new FallbackInstruction(); |
| result.setLocation(template, tok, tok); |
| return result; |
| } |
| } |
| |
| /** |
| * Production used to break out of a loop or a switch block. |
| */ |
| BreakInstruction Break() : |
| { |
| Token start; |
| } |
| { |
| start = <BREAK> |
| { |
| if (breakableDirectiveNesting < 1) { |
| throw new ParseException(start.image + " must be nested inside a directive that supports it: " |
| + " #list with \"as\", #items, #switch (or the deprecated " + forEachDirectiveSymbol() + ")", |
| template, start); |
| } |
| BreakInstruction result = new BreakInstruction(); |
| result.setLocation(template, start, start); |
| return result; |
| } |
| } |
| |
| /** |
| * Production used to skip an iteration in a loop. |
| */ |
| ContinueInstruction Continue() : |
| { |
| Token start; |
| } |
| { |
| start = <CONTINUE> |
| { |
| if (continuableDirectiveNesting < 1) { |
| throw new ParseException(start.image + " must be nested inside a directive that supports it: " |
| + " #list with \"as\", #items (or the deprecated " + forEachDirectiveSymbol() + ")", |
| template, start); |
| } |
| ContinueInstruction result = new ContinueInstruction(); |
| result.setLocation(template, start, start); |
| return result; |
| } |
| } |
| |
| /** |
| * Production used to jump out of a macro. |
| * The stop instruction terminates the rendering of the template. |
| */ |
| ReturnInstruction Return() : |
| { |
| Token start, end = null; |
| Expression exp = null; |
| } |
| { |
| ( |
| start = <SIMPLE_RETURN> { end = start; } |
| | |
| start = <RETURN> exp = Expression() end = LooseDirectiveEnd() |
| ) |
| { |
| if (inMacro) { |
| if (exp != null) { |
| 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); |
| } |
| } |
| ReturnInstruction result = new ReturnInstruction(exp); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| StopInstruction Stop() : |
| { |
| Token start = null; |
| Expression exp = null; |
| } |
| { |
| ( |
| start = <HALT> |
| | |
| start = <STOP> exp = Expression() LooseDirectiveEnd() |
| ) |
| { |
| StopInstruction result = new StopInstruction(exp); |
| result.setLocation(template, start, start); |
| return result; |
| } |
| } |
| |
| TemplateElement Nested() : |
| { |
| Token t, end; |
| ArrayList bodyParameters; |
| BodyInstruction result = null; |
| } |
| { |
| ( |
| ( |
| t = <SIMPLE_NESTED> |
| { |
| result = new BodyInstruction(null); |
| result.setLocation(template, t, t); |
| } |
| ) |
| | |
| ( |
| t = <NESTED> |
| bodyParameters = PositionalArgs() |
| end = LooseDirectiveEnd() |
| { |
| result = new BodyInstruction(bodyParameters); |
| result.setLocation(template, t, end); |
| } |
| ) |
| ) |
| { |
| if (!inMacro) { |
| throw new ParseException("Cannot use a " + t.image + " instruction outside a macro.", template, t); |
| } |
| return result; |
| } |
| } |
| |
| TemplateElement Flush() : |
| { |
| Token t; |
| } |
| { |
| t = <FLUSH> |
| { |
| FlushInstruction result = new FlushInstruction(); |
| result.setLocation(template, t, t); |
| return result; |
| } |
| } |
| |
| TemplateElement Trim() : |
| { |
| Token t; |
| TrimInstruction result = null; |
| } |
| { |
| ( |
| t = <TRIM> { result = new TrimInstruction(true, true); } |
| | |
| t = <LTRIM> { result = new TrimInstruction(true, false); } |
| | |
| t = <RTRIM> { result = new TrimInstruction(false, true); } |
| | |
| t = <NOTRIM> { result = new TrimInstruction(false, false); } |
| ) |
| { |
| result.setLocation(template, t, t); |
| return result; |
| } |
| } |
| |
| |
| TemplateElement Assign() : |
| { |
| Token start, end; |
| int scope; |
| Token id = null; |
| Token equalsOp; |
| Expression nameExp, exp, nsExp = null; |
| String varName; |
| ArrayList assignments = new ArrayList(); |
| Assignment ass; |
| TemplateElements children; |
| } |
| { |
| ( |
| start = <ASSIGN> { scope = Assignment.NAMESPACE; } |
| | |
| start = <GLOBALASSIGN> { scope = Assignment.GLOBAL; } |
| | |
| start = <LOCALASSIGN> { scope = Assignment.LOCAL; } |
| { |
| scope = Assignment.LOCAL; |
| if (!inMacro && !inFunction) { |
| throw new ParseException("Local variable assigned outside a macro.", template, start); |
| } |
| } |
| ) |
| nameExp = IdentifierOrStringLiteral() |
| { |
| varName = (nameExp instanceof StringLiteral) |
| ? ((StringLiteral) nameExp).getAsString() |
| : ((Identifier) nameExp).getName(); |
| } |
| ( |
| ( |
| ( |
| ( |
| (<EQUALS>|<PLUS_EQUALS>|<MINUS_EQUALS>|<TIMES_EQUALS>|<DIV_EQUALS>|<MOD_EQUALS>) |
| { |
| equalsOp = token; |
| } |
| exp = Expression() |
| ) |
| | |
| ( |
| (<PLUS_PLUS>|<MINUS_MINUS>) |
| { |
| equalsOp = token; |
| exp = null; |
| } |
| ) |
| ) |
| { |
| ass = new Assignment(varName, equalsOp.kind, exp, scope); |
| if (exp != null) { |
| ass.setLocation(template, nameExp, exp); |
| } else { |
| ass.setLocation(template, nameExp, equalsOp); |
| } |
| assignments.add(ass); |
| } |
| ( |
| LOOKAHEAD( |
| [<COMMA>] |
| (<ID>|<STRING_LITERAL>) |
| (<EQUALS>|<PLUS_EQUALS>|<MINUS_EQUALS>|<TIMES_EQUALS>|<DIV_EQUALS>|<MOD_EQUALS> |
| |<PLUS_PLUS>|<MINUS_MINUS>) |
| ) |
| [<COMMA>] |
| nameExp = IdentifierOrStringLiteral() |
| { |
| varName = (nameExp instanceof StringLiteral) |
| ? ((StringLiteral) nameExp).getAsString() |
| : ((Identifier) nameExp).getName(); |
| } |
| ( |
| ( |
| (<EQUALS>|<PLUS_EQUALS>|<MINUS_EQUALS>|<TIMES_EQUALS>|<DIV_EQUALS>|<MOD_EQUALS>) |
| { |
| equalsOp = token; |
| } |
| exp = Expression() |
| ) |
| | |
| ( |
| (<PLUS_PLUS>|<MINUS_MINUS>) |
| { |
| equalsOp = token; |
| exp = null; |
| } |
| ) |
| ) |
| { |
| ass = new Assignment(varName, equalsOp.kind, exp, scope); |
| if (exp != null) { |
| ass.setLocation(template, nameExp, exp); |
| } else { |
| ass.setLocation(template, nameExp, equalsOp); |
| } |
| assignments.add(ass); |
| } |
| )* |
| [ |
| id = <IN> |
| nsExp = Expression() |
| { |
| if (scope != Assignment.NAMESPACE) { |
| throw new ParseException("Cannot assign to namespace here.", template, id); |
| } |
| } |
| ] |
| end = LooseDirectiveEnd() |
| { |
| if (assignments.size() == 1) { |
| Assignment a = (Assignment) assignments.get(0); |
| a.setNamespaceExp(nsExp); |
| a.setLocation(template, start, end); |
| return a; |
| } else { |
| AssignmentInstruction ai = new AssignmentInstruction(scope); |
| for (int i = 0; i< assignments.size(); i++) { |
| ai.addAssignment((Assignment) assignments.get(i)); |
| } |
| ai.setNamespaceExp(nsExp); |
| ai.setLocation(template, start, end); |
| return ai; |
| } |
| } |
| ) |
| | |
| ( |
| [ |
| id = <IN> |
| nsExp = Expression() |
| { |
| if (scope != Assignment.NAMESPACE) { |
| throw new ParseException("Cannot assign to namespace here.", template, id); |
| } |
| } |
| ] |
| <DIRECTIVE_END> |
| children = MixedContentElements() |
| ( |
| end = <END_LOCAL> |
| { |
| if (scope != Assignment.LOCAL) { |
| throw new ParseException("Mismatched assignment tags.", template, end); |
| } |
| } |
| | |
| end = <END_ASSIGN> |
| { |
| if (scope != Assignment.NAMESPACE) { |
| throw new ParseException("Mismatched assignment tags.", template, end); |
| } |
| } |
| | |
| end = <END_GLOBAL> |
| { |
| if (scope != Assignment.GLOBAL) throw new ParseException( |
| "Mismatched assignment tags", template, end); |
| } |
| ) |
| { |
| BlockAssignment ba = new BlockAssignment( |
| children, varName, scope, nsExp, |
| getMarkupOutputFormat()); |
| ba.setLocation(template, start, end); |
| return ba; |
| } |
| ) |
| ) |
| } |
| |
| Include Include() : |
| { |
| Expression nameExp; |
| Token att, start, end; |
| Expression exp, parseExp = null, encodingExp = null, ignoreMissingExp = null; |
| } |
| { |
| start = <_INCLUDE> |
| nameExp = Expression() |
| [<SEMICOLON>] |
| ( |
| att = <ID> |
| <EQUALS> |
| exp = Expression() |
| { |
| String attString = att.image; |
| if (attString.equalsIgnoreCase("parse")) { |
| parseExp = exp; |
| } else if (attString.equalsIgnoreCase("encoding")) { |
| encodingExp = exp; |
| } else if (attString.equalsIgnoreCase("ignore_missing") || attString.equals("ignoreMissing")) { |
| token_source.checkNamingConvention(att); |
| ignoreMissingExp = exp; |
| } else { |
| String correctedName = attString.equals("ignoreMissing") ? "ignore_missing" : null; |
| throw new ParseException( |
| "Unsupported named #include parameter: \"" + attString + "\". Supported parameters are: " |
| + "\"parse\", \"encoding\", \"ignore_missing\"." |
| + (correctedName == null |
| ? "" |
| : " Supporting camelCase parameter names is planned for FreeMarker 2.4.0; " |
| + "check if an update is available, and if it indeed supports camel " |
| + "case."), |
| template, att); |
| } |
| } |
| )* |
| end = LooseDirectiveEnd() |
| { |
| Include result = new Include(template, nameExp, encodingExp, parseExp, ignoreMissingExp); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| LibraryLoad Import() : |
| { |
| Token start, end, ns; |
| Expression nameExp; |
| } |
| { |
| start = <IMPORT> |
| nameExp = Expression() |
| <AS> |
| ns = <ID> |
| end = LooseDirectiveEnd() |
| { |
| LibraryLoad result = new LibraryLoad(template, nameExp, ns.image); |
| result.setLocation(template, start, end); |
| template.addImport(result); |
| return result; |
| } |
| } |
| |
| Macro Macro() : |
| { |
| Token arg, start, end; |
| Expression nameExp; |
| String name; |
| Map<String, Expression> paramNamesWithDefault = new LinkedHashMap<String, Expression>(); |
| Expression defValue = null; |
| String catchAllParamName = null; |
| boolean isCatchAll = false; |
| List lastIteratorBlockContexts; |
| int lastBreakableDirectiveNesting; |
| int lastContinuableDirectiveNesting; |
| TemplateElements children; |
| boolean isFunction = false; |
| boolean hasDefaults = false; |
| } |
| { |
| ( |
| start = <MACRO> |
| | |
| start = <FUNCTION> { isFunction = true; } |
| ) |
| { |
| if (inMacro || inFunction) { |
| throw new ParseException("Macro or function definitions can't be nested into each other.", template, start); |
| } |
| if (isFunction) inFunction = true; else inMacro = true; |
| requireArgsSpecialVariable = false; |
| } |
| nameExp = IdentifierOrStringLiteral() |
| { |
| name = (nameExp instanceof StringLiteral) |
| ? ((StringLiteral) nameExp).getAsString() |
| : ((Identifier) nameExp).getName(); |
| } |
| [<OPEN_PAREN>] |
| ( |
| arg = <ID> { defValue = null; } |
| [ |
| <ELLIPSIS> { isCatchAll = true; } |
| ] |
| [ |
| <EQUALS> |
| defValue = Expression() |
| { |
| hasDefaults = true; |
| } |
| ] |
| [<COMMA>] |
| { |
| if (catchAllParamName != null) { |
| throw new ParseException( |
| "There may only be one \"catch-all\" parameter in a macro declaration, and it must be the last parameter.", |
| template, arg); |
| } |
| if (isCatchAll) { |
| if (defValue != null) { |
| throw new ParseException( |
| "\"Catch-all\" macro parameter may not have a default value.", |
| template, arg); |
| } |
| catchAllParamName = arg.image; |
| } else { |
| if (hasDefaults && defValue == null) { |
| throw new ParseException( |
| "In a macro declaration, parameters without a default value " |
| + "must all occur before the parameters with default values.", |
| template, arg); |
| } |
| paramNamesWithDefault.put(arg.image, defValue); |
| } |
| } |
| )* |
| [<CLOSE_PAREN>] |
| <DIRECTIVE_END> |
| { |
| // To prevent parser check loopholes like <#list ...><#macro ...><#break></#macro></#list>. |
| lastIteratorBlockContexts = iteratorBlockContexts; |
| iteratorBlockContexts = null; |
| if (incompatibleImprovements >= _VersionInts.V_2_3_23) { |
| lastBreakableDirectiveNesting = breakableDirectiveNesting; |
| lastContinuableDirectiveNesting = continuableDirectiveNesting; |
| breakableDirectiveNesting = 0; |
| continuableDirectiveNesting = 0; |
| } else { |
| lastBreakableDirectiveNesting = 0; // Just to prevent uninitialized local variable error later |
| lastContinuableDirectiveNesting = 0; |
| } |
| } |
| children = MixedContentElements() |
| ( |
| end = <END_MACRO> |
| { |
| if (isFunction) throw new ParseException("Expected function end tag here.", template, end); |
| } |
| | |
| end = <END_FUNCTION> |
| { |
| if (!isFunction) throw new ParseException("Expected macro end tag here.", template, end); |
| } |
| ) |
| { |
| iteratorBlockContexts = lastIteratorBlockContexts; |
| if (incompatibleImprovements >= _VersionInts.V_2_3_23) { |
| breakableDirectiveNesting = lastBreakableDirectiveNesting; |
| continuableDirectiveNesting = lastContinuableDirectiveNesting; |
| } |
| |
| inMacro = inFunction = false; |
| Macro result = new Macro( |
| name, paramNamesWithDefault, catchAllParamName, isFunction, requireArgsSpecialVariable, children); |
| result.setLocation(template, start, end); |
| template.addMacro(result); |
| return result; |
| } |
| } |
| |
| CompressedBlock Compress() : |
| { |
| TemplateElements children; |
| Token start, end; |
| } |
| { |
| start = <COMPRESS> |
| children = MixedContentElements() |
| end = <END_COMPRESS> |
| { |
| CompressedBlock cb = new CompressedBlock(children); |
| cb.setLocation(template, start, end); |
| return cb; |
| } |
| } |
| |
| TemplateElement UnifiedMacroTransform() : |
| { |
| Token start = null, end, t; |
| HashMap namedArgs = null; |
| ArrayList positionalArgs = null, bodyParameters = null; |
| Expression startTagNameExp; |
| TemplateElements children; |
| Expression exp; |
| int pushedCtxCount = 0; |
| } |
| { |
| start = <UNIFIED_CALL> |
| exp = Expression() |
| { |
| // To allow <@foo.bar?withArgs(...)>...</@foo.bar>, but we also remove superfluous (...): |
| Expression cleanedExp = exp; |
| if (cleanedExp instanceof MethodCall) { |
| Expression methodCallTarget = ((MethodCall) cleanedExp).getTarget(); |
| if (methodCallTarget instanceof BuiltInsForCallables.with_argsBI) { |
| cleanedExp = ((BuiltInsForCallables.with_argsBI) methodCallTarget).target; |
| } |
| } |
| |
| if (cleanedExp instanceof Identifier || (cleanedExp instanceof Dot && ((Dot) cleanedExp).onlyHasIdentifiers())) { |
| startTagNameExp = cleanedExp; |
| } else { |
| startTagNameExp = null; |
| } |
| } |
| [<TERMINATING_WHITESPACE>] |
| ( |
| LOOKAHEAD(<ID><EQUALS>) |
| namedArgs = NamedArgs() |
| | |
| positionalArgs = PositionalArgs() |
| ) |
| [ |
| <SEMICOLON> |
| { bodyParameters = new ArrayList(4); } |
| [ |
| [<TERMINATING_WHITESPACE>] t = <ID> { bodyParameters.add(t.image); } |
| ( |
| [<TERMINATING_WHITESPACE>] <COMMA> |
| [<TERMINATING_WHITESPACE>] t = <ID> {bodyParameters.add(t.image); } |
| )* |
| ] |
| ] |
| ( |
| end = <EMPTY_DIRECTIVE_END> { children = TemplateElements.EMPTY; } |
| | |
| ( |
| <DIRECTIVE_END> { |
| if (bodyParameters != null && iteratorBlockContexts != null && !iteratorBlockContexts.isEmpty()) { |
| // It's possible that we shadow a #list/#items loop variable, in which case that must be noted. |
| int ctxsLen = iteratorBlockContexts.size(); |
| int bodyParsLen = bodyParameters.size(); |
| for (int bodyParIdx = 0; bodyParIdx < bodyParsLen; bodyParIdx++) { |
| String bodyParName = (String) bodyParameters.get(bodyParIdx); |
| walkCtxSack: for (int ctxIdx = ctxsLen - 1; ctxIdx >= 0; ctxIdx--) { |
| ParserIteratorBlockContext ctx |
| = (ParserIteratorBlockContext) iteratorBlockContexts.get(ctxIdx); |
| if (ctx.loopVarName != null && ctx.loopVarName.equals(bodyParName)) { |
| // If it wasn't already shadowed, shadow it: |
| if (ctx.kind != ITERATOR_BLOCK_KIND_USER_DIRECTIVE) { |
| ParserIteratorBlockContext shadowingCtx = pushIteratorBlockContext(); |
| shadowingCtx.loopVarName = bodyParName; |
| shadowingCtx.kind = ITERATOR_BLOCK_KIND_USER_DIRECTIVE; |
| pushedCtxCount++; |
| } |
| break walkCtxSack; |
| } |
| } |
| } |
| } |
| } |
| children = MixedContentElements() |
| end = <UNIFIED_CALL_END> |
| { |
| for (int i = 0; i < pushedCtxCount; i++) { |
| popIteratorBlockContext(); |
| } |
| |
| String endTagName = end.image.substring(3, end.image.length() - 1).trim(); |
| if (endTagName.length() > 0) { |
| if (startTagNameExp == null) { |
| throw new ParseException("Expecting </@>", template, end); |
| } else { |
| String startTagName = startTagNameExp.getCanonicalForm(); |
| if (!endTagName.equals(startTagName)) { |
| throw new ParseException("Expecting </@> or </@" + startTagName + ">", template, end); |
| } |
| } |
| } |
| } |
| ) |
| ) |
| { |
| TemplateElement result = (positionalArgs != null) |
| ? new UnifiedCall(exp, positionalArgs, children, bodyParameters) |
| : new UnifiedCall(exp, namedArgs, children, bodyParameters); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| TemplateElement Call() : |
| { |
| Token start, end, id; |
| HashMap namedArgs = null; |
| ArrayList positionalArgs = null; |
| Identifier macroName= null; |
| } |
| { |
| start = <CALL> |
| id = <ID> { |
| macroName = new Identifier(id.image); |
| macroName.setLocation(template, id, id); |
| } |
| ( |
| LOOKAHEAD(<ID><EQUALS>) |
| namedArgs = NamedArgs() |
| | |
| ( |
| [ |
| LOOKAHEAD(<OPEN_PAREN>) |
| <OPEN_PAREN> |
| ] |
| positionalArgs = PositionalArgs() |
| [<CLOSE_PAREN>] |
| ) |
| ) |
| end = LooseDirectiveEnd() |
| { |
| UnifiedCall result = null; |
| if (positionalArgs != null) { |
| result = new UnifiedCall(macroName, positionalArgs, TemplateElements.EMPTY, null); |
| } else { |
| result = new UnifiedCall(macroName, namedArgs, TemplateElements.EMPTY, null); |
| } |
| result.legacySyntax = true; |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| HashMap NamedArgs() : |
| { |
| HashMap result = new HashMap(); |
| Token t; |
| Expression exp; |
| } |
| { |
| ( |
| t = <ID> |
| <EQUALS> |
| { |
| token_source.SwitchTo(token_source.NAMED_PARAMETER_EXPRESSION); |
| token_source.inInvocation = true; |
| } |
| exp = Expression() |
| { |
| result.put(t.image, exp); |
| } |
| )+ |
| { |
| token_source.inInvocation = false; |
| return result; |
| } |
| } |
| |
| ArrayList PositionalArgs() : |
| { |
| ArrayList result = new ArrayList(); |
| Expression arg; |
| } |
| { |
| [ |
| arg = Expression() { result.add(arg); } |
| ( |
| [<COMMA>] |
| arg = Expression() { result.add(arg); } |
| )* |
| ] |
| { |
| return result; |
| } |
| } |
| |
| /** |
| * Like PositionalArgs, but allows lambdas. This is separate as it's slower, while lambdas are only allowed on a few |
| * places. |
| */ |
| ArrayList PositionalMaybeLambdaArgs() : |
| { |
| ArrayList result = new ArrayList(); |
| Expression arg; |
| } |
| { |
| [ |
| arg = LocalLambdaExpression() { result.add(arg); } |
| ( |
| [<COMMA>] |
| arg = LocalLambdaExpression() { result.add(arg); } |
| )* |
| ] |
| { |
| return result; |
| } |
| } |
| |
| Comment Comment() : |
| { |
| Token start, end; |
| StringBuilder buf = new StringBuilder(); |
| } |
| { |
| ( |
| start = <COMMENT> |
| | |
| start = <TERSE_COMMENT> |
| ) |
| end = UnparsedContent(start, buf) |
| { |
| Comment result = new Comment(buf.toString()); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| TextBlock NoParse() : |
| { |
| Token start, end; |
| StringBuilder buf = new StringBuilder(); |
| } |
| { |
| start = <NOPARSE> |
| end = UnparsedContent(start, buf) |
| { |
| TextBlock result = new TextBlock(buf.toString(), true); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| TransformBlock Transform() : |
| { |
| Token start, end, argName; |
| Expression exp, argExp; |
| TemplateElements children = null; |
| HashMap args = null; |
| } |
| { |
| start = <TRANSFORM> |
| exp = Expression() |
| [<SEMICOLON>] |
| ( |
| argName = <ID> |
| <EQUALS> |
| argExp = Expression() |
| { |
| if (args == null) args = new HashMap(); |
| args.put(argName.image, argExp); |
| } |
| )* |
| ( |
| end = <EMPTY_DIRECTIVE_END> |
| | |
| ( |
| <DIRECTIVE_END> |
| children = MixedContentElements() |
| end = <END_TRANSFORM> |
| ) |
| ) |
| { |
| TransformBlock result = new TransformBlock(exp, args, children); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| SwitchBlock Switch() : |
| { |
| SwitchBlock switchBlock; |
| MixedContent ignoredSectionBeforeFirstCase = null; |
| Case caseIns; |
| Expression switchExp; |
| Token start, end; |
| boolean defaultFound = false; |
| } |
| { |
| ( |
| start = <SWITCH> |
| switchExp = Expression() |
| <DIRECTIVE_END> |
| [ ignoredSectionBeforeFirstCase = WhitespaceAndComments() ] |
| ) |
| { |
| breakableDirectiveNesting++; |
| switchBlock = new SwitchBlock(switchExp, ignoredSectionBeforeFirstCase); |
| } |
| [ |
| ( |
| caseIns = Case() |
| { |
| if (caseIns.condition == null) { |
| if (defaultFound) { |
| throw new ParseException( |
| "You can only have one default case in a switch statement", template, start); |
| } |
| defaultFound = true; |
| } |
| switchBlock.addCase(caseIns); |
| } |
| )+ |
| [<STATIC_TEXT_WS>] |
| ] |
| end = <END_SWITCH> |
| { |
| breakableDirectiveNesting--; |
| switchBlock.setLocation(template, start, end); |
| return switchBlock; |
| } |
| } |
| |
| Case Case() : |
| { |
| Expression exp; |
| TemplateElements children; |
| Token start; |
| } |
| { |
| ( |
| start = <CASE> exp = Expression() <DIRECTIVE_END> |
| | |
| start = <DEFAUL> { exp = null; } |
| ) |
| children = MixedContentElements() |
| { |
| Case result = new Case(exp, children); |
| result.setLocation(template, start, start, children); |
| return result; |
| } |
| } |
| |
| EscapeBlock Escape() : |
| { |
| Token variable, start, end; |
| Expression escapeExpr; |
| TemplateElements children; |
| } |
| { |
| start = <ESCAPE> |
| { |
| if (outputFormat instanceof MarkupOutputFormat && autoEscaping) { |
| throw new ParseException( |
| "Using the \"escape\" directive (legacy escaping) is not allowed when auto-escaping is on with " |
| + "a markup output format (" + outputFormat.getName() |
| + "), to avoid confusion and double-escaping mistakes.", |
| template, start); |
| } |
| } |
| variable = <ID> |
| <AS> |
| escapeExpr = Expression() |
| <DIRECTIVE_END> |
| { |
| EscapeBlock result = new EscapeBlock(variable.image, escapeExpr, escapedExpression(escapeExpr)); |
| escapes.addFirst(result); |
| } |
| children = MixedContentElements() |
| { |
| result.setContent(children); |
| escapes.removeFirst(); |
| } |
| end = <END_ESCAPE> |
| { |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| NoEscapeBlock NoEscape() : |
| { |
| Token start, end; |
| TemplateElements children; |
| } |
| { |
| start = <NOESCAPE> |
| { |
| if (escapes.isEmpty()) { |
| throw new ParseException("#noescape with no matching #escape encountered.", template, start); |
| } |
| Object escape = escapes.removeFirst(); |
| } |
| children = MixedContentElements() |
| end = <END_NOESCAPE> |
| { |
| escapes.addFirst(escape); |
| NoEscapeBlock result = new NoEscapeBlock(children); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| OutputFormatBlock OutputFormat() : |
| { |
| Token start, end; |
| Expression paramExp; |
| TemplateElements children; |
| OutputFormat lastOutputFormat; |
| } |
| { |
| start = <OUTPUTFORMAT> |
| paramExp = Expression() |
| <DIRECTIVE_END> |
| { |
| if (!paramExp.isLiteral()) { |
| throw new ParseException( |
| "Parameter expression must be parse-time evaluable (constant): " |
| + paramExp.getCanonicalForm(), |
| paramExp); |
| } |
| |
| TemplateModel paramTM; |
| try { |
| paramTM = paramExp.eval(null); |
| } catch (Exception e) { |
| throw new ParseException( |
| "Could not evaluate expression (on parse-time): " + paramExp.getCanonicalForm() |
| + "\nUnderlying cause: " + e, |
| paramExp, e); |
| } |
| String paramStr; |
| if (paramTM instanceof TemplateScalarModel) { |
| try { |
| paramStr = ((TemplateScalarModel) paramTM).getAsString(); |
| } catch (TemplateModelException e) { |
| throw new ParseException( |
| "Could not evaluate expression (on parse-time): " + paramExp.getCanonicalForm() |
| + "\nUnderlying cause: " + e, |
| paramExp, e); |
| } |
| } else { |
| throw new ParseException( |
| "Parameter must be a string, but was: " + ClassUtil.getFTLTypeDescription(paramTM), |
| paramExp); |
| } |
| |
| lastOutputFormat = outputFormat; |
| try { |
| if (paramStr.startsWith("{")) { |
| if (!paramStr.endsWith("}")) { |
| throw new ParseException("Output format name that starts with '{' must end with '}': " + paramStr, |
| template, start); |
| } |
| OutputFormat innerOutputFormat = template.getConfiguration().getOutputFormat( |
| paramStr.substring(1, paramStr.length() - 1)); |
| if (!(innerOutputFormat instanceof MarkupOutputFormat)) { |
| throw new ParseException( |
| "The output format inside the {...} must be a markup format, but was: " |
| + innerOutputFormat, |
| template, start); |
| } |
| if (!(outputFormat instanceof MarkupOutputFormat)) { |
| throw new ParseException( |
| "The current output format must be a markup format when using {...}, but was: " |
| + outputFormat, |
| template, start); |
| } |
| outputFormat = new CombinedMarkupOutputFormat( |
| (MarkupOutputFormat) outputFormat, (MarkupOutputFormat) innerOutputFormat); |
| } else { |
| outputFormat = template.getConfiguration().getOutputFormat(paramStr); |
| } |
| recalculateAutoEscapingField(); |
| } catch (IllegalArgumentException e) { |
| throw new ParseException("Invalid format name: " + e.getMessage(), template, start, e.getCause()); |
| } catch (UnregisteredOutputFormatException e) { |
| throw new ParseException(e.getMessage(), template, start, e.getCause()); |
| } |
| } |
| children = MixedContentElements() |
| end = <END_OUTPUTFORMAT> |
| { |
| OutputFormatBlock result = new OutputFormatBlock(children, paramExp); |
| result.setLocation(template, start, end); |
| |
| outputFormat = lastOutputFormat; |
| recalculateAutoEscapingField(); |
| return result; |
| } |
| } |
| |
| AutoEscBlock AutoEsc() : |
| { |
| Token start, end; |
| TemplateElements children; |
| int lastAutoEscapingPolicy; |
| } |
| { |
| start = <AUTOESC> |
| { |
| checkCurrentOutputFormatCanEscape(start); |
| lastAutoEscapingPolicy = autoEscapingPolicy; |
| autoEscapingPolicy = Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY; |
| recalculateAutoEscapingField(); |
| } |
| children = MixedContentElements() |
| end = <END_AUTOESC> |
| { |
| AutoEscBlock result = new AutoEscBlock(children); |
| result.setLocation(template, start, end); |
| |
| autoEscapingPolicy = lastAutoEscapingPolicy; |
| recalculateAutoEscapingField(); |
| return result; |
| } |
| } |
| |
| NoAutoEscBlock NoAutoEsc() : |
| { |
| Token start, end; |
| TemplateElements children; |
| int lastAutoEscapingPolicy; |
| } |
| { |
| start = <NOAUTOESC> |
| { |
| lastAutoEscapingPolicy = autoEscapingPolicy; |
| autoEscapingPolicy = Configuration.DISABLE_AUTO_ESCAPING_POLICY; |
| recalculateAutoEscapingField(); |
| } |
| children = MixedContentElements() |
| end = <END_NOAUTOESC> |
| { |
| NoAutoEscBlock result = new NoAutoEscBlock(children); |
| result.setLocation(template, start, end); |
| |
| autoEscapingPolicy = lastAutoEscapingPolicy; |
| recalculateAutoEscapingField(); |
| return result; |
| } |
| } |
| |
| /** |
| * Production to terminate potentially empty elements. Either a ">" or "/>" |
| */ |
| Token LooseDirectiveEnd() : |
| { |
| Token t; |
| } |
| { |
| ( |
| t = <DIRECTIVE_END> |
| | |
| t = <EMPTY_DIRECTIVE_END> |
| ) |
| { |
| return t; |
| } |
| } |
| |
| PropertySetting Setting() : |
| { |
| Token start, end, key; |
| Expression value; |
| } |
| { |
| start = <SETTING> |
| key = <ID> |
| <EQUALS> |
| value = Expression() |
| end = LooseDirectiveEnd() |
| { |
| token_source.checkNamingConvention(key); |
| PropertySetting result = new PropertySetting(key, token_source, value, template.getConfiguration()); |
| result.setLocation(template, start, end); |
| return result; |
| } |
| } |
| |
| /** |
| * A production for FreeMarker directives. |
| */ |
| TemplateElement FreemarkerDirective() : |
| { |
| TemplateElement tp; |
| } |
| { |
| // Note that this doesn't include elements like "else", "recover", etc., because those indicate the end |
| // of the MixedContentElements of "if", "attempt", etc. |
| ( |
| tp = If() |
| | |
| tp = List() |
| | |
| tp = ForEach() |
| | |
| tp = Assign() |
| | |
| tp = Include() |
| | |
| tp = Import() |
| | |
| tp = Macro() |
| | |
| tp = Compress() |
| | |
| tp = UnifiedMacroTransform() |
| | |
| tp = Items() |
| | |
| tp = Sep() |
| | |
| tp = Call() |
| | |
| tp = Comment() |
| | |
| tp = NoParse() |
| | |
| tp = Transform() |
| | |
| tp = Switch() |
| | |
| tp = Setting() |
| | |
| tp = Break() |
| | |
| tp = Continue() |
| | |
| tp = Return() |
| | |
| tp = Stop() |
| | |
| tp = Flush() |
| | |
| tp = Trim() |
| | |
| tp = Nested() |
| | |
| tp = Escape() |
| | |
| tp = NoEscape() |
| | |
| tp = Visit() |
| | |
| tp = Recurse() |
| | |
| tp = FallBack() |
| | |
| tp = Attempt() |
| | |
| tp = OutputFormat() |
| | |
| tp = AutoEsc() |
| | |
| tp = NoAutoEsc() |
| ) |
| { |
| return tp; |
| } |
| } |
| |
| /** |
| * Production for a block of raw text |
| * i.e. text that contains no |
| * FreeMarker directives. |
| */ |
| TextBlock PCData() : |
| { |
| StringBuilder buf = new StringBuilder(); |
| Token t = null, start = null, prevToken = null; |
| } |
| { |
| ( |
| ( |
| t = <STATIC_TEXT_WS> |
| | |
| t = <STATIC_TEXT_NON_WS> |
| | |
| t = <STATIC_TEXT_FALSE_ALARM> |
| ) |
| { |
| buf.append(t.image); |
| if (start == null) start = t; |
| if (prevToken != null) prevToken.next = null; |
| prevToken = t; |
| } |
| )+ |
| { |
| if (stripText && mixedContentNesting == 1 && !preventStrippings) return null; |
| |
| TextBlock result = new TextBlock(buf.toString(), false); |
| result.setLocation(template, start, t); |
| return result; |
| } |
| } |
| |
| TextBlock WhitespaceText() : |
| { |
| Token t = null, start = null; |
| } |
| { |
| t = <STATIC_TEXT_WS> |
| { |
| if (stripText && mixedContentNesting == 1 && !preventStrippings) return null; |
| |
| TextBlock result = new TextBlock(t.image, false); |
| result.setLocation(template, t, t); |
| return result; |
| } |
| } |
| |
| /** |
| * Production for dealing with unparsed content, |
| * i.e. what is inside a comment or noparse tag. |
| * It returns the ending token. The content |
| * of the tag is put in buf. |
| */ |
| Token UnparsedContent(Token start, StringBuilder buf) : |
| { |
| Token t; |
| } |
| { |
| ( |
| (t = <KEEP_GOING> | t = <MAYBE_END> | t = <TERSE_COMMENT_END> | t = <LONE_LESS_THAN_OR_DASH>) |
| { |
| buf.append(t.image); |
| } |
| )+ |
| { |
| buf.setLength(buf.length() - t.image.length()); |
| if (!t.image.endsWith(";") |
| && _TemplateAPI.getTemplateLanguageVersionAsInt(template) >= _VersionInts.V_2_3_21) { |
| throw new ParseException("Unclosed \"" + start.image + "\"", template, start); |
| } |
| return t; |
| } |
| } |
| |
| TemplateElements MixedContentElements() : |
| { |
| TemplateElement[] childBuffer = null; |
| int childCount = 0; |
| TemplateElement elem; |
| mixedContentNesting++; |
| } |
| { |
| ( |
| LOOKAHEAD(1) // Just tells javacc that we know what we're doing. |
| ( |
| elem = PCData() |
| | |
| elem = StringOutput() |
| | |
| elem = NumericalOutput() |
| | |
| elem = FreemarkerDirective() |
| ) |
| { |
| // Note: elem == null when it's was top-level PCData removed by stripText |
| if (elem != null) { |
| childCount++; |
| if (childBuffer == null) { |
| childBuffer = new TemplateElement[16]; |
| } else if (childBuffer.length < childCount) { |
| TemplateElement[] newChildBuffer = new TemplateElement[childCount * 2]; |
| for (int i = 0; i < childBuffer.length; i++) { |
| newChildBuffer[i] = childBuffer[i]; |
| } |
| childBuffer = newChildBuffer; |
| } |
| childBuffer[childCount - 1] = elem; |
| } |
| } |
| )* |
| { |
| mixedContentNesting--; |
| return childBuffer != null ? new TemplateElements(childBuffer, childCount) : TemplateElements.EMPTY; |
| } |
| } |
| |
| /** |
| * Not used anymore; kept for backward compatibility. |
| * |
| * @deprecated Use {@link #MixedContentElements} instead. |
| */ |
| MixedContent MixedContent() : |
| { |
| MixedContent mixedContent = new MixedContent(); |
| TemplateElement elem, begin = null; |
| mixedContentNesting++; |
| } |
| { |
| ( |
| LOOKAHEAD(1) // Just tells javacc that we know what we're doing. |
| ( |
| elem = PCData() |
| | |
| elem = StringOutput() |
| | |
| elem = NumericalOutput() |
| | |
| elem = FreemarkerDirective() |
| ) |
| { |
| if (begin == null) { |
| begin = elem; |
| } |
| mixedContent.addElement(elem); |
| } |
| )+ |
| { |
| mixedContentNesting--; |
| mixedContent.setLocation(template, begin, elem); |
| return mixedContent; |
| } |
| } |
| |
| /** |
| * Not used anymore; kept for backward compatibility. |
| * |
| * <p>A production for a block of optional content. |
| * Returns an empty Text block if there is no |
| * content. |
| * |
| * @deprecated Use {@link #MixedContentElements} instead. |
| */ |
| TemplateElement OptionalBlock() : |
| { |
| TemplateElement tp = null; |
| } |
| { |
| [ |
| LOOKAHEAD(1) // has no effect but to get rid of a spurious warning. |
| tp = MixedContent() |
| ] |
| { |
| return tp != null ? tp : new TextBlock(CollectionUtils.EMPTY_CHAR_ARRAY, false); |
| } |
| } |
| |
| /** |
| * A production freemarker text that may contain |
| * ${...} and #{...} but no directives. |
| */ |
| TemplateElement FreeMarkerText() : |
| { |
| MixedContent nodes = new MixedContent(); |
| TemplateElement elem, begin = null; |
| } |
| { |
| ( |
| ( |
| elem = PCData() |
| | |
| elem = StringOutput() |
| | |
| elem = NumericalOutput() |
| ) |
| { |
| if (begin == null) { |
| begin = elem; |
| } |
| nodes.addChild(elem); |
| } |
| )+ |
| { |
| nodes.setLocation(template, begin, elem); |
| return nodes; |
| } |
| } |
| |
| /** |
| * To be used between tags that in theory has nothing between, such between #switch and the first #case. |
| */ |
| MixedContent WhitespaceAndComments() : |
| { |
| MixedContent nodes = new MixedContent(); |
| TemplateElement elem, begin = null; |
| } |
| { |
| ( |
| ( |
| elem = WhitespaceText() |
| | |
| elem = Comment() |
| ) |
| { |
| if (elem != null) { // not removed by stripText |
| if (begin == null) { |
| begin = elem; |
| } |
| nodes.addChild(elem); |
| } |
| } |
| )+ |
| { |
| if (begin == null // Was is removed by stripText? |
| // Nodes here won't be ever executed anyway, but whitespace stripping should still remove the |
| // lonely TextBlock from the AST, as that's purely source code formatting. If it's not lonely, then |
| // there must be a comment, in which case the generic whitespace stripping algorithm will kick in. |
| || stripWhitespace && !preventStrippings |
| && nodes.getChildCount() == 1 && nodes.getChild(0) instanceof TextBlock) { |
| return null; |
| } |
| nodes.setLocation(template, begin, elem); |
| return nodes; |
| } |
| } |
| |
| void HeaderElement() : |
| { |
| Token key; |
| Expression exp = null; |
| Token autoEscRequester = null; |
| } |
| { |
| [<STATIC_TEXT_WS>] |
| ( |
| <TRIVIAL_FTL_HEADER> |
| | |
| ( |
| <FTL_HEADER> |
| ( |
| key = <ID> |
| <EQUALS> |
| exp = Expression() |
| { |
| token_source.checkNamingConvention(key); |
| |
| String ks = key.image; |
| TemplateModel value = null; |
| try { |
| value = exp.eval(null); |
| } catch (Exception e) { |
| throw new ParseException( |
| "Could not evaluate expression (on parse-time): " + exp.getCanonicalForm() |
| + " \nUnderlying cause: " + e, |
| exp, e); |
| } |
| String vs = null; |
| if (value instanceof TemplateScalarModel) { |
| try { |
| vs = ((TemplateScalarModel) exp).getAsString(); |
| } catch (TemplateModelException tme) {} |
| } |
| if (template != null) { |
| if (ks.equalsIgnoreCase("encoding")) { |
| if (vs == null) { |
| throw new ParseException("Expected a string constant for \"" + ks + "\".", exp); |
| } |
| String encoding = template.getEncoding(); |
| if (encoding != null && !encoding.equalsIgnoreCase(vs)) { |
| throw new Template.WrongEncodingException(vs, encoding); |
| } |
| } else if (ks.equalsIgnoreCase("STRIP_WHITESPACE") || ks.equals("stripWhitespace")) { |
| this.stripWhitespace = getBoolean(exp, true); |
| } else if (ks.equalsIgnoreCase("STRIP_TEXT") || ks.equals("stripText")) { |
| this.stripText = getBoolean(exp, true); |
| } else if (ks.equalsIgnoreCase("STRICT_SYNTAX") || ks.equals("strictSyntax")) { |
| this.token_source.strictSyntaxMode = getBoolean(exp, true); |
| } else if (ks.equalsIgnoreCase("auto_esc") || ks.equals("autoEsc")) { |
| if (getBoolean(exp, false)) { |
| autoEscRequester = key; |
| autoEscapingPolicy = Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY; |
| } else { |
| autoEscapingPolicy = Configuration.DISABLE_AUTO_ESCAPING_POLICY; |
| } |
| recalculateAutoEscapingField(); |
| _TemplateAPI.setAutoEscaping(template, autoEscaping); |
| } else if (ks.equalsIgnoreCase("output_format") || ks.equals("outputFormat")) { |
| if (vs == null) { |
| throw new ParseException("Expected a string constant for \"" + ks + "\".", exp); |
| } |
| try { |
| outputFormat = template.getConfiguration().getOutputFormat(vs); |
| } catch (IllegalArgumentException e) { |
| throw new ParseException("Invalid format name: " + e.getMessage(), exp, e.getCause()); |
| } catch (UnregisteredOutputFormatException e) { |
| throw new ParseException(e.getMessage(), exp, e.getCause()); |
| } |
| recalculateAutoEscapingField(); |
| _TemplateAPI.setOutputFormat(template, outputFormat); |
| _TemplateAPI.setAutoEscaping(template, autoEscaping); |
| } else if (ks.equalsIgnoreCase("ns_prefixes") || ks.equals("nsPrefixes")) { |
| if (!(value instanceof TemplateHashModelEx)) { |
| throw new ParseException("Expecting a hash of prefixes to namespace URI's.", exp); |
| } |
| TemplateHashModelEx prefixMap = (TemplateHashModelEx) value; |
| try { |
| TemplateCollectionModel keys = prefixMap.keys(); |
| for (TemplateModelIterator it = keys.iterator(); it.hasNext();) { |
| String prefix = ((TemplateScalarModel) it.next()).getAsString(); |
| TemplateModel valueModel = prefixMap.get(prefix); |
| if (!(valueModel instanceof TemplateScalarModel)) { |
| throw new ParseException("Non-string value in prefix to namespace hash.", exp); |
| } |
| String nsURI = ((TemplateScalarModel) valueModel).getAsString(); |
| try { |
| template.addPrefixNSMapping(prefix, nsURI); |
| } catch (IllegalArgumentException iae) { |
| throw new ParseException(iae.getMessage(), exp); |
| } |
| } |
| } catch (TemplateModelException tme) { |
| } |
| } else if (ks.equalsIgnoreCase("attributes")) { |
| if (!(value instanceof TemplateHashModelEx)) { |
| throw new ParseException("Expecting a hash of attribute names to values.", exp); |
| } |
| TemplateHashModelEx attributeMap = (TemplateHashModelEx) value; |
| try { |
| TemplateCollectionModel keys = attributeMap.keys(); |
| for (TemplateModelIterator it = keys.iterator(); it.hasNext();) { |
| String attName = ((TemplateScalarModel) it.next()).getAsString(); |
| Object attValue = DeepUnwrap.unwrap(attributeMap.get(attName)); |
| template.setCustomAttribute(attName, attValue); |
| } |
| } catch (TemplateModelException tme) { |
| } |
| } else { |
| String correctName; |
| if (ks.equals("charset")) { |
| correctName = "encoding"; |
| } else if (ks.equals("xmlns")) { |
| // [2.4] If camel case will be the default, update this |
| correctName |
| = token_source.namingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION |
| ? "nsPrefixes" : "ns_prefixes"; |
| } else if (ks.equals("auto_escape") || ks.equals("auto_escaping") || ks.equals("autoesc")) { |
| correctName = "auto_esc"; |
| } else if (ks.equals("autoEscape") || ks.equals("autoEscaping")) { |
| correctName = "autoEsc"; |
| } else { |
| correctName = null; |
| } |
| throw new ParseException( |
| "Unknown FTL header parameter: " + key.image |
| + (correctName == null ? "" : ". You may meant: " + correctName), |
| template, key); |
| } |
| } |
| } |
| )* |
| ) |
| { |
| if (autoEscRequester != null) { |
| checkCurrentOutputFormatCanEscape(autoEscRequester); |
| } |
| } |
| LooseDirectiveEnd() |
| ) |
| } |
| |
| Map ParamList() : |
| { |
| Identifier id; |
| Expression exp; |
| Map result = new HashMap(); |
| } |
| { |
| ( |
| id = Identifier() |
| <EQUALS> |
| exp = Expression() { result.put(id.toString(), exp); } |
| [<COMMA>] |
| )+ |
| { |
| return result; |
| } |
| } |
| |
| /** |
| * Parses the already un-escaped content of a string literal (input must not include the quotation marks). |
| * |
| * @return A {@link List} of {@link String}-s and {@link Interpolation}-s. |
| */ |
| List<Object> StaticTextAndInterpolations() : |
| { |
| Token t; |
| Interpolation interpolation; |
| StringBuilder staticTextCollector = null; |
| ArrayList<Object> parts = new ArrayList<Object>(); |
| } |
| { |
| ( |
| ( |
| t = <STATIC_TEXT_WS> |
| | |
| t = <STATIC_TEXT_NON_WS> |
| | |
| t = <STATIC_TEXT_FALSE_ALARM> |
| ) |
| { |
| String s = t.image; |
| if (s.length() != 0) { |
| if (staticTextCollector == null) { |
| staticTextCollector = new StringBuilder(t.image); |
| } else { |
| staticTextCollector.append(t.image); |
| } |
| } |
| } |
| | |
| ( |
| LOOKAHEAD(<DOLLAR_INTERPOLATION_OPENING>|<SQUARE_BRACKET_INTERPOLATION_OPENING>) |
| ( |
| interpolation = StringOutput() |
| ) |
| | |
| LOOKAHEAD(<HASH_INTERPOLATION_OPENING>) |
| ( |
| interpolation = NumericalOutput() |
| ) |
| ) |
| { |
| if (staticTextCollector != null) { |
| parts.add(staticTextCollector.toString()); |
| staticTextCollector.setLength(0); |
| } |
| parts.add(interpolation); |
| } |
| )* |
| { |
| if (staticTextCollector != null && staticTextCollector.length() != 0) { |
| parts.add(staticTextCollector.toString()); |
| } |
| parts.trimToSize(); |
| return parts; |
| } |
| } |
| |
| /** |
| * Root production to be used when parsing |
| * an entire file. |
| */ |
| TemplateElement Root() : |
| { |
| TemplateElements children; |
| } |
| { |
| [ |
| LOOKAHEAD([<STATIC_TEXT_WS>](<TRIVIAL_FTL_HEADER>|<FTL_HEADER>)) |
| HeaderElement() |
| ] |
| children = MixedContentElements() |
| <EOF> |
| { |
| TemplateElement root = children.asSingleElement(); |
| root.setFieldsForRootElement(); |
| if (!preventStrippings) { |
| root = root.postParseCleanup(stripWhitespace); |
| } |
| // The cleanup result is possibly an element from deeper: |
| root.setFieldsForRootElement(); |
| return root; |
| } |
| } |