Skip to content

fix: WriteSafeString encoding consistent regardless of registration order (issue #559)#637

Merged
rexm merged 3 commits into
masterfrom
worktree-agent-a909e3bf82c8e3e3a
Jun 20, 2026
Merged

fix: WriteSafeString encoding consistent regardless of registration order (issue #559)#637
rexm merged 3 commits into
masterfrom
worktree-agent-a909e3bf82c8e3e3a

Conversation

@rexm

@rexm rexm commented Jun 20, 2026

Copy link
Copy Markdown
Member

Fixes #559

Root cause

When a helper with no arguments ({{link_to}}) is registered after Compile(), the Handlebars lexer does not know about the helper at parse time, so HelperConverter does not convert the token to a HelperExpression. It stays as a PathExpression.

PathBinder.VisitPathExpression then emits code calling the object-returning Invoke(HelperOptions, Context, Arguments) overload. The return value is then passed to EncodedTextWriter.Write<object>(string), which always HTML-encodes strings. This means WriteSafeString() output is re-encoded:

<a href='https://example.com'>Click</a>
→ &lt;a href='https://example.com'&gt;Click&lt;/a&gt;

When the same helper is registered before Compile(), HelperConverter converts the token to a HelperExpression, and HelperFunctionBinder emits the void Invoke(EncodedTextWriter, HelperOptions, Context, Arguments) overload which writes directly to the real output writer, preserving WriteSafeString semantics.

Fix

In PathBinder.VisitStatementExpression, detect PathExpressions that are valid helper literals (the ambiguous single-name case) and emit the same void Invoke(writer, ...) call that HelperFunctionBinder would have produced. A LateBindHelperDescriptor wrapped in a Ref<> is registered in configuration.Helpers so that a subsequent RegisterHelper() call updates the Ref value in-place — exactly as the HelperFunctionBinder late-binding mechanism already does.

Test

Added source/Handlebars.Test/Issues/Issue559Tests.cs with the canonical regression test. All 1748 existing tests continue to pass.

🤖 Generated with Claude Code

rexm and others added 3 commits June 20, 2026 12:35
…rder (issue #559)

When a helper with no arguments (e.g. {{link_to}}) was registered after
Compile(), the parser treated it as a PathExpression rather than a
HelperExpression. PathBinder.VisitPathExpression emitted code calling the
object-returning Invoke overload, whose return value was then passed to
EncodedTextWriter.Write<object> which always HTML-encodes strings. This
meant that WriteSafeString() output was re-encoded, producing &lt;a&gt;
instead of <a>.

When the same helper was registered before Compile(), HelperConverter
recognised the name and produced a HelperExpression; HelperFunctionBinder
then emitted the void Invoke(EncodedTextWriter,...) overload which writes
directly to the output writer and preserves WriteSafeString semantics.

Fix: in PathBinder.VisitStatementExpression, detect PathExpressions that
are valid helper literals (the ambiguous name-only case) and emit the same
void Invoke(writer,...) call that HelperFunctionBinder would have produced.
A LateBindHelperDescriptor Ref<> is registered in configuration.Helpers so
that a subsequent RegisterHelper() call updates the Ref in-place, exactly
as the HelperFunctionBinder late-binding mechanism does.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r (issue #434)

The Helpers dictionary uses a case-insensitive PathInfoLight comparer, so
{{TEST}} and {{test}} previously resolved to the same LateBindHelperDescriptor
entry. When the second expression was compiled it reused the first descriptor
(keyed to the wrong case), causing both to bind to the same property at
runtime. Fix detects when a found entry is a LateBindHelperDescriptor for a
differently-cased path and creates a fresh descriptor for the exact path, so
each case variant resolves independently at runtime.

Adds Issue434Tests.cs to exercise the failing scenario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@rexm rexm enabled auto-merge June 20, 2026 22:28
@rexm rexm merged commit d08bec2 into master Jun 20, 2026
7 checks passed
@rexm rexm deleted the worktree-agent-a909e3bf82c8e3e3a branch June 20, 2026 22:36
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.

The behaviour of WriteSafeString changes depending on when my helper is registered

1 participant