Java tutorial
/** * 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<Object> { /** * 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<String> qualifier = new LinkedList<String>(); /** * 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.<String, Object>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<Object> params, final Map<String, Object> 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<Object> helper = handlebars.helper(varName); if (helper == null && isHelper) { Helper<Object> 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<String, Object> hash(final List<HashContext> ctx) { if (ctx == null || ctx.size() == 0) { return Collections.emptyMap(); } Map<String, Object> result = new LinkedHashMap<String, Object>(); 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<Object> params(final List<ParamContext> params) { if (params == null || params.size() == 0) { return Collections.emptyList(); } List<Object> result = new ArrayList<Object>(); 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<TemplateSource> invocationStack = context.data(Context.INVOCATION_STACK); invocationStack.addLast(source); } @Override protected void afterApply(final Context context) { LinkedList<TemplateSource> 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<StatementContext> 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); }