r.freemarker.2.3.27-incubating.source-code.FTL.jj Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of freemarker Show documentation
Show all versions of freemarker Show documentation
FreeMarker is a "template engine"; a generic tool to generate text output based on templates.
/*
* 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.template.*;
import freemarker.template.utility.*;
import java.io.*;
import java.util.*;
/**
* 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 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;
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 strictEscapeSyntax
* 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 strictEscapeSyntax, boolean stripWhitespace) {
this(template, reader, strictEscapeSyntax, 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 strictEscapeSyntax, boolean stripWhitespace, int tagSyntax) {
this(template, reader, strictEscapeSyntax, 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 strictEscapeSyntax, boolean stripWhitespace,
int tagSyntax, int incompatibleImprovements) {
this(template, reader, strictEscapeSyntax, 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, 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.strictEscapeSyntax = 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);
}
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(NODIRECTIVE);
this.outputFormat = outputFormat;
recalculateAutoEscapingField();
if (incompatibleImprovements < _TemplateAPI.VERSION_INT_2_3_24) {
// Emulate bug, where the string literal parser haven't inherited the IcI:
incompatibleImprovements = _TemplateAPI.VERSION_INT_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() + ". 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) {
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(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 ""
* 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 and
* . This variable tells us what the closing tag should be, and when we hit that, we resume parsing. Note
* that with this scheme, and tags cannot nest recursively, but it is not clear how important
* that is.
*/
String noparseTag;
/**
* Keeps track of how deeply nested we have the hash literals. This is necessary since we need to be able to
* distinguish the } used to close a hash literal and the one used to close a ${
*/
private FMParser parser;
private int postInterpolationLexState = -1;
private int hashLiteralNesting;
private int parenthesisNesting;
private int bracketNesting;
private boolean inFTLHeader;
boolean strictEscapeSyntax,
squBracTagSyntax,
autodetectTagSyntax,
directiveSyntaxEstablished,
inInvocation;
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 strictSyntaxCheck(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 (!strictEscapeSyntax
&& (tokenNamingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION)
&& !isStrictTag(image)) {
tok.kind = STATIC_TEXT_NON_WS;
return;
}
char firstChar = image.charAt(0);
if (autodetectTagSyntax && !directiveSyntaxEstablished) {
squBracTagSyntax = (firstChar == '[');
}
if ((firstChar == '[' && !squBracTagSyntax) || (firstChar == '<' && squBracTagSyntax)) {
tok.kind = STATIC_TEXT_NON_WS;
return;
}
if (!strictEscapeSyntax) {
// Legacy feature (or bug?): Tag syntax never gets estabilished in non-strict mode.
// We do establilish 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.
directiveSyntaxEstablished = true;
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 strictSyntaxCheck(Token tok, int newLexState) {
strictSyntaxCheck(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 && !directiveSyntaxEstablished) {
squBracTagSyntax = (firstChar == '[');
}
if (squBracTagSyntax && firstChar == '<') {
tok.kind = STATIC_TEXT_NON_WS;
return;
}
if (!squBracTagSyntax && firstChar == '[') {
tok.kind = STATIC_TEXT_NON_WS;
return;
}
directiveSyntaxEstablished = true;
SwitchTo(NO_SPACE_EXPRESSION);
}
private void unifiedCallEnd(Token tok) {
char firstChar = tok.image.charAt(0);
if (squBracTagSyntax && firstChar == '<') {
tok.kind = STATIC_TEXT_NON_WS;
return;
}
if (!squBracTagSyntax && firstChar == '[') {
tok.kind = STATIC_TEXT_NON_WS;
return;
}
}
private void closeBracket(Token tok) {
if (bracketNesting > 0) {
--bracketNesting;
} else {
tok.kind = DIRECTIVE_END;
if (inFTLHeader) {
eatNewline();
inFTLHeader = false;
}
SwitchTo(DEFAULT);
}
}
private void startInterpolation(Token tok) {
if (postInterpolationLexState != -1) {
char c = tok.image.charAt(0);
throw new TokenMgrError(
"You can't start an interpolation (" + c + "{...}) here "
+ "as you are inside another interpolation.)",
TokenMgrError.LEXICAL_ERROR,
tok.beginLine, tok.beginColumn,
tok.endLine, tok.endColumn);
}
postInterpolationLexState = curLexState;
SwitchTo(FM_EXPRESSION);
}
/**
* @param tok
* Assumed to be an '}', or something that is the closing pair of another "mirror image" character.
*/
private void endInterpolation(Token tok) {
if (postInterpolationLexState == -1) {
char c = tok.image.charAt(0);
throw new TokenMgrError(
"You can't have an \"" + c + "\" here, as there's nothing open that it could close.",
TokenMgrError.LEXICAL_ERROR,
tok.beginLine, tok.beginColumn,
tok.endLine, tok.endColumn);
}
SwitchTo(postInterpolationLexState);
postInterpolationLexState = -1;
}
private void eatNewline() {
int charsRead = 0;
try {
while (true) {
char c = input_stream.readChar();
++charsRead;
if (!Character.isWhitespace(c)) {
input_stream.backup(charsRead);
return;
} else if (c == '\r') {
char next = input_stream.readChar();
++charsRead;
if (next != '\n') {
input_stream.backup(1);
}
return;
} else if (c == '\n') {
return;
}
}
} catch (IOException ioe) {
input_stream.backup(charsRead);
}
}
private void ftlHeader(Token matchedToken) {
if (!directiveSyntaxEstablished) {
squBracTagSyntax = matchedToken.image.charAt(0) == '[';
directiveSyntaxEstablished = true;
autodetectTagSyntax = false;
}
String img = matchedToken.image;
char firstChar = img.charAt(0);
char lastChar = img.charAt(img.length() - 1);
if ((firstChar == '[' && !squBracTagSyntax) || (firstChar == '<' && squBracTagSyntax)) {
matchedToken.kind = STATIC_TEXT_NON_WS;
}
if (matchedToken.kind != STATIC_TEXT_NON_WS) {
if (lastChar != '>' && lastChar != ']') {
SwitchTo(FM_EXPRESSION);
inFTLHeader = true;
} else {
eatNewline();
}
}
}
}
TOKEN:
{
<#BLANK : " " | "\t" | "\n" | "\r">
|
<#START_TAG : "<" | "<#" | "[#">
|
<#END_TAG : "" | "#" | "[/#">
|
<#CLOSE_TAG1 : ()* (">" | "]")>
|
<#CLOSE_TAG2 : ()* ("/")? (">" | "]")>
|
/*
* ATTENTION: Update _CoreAPI.*_BUILT_IN_DIRECTIVE_NAMES if you add new directives!
*/
"attempt" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"recover" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"if" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"else" ("i" | "I") "f" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 4), FM_EXPRESSION);
}
|
"list" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"items" ()+ > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"sep" >
|
"for" ("e" | "E") "ach" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 3), FM_EXPRESSION);
}
|
"switch" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"case" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"assign" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"global" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"local" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
<_INCLUDE : "include" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"import" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"function" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"macro" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"transform" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"visit" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"stop" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"return" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"call" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"setting" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"output" ("f"|"F") "ormat" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 6), FM_EXPRESSION);
}
|
"auto" ("e"|"E") "sc" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 4), DEFAULT);
}
|
"no" ("autoe"|"AutoE") "sc" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
}
|
"compress" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"comment" > {
strictSyntaxCheck(matchedToken, NO_PARSE); noparseTag = "comment";
}
|
{ noparseTag = "-->"; strictSyntaxCheck(matchedToken, NO_PARSE); }
|
"no" ("p" | "P") "arse" > {
int tagNamingConvention = getTagNamingConvention(matchedToken, 2);
strictSyntaxCheck(matchedToken, tagNamingConvention, NO_PARSE);
noparseTag = tagNamingConvention == Configuration.CAMEL_CASE_NAMING_CONVENTION ? "noParse" : "noparse";
}
|
"if" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"list" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"items" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"sep" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"recover" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"attempt" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"for" ("e" | "E") "ach" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 3), DEFAULT);
}
|
"local" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"global" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"assign" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"function" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"macro" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"output" ("f" | "F") "ormat" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 6), DEFAULT);
}
|
"auto" ("e" | "E") "sc" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 4), DEFAULT);
}
|
"no" ("autoe"|"AutoE") "sc" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
}
|
"compress" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"transform" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"switch" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"else" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"break" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"continue" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"return" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"stop" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"flush" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"t" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"lt" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"rt" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"nt" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"default" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"nested" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"nested" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"recurse" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"recurse" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"fallback" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"escape" > { strictSyntaxCheck(matchedToken, FM_EXPRESSION); }
|
"escape" > { strictSyntaxCheck(matchedToken, DEFAULT); }
|
"no" ("e" | "E") "scape" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
}
|
"no" ("e" | "E") "scape" > {
strictSyntaxCheck(matchedToken, getTagNamingConvention(matchedToken, 2), DEFAULT);
}
|
{ unifiedCall(matchedToken); }
|
) (".")*)? > { unifiedCallEnd(matchedToken); }
|
> { ftlHeader(matchedToken); }
|
" | "]")> { ftlHeader(matchedToken); }
|
/*
* ATTENTION: Update _CoreAPI.*_BUILT_IN_DIRECTIVE_NAMES if you add new directives!
*/
{
if (!directiveSyntaxEstablished && incompatibleImprovements < _TemplateAPI.VERSION_INT_2_3_19) {
matchedToken.kind = STATIC_TEXT_NON_WS;
} else {
char firstChar = matchedToken.image.charAt(0);
if (!directiveSyntaxEstablished && autodetectTagSyntax) {
squBracTagSyntax = (firstChar == '[');
directiveSyntaxEstablished = true;
}
if (firstChar == '<' && squBracTagSyntax) {
matchedToken.kind = STATIC_TEXT_NON_WS;
} else if (firstChar == '[' && !squBracTagSyntax) {
matchedToken.kind = STATIC_TEXT_NON_WS;
} else if (strictEscapeSyntax) {
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);
}
}
}
}
TOKEN :
{
|
|
// to handle a lone dollar sign or "<" or "# or <@ with whitespace after"
|
{ startInterpolation(matchedToken); }
|
{ startInterpolation(matchedToken); }
}
SKIP :
{
< ( " " | "\t" | "\n" | "\r" )+ >
|
< ("<" | "[") ("#" | "!") "--"> : EXPRESSION_COMMENT
}
SKIP:
{
< (~["-", ">", "]"])+ >
|
< ">">
|
< "]">
|
< "-">
|
< "-->" | "--]">
{
if (parenthesisNesting > 0) SwitchTo(IN_PAREN);
else if (inInvocation) SwitchTo(NAMED_PARAMETER_EXPRESSION);
else SwitchTo(FM_EXPRESSION);
}
}
TOKEN :
{
<#ESCAPED_CHAR :
"\\"
(
("n" | "t" | "r" | "f" | "b" | "g" | "l" | "a" | "\\" | "'" | "\"" | "$" | "{")
|
("x" ["0"-"9", "A"-"F", "a"-"f"])
)
>
|
)*
"\""
)
|
(
"'"
((~["'", "\\"]) | )*
"'"
)
>
|
|
|
|
|
"." >
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{
++bracketNesting;
}
|
{
closeBracket(matchedToken);
}
|
{
++parenthesisNesting;
if (parenthesisNesting == 1) SwitchTo(IN_PAREN);
}
|
{
--parenthesisNesting;
if (parenthesisNesting == 0) {
if (inInvocation) SwitchTo(NAMED_PARAMETER_EXPRESSION);
else SwitchTo(FM_EXPRESSION);
}
}
|
{
++hashLiteralNesting;
}
|
{
if (hashLiteralNesting == 0) endInterpolation(matchedToken);
else --hashLiteralNesting;
}
|
|
|
|
(|)*> {
// 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);
}
}
|
{
if ("".length() == 0) { // prevents unreachabe "break" compilation error in generated Java
char c = matchedToken.image.charAt(0);
throw new TokenMgrError(
"You can't use \"" + c + "{\" here as you are already in FreeMarker-expression-mode. Thus, instead "
+ "of " + c + "{myExpression}, just write myExpression. "
+ "(" + c + "{...} is only needed where otherwise static text is expected, i.e, outside "
+ "FreeMarker tags and ${...}-s.)",
TokenMgrError.LEXICAL_ERROR,
matchedToken.beginLine, matchedToken.beginColumn,
matchedToken.endLine, matchedToken.endColumn);
}
}
|
<#NON_ESCAPED_ID_START_CHAR:
[
// This was generated on JDK 1.8.0_20 Win64 with src/main/misc/identifierChars/IdentifierCharGenerator.java
"$",
"@" - "Z",
"_",
"a" - "z",
"\u00AA",
"\u00B5",
"\u00BA",
"\u00C0" - "\u00D6",
"\u00D8" - "\u00F6",
"\u00F8" - "\u1FFF",
"\u2071",
"\u207F",
"\u2090" - "\u209C",
"\u2102",
"\u2107",
"\u210A" - "\u2113",
"\u2115",
"\u2119" - "\u211D",
"\u2124",
"\u2126",
"\u2128",
"\u212A" - "\u212D",
"\u212F" - "\u2139",
"\u213C" - "\u213F",
"\u2145" - "\u2149",
"\u214E",
"\u2183" - "\u2184",
"\u2C00" - "\u2C2E",
"\u2C30" - "\u2C5E",
"\u2C60" - "\u2CE4",
"\u2CEB" - "\u2CEE",
"\u2CF2" - "\u2CF3",
"\u2D00" - "\u2D25",
"\u2D27",
"\u2D2D",
"\u2D30" - "\u2D67",
"\u2D6F",
"\u2D80" - "\u2D96",
"\u2DA0" - "\u2DA6",
"\u2DA8" - "\u2DAE",
"\u2DB0" - "\u2DB6",
"\u2DB8" - "\u2DBE",
"\u2DC0" - "\u2DC6",
"\u2DC8" - "\u2DCE",
"\u2DD0" - "\u2DD6",
"\u2DD8" - "\u2DDE",
"\u2E2F",
"\u3005" - "\u3006",
"\u3031" - "\u3035",
"\u303B" - "\u303C",
"\u3040" - "\u318F",
"\u31A0" - "\u31BA",
"\u31F0" - "\u31FF",
"\u3300" - "\u337F",
"\u3400" - "\u4DB5",
"\u4E00" - "\uA48C",
"\uA4D0" - "\uA4FD",
"\uA500" - "\uA60C",
"\uA610" - "\uA62B",
"\uA640" - "\uA66E",
"\uA67F" - "\uA697",
"\uA6A0" - "\uA6E5",
"\uA717" - "\uA71F",
"\uA722" - "\uA788",
"\uA78B" - "\uA78E",
"\uA790" - "\uA793",
"\uA7A0" - "\uA7AA",
"\uA7F8" - "\uA801",
"\uA803" - "\uA805",
"\uA807" - "\uA80A",
"\uA80C" - "\uA822",
"\uA840" - "\uA873",
"\uA882" - "\uA8B3",
"\uA8D0" - "\uA8D9",
"\uA8F2" - "\uA8F7",
"\uA8FB",
"\uA900" - "\uA925",
"\uA930" - "\uA946",
"\uA960" - "\uA97C",
"\uA984" - "\uA9B2",
"\uA9CF" - "\uA9D9",
"\uAA00" - "\uAA28",
"\uAA40" - "\uAA42",
"\uAA44" - "\uAA4B",
"\uAA50" - "\uAA59",
"\uAA60" - "\uAA76",
"\uAA7A",
"\uAA80" - "\uAAAF",
"\uAAB1",
"\uAAB5" - "\uAAB6",
"\uAAB9" - "\uAABD",
"\uAAC0",
"\uAAC2",
"\uAADB" - "\uAADD",
"\uAAE0" - "\uAAEA",
"\uAAF2" - "\uAAF4",
"\uAB01" - "\uAB06",
"\uAB09" - "\uAB0E",
"\uAB11" - "\uAB16",
"\uAB20" - "\uAB26",
"\uAB28" - "\uAB2E",
"\uABC0" - "\uABE2",
"\uABF0" - "\uABF9",
"\uAC00" - "\uD7A3",
"\uD7B0" - "\uD7C6",
"\uD7CB" - "\uD7FB",
"\uF900" - "\uFB06",
"\uFB13" - "\uFB17",
"\uFB1D",
"\uFB1F" - "\uFB28",
"\uFB2A" - "\uFB36",
"\uFB38" - "\uFB3C",
"\uFB3E",
"\uFB40" - "\uFB41",
"\uFB43" - "\uFB44",
"\uFB46" - "\uFBB1",
"\uFBD3" - "\uFD3D",
"\uFD50" - "\uFD8F",
"\uFD92" - "\uFDC7",
"\uFDF0" - "\uFDFB",
"\uFE70" - "\uFE74",
"\uFE76" - "\uFEFC",
"\uFF10" - "\uFF19",
"\uFF21" - "\uFF3A",
"\uFF41" - "\uFF5A",
"\uFF66" - "\uFFBE",
"\uFFC2" - "\uFFC7",
"\uFFCA" - "\uFFCF",
"\uFFD2" - "\uFFD7",
"\uFFDA" - "\uFFDC"
]
>
|
<#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":")>
|
<#ID_START_CHAR: |>
|
<#ASCII_DIGIT: ["0" - "9"]>
}
TOKEN :
{
">
{
if (inFTLHeader) eatNewline();
inFTLHeader = false;
if (squBracTagSyntax) {
matchedToken.kind = NATURAL_GT;
} else {
SwitchTo(DEFAULT);
}
}
|
" | "/]">
{
if (inFTLHeader) eatNewline();
inFTLHeader = false;
SwitchTo(DEFAULT);
}
}
TOKEN :
{
">
|
=">
}
TOKEN :
{
: FM_EXPRESSION
}
TOKEN :
{
: FM_EXPRESSION
}
TOKEN :
{
" | "--]">
{
if (noparseTag.equals("-->")) {
boolean squareBracket = matchedToken.image.endsWith("]");
if ((squBracTagSyntax && squareBracket) || (!squBracTagSyntax && !squareBracket)) {
matchedToken.image = matchedToken.image + ";";
SwitchTo(DEFAULT);
}
}
}
|
" | "]")
>
{
StringTokenizer st = new StringTokenizer(image.toString(), " \t\n\r<>[]/#", false);
if (st.nextToken().equals(noparseTag)) {
matchedToken.image = matchedToken.image + ";";
SwitchTo(DEFAULT);
}
}
|
|
}
// Now the actual parsing code, starting
// with the productions for FreeMarker's
// expression syntax.
/**
* This is the same as OrExpression, since
* the OR is the operator with the lowest
* precedence.
*/
Expression Expression() :
{
Expression exp;
}
{
exp = OrExpression()
{
return exp;
}
}
/**
* Lowest level expression, a literal, a variable,
* or a possibly more complex expression bounded
* by parentheses.
*/
Expression PrimaryExpression() :
{
Expression exp;
}
{
(
exp = NumberLiteral()
|
exp = HashLiteral()
|
exp = StringLiteral(true)
|
exp = BooleanLiteral()
|
exp = ListLiteral()
|
exp = Identifier()
|
exp = Parenthesis()
|
exp = BuiltinVariable()
)
(
LOOKAHEAD( | | | | | | )
exp = AddSubExpression(exp)
)*
{
return exp;
}
}
Expression Parenthesis() :
{
Expression exp, result;
Token start, end;
}
{
start =
exp = Expression()
end =
{
result = new ParentheticalExpression(exp);
result.setLocation(template, start, end);
return result;
}
}
/**
* A primary expression preceded by zero or
* more unary operators. (The only unary operator we
* currently have is the NOT.)
*/
Expression UnaryExpression() :
{
Expression exp, result;
boolean haveNot = false;
Token t = null, start = null;
}
{
(
result = UnaryPlusMinusExpression()
|
result = NotExpression()
|
result = PrimaryExpression()
)
{
return result;
}
}
Expression NotExpression() :
{
Token t;
Expression exp, result = null;
ArrayList nots = new ArrayList();
}
{
(
t = { 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 =
|
t = { 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 = true; }
|
{ 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 expression followed by zero or more
* unary expressions with operators in between.
*/
Expression MultiplicativeExpression() :
{
Expression lhs, rhs, result;
int operation = ArithmeticExpression.TYPE_MULTIPLICATION;
}
{
lhs = UnaryExpression() { result = lhs; }
(
LOOKAHEAD(||)
(
(