All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jknack.handlebars.internal.TemplateBuilder Maven / Gradle / Ivy

There is a newer version: 4.4.0
Show newest version
/**
 * 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);
}