diff --git a/source/Handlebars.Test/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index 91bf5e96..911dfec9 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -734,6 +734,34 @@ 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); + } + // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/546 // Single quote should be HTML-encoded as ' in standard {{expression}} output [Fact] 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(); } }