diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/Context.java b/handlebars/src/main/java/com/github/jknack/handlebars/Context.java index 4e4cd9ca7..9aeb26d8f 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/Context.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/Context.java @@ -402,6 +402,11 @@ public Object eval(final ValueResolver resolver, final Context context, final Ob */ public static final String PARAM_SIZE = Context.class.getName() + "#paramSize"; + /** + * Last callee of a partial block. Internal use. + */ + public static final String CALLEE = Context.class.getName() + "#callee"; + /** * The parent context. Optional. */ diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java b/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java index d08642cae..d897a7c36 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/Handlebars.java @@ -261,6 +261,12 @@ public static CharSequence escapeExpression(final CharSequence input) { */ private boolean prettyPrint; + /** + * If true, given partial blocks are not evaluated when defined but when used. + * If false, partial blocks are evaluated when defined and used. + */ + private boolean lazyPartialBlockEvaluation; + /** * The helper registry. */ @@ -740,6 +746,39 @@ public boolean stringParams() { return stringParams; } + + /** + * If true, given partial blocks are not evaluated when defined but when used. + * If false, partial blocks are evaluated when defined and used. + * @return If true, given partial blocks are not evaluated when defined but when used. + * If false, partial blocks are evaluated when defined and used. + */ + public boolean lazyPartialBlockEvaluation() { + return lazyPartialBlockEvaluation; + } + + + /** + * If true, given partial blocks are not evaluated when defined but when used. + * If false, partial blocks are evaluated when defined and used. + * @param lazyPartialBlockEvaluation Flag to turn it off and on + */ + public void setLazyPartialBlockEvaluation(final boolean lazyPartialBlockEvaluation) { + this.lazyPartialBlockEvaluation = lazyPartialBlockEvaluation; + } + + /** + * If true, given partial blocks are not evaluated when defined but when used. + * If false, partial blocks are evaluated when defined and used. + * @param lazyPartialBlockEvaluation Flag to turn it off and on + * @return The handlebars object. + */ + public Handlebars lazyPartialBlockEvaluation(final boolean lazyPartialBlockEvaluation) { + setLazyPartialBlockEvaluation(lazyPartialBlockEvaluation); + return this; + } + + /** * If true, unnecessary spaces and new lines will be removed from output. Default is: false. * diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/internal/Partial.java b/handlebars/src/main/java/com/github/jknack/handlebars/internal/Partial.java index 2465e1b0d..bbc860ea0 100644 --- a/handlebars/src/main/java/com/github/jknack/handlebars/internal/Partial.java +++ b/handlebars/src/main/java/com/github/jknack/handlebars/internal/Partial.java @@ -23,6 +23,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.Writer; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -120,10 +121,31 @@ protected void merge(final Context context, final Writer writer) /** Inline partial? */ LinkedList> partials = context.data(Context.INLINE_PARTIALS); Map inlineTemplates = partials.getLast(); + Template callee = context.data(Context.CALLEE); + + final boolean pathIsPartialBlock = "@partial-block".equals(path); + final Template lastPartialBlock = inlineTemplates.get("@partial-block"); + final boolean parentIsNotLastPartialBlock = !isCalleeOf(callee, lastPartialBlock); + + if (pathIsPartialBlock && parentIsNotLastPartialBlock) { + throw new IllegalArgumentException( + callee + " does not provide a @partial-block for " + this + ); + } if (this.partial != null) { - this.partial.apply(context); - inlineTemplates.put("@partial-block", this.partial); + if (!handlebars.lazyPartialBlockEvaluation()) { + this.partial.apply(context); + } + + inlineTemplates.put("@partial-block", + new PartialBlockForwardingTemplate(this, + this.partial, + inlineTemplates.get("@partial-block"), + callee, + handlebars + ) + ); } Template template = inlineTemplates.get(path); @@ -171,8 +193,10 @@ protected void merge(final Context context, final Writer writer) } } + context.data(Context.CALLEE, this); Context ctx = Context.newPartialContext(context, this.scontext, hash(context)); template.apply(ctx, writer); + context.data(Context.CALLEE, callee); } catch (IOException ex) { String reason = String.format("The partial '%s' at '%s' could not be found", loader.resolve(path.text()), ex.getMessage()); @@ -183,6 +207,23 @@ protected void merge(final Context context, final Writer writer) } } + /** + * @param callee parent template of the currently traversed template + * @param partialBlock partial block candidate + * @return returns if callee and partialBlock are the same + */ + private boolean isCalleeOf(final Template callee, final Template partialBlock) { + if (callee == null || partialBlock == null) { + return false; + } + + if (!callee.filename().equalsIgnoreCase(partialBlock.filename())) { + return false; + } + + return Arrays.equals(callee.position(), partialBlock.position()); + } + /** * True, if the file was already processed. * diff --git a/handlebars/src/main/java/com/github/jknack/handlebars/internal/PartialBlockForwardingTemplate.java b/handlebars/src/main/java/com/github/jknack/handlebars/internal/PartialBlockForwardingTemplate.java new file mode 100644 index 000000000..c4e8a9f07 --- /dev/null +++ b/handlebars/src/main/java/com/github/jknack/handlebars/internal/PartialBlockForwardingTemplate.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2012-2015 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 com.github.jknack.handlebars.Context; +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Template; + +import java.io.IOException; +import java.io.Writer; +import java.util.LinkedList; +import java.util.Map; + +/** + * + */ +public class PartialBlockForwardingTemplate extends BaseTemplate { + /** + * The block to be passed as partial-block. + */ + private final Template block; + + /** + * The previous partial-block definition of the template which contains this partial. + */ + private final Template parentPartialBlock; + + /** + * The callee of the parent partial. + */ + private final Template callee; + + /** + * Constructs a PartialBlockForwardingTemplate. + * + * @param parent the parent partial + * @param block the block to be passed as partial-block. + * @param parentPartialBlock the previous partial-block definition of + * the template which contains this partial. + * @param callee the template that renders the parent + * @param handlebars handlebars + */ + public PartialBlockForwardingTemplate( + final Template parent, + final Template block, + final Template parentPartialBlock, + final Template callee, + final Handlebars handlebars + ) { + super(handlebars); + this.block = block; + this.parentPartialBlock = parentPartialBlock; + this.callee = callee; + this.filename(block.filename()); + this.position(parent.position()[0], parent.position()[1]); + } + + /** + * {@inheritDoc} + */ + @Override + protected void merge(final Context context, final Writer writer) throws IOException { + LinkedList> partials = context.data(Context.INLINE_PARTIALS); + Map inlineTemplates = partials.getLast(); + Template oldPartialBlock = inlineTemplates.get("@partial-block"); + Template oldCallee = context.data(Context.CALLEE); + + context.data(Context.CALLEE, callee); + inlineTemplates.put("@partial-block", parentPartialBlock); + block.apply(context, writer); + inlineTemplates.put("@partial-block", oldPartialBlock); + context.data(Context.CALLEE, oldCallee); + } + + /** + * {@inheritDoc} + */ + @Override + public String text() { + return block.text(); + } +} diff --git a/handlebars/src/test/java/com/github/jknack/handlebars/LazyPartialBlockEvaluationTest.java b/handlebars/src/test/java/com/github/jknack/handlebars/LazyPartialBlockEvaluationTest.java new file mode 100644 index 000000000..3cacee005 --- /dev/null +++ b/handlebars/src/test/java/com/github/jknack/handlebars/LazyPartialBlockEvaluationTest.java @@ -0,0 +1,32 @@ +package com.github.jknack.handlebars; + +import org.junit.Test; + +import java.io.IOException; + +import static junit.framework.TestCase.assertEquals; + +public class LazyPartialBlockEvaluationTest extends AbstractTest { + @Override + protected void configure(Handlebars handlebars) { + handlebars.setLazyPartialBlockEvaluation(true); + } + + @Test + public void shouldSupportMultipleLevelsOfNestedPartialBlocks() throws IOException { + String myMoreNestedPartial = "I{{> @partial-block}}I"; + String myNestedPartial = "A{{#> myMoreNestedPartial}}{{> @partial-block}}{{/myMoreNestedPartial}}B"; + String myPartial = "{{#> myNestedPartial}}{{> @partial-block}}{{/myNestedPartial}}"; + Template t = compile("C{{#> myPartial}}hello{{/myPartial}}D", new Hash(), $("myPartial", myPartial, "myNestedPartial", myNestedPartial,"myMoreNestedPartial", myMoreNestedPartial)); + String result = t.apply(null); + assertEquals("'CAIhelloIBD' should === '" + result + "': ", "CAIhelloIBD", result); + } + + @Test(expected = HandlebarsException.class) + public void shouldNotDefineInlinePartialsInPartialBlockCall() throws IOException { + // myPartial should not be defined and thus throw a handlebars exception + shouldCompileToWithPartials( + "{{#> dude}}{{#*inline \"myPartial\"}}success{{/inline}}{{/dude}}", + $, $("dude", "{{> myPartial }}"), ""); + } +} diff --git a/handlebars/src/test/java/com/github/jknack/handlebars/PartialBlockTest.java b/handlebars/src/test/java/com/github/jknack/handlebars/PartialBlockTest.java index 784dace34..19da6b77b 100644 --- a/handlebars/src/test/java/com/github/jknack/handlebars/PartialBlockTest.java +++ b/handlebars/src/test/java/com/github/jknack/handlebars/PartialBlockTest.java @@ -8,6 +8,11 @@ public class PartialBlockTest extends AbstractTest { + @Override + protected Handlebars newHandlebars() { + return super.newHandlebars().lazyPartialBlockEvaluation(false); + } + @Test public void text() throws IOException { assertEquals("{{#>dude}}{{#*inline \"myPartial\"}}success{{/inline}}{{/dude}}",