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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ jacoco.exec

out
tmp
tmp/spotless
tmp/spotless
.claude.json
.claudeingore
.vscode/
CLAUDE.md
logs/

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,67 @@ protected void merge(final Context context, final Writer writer) throws IOExcept
}
}
}

// Check if preserveParentContext mode is enabled for pure parent path navigation:
// Supports: this, .., ../.., ../../xxx, ../../xxx/yyy (nested property paths)
// Uses PathCompiler to resolve property paths like ../../target/nested/value
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;
}

// Handle paths like ../../xxx or ../../xxx/yyy
// After navigating up parent chain, remaining path is resolved using PathCompiler
// Example: "../../target/nested/value" -> navigate up 2 levels, resolve "target/nested/value"
if (currentContext != null && !"".equals(remainingContext)) {
// Compile the remaining path (e.g., "xxx" or "xxx/yyy") using PathCompiler
// This leverages Handlebars' existing value resolution mechanism
List<PathExpression> compiledPath = PathCompiler.compile(remainingContext);
Object resolvedValue = currentContext.get(compiledPath);

// Create a new context with the resolved value as the model
// This preserves the parent-child relationship correctly
// If resolvedValue is null, the template will render empty string (Handlebars standard behavior)
Context targetContext = Context.newContext(currentContext, resolvedValue);

// Apply template with the resolved context
context.data(Context.CALLEE, this);
Map<String, Object> hash = hash(context);
override(context, hash, OVERRIDE_PROPERTIES);
template.apply(targetContext, writer);
context.data(Context.CALLEE, callee);
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