Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eager expression node #548

Merged
merged 15 commits into from
Dec 9, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,10 @@ public void setPosition(int position) {
}

public void addError(TemplateError templateError) {
if (context.getHideInterpreterErrors()) {
// Hiding errors when resolving chunks.
return;
}
// fix line numbers not matching up with source template
if (!context.getCurrentPathStack().isEmpty()) {
if (!templateError.getSourceTemplate().isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
package com.hubspot.jinjava.lib.expression;

import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.lib.filter.EscapeFilter;
import com.hubspot.jinjava.lib.tag.RawTag;
import com.hubspot.jinjava.lib.tag.eager.EagerStringResult;
import com.hubspot.jinjava.lib.tag.eager.EagerTagDecorator;
import com.hubspot.jinjava.lib.tag.eager.EagerToken;
import com.hubspot.jinjava.tree.output.RenderedOutputNode;
import com.hubspot.jinjava.tree.parse.ExpressionToken;
import com.hubspot.jinjava.tree.parse.TagToken;
import com.hubspot.jinjava.util.ChunkResolver;
import com.hubspot.jinjava.util.Logging;
import com.hubspot.jinjava.util.WhitespaceUtils;
import org.apache.commons.lang3.StringUtils;

public class EagerExpressionStrategy implements ExpressionStrategy {

Expand All @@ -11,6 +21,113 @@ public RenderedOutputNode interpretOutput(
ExpressionToken master,
JinjavaInterpreter interpreter
) {
return new DefaultExpressionStrategy().interpretOutput(master, interpreter); // TODO replace with actual functionality
EagerStringResult eagerStringResult = eagerResolveExpression(master, interpreter);
return new RenderedOutputNode(
eagerStringResult.getPrefixToPreserveState() + eagerStringResult.getResult()
);
}

private EagerStringResult eagerResolveExpression(
ExpressionToken master,
JinjavaInterpreter interpreter
) {
ChunkResolver chunkResolver = new ChunkResolver(
master.getExpr(),
master,
interpreter
);
EagerStringResult resolvedExpression = EagerTagDecorator.executeInChildContext(
eagerInterpreter -> chunkResolver.resolveChunks(),
interpreter,
true
);
StringBuilder prefixToPreserveState = new StringBuilder(
interpreter.getContext().isProtectedMode()
? resolvedExpression.getPrefixToPreserveState()
: ""
);
if (chunkResolver.getDeferredWords().isEmpty()) {
String result = WhitespaceUtils.unquote(resolvedExpression.getResult());
if (
!StringUtils.equals(result, master.getImage()) &&
(
StringUtils.contains(result, master.getSymbols().getExpressionStart()) ||
StringUtils.contains(result, master.getSymbols().getExpressionStartWithTag())
)
) {
if (interpreter.getConfig().isNestedInterpretationEnabled()) {
try {
result = interpreter.renderFlat(result);
} catch (Exception e) {
Logging.ENGINE_LOG.warn("Error rendering variable node result", e);
}
} else {
// Possible macro/set tag in front of this one. Includes result
result = wrapInRawOrExpressionIfNeeded(result, interpreter);
}
}

if (interpreter.getContext().isAutoEscape()) {
result = EscapeFilter.escapeHtmlEntities(result);
}
return new EagerStringResult(result, prefixToPreserveState.toString());
}
prefixToPreserveState.append(
EagerTagDecorator.reconstructFromContextBeforeDeferring(
chunkResolver.getDeferredWords(),
interpreter
)
);
String helpers = wrapInExpression(resolvedExpression.getResult(), interpreter);
interpreter
.getContext()
.handleEagerToken(
new EagerToken(
new TagToken(
helpers,
master.getLineNumber(),
master.getStartPosition(),
master.getSymbols()
),
chunkResolver.getDeferredWords()
)
);
// There is no result because it couldn't be entirely evaluated.
return new EagerStringResult(
"",
EagerTagDecorator.wrapInAutoEscapeIfNeeded(
prefixToPreserveState.toString() + helpers,
interpreter
)
);
}

private static String wrapInRawOrExpressionIfNeeded(
String output,
JinjavaInterpreter interpreter
) {
if (
interpreter.getConfig().getExecutionMode().isPreserveRawTags() &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to a create a variable for interpreter.getConfig() to shorten up these lines.

(
output.contains(
interpreter.getConfig().getTokenScannerSymbols().getExpressionStart()
) ||
output.contains(
interpreter.getConfig().getTokenScannerSymbols().getExpressionStartWithTag()
)
)
) {
return EagerTagDecorator.wrapInTag(output, RawTag.TAG_NAME, interpreter);
}
return output;
}

private static String wrapInExpression(String output, JinjavaInterpreter interpreter) {
return String.format(
"%s %s %s",
interpreter.getConfig().getTokenScannerSymbols().getExpressionStart(),
output,
interpreter.getConfig().getTokenScannerSymbols().getExpressionEnd()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public Object doEvaluate(
) {
Optional<String> importFile = macroFunction.getImportFile(interpreter);
try (InterpreterScopeClosable c = interpreter.enterScope()) {
interpreter.getContext().setProtectedMode(true);
return macroFunction.getEvaluationResult(argMap, kwargMap, varArgs, interpreter);
} finally {
importFile.ifPresent(path -> interpreter.getContext().getCurrentPathStack().pop());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.hubspot.jinjava.lib.fn.MacroFunction;
import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction;
import com.hubspot.jinjava.lib.tag.AutoEscapeTag;
import com.hubspot.jinjava.lib.tag.MacroTag;
import com.hubspot.jinjava.lib.tag.RawTag;
import com.hubspot.jinjava.lib.tag.SetTag;
import com.hubspot.jinjava.lib.tag.Tag;
Expand Down Expand Up @@ -118,7 +119,7 @@ public String eagerInterpret(TagNode tagNode, JinjavaInterpreter interpreter) {
* Render all children of this TagNode.
* @param tagNode TagNode to render the children of.
* @param interpreter The JinjavaInterpreter.
* @return
* @return the string output of this tag node's children.
*/
public String renderChildren(TagNode tagNode, JinjavaInterpreter interpreter) {
StringBuilder sb = new StringBuilder();
Expand Down Expand Up @@ -231,6 +232,27 @@ public static EagerStringResult executeInChildContext(
return new EagerStringResult(result.toString());
}

/**
* Reconstruct the macro functions and variables from the context before they
* get deferred.
* Those macro functions and variables found within {@code deferredWords} are
* reconstructed with {@link MacroTag}(s) and a {@link SetTag}, respectively to
* preserve the context within the Jinjava template itself.
* @param deferredWords set of words that will need to be deferred based on the
* previously performed operation.
* @param interpreter the Jinjava interpreter.
* @return a Jinjava-syntax string of 0 or more macro tags and 0 or 1 set tags.
*/
public static String reconstructFromContextBeforeDeferring(
Set<String> deferredWords,
JinjavaInterpreter interpreter
) {
return (
reconstructMacroFunctionsBeforeDeferring(deferredWords, interpreter) +
reconstructVariablesBeforeDeferring(deferredWords, interpreter)
);
}

/**
* Build macro tag images for any macro functions that are included in deferredWords
* and remove those macro functions from the deferredWords set.
Expand All @@ -242,7 +264,7 @@ public static EagerStringResult executeInChildContext(
* @return A jinjava-syntax string that is the images of any macro functions that must
* be evaluated at a later time.
*/
public static String getNewlyDeferredFunctionImages(
private static String reconstructMacroFunctionsBeforeDeferring(
Set<String> deferredWords,
JinjavaInterpreter interpreter
) {
Expand Down Expand Up @@ -279,12 +301,42 @@ public static String getNewlyDeferredFunctionImages(
)
.map(EagerStringResult::toString)
.collect(Collectors.joining());
// Remove macro functions from the set because they've been fully processed now.
deferredWords.removeAll(toRemove);
return result;
}

private static String reconstructVariablesBeforeDeferring(
Set<String> deferredWords,
JinjavaInterpreter interpreter
) {
if (interpreter.getContext().isProtectedMode()) {
return ""; // This will be handled outside of the protected mode.
}
Map<String, String> deferredMap = new HashMap<>();
deferredWords
.stream()
.map(w -> w.split("\\.", 2)[0]) // get base prop
.filter(
w ->
interpreter.getContext().containsKey(w) &&
!(interpreter.getContext().get(w) instanceof DeferredValue)
)
.forEach(
w -> {
try {
deferredMap.put(
w,
ChunkResolver.getValueAsJinjavaString(interpreter.getContext().get(w))
);
} catch (JsonProcessingException ignored) {}
}
);
return buildSetTagForDeferredInChildContext(deferredMap, interpreter, true);
}

/**
* Build the image for a set tag which preserves the values of objects on the context
* Build the image for a {@link SetTag} which preserves the values of objects on the context
* for a later rendering pass. The set tag will set the keys to the values within
* the {@code deferredValuesToSet} Map.
* @param deferredValuesToSet Map that specifies what the context objects should be set
Expand All @@ -300,6 +352,9 @@ public static String buildSetTagForDeferredInChildContext(
JinjavaInterpreter interpreter,
boolean registerEagerToken
) {
if (deferredValuesToSet.size() == 0) {
return "";
}
if (
interpreter.getConfig().getDisabled().containsKey(Library.TAG) &&
interpreter.getConfig().getDisabled().get(Library.TAG).contains(SetTag.TAG_NAME)
Expand Down Expand Up @@ -395,7 +450,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter
joiner.add(resolvedChunks);
}
joiner.add(tagToken.getSymbols().getExpressionEndWithTag());
String newlyDeferredFunctionImages = getNewlyDeferredFunctionImages(
String reconstructedFromContext = reconstructFromContextBeforeDeferring(
chunkResolver.getDeferredWords(),
interpreter
);
Expand All @@ -414,7 +469,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter
)
);

return (newlyDeferredFunctionImages + joiner.toString());
return (reconstructedFromContext + joiner.toString());
}

public static String reconstructEnd(TagNode tagNode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public abstract class TokenScannerSymbols implements Serializable {
private String expressionStart = null;
private String expressionStartWithTag = null;
private String closingComment = null;
private String expressionEnd = null;
private String expressionEndWithTag = null;

public abstract char getPrefixChar();
Expand Down Expand Up @@ -86,6 +87,13 @@ public String getExpressionStart() {
return expressionStart;
}

public String getExpressionEnd() {
if (expressionEnd == null) {
expressionEnd = String.valueOf(getExprEndChar()) + getPostfixChar();
}
return expressionEnd;
}

public String getExpressionStartWithTag() {
if (expressionStartWithTag == null) {
expressionStartWithTag = String.valueOf(getPrefixChar()) + getTagChar();
Expand Down
10 changes: 0 additions & 10 deletions src/test/java/com/hubspot/jinjava/EagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,6 @@ public void itPrependsSetIfStateChanges() {
}

@Test
@Ignore
public void itHandlesLoopVarAgainstDeferredInLoop() {
expectedTemplateInterpreter.assertExpectedOutput(
"handles-loop-var-against-deferred-in-loop"
Expand Down Expand Up @@ -585,15 +584,13 @@ public void itDefersMacroInIf() {
}

@Test
@Ignore
public void itPutsDeferredImportedMacroInOutput() {
expectedTemplateInterpreter.assertExpectedOutput(
"puts-deferred-imported-macro-in-output"
);
}

@Test
@Ignore
public void itPutsDeferredImportedMacroInOutputSecondPass() {
localContext.put("deferred", 1);
expectedTemplateInterpreter.assertExpectedOutput(
Expand All @@ -605,7 +602,6 @@ public void itPutsDeferredImportedMacroInOutputSecondPass() {
}

@Test
@Ignore
public void itPutsDeferredFromedMacroInOutput() {
expectedTemplateInterpreter.assertExpectedOutput(
"puts-deferred-fromed-macro-in-output"
Expand All @@ -630,13 +626,11 @@ public void itEagerlyDefersMacroSecondPass() {
}

@Test
@Ignore
public void itLoadsImportedMacroSyntax() {
expectedTemplateInterpreter.assertExpectedOutput("loads-imported-macro-syntax");
}

@Test
@Ignore
public void itDefersCaller() {
expectedTemplateInterpreter.assertExpectedOutput("defers-caller");
}
Expand All @@ -649,7 +643,6 @@ public void itDefersCallerSecondPass() {
}

@Test
@Ignore
public void itDefersMacroInExpression() {
expectedTemplateInterpreter.assertExpectedOutput("defers-macro-in-expression");
}
Expand All @@ -667,7 +660,6 @@ public void itDefersMacroInExpressionSecondPass() {
}

@Test
@Ignore
public void itHandlesDeferredInIfchanged() {
expectedTemplateInterpreter.assertExpectedOutput("handles-deferred-in-ifchanged");
}
Expand Down Expand Up @@ -727,7 +719,6 @@ public void itHandlesNonDeferringCycles() {
}

@Test
@Ignore
public void itHandlesAutoEscape() {
localContext.put("myvar", "foo < bar");
expectedTemplateInterpreter.assertExpectedOutput("handles-auto-escape");
Expand Down Expand Up @@ -765,7 +756,6 @@ public void itHandlesDeferredImportVars() {
}

@Test
@Ignore
public void itHandlesDeferredImportVarsSecondPass() {
localContext.put("deferred", 1);
expectedTemplateInterpreter.assertExpectedOutput(
Expand Down
Loading