From 5d9f418c8950cc8cbedc5272f10440067c9764a0 Mon Sep 17 00:00:00 2001 From: Rex Morgan Date: Fri, 19 Jun 2026 20:56:24 -0400 Subject: [PATCH] fix: preserve backslashes in template literal text (issue #462) Backslashes in template literal text are no longer treated as escape sequences unless they appear as \\ immediately before a {{ expression. The pattern \\{{ still collapses to a single literal backslash followed by the evaluated expression (per Handlebars.js spec), but \\ elsewhere in template text passes through verbatim. Co-Authored-By: Claude Sonnet 4.6 --- source/Handlebars.Test/IssueTests.cs | 42 +++++++++++++++++++ source/Handlebars/Compiler/Lexer/Tokenizer.cs | 14 ++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index 31136117..cb8b2a18 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -733,5 +733,47 @@ public void UnrecognisedExpressionThrowsOutOfMemoryException() Assert.Throws(()=> Handlebars.Compile(source)); } + + // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/462 + // Compile replaces \\ (double backslash) with \ (single backslash) + [Fact] + public void Issue462_DoubleBackslashPreservedInOutput() + { + var handlebars = Handlebars.Create(); + // Template contains two backslashes as literal text + var compiledTemplate = handlebars.Compile(@"\\"); + var result = compiledTemplate(null); + Assert.Equal(@"\\", result); + } + + [Fact] + public void Issue462_SingleBackslashPreservedInOutput() + { + var handlebars = Handlebars.Create(); + var compiledTemplate = handlebars.Compile(@"\"); + var result = compiledTemplate(null); + Assert.Equal(@"\", result); + } + + [Fact] + public void Issue462_DoubleBackslashBeforeExpressionStillCollapses() + { + // \\{{name}} should still produce a single literal backslash followed by the evaluated expression + var handlebars = Handlebars.Create(); + var compiledTemplate = handlebars.Compile(@"\\{{name}}"); + var result = compiledTemplate(new { name = "World" }); + Assert.Equal(@"\World", result); + } + + [Fact] + public void Issue462_DoubleBackslashInMixedTemplate() + { + // Template with backslashes mixed: \\to preserves both backslashes (not before {{), + // but \\{{name}} collapses to single backslash + evaluated expression (spec behavior) + var handlebars = Handlebars.Create(); + var compiledTemplate = handlebars.Compile(@"path\\to\\{{name}}"); + var result = compiledTemplate(new { name = "file" }); + Assert.Equal(@"path\\to\file", result); + } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Tokenizer.cs b/source/Handlebars/Compiler/Lexer/Tokenizer.cs index d0d051c7..1f03b8b0 100644 --- a/source/Handlebars/Compiler/Lexer/Tokenizer.cs +++ b/source/Handlebars/Compiler/Lexer/Tokenizer.cs @@ -122,8 +122,18 @@ private static IEnumerable Parse(ExtendedStringReader source) { if ((char)node == '\\' && (char)source.Peek() == '\\') { - source.Read(); - buffer.Append('\\'); + source.Read(); // consume second '\' + if ((char)source.Peek() == '{') + { + // \\{{ → single literal backslash followed by evaluated expression + buffer.Append('\\'); + } + else + { + // \\ not followed by {{ → preserve both backslashes verbatim + buffer.Append('\\'); + buffer.Append('\\'); + } node = source.Read(); } else if ((char)node == '\\' && (char)source.Peek() == '{')