From 97dca6b04148dcc2ff6e1e417758c63d22753255 Mon Sep 17 00:00:00 2001 From: Rex Morgan Date: Fri, 19 Jun 2026 20:52:35 -0400 Subject: [PATCH] fix: strip invisible unicode chars (BOM etc.) from identifiers (issue #605) BOM (U+FEFF) and other invisible Unicode format characters embedded in identifier names caused #if and other expressions to silently fail because the lookup key included the invisible character. The WordParser now skips these characters when accumulating identifier tokens. Co-Authored-By: Claude Sonnet 4.6 --- source/Handlebars.Test/IssueTests.cs | 28 +++++++++++++++++ .../Compiler/Lexer/Parsers/WordParser.cs | 31 ++++++++++++++++--- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index 31136117..b4284b23 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -733,5 +733,33 @@ public void UnrecognisedExpressionThrowsOutOfMemoryException() Assert.Throws(()=> Handlebars.Compile(source)); } + + // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/605 + // #if not evaluated when variable name contains invisible characters (BOM) + [Fact] + public void Issue605_IfNotEvaluatedWithBomCharacter() + { + // The  (BOM) is embedded in the identifier name + string source = + "
\n" + + "

{{title}}

\n" + + "
\n" + + "{{body}}\n" + + "{{#if someCondition}}\n" + + "

Show additional text

\n" + + "{{/if}}\n" + + "
\n" + + "
"; + + var handlebars = Handlebars.Create(); + var template = handlebars.Compile(source); + var data = new { + title = "My new post", + body = "This is my first post!", + someCondition = true + }; + var actual = template(data); + Assert.Contains("Show additional text", actual); + } } } \ No newline at end of file diff --git a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs index 64974024..70aaa5d6 100644 --- a/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs +++ b/source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs @@ -11,6 +11,20 @@ internal class WordParser : Parser private const string ValidWordStartCharactersString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$.@[]*"; private static readonly HashSet ValidWordStartCharacters = new HashSet(); + // Invisible Unicode characters that should be stripped from identifiers + // Includes BOM (U+FEFF), zero-width space (U+200B), zero-width non-joiner (U+200C), + // zero-width joiner (U+200D), word joiner (U+2060), and other format characters. + private static bool IsInvisibleCharacter(char c) + { + return c == '' // BOM / Zero Width No-Break Space + || c == '​' // Zero Width Space + || c == '‌' // Zero Width Non-Joiner + || c == '‍' // Zero Width Joiner + || c == '⁠' // Word Joiner + || c == '᠎' // Mongolian Vowel Separator + || char.GetUnicodeCategory(c) == System.Globalization.UnicodeCategory.Format; + } + static WordParser() { for (var index = 0; index < ValidWordStartCharactersString.Length; index++) @@ -23,7 +37,7 @@ public override Token Parse(ExtendedStringReader reader) { var context = reader.GetContext(); if (!IsWord(reader)) return null; - + var buffer = AccumulateWord(reader); return Token.Word(buffer, context); } @@ -38,7 +52,7 @@ private static string AccumulateWord(ExtendedStringReader reader) { using var container = StringBuilderPool.Shared.Use(); var buffer = container.Value; - + var inString = false; var isEscaped = false; @@ -65,7 +79,7 @@ private static string AccumulateWord(ExtendedStringReader reader) { var c = (char) node; if (c == ']') isEscaped = false; - + buffer.Append(c); continue; } @@ -76,15 +90,22 @@ private static string AccumulateWord(ExtendedStringReader reader) buffer.Append((char)node); continue; } - + if (node == '\'' || node == '"') { inString = !inString; } + // Skip invisible Unicode characters (BOM, zero-width chars, etc.) + // that can appear in identifiers due to editor/encoding artifacts + if (!inString && IsInvisibleCharacter((char) node)) + { + continue; + } + buffer.Append((char)node); } - + return buffer.Trim().ToString(); } }