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

Preserve Raw Tags config #518

Merged
merged 12 commits into from
Oct 26, 2020
Merged

Conversation

jasmith-hs
Copy link
Contributor

Adds an option to the JinjavaConfig to preserve raw tags. This option can be set while performing a pre-render with the possibility that there will be deferred values. If a value like contact is deferred, and there is a raw tag like: {% raw %}Hello {{contact.firstname}}!{% endraw %}, the raw tags must be preserved during the first rendering pass, otherwise, on the second pass, Hello {{contact.firstname}}! will get resolved like Hello Jack! instead of remaining, verbatim.

When no values are being deferred, (ie the final render), preserveRawTags should be set to false, and then raw tags will be removed from the rendered output, and leave their children as they were written.

@boulter boulter requested review from Joeoh and gobimcp October 13, 2020 14:27
Copy link
Contributor

@boulter boulter left a comment

Choose a reason for hiding this comment

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

I have a vague recollection of some other workaround for raw tags, and I'm surprised that @Joeoh's work doesn't already cover it. I could be wrong though.

@@ -290,6 +291,21 @@ public void handleDeferredNode(Node node) {
return ImmutableSet.copyOf(deferredNodes);
}

public void handlePreservedRawTag() {
hasPreservedRawTags = true;
if (getParent() != null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this check all the way up the stack (excluding global) instead of just one level?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not too sure what you mean. I am doing a recursive call, which I believe checks up the stack, excluding global: getParent().handlePreservedRawTag();

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, I was looking for a while and I missed the recursion.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess if (parent.getParent() != null) { will execute the handlePreservedRawTag() for Global as well. Can you verify whether we need to have parent.getParent().getParent() != null to exclude the global one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Like in #449, we don't want the shared global context to be altered here as it can be shared across threads. My goal with marking a context as hasPreservedTags is so that we know that a second pass must always be done if raw tags get preserved.
In the instance that we preserve all the raw tags, but nothing gets deferred, a second pass can be done when pre-rendering. If something is deferred, that second pass will happen at serve-time. I want to exclude it for global because I just want it to mark for one run of renderForResult as mentioned here: #447.
Does that answer what you were wondering?

Copy link
Contributor Author

@jasmith-hs jasmith-hs Oct 21, 2020

Choose a reason for hiding this comment

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

Sorry, I misread that question. We only need to check if the parent of what we are calling handlePreservedTag() on is not null. I think maybe line 300: getParent().handlePreservedRawTag() is confusing, as it would make more sense for it to be parent.handlePreservedRawTag().
The if statement is checking to make sure that parent's parent is not null, if so then handle the preserved raw tag for parent

With a context tree shape like G-a-b, here's some sudo-sequence-code that uses questionable syntax:

this == b
parent == getParent() == a
parent.getParent() == G != null
parent.handlePreservedRawTag():
-this == a
-parent == getParent() == G
-parent.getParent() == null
-return
return

jinjava.getGlobalContextCopy(),
JinjavaConfig.newBuilder().withPreserveRawTags(true).build()
);
Throwable throwable = catchThrowable(
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use assertThatThrownBy() here.

@@ -33,6 +34,14 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) {
LengthLimitingStringBuilder result = new LengthLimitingStringBuilder(
interpreter.getConfig().getMaxOutputSize()
);
if (interpreter.getConfig().isPreserveRawTags()) {
result.append(renderNodeRaw(tagNode));
throw new PreservedRawTagException(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why throw exception here instead of

return result.toString();

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After working with the code base a bit more, I agree that it is simpler to return rather than throw an exception, and call interpreter.getContext().handlePreservedRawTag(); from here instead of in the TagNode

.newBuilder()
.withPreserveRawTags(true)
.build();
String input = "1{% raw %}2{% endraw %}3";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you also add a test for deferred values? I thought deferred values will not get evaluated normally. Do they get evaluated inside raw tags?

Copy link
Collaborator

Choose a reason for hiding this comment

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

My testing shows that deferred inside raw does not get evaluated either. https://github.com/HubSpot/jinjava/compare/test-deferred?expand=1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nothing, including deferred tags, will get evaluated inside raw tags. However without the preserveRawTags flag, on the second pass, the deferred values will get evaluated once the real value is put in the context.

Copy link
Collaborator

@hs-lsong hs-lsong Oct 21, 2020

Choose a reason for hiding this comment

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

In my test above, the deferred values do not get evaluated in the second rendering (there is no preserveRawTags) . Maybe we are not talking about the same case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After line 83:

interpreter.getContext().put("deferred", "resolved value");

In the second pass, deferred values will no longer be deferred (such as a contact, which have the actual contact value in the second pass, rather than being a deferred value). If you run the test after putting that line in, I believe that it will get resolved, which is not what is desired.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think what Jack meant by second pass is another renderer pass without having DeferredValue.instance() in the context. But in your test case, the test setUp has localContext.put("deferred", DeferredValue.instance()); so it gets deferred in the second pass as well. Yo need to remove that from the context to actually resolve them in the second pass.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, what is the use case for those? First round rendering makes it deferred, then second round makes it not deferred, but still don't want to render it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

<div>
Hi {{ contact.firstname }}!
I personalized this with Jinjava:
{% raw %}Hi {{ contact.firstname }}!{% endraw %}
</div>

This should display for someone like:

<div>
Hi Jack!
I personalized this with Jinjava:
Hi {{ contact.firstname }}!
</div>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We wouldn't want it to render like:

<div>
Hi Jack!
I personalized this with Jinjava:
Hi Jack!
</div>

Copy link
Collaborator

Choose a reason for hiding this comment

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

Got it. Thanks.

}
}

public boolean getHasPreservedRawTags() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see this function is used. Is this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's for rendering implementations such as with HubSpot's internal pre-renderer

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I am missing the whole picture. Is this the case, the caller set the config to preserve raw tags, later it wants to confirm it did render with the flag set?

Copy link
Contributor Author

@jasmith-hs jasmith-hs Oct 21, 2020

Choose a reason for hiding this comment

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

So if the caller sets to preserve raw tags, but there are no raw tags to preserve and no deferred values, the pre-rendered template can be served as-is.

If however, there are any deferred values, then the second render will also get deferred until later.

Finally, if there are no deferred values, but raw tags are preserved, then the second-pass should happen immediately with the flag off to remove the raw tags and the result can be served as-is.

This is because there isn't a way of knowing if there are going to be deferred values down the line so it must always preserve raw tags if the flag is set. 3 examples assuming deferred is deferred:

Example 1, no deferred values, but a raw tag is preserved:

{% raw %}{{ foo.bar }}{% endraw %}
{{ resolved.variable }}

With preserveRawTags=true the output of the first render is:

{% raw %}{{ foo.bar }}{% endraw %}
I am resolved.

Since there are no deferred values, it should be rendered again with preserveRawTags=false. The output of the second render is:

{{ foo.bar }}
I am resolved.

Example 2, there are no deferred values or raw tags preserved:

Hello
{{ resolved.variable }}

With preserveRawTags=true the output of the first render is:

Hello
I am resolved.

Since there are no deferred values or preserved tags, the output of the first render can be served as-is.

Example 3, there is a deferred value and preserved raw tag:

{% raw %}{{ foo.bar }}{% endraw %}
{{ deferred.variable }}

With preserveRawTags=true the output of the first render is:

{% raw %}{{ foo.bar }}{% endraw %}
{{ deferred.variable }}

Since there is a deferred variable, it should be rendered again at serve-time with preserveRawTags=false. The output of the second render is:

{{ foo.bar }}
I was deferred, but am resolved at serve-time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the detailed examples. It helps a lot. But I still don't see where it needs to call getHasPreservedRawTags(). If the caller set it right before then calling the rendering, it should know it (local variable, etc).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you're mistaking the config option for this. The config option tells the interpreter to preserve the raw tags. getHasPreservedRawTags() is only true if a raw tag was encountered and preserved. If there were no raw tags, then getHasPreservedRawTags() will be false.

Essentially, the method lets the caller know if any raw tags were preserved. It prevents an unnecessary second pass from happening. If this method didn't exist, it wouldn't break anything, but Example 2 would have an extra rendering call that would yield the same result, which can be avoided by using this method.

There isn't a method to set private boolean hasPreservedRawTags, it is only set in the context when handlePreservedRawTag() gets called.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, my bad. I mis-read it. I am OK with this. However, if we do not have this, as you said, in example 2, we would have an extra rendering call -- it should be fast because there is nothing to render.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is true. I think it would be a minimal tradeoff of speed for more simplicity, so I'll just take this out.
This has shown me that it is too confusing to be worth the minimal performance gain

@jasmith-hs
Copy link
Contributor Author

I simplified this further by removing the handle method, and made the config option more generic: preserveForSecondPass.
Let me know if this looks good to merge.

Copy link
Collaborator

@hs-lsong hs-lsong left a comment

Choose a reason for hiding this comment

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

LGTM. However, I think preserveRawTags is more explicit. Just reading preserveForSecondPass, people may not clear on what are preserved.

@jasmith-hs jasmith-hs merged commit cc5905f into HubSpot:master Oct 26, 2020
@jasmith-hs jasmith-hs deleted the preserve-raw-first-pass branch October 26, 2020 14:22
@boulter
Copy link
Contributor

boulter commented Oct 26, 2020

Agreed with @hs-lsong on the naming. What if we wanted to render raw tags on the nth pass?

@jasmith-hs
Copy link
Contributor Author

My reasoning is that we may want to preserve more than just raw tags when pre-rendering so rather than creating config options for each type of tag to preserve, it would be better to have one config option that signifies that a second pass will happen, and raw, etc. tags should be preserved.

I could take away the "second" which implies 2 passes total and make it preserveForFinalPass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants