io.marioslab.basis.template.parsing.Parser Maven / Gradle / Ivy
package io.marioslab.basis.template.parsing;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.marioslab.basis.template.Error;
import io.marioslab.basis.template.Template;
import io.marioslab.basis.template.TemplateLoader.Source;
import io.marioslab.basis.template.parsing.Ast.BinaryOperation;
import io.marioslab.basis.template.parsing.Ast.BooleanLiteral;
import io.marioslab.basis.template.parsing.Ast.Break;
import io.marioslab.basis.template.parsing.Ast.ByteLiteral;
import io.marioslab.basis.template.parsing.Ast.CharacterLiteral;
import io.marioslab.basis.template.parsing.Ast.Continue;
import io.marioslab.basis.template.parsing.Ast.DoubleLiteral;
import io.marioslab.basis.template.parsing.Ast.Expression;
import io.marioslab.basis.template.parsing.Ast.FloatLiteral;
import io.marioslab.basis.template.parsing.Ast.ForStatement;
import io.marioslab.basis.template.parsing.Ast.FunctionCall;
import io.marioslab.basis.template.parsing.Ast.IfStatement;
import io.marioslab.basis.template.parsing.Ast.Include;
import io.marioslab.basis.template.parsing.Ast.IncludeRaw;
import io.marioslab.basis.template.parsing.Ast.IntegerLiteral;
import io.marioslab.basis.template.parsing.Ast.ListLiteral;
import io.marioslab.basis.template.parsing.Ast.LongLiteral;
import io.marioslab.basis.template.parsing.Ast.Macro;
import io.marioslab.basis.template.parsing.Ast.MapLiteral;
import io.marioslab.basis.template.parsing.Ast.MapOrArrayAccess;
import io.marioslab.basis.template.parsing.Ast.MemberAccess;
import io.marioslab.basis.template.parsing.Ast.MethodCall;
import io.marioslab.basis.template.parsing.Ast.Node;
import io.marioslab.basis.template.parsing.Ast.NullLiteral;
import io.marioslab.basis.template.parsing.Ast.Return;
import io.marioslab.basis.template.parsing.Ast.ShortLiteral;
import io.marioslab.basis.template.parsing.Ast.StringLiteral;
import io.marioslab.basis.template.parsing.Ast.TernaryOperation;
import io.marioslab.basis.template.parsing.Ast.Text;
import io.marioslab.basis.template.parsing.Ast.UnaryOperation;
import io.marioslab.basis.template.parsing.Ast.VariableAccess;
import io.marioslab.basis.template.parsing.Ast.WhileStatement;
/** Parses a {@link Source} into a {@link Template}. The implementation is a simple recursive descent parser with a lookahead of
* 1. **/
public class Parser {
/** Parses a {@link Source} into a {@link Template}. **/
public ParserResult parse (Source source) {
List nodes = new ArrayList();
Macros macros = new Macros();
List includes = new ArrayList();
List rawIncludes = new ArrayList();
TokenStream stream = new TokenStream(new Tokenizer().tokenize(source));
while (stream.hasMore()) {
nodes.add(parseStatement(stream, true, macros, includes, rawIncludes));
}
return new ParserResult(nodes, macros, includes, rawIncludes);
}
/** Parse a statement, which may either be a text block, if statement, for statement, while statement, macro definition,
* include statement or an expression. **/
private Node parseStatement (TokenStream tokens, boolean allowMacros, Macros macros, List includes, List rawIncludes) {
Node result = null;
if (tokens.match(TokenType.TextBlock, false))
result = new Text(tokens.consume().getSpan());
else if (tokens.match("if", false))
result = parseIfStatement(tokens, includes, rawIncludes);
else if (tokens.match("for", false))
result = parseForStatement(tokens, includes, rawIncludes);
else if (tokens.match("while", false))
result = parseWhileStatement(tokens, includes, rawIncludes);
else if (tokens.match("continue", false))
result = new Continue(tokens.consume().getSpan());
else if (tokens.match("break", false))
result = new Break(tokens.consume().getSpan());
else if (tokens.match("macro", false)) {
if (!allowMacros) {
Error.error("Macros can only be defined at the top level of a template.", tokens.consume().getSpan());
result = null; // never reached
} else {
Macro macro = parseMacro(tokens, includes, rawIncludes);
macros.put(macro.getName().getText(), macro);
result = macro; //
}
} else if (tokens.match("include", false)) {
result = parseInclude(tokens, includes, rawIncludes);
} else if (tokens.match("return", false)) {
result = parseReturn(tokens);
} else
result = parseExpression(tokens);
// consume semi-colons as statement delimiters
while (tokens.match(";", true))
;
return result;
}
private IfStatement parseIfStatement (TokenStream stream, List includes, List rawIncludes) {
Span openingIf = stream.expect("if").getSpan();
Expression condition = parseExpression(stream);
List trueBlock = new ArrayList();
while (stream.hasMore() && !stream.match(false, "elseif", "else", "end")) {
trueBlock.add(parseStatement(stream, false, null, includes, rawIncludes));
}
List elseIfs = new ArrayList();
while (stream.hasMore() && stream.match(false, "elseif")) {
Span elseIfOpening = stream.expect("elseif").getSpan();
Expression elseIfCondition = parseExpression(stream);
List elseIfBlock = new ArrayList();
while (stream.hasMore() && !stream.match(false, "elseif", "else", "end")) {
elseIfBlock.add(parseStatement(stream, false, null, includes, rawIncludes));
}
Span elseIfSpan = new Span(elseIfOpening, elseIfBlock.size() > 0 ? elseIfBlock.get(elseIfBlock.size() - 1).getSpan() : elseIfOpening);
elseIfs.add(new IfStatement(elseIfSpan, elseIfCondition, elseIfBlock, new ArrayList(), new ArrayList()));
}
List falseBlock = new ArrayList();
if (stream.match("else", true)) {
while (stream.hasMore() && !stream.match(false, "end")) {
falseBlock.add(parseStatement(stream, false, null, includes, rawIncludes));
}
}
Span closingEnd = stream.expect("end").getSpan();
return new IfStatement(new Span(openingIf, closingEnd), condition, trueBlock, elseIfs, falseBlock);
}
private ForStatement parseForStatement (TokenStream stream, List includes, List rawIncludes) {
Span openingFor = stream.expect("for").getSpan();
Span index = null;
Span value = stream.expect(TokenType.Identifier).getSpan();
if (stream.match(TokenType.Comma, true)) {
index = value;
value = stream.expect(TokenType.Identifier).getSpan();
}
stream.expect("in");
Expression mapOrArray = parseExpression(stream);
List body = new ArrayList();
while (stream.hasMore() && !stream.match(false, "end")) {
body.add(parseStatement(stream, false, null, includes, rawIncludes));
}
Span closingEnd = stream.expect("end").getSpan();
return new ForStatement(new Span(openingFor, closingEnd), index != null ? index : null, value, mapOrArray, body);
}
private WhileStatement parseWhileStatement (TokenStream stream, List includes, List rawIncludes) {
Span openingWhile = stream.expect("while").getSpan();
Expression condition = parseExpression(stream);
List body = new ArrayList();
while (stream.hasMore() && !stream.match(false, "end")) {
body.add(parseStatement(stream, false, null, includes, rawIncludes));
}
Span closingEnd = stream.expect("end").getSpan();
return new WhileStatement(new Span(openingWhile, closingEnd), condition, body);
}
private Macro parseMacro (TokenStream stream, List includes, List rawIncludes) {
Span openingWhile = stream.expect("macro").getSpan();
Span name = stream.expect(TokenType.Identifier).getSpan();
List argumentNames = parseArgumentNames(stream);
stream.expect(TokenType.RightParantheses);
List body = new ArrayList();
while (stream.hasMore() && !stream.match(false, "end")) {
body.add(parseStatement(stream, false, null, includes, rawIncludes));
}
Span closingEnd = stream.expect("end").getSpan();
return new Macro(new Span(openingWhile, closingEnd), name, argumentNames, body);
}
/** Does not consume the closing parentheses. **/
private List parseArgumentNames (TokenStream stream) {
stream.expect(TokenType.LeftParantheses);
List arguments = new ArrayList();
while (stream.hasMore() && !stream.match(TokenType.RightParantheses, false)) {
arguments.add(stream.expect(TokenType.Identifier).getSpan());
if (!stream.match(TokenType.RightParantheses, false)) stream.expect(TokenType.Comma);
}
return arguments;
}
private Node parseInclude (TokenStream stream, List includes, List rawIncludes) {
Span openingInclude = stream.expect("include").getSpan();
if (stream.match("raw", true)) {
Span path = stream.expect(TokenType.StringLiteral).getSpan();
IncludeRaw rawInclude = new IncludeRaw(new Span(openingInclude, path), path);
rawIncludes.add(rawInclude);
return rawInclude;
}
Span path = stream.expect(TokenType.StringLiteral).getSpan();
Span closing = path;
Include include = null;
if (stream.match("with", true)) {
Map context = parseMap(stream);
closing = stream.expect(TokenType.RightParantheses).getSpan();
include = new Include(new Span(openingInclude, closing), path, context, false, null);
} else if (stream.match("as", true)) {
Span alias = stream.expect(TokenType.Identifier).getSpan();
closing = alias;
include = new Include(new Span(openingInclude, closing), path, null, true, alias);
} else {
include = new Include(new Span(openingInclude, closing), path, new HashMap(), false, null);
}
includes.add(include);
return include;
}
/** Does not consume the closing parentheses. **/
private Map parseMap (TokenStream stream) {
stream.expect(TokenType.LeftParantheses);
Map map = new HashMap();
while (stream.hasMore() && !stream.match(TokenType.RightParantheses, false)) {
Span key = stream.expect(TokenType.Identifier).getSpan();
stream.expect(TokenType.Colon);
map.put(key, parseExpression(stream));
if (!stream.match(TokenType.RightParantheses, false)) stream.expect(TokenType.Comma);
}
return map;
}
private Expression parseExpression (TokenStream stream) {
return parseTernaryOperator(stream);
}
private Expression parseTernaryOperator (TokenStream stream) {
Expression condition = parseBinaryOperator(stream, 0);
if (stream.match(TokenType.Questionmark, true)) {
Expression trueExpression = parseTernaryOperator(stream);
stream.expect(TokenType.Colon);
Expression falseExpression = parseTernaryOperator(stream);
return new TernaryOperation(condition, trueExpression, falseExpression);
} else {
return condition;
}
}
TokenType[][] binaryOperatorPrecedence = new TokenType[][] {new TokenType[] {TokenType.Assignment},
new TokenType[] {TokenType.Or, TokenType.And, TokenType.Xor}, new TokenType[] {TokenType.Equal, TokenType.NotEqual},
new TokenType[] {TokenType.Less, TokenType.LessEqual, TokenType.Greater, TokenType.GreaterEqual}, new TokenType[] {TokenType.Plus, TokenType.Minus},
new TokenType[] {TokenType.ForwardSlash, TokenType.Asterisk, TokenType.Percentage}};
private Expression parseBinaryOperator (TokenStream stream, int level) {
int nextLevel = level + 1;
Expression left = nextLevel == binaryOperatorPrecedence.length ? parseUnaryOperator(stream) : parseBinaryOperator(stream, nextLevel);
TokenType[] operators = binaryOperatorPrecedence[level];
while (stream.hasMore() && stream.match(false, operators)) {
Token operator = stream.consume();
Expression right = nextLevel == binaryOperatorPrecedence.length ? parseUnaryOperator(stream) : parseBinaryOperator(stream, nextLevel);
left = new BinaryOperation(left, operator, right);
}
return left;
}
TokenType[] unaryOperators = new TokenType[] {TokenType.Not, TokenType.Plus, TokenType.Minus};
private Expression parseUnaryOperator (TokenStream stream) {
if (stream.match(false, unaryOperators)) {
return new UnaryOperation(stream.consume(), parseUnaryOperator(stream));
} else {
if (stream.match(TokenType.LeftParantheses, true)) {
Expression expression = parseExpression(stream);
stream.expect(TokenType.RightParantheses);
return expression;
} else {
return parseAccessOrCallOrLiteral(stream);
}
}
}
private Expression parseAccessOrCallOrLiteral (TokenStream stream) {
if (stream.match(TokenType.Identifier, false)) {
return parseAccessOrCall(stream);
} else if (stream.match(TokenType.LeftCurly, false)) {
return parseMapLiteral(stream);
} else if (stream.match(TokenType.LeftBracket, false)) {
return parseListLiteral(stream);
} else if (stream.match(TokenType.StringLiteral, false)) {
return new StringLiteral(stream.expect(TokenType.StringLiteral).getSpan());
} else if (stream.match(TokenType.BooleanLiteral, false)) {
return new BooleanLiteral(stream.expect(TokenType.BooleanLiteral).getSpan());
} else if (stream.match(TokenType.DoubleLiteral, false)) {
return new DoubleLiteral(stream.expect(TokenType.DoubleLiteral).getSpan());
} else if (stream.match(TokenType.FloatLiteral, false)) {
return new FloatLiteral(stream.expect(TokenType.FloatLiteral).getSpan());
} else if (stream.match(TokenType.ByteLiteral, false)) {
return new ByteLiteral(stream.expect(TokenType.ByteLiteral).getSpan());
} else if (stream.match(TokenType.ShortLiteral, false)) {
return new ShortLiteral(stream.expect(TokenType.ShortLiteral).getSpan());
} else if (stream.match(TokenType.IntegerLiteral, false)) {
return new IntegerLiteral(stream.expect(TokenType.IntegerLiteral).getSpan());
} else if (stream.match(TokenType.LongLiteral, false)) {
return new LongLiteral(stream.expect(TokenType.LongLiteral).getSpan());
} else if (stream.match(TokenType.CharacterLiteral, false)) {
return new CharacterLiteral(stream.expect(TokenType.CharacterLiteral).getSpan());
} else if (stream.match(TokenType.NullLiteral, false)) {
return new NullLiteral(stream.expect(TokenType.NullLiteral).getSpan());
} else {
Error.error("Expected a variable, field, map, array, function or method call, or literal.", stream);
return null; // not reached
}
}
private Expression parseMapLiteral (TokenStream stream) {
Span openCurly = stream.expect(TokenType.LeftCurly).getSpan();
List keys = new ArrayList<>();
List values = new ArrayList<>();
while (stream.hasMore() && !stream.match(TokenType.RightCurly, false)) {
keys.add(stream.expect(TokenType.Identifier).getSpan());
stream.expect(":");
values.add(parseExpression(stream));
if (!stream.match(TokenType.RightCurly, false)) stream.expect(TokenType.Comma);
}
Span closeCurly = stream.expect(TokenType.RightCurly).getSpan();
return new MapLiteral(new Span(openCurly, closeCurly), keys, values);
}
private Expression parseListLiteral (TokenStream stream) {
Span openBracket = stream.expect(TokenType.LeftBracket).getSpan();
List values = new ArrayList<>();
while (stream.hasMore() && !stream.match(TokenType.RightBracket, false)) {
values.add(parseExpression(stream));
if (!stream.match(TokenType.RightBracket, false)) stream.expect(TokenType.Comma);
}
Span closeBracket = stream.expect(TokenType.RightBracket).getSpan();
return new ListLiteral(new Span(openBracket, closeBracket), values);
}
private Expression parseAccessOrCall (TokenStream stream) {
Span identifier = stream.expect(TokenType.Identifier).getSpan();
Expression result = new VariableAccess(identifier);
while (stream.hasMore() && stream.match(false, TokenType.LeftParantheses, TokenType.LeftBracket, TokenType.Period)) {
// function or method call
if (stream.match(TokenType.LeftParantheses, false)) {
List arguments = parseArguments(stream);
Span closingSpan = stream.expect(TokenType.RightParantheses).getSpan();
if (result instanceof VariableAccess || result instanceof MapOrArrayAccess)
result = new FunctionCall(new Span(result.getSpan(), closingSpan), result, arguments);
else if (result instanceof MemberAccess) {
result = new MethodCall(new Span(result.getSpan(), closingSpan), (MemberAccess)result, arguments);
} else {
Error.error("Expected a variable, field or method.", stream);
}
}
// map or array access
else if (stream.match(TokenType.LeftBracket, true)) {
Expression keyOrIndex = parseExpression(stream);
Span closingSpan = stream.expect(TokenType.RightBracket).getSpan();
result = new MapOrArrayAccess(new Span(result.getSpan(), closingSpan), result, keyOrIndex);
}
// field or method access
else if (stream.match(TokenType.Period, true)) {
identifier = stream.expect(TokenType.Identifier).getSpan();
result = new MemberAccess(result, identifier);
}
}
return result;
}
/** Does not consume the closing parentheses. **/
private List parseArguments (TokenStream stream) {
stream.expect(TokenType.LeftParantheses);
List arguments = new ArrayList();
while (stream.hasMore() && !stream.match(TokenType.RightParantheses, false)) {
arguments.add(parseExpression(stream));
if (!stream.match(TokenType.RightParantheses, false)) stream.expect(TokenType.Comma);
}
return arguments;
}
private Node parseReturn (TokenStream tokens) {
Span returnSpan = tokens.expect("return").getSpan();
if (tokens.match(";", false)) return new Return(returnSpan, null);
Expression returnValue = parseExpression(tokens);
return new Return(new Span(returnSpan, returnValue.getSpan()), returnValue);
}
@SuppressWarnings("serial")
public static class Macros extends HashMap {
}
public static class ParserResult {
private final List nodes;
private final Macros macros;
private final List includes;
private final List rawIncludes;
public ParserResult (List nodes, Macros macros, List includes, List rawIncludes) {
this.nodes = nodes;
this.macros = macros;
this.includes = includes;
this.rawIncludes = rawIncludes;
}
public List getNodes () {
return nodes;
}
public Macros getMacros () {
return macros;
}
public List getIncludes () {
return includes;
}
public List getRawIncludes () {
return rawIncludes;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy