Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.github.jknack.handlebars.internal.TemplateBuilder Maven / Gradle / Ivy
/**
* Copyright (c) 2012-2013 Edgar Espina
*
* This file is part of Handlebars.java.
*
* Licensed 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.
*/
package com.github.jknack.handlebars.internal;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.Validate.notNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.antlr.v4.runtime.CommonToken;
import org.antlr.v4.runtime.RuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.apache.commons.lang3.math.NumberUtils;
import com.github.jknack.handlebars.Context;
import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.HandlebarsError;
import com.github.jknack.handlebars.HandlebarsException;
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.HelperRegistry;
import com.github.jknack.handlebars.TagType;
import com.github.jknack.handlebars.Template;
import com.github.jknack.handlebars.internal.HbsParser.AmpvarContext;
import com.github.jknack.handlebars.internal.HbsParser.BlockContext;
import com.github.jknack.handlebars.internal.HbsParser.BodyContext;
import com.github.jknack.handlebars.internal.HbsParser.BoolHashContext;
import com.github.jknack.handlebars.internal.HbsParser.BoolParamContext;
import com.github.jknack.handlebars.internal.HbsParser.CharHashContext;
import com.github.jknack.handlebars.internal.HbsParser.CharParamContext;
import com.github.jknack.handlebars.internal.HbsParser.CommentContext;
import com.github.jknack.handlebars.internal.HbsParser.ElseBlockContext;
import com.github.jknack.handlebars.internal.HbsParser.EscapeContext;
import com.github.jknack.handlebars.internal.HbsParser.HashContext;
import com.github.jknack.handlebars.internal.HbsParser.IntHashContext;
import com.github.jknack.handlebars.internal.HbsParser.IntParamContext;
import com.github.jknack.handlebars.internal.HbsParser.NewlineContext;
import com.github.jknack.handlebars.internal.HbsParser.ParamContext;
import com.github.jknack.handlebars.internal.HbsParser.PartialContext;
import com.github.jknack.handlebars.internal.HbsParser.RefHashContext;
import com.github.jknack.handlebars.internal.HbsParser.RefPramContext;
import com.github.jknack.handlebars.internal.HbsParser.SexprContext;
import com.github.jknack.handlebars.internal.HbsParser.SpacesContext;
import com.github.jknack.handlebars.internal.HbsParser.StatementContext;
import com.github.jknack.handlebars.internal.HbsParser.StringHashContext;
import com.github.jknack.handlebars.internal.HbsParser.StringParamContext;
import com.github.jknack.handlebars.internal.HbsParser.SubexpressionContext;
import com.github.jknack.handlebars.internal.HbsParser.TemplateContext;
import com.github.jknack.handlebars.internal.HbsParser.TextContext;
import com.github.jknack.handlebars.internal.HbsParser.TvarContext;
import com.github.jknack.handlebars.internal.HbsParser.UnlessContext;
import com.github.jknack.handlebars.internal.HbsParser.VarContext;
import com.github.jknack.handlebars.io.TemplateSource;
/**
* Traverse the parse tree and build templates.
*
* @author edgar.espina
* @since 0.10.0
*/
abstract class TemplateBuilder extends HbsParserBaseVisitor {
/**
* A handlebars object. required.
*/
private Handlebars handlebars;
/**
* The template source. Required.
*/
private TemplateSource source;
/**
* Flag to track dead spaces and lines.
*/
private Boolean hasTag;
/**
* Keep track of the current line.
*/
protected StringBuilder line = new StringBuilder();
/**
* Keep track of block helpers.
*/
private LinkedList qualifier = new LinkedList();
/**
* Creates a new {@link TemplateBuilder}.
*
* @param handlebars A handlbars object. required.
* @param source The template source. required.
*/
public TemplateBuilder(final Handlebars handlebars, final TemplateSource source) {
this.handlebars = notNull(handlebars, "The handlebars can't be null.");
this.source = notNull(source, "The template source is requied.");
}
@Override
public Template visit(final ParseTree tree) {
return (Template) super.visit(tree);
}
@Override
public Template visitBlock(final BlockContext ctx) {
SexprContext sexpr = ctx.sexpr();
Token nameStart = sexpr.QID().getSymbol();
String name = nameStart.getText();
qualifier.addLast(name);
String nameEnd = ctx.nameEnd.getText();
if (!name.equals(nameEnd)) {
reportError(null, ctx.nameEnd.getLine(), ctx.nameEnd.getCharPositionInLine()
, String.format("found: '%s', expected: '%s'", nameEnd, name));
}
hasTag(true);
Block block = new Block(handlebars, name, false, params(sexpr.param()),
hash(sexpr.hash()));
block.filename(source.filename());
block.position(nameStart.getLine(), nameStart.getCharPositionInLine());
String startDelim = ctx.start.getText();
startDelim = startDelim.substring(0, startDelim.length() - 1);
block.startDelimiter(startDelim);
block.endDelimiter(ctx.stop.getText());
Template body = visitBody(ctx.thenBody);
if (body != null) {
block.body(body);
}
ElseBlockContext elseBlock = ctx.elseBlock();
if (elseBlock != null) {
Template unless = visitBody(elseBlock.unlessBody);
if (unless != null) {
String inverseLabel = elseBlock.inverseToken.getText();
if (inverseLabel.startsWith(startDelim)) {
inverseLabel = inverseLabel.substring(startDelim.length());
}
block.inverse(inverseLabel, unless);
}
}
hasTag(true);
qualifier.removeLast();
return block;
}
@Override
public Template visitUnless(final UnlessContext ctx) {
hasTag(true);
Block block = new Block(handlebars, ctx.nameStart.getText(), true, Collections.emptyList(),
Collections. emptyMap());
block.filename(source.filename());
block.position(ctx.nameStart.getLine(), ctx.nameStart.getCharPositionInLine());
String startDelim = ctx.start.getText();
block.startDelimiter(startDelim.substring(0, startDelim.length() - 1));
block.endDelimiter(ctx.stop.getText());
Template body = visitBody(ctx.body());
if (body != null) {
block.body(body);
}
hasTag(true);
return block;
}
@Override
public Template visitVar(final VarContext ctx) {
hasTag(false);
SexprContext sexpr = ctx.sexpr();
return newVar(sexpr.QID().getSymbol(), TagType.VAR, params(sexpr.param()), hash(sexpr.hash()),
ctx.start.getText(), ctx.stop.getText());
}
@Override
public Object visitEscape(final EscapeContext ctx) {
Token token = ctx.ESC_VAR().getSymbol();
String text = token.getText().substring(1);
line.append(text);
return new Text(text, "\\")
.filename(source.filename())
.position(token.getLine(), token.getCharPositionInLine());
}
@Override
public Template visitTvar(final TvarContext ctx) {
hasTag(false);
SexprContext sexpr = ctx.sexpr();
return newVar(sexpr.QID().getSymbol(), TagType.TRIPLE_VAR, params(sexpr.param()),
hash(sexpr.hash()),
ctx.start.getText(), ctx.stop.getText());
}
@Override
public Template visitAmpvar(final AmpvarContext ctx) {
hasTag(false);
SexprContext sexpr = ctx.sexpr();
return newVar(sexpr.QID().getSymbol(), TagType.AMP_VAR, params(sexpr.param()),
hash(sexpr.hash()),
ctx.start.getText(), ctx.stop.getText());
}
/**
* Build a new {@link Variable}.
*
* @param name The var's name.
* @param varType The var's type.
* @param params The var params.
* @param hash The var hash.
* @param startDelimiter The current start delimiter.
* @param endDelimiter The current end delimiter.
* @return A new {@link Variable}.
*/
private Template newVar(final Token name, final TagType varType, final List params,
final Map hash, final String startDelimiter, final String endDelimiter) {
String varName = name.getText();
boolean isHelper = ((params.size() > 0 || hash.size() > 0)
|| varType == TagType.SUB_EXPRESSION);
if (!isHelper && qualifier.size() > 0 && "with".equals(qualifier.getLast())
&& !varName.startsWith(".")) {
// HACK to qualified 'with' in order to improve handlebars.js compatibility
varName = "this." + varName;
}
String[] parts = varName.split("\\./");
// TODO: try to catch this with ANTLR...
// foo.0 isn't allowed, it must be foo.0.
if (parts.length > 0 && NumberUtils.isNumber(parts[parts.length - 1])
&& !varName.endsWith(".")) {
String evidence = varName;
String reason = "found: " + varName + ", expecting: " + varName + ".";
String message =
source.filename() + ":" + name.getLine() + ":" + name.getChannel() + ": "
+ reason + "\n";
throw new HandlebarsException(new HandlebarsError(source.filename(), name.getLine(),
name.getCharPositionInLine(), reason, evidence, message));
}
Helper helper = handlebars.helper(varName);
if (helper == null && isHelper) {
Helper helperMissing =
handlebars.helper(HelperRegistry.HELPER_MISSING);
if (helperMissing == null) {
reportError(null, name.getLine(), name.getCharPositionInLine(), "could not find helper: '"
+ varName + "'");
}
}
return new Variable(handlebars, varName, varType, params, hash)
.startDelimiter(startDelimiter)
.endDelimiter(endDelimiter)
.filename(source.filename())
.position(name.getLine(), name.getCharPositionInLine());
}
/**
* Build a hash.
*
* @param ctx The hash context.
* @return A new hash.
*/
private Map hash(final List ctx) {
if (ctx == null || ctx.size() == 0) {
return Collections.emptyMap();
}
Map result = new LinkedHashMap();
for (HashContext hc : ctx) {
result.put(hc.QID().getText(), super.visit(hc.hashValue()));
}
return result;
}
/**
* Build a param list.
*
* @param params The param context.
* @return A new param list.
*/
private List params(final List params) {
if (params == null || params.size() == 0) {
return Collections.emptyList();
}
List result = new ArrayList();
for (ParamContext param : params) {
result.add(super.visit(param));
}
return result;
}
@Override
public Object visitBoolParam(final BoolParamContext ctx) {
return Boolean.valueOf(ctx.getText());
}
@Override
public Object visitSubexpression(final SubexpressionContext ctx) {
SexprContext sexpr = ctx.sexpr();
return newVar(sexpr.QID().getSymbol(), TagType.SUB_EXPRESSION, params(sexpr.param()),
hash(sexpr.hash()), ctx.start.getText(), ctx.stop.getText());
}
@Override
public Object visitBoolHash(final BoolHashContext ctx) {
return Boolean.valueOf(ctx.getText());
}
@Override
public Object visitCharHash(final CharHashContext ctx) {
return charLiteral(ctx);
}
@Override
public Object visitStringHash(final StringHashContext ctx) {
return stringLiteral(ctx);
}
@Override
public Object visitStringParam(final StringParamContext ctx) {
return stringLiteral(ctx);
}
@Override
public Object visitCharParam(final CharParamContext ctx) {
return charLiteral(ctx);
}
/**
* @param ctx The char literal context.
* @return A char literal.
*/
private String charLiteral(final RuleContext ctx) {
return ctx.getText().replace("\\\'", "\'");
}
/**
* @param ctx The string literal context.
* @return A string literal.
*/
private String stringLiteral(final RuleContext ctx) {
return ctx.getText().replace("\\\"", "\"");
}
@Override
public Object visitRefHash(final RefHashContext ctx) {
return ctx.getText();
}
@Override
public Object visitRefPram(final RefPramContext ctx) {
return ctx.getText();
}
@Override
public Object visitIntHash(final IntHashContext ctx) {
return Integer.parseInt(ctx.getText());
}
@Override
public Object visitIntParam(final IntParamContext ctx) {
return Integer.parseInt(ctx.getText());
}
@Override
public Template visitTemplate(final TemplateContext ctx) {
Template template = visitBody(ctx.body());
if (!handlebars.infiniteLoops() && template instanceof BaseTemplate) {
template = infiniteLoop(source, (BaseTemplate) template);
}
destroy();
return template;
}
/**
* Creates a {@link Template} that detects recursively calls.
*
* @param source The template source.
* @param template The original template.
* @return A new {@link Template} that detects recursively calls.
*/
private static Template infiniteLoop(final TemplateSource source, final BaseTemplate template) {
return new ForwardingTemplate(template) {
@Override
protected void beforeApply(final Context context) {
LinkedList invocationStack = context.data(Context.INVOCATION_STACK);
invocationStack.addLast(source);
}
@Override
protected void afterApply(final Context context) {
LinkedList invocationStack = context.data(Context.INVOCATION_STACK);
if (!invocationStack.isEmpty()) {
invocationStack.removeLast();
}
}
};
}
@Override
public Template visitPartial(final PartialContext ctx) {
hasTag(true);
Token pathToken = ctx.PATH().getSymbol();
String uri = pathToken.getText();
if (uri.startsWith("[") && uri.endsWith("]")) {
uri = uri.substring(1, uri.length() - 1);
}
if (uri.startsWith("/")) {
String message = "found: '/', partial shouldn't start with '/'";
reportError(null, pathToken.getLine(), pathToken.getCharPositionInLine(), message);
}
String indent = line.toString();
if (hasTag()) {
if (isEmpty(indent) || !isEmpty(indent.trim())) {
indent = null;
}
} else {
indent = null;
}
TerminalNode partialContext = ctx.QID();
String startDelim = ctx.start.getText();
Template partial = new Partial(handlebars, uri,
partialContext != null ? partialContext.getText() : null)
.startDelimiter(startDelim.substring(0, startDelim.length() - 1))
.endDelimiter(ctx.stop.getText())
.indent(indent)
.filename(source.filename())
.position(pathToken.getLine(), pathToken.getCharPositionInLine());
return partial;
}
@Override
public Template visitBody(final BodyContext ctx) {
List stats = ctx.statement();
if (stats.size() == 0) {
return Template.EMPTY;
}
if (stats.size() == 1) {
return visit(stats.get(0));
}
TemplateList list = new TemplateList();
Template prev = null;
for (StatementContext statement : stats) {
Template candidate = visit(statement);
if (candidate != null) {
// join consecutive piece of text
if (candidate instanceof Text) {
if (!(prev instanceof Text)) {
list.add(candidate);
prev = candidate;
} else {
((Text) prev).append(((Text) candidate).textWithoutEscapeChar());
}
} else {
list.add(candidate);
prev = candidate;
}
}
}
if (list.size() == 1) {
return list.iterator().next();
}
return list;
}
@Override
public Object visitComment(final CommentContext ctx) {
return Template.EMPTY;
}
@Override
public Template visitStatement(final StatementContext ctx) {
return visit(ctx.getChild(0));
}
@Override
public Template visitText(final TextContext ctx) {
String text = ctx.getText();
line.append(text);
return new Text(text)
.filename(source.filename())
.position(ctx.start.getLine(), ctx.start.getCharPositionInLine());
}
@Override
public Template visitSpaces(final SpacesContext ctx) {
Token space = ctx.SPACE().getSymbol();
String text = space.getText();
line.append(text);
if (space.getChannel() == Token.HIDDEN_CHANNEL) {
return null;
}
return new Text(text)
.filename(source.filename())
.position(ctx.start.getLine(), ctx.start.getCharPositionInLine());
}
@Override
public BaseTemplate visitNewline(final NewlineContext ctx) {
Token newline = ctx.NL().getSymbol();
if (newline.getChannel() == Token.HIDDEN_CHANNEL) {
return null;
}
line.setLength(0);
return new Text(newline.getText())
.filename(source.filename())
.position(newline.getLine(), newline.getCharPositionInLine());
}
/**
* True, if tag instruction was processed.
*
* @return True, if tag instruction was processed.
*/
private boolean hasTag() {
if (handlebars.prettyPrint()) {
return hasTag == null ? false : hasTag.booleanValue();
}
return false;
}
/**
* Set if a new tag instruction was processed.
*
* @param hasTag True, if a new tag instruction was processed.
*/
private void hasTag(final boolean hasTag) {
if (this.hasTag != Boolean.FALSE) {
this.hasTag = hasTag;
}
}
/**
* Cleanup resources.
*/
private void destroy() {
this.handlebars = null;
this.source = null;
this.hasTag = null;
this.line.delete(0, line.length());
this.line = null;
}
/**
* Report a semantic error.
*
* @param offendingToken The offending token.
* @param message An error message.
*/
protected void reportError(final CommonToken offendingToken, final String message) {
reportError(offendingToken, offendingToken.getLine(), offendingToken.getCharPositionInLine(),
message);
}
/**
* Report a semantic error.
*
* @param offendingToken The offending token.
* @param line The offending line.
* @param column The offending column.
* @param message An error message.
*/
protected abstract void reportError(final CommonToken offendingToken, final int line,
final int column, final String message);
}