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.squarespace.template.Instructions Maven / Gradle / Ivy
/**
* Copyright (c) 2014 SQUARESPACE, Inc.
*
* 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.squarespace.template;
import static com.squarespace.template.ExecuteErrorType.INCLUDE_PARTIAL_SYNTAX;
import static com.squarespace.template.GeneralUtils.splitVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BigIntegerNode;
import com.fasterxml.jackson.databind.node.DecimalNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.squarespace.template.expr.Expr;
import com.squarespace.template.expr.Formats;
import com.squarespace.template.expr.Tokens;
/**
* Implementations of specific JSONT instructions.
*/
public class Instructions {
// Reasonable defaults for initial embedded instruction list sizes.
private static final int VARIABLE_LIST_LEN = 2;
private static final int ROOT_BLOCK_LEN = 10;
private static final int CONSEQUENT_BLOCK_LEN = 4;
private static final int ALTERNATES_BLOCK_LEN = 2;
/**
* Special case instruction. Contains a block but never executes an alternate,
* as it is not conditional.. it only exists to enhance the implementation of
* the REPEAT instruction.
*/
public static class AlternatesWithInst extends BlockInst {
AlternatesWithInst() {
super(ALTERNATES_BLOCK_LEN);
}
@Override
public boolean equals(Object obj) {
return (obj instanceof AlternatesWithInst) && blockEquals((AlternatesWithInst)obj);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.ALTERNATES_WITH;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
ctx.execute(consequent.getInstructions());
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf, recurse);
}
}
/**
* Set a local variable's value.
*/
public static class BindVarInst extends BaseInstruction implements Formattable {
private final String name;
private final Variables variables;
private List formatters;
BindVarInst(String key, String variable) {
this(key, new Variables(variable));
}
BindVarInst(String key, Variables variables) {
this.name = key;
this.variables = variables;
setFormatters(null);
}
public String getName() {
return name;
}
public Variables getVariables() {
return variables;
}
@Override
public List getFormatters() {
return formatters;
}
@Override
public void setFormatters(List formatters) {
this.formatters = formatters == null ? Collections.emptyList() : formatters;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
variables.resolve(ctx);
applyFormatters(ctx, formatters, variables);
ctx.setVar(name, variables.first().node());
}
@Override
public boolean equals(Object obj) {
if (obj instanceof BindVarInst) {
BindVarInst other = (BindVarInst)obj;
return name.equals(other.name)
&& Objects.equals(variables, other.variables)
&& Objects.equals(formatters, other.formatters);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.BINDVAR;
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
}
/**
* Represents a generic block of instructions (consequent) and an optional
* alternative block.
*/
public static abstract class BlockInst extends BaseInstruction implements BlockInstruction {
/**
* Consequent block of instructions.
*/
protected Block consequent;
/**
* Alternative instruction.
*/
protected Instruction alternative;
/**
* Constructs a block instruction with the initial capacity for the consequent block.
*/
BlockInst(int consequentsLen) {
consequent = new Block(consequentsLen);
}
/**
* Returns the consequent block.
*/
@Override
public Block getConsequent() {
return consequent;
}
/**
* Set the instruction to execute instead of the consequents.
*
* For non-conditional blocks, like section, the alternate is simply the END
* instruction, indicating a complete parse.
*/
@Override
public void setAlternative(Instruction inst) {
alternative = inst;
}
/**
* Sets the alternative instruction.
*/
@Override
public Instruction getAlternative() {
return alternative;
}
/**
* Indicates if the two lists of {@code Object[]} are equal.
*/
protected boolean variableListEquals(List t1, List t2) {
if (t1 == null) {
return (t2 == null);
}
if (t2 != null) {
int sz = t1.size();
if (sz != t2.size()) {
return false;
}
for (int i = 0; i < sz; i++) {
if (!Arrays.equals(t1.get(i), t2.get(i))) {
return false;
}
}
return true;
}
return false;
}
/**
* Indicates if the current instruction's consequent and alternative are equal to that
* of the {@code other} instruction.
*/
protected boolean blockEquals(BlockInst other) {
boolean res = (consequent == null) ? (other.consequent == null) : consequent.equals(other.consequent);
if (!res) {
return false;
}
return (alternative == null) ? (other.alternative == null) : alternative.equals(other.alternative);
}
/**
* Helper equals() method to simplify null checking.
*/
protected static boolean equals(Instruction i1, Instruction i2) {
return (i1 == null) ? (i2 == null) : i1.equals(i2);
}
/**
* Helper equals() method to simplify null checking.
*/
protected static boolean blockEquals(Block b1, Block b2) {
return (b1 == null) ? (b2 == null) : b1.equals(b2);
}
}
/**
* Terminal instruction representing a comment. Implementation is a NOOP.
*/
public static class CommentInst extends BaseInstruction {
private final StringView view;
private final boolean multiLine;
CommentInst(StringView view) {
this(view, false);
}
CommentInst(StringView view, boolean multiLine) {
this.view = view;
this.multiLine = multiLine;
}
public StringView getView() {
return view;
}
public boolean isMultiLine() {
return multiLine;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CommentInst) {
CommentInst other = (CommentInst) obj;
return multiLine == other.multiLine && view.equals(other.view);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.COMMENT;
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
}
/**
* Instruction that creates a new context for passing to a partial template.
*/
public static class CtxVarInst extends BaseInstruction {
private final String name;
private final List bindings;
public CtxVarInst(String name, List bindings) {
this.name = name;
this.bindings = bindings;
}
public String getName() {
return name;
}
public List getBindings() {
return bindings;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CtxVarInst) {
CtxVarInst other = (CtxVarInst)obj;
return name.equals(other.name)
&& Objects.equals(bindings, other.bindings);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.CTXVAR;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
ObjectNode obj = JsonUtils.createObjectNode();
for (Binding binding : bindings) {
JsonNode node = ctx.resolve(binding.getReference());
obj.set(binding.getName(), node);
}
ctx.setVar(name, obj);
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
}
/**
* Instruction that closes a block instruction. Implementation is a NOOP.
*/
public static class EndInst extends BaseInstruction {
@Override
public boolean equals(Object obj) {
return (obj instanceof EndInst);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.END;
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
}
/**
* Marker instruction indicating the end of the parse. Implementation is a NOOP,
* and it has no visible representation in the template.
*/
public static class EofInst extends BaseInstruction {
@Override
public boolean equals(Object obj) {
return (obj instanceof EofInst);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.EOF;
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
// NO VISIBLE REPRESENTATION
}
}
/**
* Eval instruction, evaluates an expression.
*/
public static class EvalInst extends BaseInstruction {
/**
* Raw expression
*/
private final String raw;
/**
* Debug mode.
*/
private final boolean debug;
/**
* Parsed and assembled expression, ready for evaluation.
* The expression is parsed the first time it executes.
*/
private Expr expr;
public EvalInst(String raw) {
this.debug = raw.startsWith("#");
if (this.debug) {
raw = raw.substring(1);
}
this.raw = raw;
}
/**
* Body of the expression.
*/
public String body() {
return this.raw;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof EvalInst) {
EvalInst inst = (EvalInst) obj;
return this.raw.equals(inst.raw);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.EVAL;
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
List errors;
if (this.expr == null) {
// Construct the expression. This tokenizes the input.
this.expr = new Expr(this.raw, ctx.getExprOptions());
// Build the expression. This assembles the expression in
// reverse polish notation so it can be evaluated later.
this.expr.build();
// Check if the expression has a parse error and emit it.
errors = this.expr.errors();
if (!errors.isEmpty()) {
for (String error : errors) {
ErrorInfo info = ctx.error(ExecuteErrorType.EXPRESSION_PARSE)
.data(error);
ctx.addError(info);
}
}
} else {
// Repeated evaluations, get a reference to the expression's
// errors list.
errors = this.expr.errors();
}
if (debug) {
ctx.buffer().append("EVAL=");
Tokens.debug(this.expr.expressions(), ctx.buffer());
}
// Evaluate the expression against the current context and append
// any output. We only attempt to reduce the expression if there
// were no parse errors.
if (errors.isEmpty()) {
// Track the error count to detect if reduce produces an error.
int errs = errors.size();
// Create a temporary stack frame. This will collect local variables
// created by the expression. If reducing the expression produces an
// error, the local variables created by the expression will be discarded.
ctx.push(ctx.node());
// Reduce the expression
JsonNode result = this.expr.reduce(ctx);
// Collect all local variables created by the expression.
Map vars = ctx.frame().getVars();
// Pop the temporary stack frame.
ctx.pop();
// If an error occurred during reduce we suppress the output. The
// temporary stack frame allows us to "undo" the effects of the
// evaluation, removing any local variables created by the
// invalid expression.
if (errors.size() == errs) {
// Reduce produced no errors, so retain the local variables.
if (vars != null) {
// Copy the local variables to the stack frame.
Frame frame = ctx.frame();
for (Map.Entry var : vars.entrySet()) {
frame.setVar(var.getKey(), var.getValue());
}
}
// If the expression produced immediate output, emit it.
if (result != null) {
if (this.debug) {
ctx.buffer().append(" -> ");
}
emitJsonNode(ctx.buffer(), result);
}
}
}
}
}
/**
* Conditional block which tests one or more variables for "truthiness" and then
* joins the boolean values with either an OR or AND operator.
*
* Note: this was added to the JavaScript JSON-Template engine by someone at Squarespace
* before my time, and is not as expressive as it could be, e.g. there is no operator
* precedence, grouping, etc. This implementation replicates the behavior of the JS
* version. - phensley
*/
public static class IfInst extends BlockInst {
private static final List EMPTY_OPS = Arrays.asList();
private final List variables = new ArrayList<>(VARIABLE_LIST_LEN);
private final List operators;
IfInst(List vars, List ops) {
super(CONSEQUENT_BLOCK_LEN);
for (String name : vars) {
Object[] parts = splitVariable(name);
variables.add(parts);
}
this.operators = (ops == null) ? EMPTY_OPS : ops;
}
public List getVariables() {
return variables;
}
public List getOperators() {
return operators;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof IfInst)) {
return false;
}
IfInst other = (IfInst) obj;
return variableListEquals(variables, other.variables)
&& operators.equals(other.operators)
&& blockEquals(other);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.IF;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
// Set initial boolean using truth value of first var.
boolean result = GeneralUtils.isTruthy(ctx.resolve(variables.get(0)));
for (int i = 1, size = variables.size(); i < size; i++) {
Object[] var = variables.get(i);
Operator op = operators.get(i - 1);
boolean value = GeneralUtils.isTruthy(ctx.resolve(var));
result = (op == Operator.LOGICAL_OR) ? (result || value) : (result && value);
if (op == Operator.LOGICAL_OR) {
if (result) {
break;
}
} else if (!result) {
break;
}
}
// Based on the boolean result, take a branch.
if (result) {
ctx.execute(consequent.getInstructions());
} else {
ctx.execute(alternative);
}
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf, recurse);
}
}
/**
* Represents a conditional which tests a predicate.
*
* NOTE: I'm not sure why this form of if expression was added to the language,
* since it is redundant with a normal predicate call.
*
* Example: {.if foo?} does the same thing as {.foo?}
*/
public static class IfPredicateInst extends BlockInst {
private final Predicate predicate;
private final Arguments arguments;
IfPredicateInst(Predicate predicate, Arguments arguments) {
super(CONSEQUENT_BLOCK_LEN);
this.predicate = predicate;
this.arguments = arguments;
}
public Predicate getPredicate() {
return predicate;
}
public Arguments getArguments() {
return arguments;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof IfPredicateInst) {
IfPredicateInst other = (IfPredicateInst) obj;
return predicate.equals(other.predicate);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.IF;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
if (predicate.apply(ctx, arguments)) {
ctx.execute(consequent.getInstructions());
} else {
ctx.execute(alternative);
}
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf, recurse);
}
}
/**
* Used to include a partial template or macro.
*/
public static class IncludeInst extends BaseInstruction {
private final Arguments args;
private final String name;
private boolean output;
IncludeInst(Arguments args) {
this.args = args;
this.name = args.isEmpty() ? "" : args.first();
for (int i = 1; i < args.count(); i++) {
String arg = args.get(i);
switch (arg) {
case "output":
this.output = true;
break;
}
}
}
public Arguments getArguments() {
return args;
}
@Override
public InstructionType getType() {
return InstructionType.INCLUDE;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof IncludeInst) {
IncludeInst other = (IncludeInst) obj;
return args.equals(other.args);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
// Refuse to evaluate the instruction if not explicitly enabled
if (!ctx.getEnableInclude()) {
return;
}
// Fetch the partial or macro code
Instruction code = null;
try {
code = ctx.getPartial(name);
} catch (CodeSyntaxException e) {
ErrorInfo parent = ctx.error(INCLUDE_PARTIAL_SYNTAX).name(name).data(e.getMessage());
parent.child(e.getErrorInfo());
throw new CodeExecuteException(parent);
}
if (code == null) {
ErrorInfo error = ctx.error(ExecuteErrorType.INCLUDE_PARTIAL_MISSING).name(name);
if (ctx.safeExecutionEnabled()) {
ctx.addError(error);
return;
} else {
throw new CodeExecuteException(error);
}
}
// By default we suppress output from the partial or macro
StringBuilder buf = null;
if (!output) {
buf = ctx.swapBuffer(new StringBuilder());
}
// Execute the partial or macro inline.
if (ctx.enterPartial(name)) {
InstructionType type = code.getType();
switch (type) {
case ROOT:
((RootInst)code).invoke(ctx);
break;
case MACRO:
((MacroInst)code).root().invoke(ctx);
break;
default:
break;
}
ctx.exitPartial(name);
}
if (!output && buf != null) {
ctx.swapBuffer(buf);
}
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
}
/**
* Used to inject a JSON object and bind it to a variable. Optional
* arguments to add this to global scope.
*/
public static class InjectInst extends BaseInstruction {
private final String variable;
private final String filename;
private final Arguments arguments;
InjectInst(String variable, String filename, Arguments arguments) {
this.variable = variable;
this.filename = filename;
this.arguments = arguments;
}
public String variable() {
return variable;
}
public String filename() {
return filename;
}
public Arguments arguments() {
return arguments;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof InjectInst) {
InjectInst other = (InjectInst) obj;
return variable.equals(other.variable)
&& filename.equals(other.filename)
&& arguments.equals(other.arguments);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.INJECT;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
JsonNode node = ctx.getInjectable(filename);
ctx.setVar(variable, node);
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf, recurse);
}
}
/**
* Base class for instructions which emit literal characters directly into the output.
*/
public static abstract class LiteralInst extends BaseInstruction {
private final String name;
private final String value;
LiteralInst(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof LiteralInst)) {
return false;
}
LiteralInst other = (LiteralInst) obj;
return name.equals(other.name) && value.equals(other.value);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public void invoke(Context ctx) {
ctx.buffer().append(value);
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
}
/**
* Represents a named block of template code that can be applied.
*/
public static class MacroInst extends BaseInstruction implements BlockInstruction {
private final String name;
private final RootInst root;
MacroInst(String name) {
this.name = name;
this.root = new RootInst();
}
public String name() {
return name;
}
public RootInst root() {
return root;
}
@Override
public Block getConsequent() {
return root.getConsequent();
}
@Override
public Instruction getAlternative() {
return root.getAlternative();
}
@Override
public void setAlternative(Instruction inst) {
root.setAlternative(inst);
}
@Override
public InstructionType getType() {
return InstructionType.MACRO;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof MacroInst)) {
return false;
}
MacroInst other = (MacroInst) obj;
return name.equals(other.name) && root.equals(other.root);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
// This registers the macro in the current scope. Macros must be
// applied to produce output, e.g. {@|apply }
ctx.setMacro(name, root);
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf, recurse);
}
}
/**
* Represents either a left or right meta character, e.g. '{' or '}'.
* Made this a runtime determination in case in the future we move
* to a 2-character meta sequence, like "{{" and "}}".
*/
public static class MetaInst extends BaseInstruction {
private final boolean isLeft;
MetaInst(boolean isLeft) {
this.isLeft = isLeft;
}
public boolean isLeft() {
return this.isLeft;
}
@Override
public boolean equals(Object obj) {
return (obj instanceof MetaInst) ? ((MetaInst)obj).isLeft == isLeft : false;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return (isLeft) ? InstructionType.META_LEFT : InstructionType.META_RIGHT;
}
@Override
public void invoke(Context ctx) {
ctx.buffer().append(isLeft ? ctx.getMetaLeft() : ctx.getMetaRight());
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
}
/**
* Emits a newline character to the output.
*/
public static class NewlineInst extends LiteralInst {
NewlineInst() {
super("newline", "\n");
}
@Override
public InstructionType getType() {
return InstructionType.NEWLINE;
}
}
/**
* Does nothing.
*/
public static class NoopInst extends BaseInstruction {
@Override
public boolean equals(Object obj) {
return (obj instanceof NoopInst);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.NOOP;
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
// NO VISIBLE REPRESENTATION
}
}
/**
* Represents a boolean-valued function.
*/
public static class PredicateInst extends BlockInst {
private InstructionType type = InstructionType.PREDICATE;
private final Predicate impl;
private final Arguments args;
PredicateInst(Predicate impl, Arguments args) {
super(CONSEQUENT_BLOCK_LEN);
this.impl = impl;
this.args = args;
}
public Predicate getPredicate() {
return impl;
}
public Arguments getArguments() {
return args;
}
/**
* Set the instruction type to OR. This is identical to a normal predicate,
* but serves as a marker to detect invalid placement of {.or} directives.
*/
public void setOr() {
type = InstructionType.OR_PREDICATE;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof PredicateInst)) {
return false;
}
PredicateInst other = (PredicateInst) obj;
if (impl != other.impl) {
return false;
}
return args.equals(other.args) && blockEquals(other);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return type;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
if (impl != null) {
// If we have a predicate instance, we execute the consequents only if the
// predicate evaluates to true. If the predicate evaluates to false, we
// execute the alternative.
if (impl.apply(ctx, args)) {
ctx.execute(consequent.getInstructions());
} else {
ctx.execute(alternative);
}
} else {
// Without a predicate we always execute the consequents. This represents
// the "else" in an if / else chain.
ctx.execute(consequent.getInstructions());
}
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf, recurse);
}
}
/**
* Represents a block of instructions to be executed with context set to each
* element of an array.
*/
public static class RepeatedInst extends BlockInst {
private final Object[] variable;
private AlternatesWithInst alternatesWith;
RepeatedInst(String name) {
super(CONSEQUENT_BLOCK_LEN);
this.variable = splitVariable(name);
}
public Object[] getVariable() {
return variable;
}
/**
* Optional block which is executed once between executions of the consequent block.
*/
public void setAlternatesWith(AlternatesWithInst inst) {
alternatesWith = inst;
}
public AlternatesWithInst getAlternatesWith() {
return alternatesWith;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof RepeatedInst)) {
return false;
}
RepeatedInst other = (RepeatedInst) obj;
if (!Arrays.equals(variable, other.variable)) {
return false;
}
return equals(alternatesWith, other.alternatesWith) && blockEquals(other);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.REPEATED;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
ctx.pushSection(variable);
if (ctx.initIteration()) {
// We have an array node and can now iterate.
int lastIndex = ctx.arraySize() - 1;
while (ctx.hasNext()) {
int index = ctx.currentIndex();
// Push the array element onto the stack to be processed by the consequent.
ctx.pushNext();
ctx.execute(consequent.getInstructions());
// In between each pass, execute the alternatesWith block.
// Note: We must do this here to ensure any variables created inside the
// consequent block are available to the alternates-with block.
if (index < lastIndex) {
ctx.execute(alternatesWith);
}
ctx.pop();
// Point to next array element.
ctx.increment();
}
ctx.pop();
} else {
ctx.pop();
ctx.execute(alternative);
}
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf, recurse);
}
}
/**
* Represents the root instruction for a whole template. A special marker
* instruction, it helps enforce that all scopes were properly closed before
* accepting the template as valid.
*
* For example, you could create a SECTION at the start of your template and
* never close it, which would execute fine, but would be considered invalid,
* since each SECTION *must* have a corresponding END tag.
*/
public static class RootInst extends BlockInst {
RootInst() {
super(ROOT_BLOCK_LEN);
}
@Override
public boolean equals(Object obj) {
return (obj instanceof RootInst) && blockEquals((RootInst)obj);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.ROOT;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
ctx.execute(consequent.getInstructions());
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf, recurse);
}
}
/**
* Represents a nested scope within the JSON tree.
*/
public static class SectionInst extends BlockInst {
private final Object[] variable;
SectionInst(String name) {
super(CONSEQUENT_BLOCK_LEN);
this.variable = splitVariable(name);
}
public Object[] getVariable() {
return variable;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SectionInst)) {
return false;
}
SectionInst other = (SectionInst) obj;
return Arrays.equals(variable, other.variable) && blockEquals(other);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.SECTION;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
ctx.pushSection(variable);
JsonNode node = ctx.node();
if (GeneralUtils.isTruthy(node)) {
ctx.execute(consequent.getInstructions());
ctx.pop();
} else {
ctx.pop();
ctx.execute(alternative);
}
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf, recurse);
}
}
/** Outputs a literal space character */
public static class SpaceInst extends LiteralInst {
SpaceInst() {
super("space", " ");
}
@Override
public InstructionType getType() {
return InstructionType.SPACE;
}
}
/** Outputs a literal tab character */
static class TabInst extends LiteralInst {
TabInst() {
super("tab", "\t");
}
@Override
public InstructionType getType() {
return InstructionType.TAB;
}
}
/**
* Represents a range of characters to be copied directly into the output buffer.
*/
public static class TextInst extends BaseInstruction {
private final StringView view;
TextInst(StringView view) {
this.view = view;
}
public StringView getView() {
return view;
}
@Override
public boolean equals(Object obj) {
return (obj instanceof TextInst) && view.equals(((TextInst)obj).view);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.TEXT;
}
@Override
public void invoke(Context ctx) {
ctx.buffer().append(view.data(), view.start(), view.end());
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
}
/**
* Represents the value of a JSON node, with an optional list of formatters,
* For example, "{name|foo|bar}" will do the following:
*
* First the "foo" formatter will modify the current stack frame's node and
* replace it with a new value. Then "bar" will do the same. Once all the
* formatters have been applied, the switch statement at the end of the
* invoke() method will determine which representation to emit for the final
* value.
*/
public static class VariableInst extends BaseInstruction implements Formattable {
private final Variables variables;
private List formatters;
VariableInst(String name) {
this(name, null);
}
VariableInst(String name, List formatters) {
this(new Variables(name), formatters);
}
VariableInst(Variables variables, List formatters) {
this.variables = variables;
setFormatters(formatters);
}
public Variables getVariables() {
return variables;
}
@Override
public List getFormatters() {
return formatters;
}
@Override
public void setFormatters(List formatters) {
this.formatters = formatters == null ? Collections.emptyList() : formatters;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof VariableInst) {
VariableInst other = (VariableInst) obj;
return Objects.equals(variables, other.variables)
&& Objects.equals(formatters, other.formatters);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public InstructionType getType() {
return InstructionType.VARIABLE;
}
@Override
public void invoke(Context ctx) throws CodeExecuteException {
int count = variables.count();
for (int i = 0; i < count; i++) {
variables.get(i).resolve(ctx);
}
Variable first = variables.first();
ctx.push(first.node());
applyFormatters(ctx, formatters, variables);
// Finally, output the result.
if (!first.missing()) {
emitJsonNode(ctx.buffer(), first.node());
}
ctx.pop();
}
@Override
public void repr(StringBuilder buf, boolean recurse) {
ReprEmitter.emit(this, buf);
}
}
private static void applyFormatters(Context ctx, List formatters, Variables variables)
throws CodeExecuteException {
CodeLimiter limiter = ctx.getCodeLimiter();
int size = formatters.size();
for (int i = 0; i < size; i++) {
FormatterCall call = formatters.get(i);
limiter.check();
Formatter impl = call.getFormatter();
impl.apply(ctx, call.getArguments(), variables);
}
}
private static void emitJsonNode(StringBuilder buf, JsonNode node) {
if (node.isNumber()) {
// Formatting of numbers depending on type
switch (node.numberType()) {
case BIG_INTEGER:
buf.append(((BigIntegerNode)node).bigIntegerValue().toString());
break;
case BIG_DECIMAL:
buf.append(((DecimalNode)node).decimalValue().toPlainString());
break;
case INT:
case LONG:
buf.append(node.asLong());
break;
case FLOAT:
case DOUBLE:
double val = node.asDouble();
buf.append(Formats.number(val));
break;
default:
break;
}
} else if (node.isArray()) {
// JavaScript Array.toString() will comma-delimit the elements.
for (int i = 0, size = node.size(); i < size; i++) {
if (i >= 1) {
buf.append(",");
}
buf.append(node.path(i).asText());
}
} else if (!node.isNull() && !node.isMissingNode()) {
buf.append(node.asText());
}
}
}