Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,27 @@ Hello edgar!
The Mustache Spec has some rules for removing spaces and new lines. This feature is disabled by default.
You can turn this on by setting the: ```Handlebars.prettyPrint(true)```.

### Preserve Parent Context in Partials
By default, Handlebars.java creates a new partial context when invoking a partial, which means the `{{..}}` operator in a partial references the partial call site rather than the original parent scope.

You can change this behavior by setting: ```Handlebars.preserveParentContext(true)```.

When enabled, partials like `{{> partial this}}` or `{{> partial ..}}` will preserve the parent context chain, allowing `{{..}}` inside partials to reference the original parent scope.

Example:

```java
Handlebars handlebars = new Handlebars().preserveParentContext(true);
```

When `preserveParentContext` is enabled:
* `{{> partial}}` uses the current context directly
* `{{> partial this}}` uses the current context directly
* `{{> partial ..}}` navigates up one parent level
* `{{> partial ../..}}` navigates up multiple parent levels

Default is: `false` (for backward compatibility).


# Modules

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,26 @@ private static <E extends Throwable> void sneakyThrow0(final Throwable x) throws
*/
private boolean preEvaluatePartialBlocks = true;

/**
* If true, preserves the parent context when invoking partials without creating a new PartialCtx.
* This allows {{..}} in partials to reference the original parent scope.
*
* <p>When enabled:
* <ul>
* <li>{{> partial}} uses the current context directly</li>
* <li>{{> partial this}} uses the current context directly</li>
* <li>{{> partial ..}} navigates up one parent level</li>
* <li>{{> partial ../..}} navigates up multiple parent levels</li>
* </ul>
*
* <p>When disabled (default): Creates a new PartialCtx for each partial (traditional behavior).
*
* <p>Default: false (for backward compatibility)
*
* @since 4.6.0
*/
private boolean preserveParentContext = false;

/** Standard charset. */
private Charset charset = StandardCharsets.UTF_8;

Expand Down Expand Up @@ -1300,6 +1320,39 @@ public Handlebars preEvaluatePartialBlocks(final boolean preEvaluatePartialBlock
return this;
}

/**
* Get the preserve parent context flag.
*
* @return True if parent context is preserved, false otherwise.
*/
public boolean preserveParentContext() {
return preserveParentContext;
}

/**
* Set the preserve parent context flag.
*
* @param preserveParentContext True to preserve parent context, false to use traditional behavior.
*/
public void setPreserveParentContext(final boolean preserveParentContext) {
this.preserveParentContext = preserveParentContext;
}

/**
* Set the preserve parent context flag.
*
* <p>When enabled, partials like {{> partial this}} or {{> partial ..}} will preserve the parent
* context chain, allowing {{..}} inside partials to reference the original parent scope instead of
* the partial call site.
*
* @param preserveParentContext True to preserve parent context, false to use traditional behavior.
* @return This handlebars object.
*/
public Handlebars preserveParentContext(final boolean preserveParentContext) {
setPreserveParentContext(preserveParentContext);
return this;
}

/**
* Return a parser factory.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,43 @@ protected void merge(final Context context, final Writer writer) throws IOExcept
}
}
}

// Check if preserveParentContext mode is enabled for pure parent path navigation:
// Supports: this, .., ../.. (does not support mixed paths like ../foo/bar or hash arguments)
if (handlebars.preserveParentContext()
&& ("this".equals(this.scontext) || this.scontext.startsWith(".."))) {

if ("this".equals(this.scontext)) {
// Use current context directly, don't create new context
template.apply(context, writer);
return;
}

// Handle .. and ../.. paths
Context currentContext = context;
String remainingContext = this.scontext;

// Navigate up the parent chain for each ../ encountered
while (remainingContext.startsWith("../") && currentContext != null) {
currentContext = currentContext.parent();
remainingContext = remainingContext.substring(3);
}

if ("".equals(remainingContext) && currentContext != null) {
// "../" or "../../" etc. - use the navigated context
template.apply(currentContext, writer);
return;
}

if ("..".equals(remainingContext) && currentContext != null
&& currentContext.parent() != null) {
// Single ".." - navigate to parent
template.apply(currentContext.parent(), writer);
return;
}
}

// Traditional mode: create new PartialCtx (default behavior for backward compatibility)
context.data(Context.CALLEE, this);
Map<String, Object> hash = hash(context);
// HACK: hide/override local attribute with parent version (if any)
Expand Down
Loading