Skip to content

Commit 07aeaec

Browse files
committed
Allow Unicode characters in Selectors
Resolves #454
1 parent 058f615 commit 07aeaec

5 files changed

Lines changed: 186 additions & 115 deletions

File tree

src/SmartFormat.Tests/Core/ParserTests.cs

Lines changed: 71 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
using NUnit.Framework;
2-
using SmartFormat.Core.Parsing;
3-
using SmartFormat.Core.Settings;
4-
using SmartFormat.Tests.TestUtils;
5-
using System;
1+
using System;
2+
using System.Collections.Generic;
63
using System.IO;
74
using System.Linq;
85
using System.Text.RegularExpressions;
6+
using NUnit.Framework;
7+
using SmartFormat.Core.Parsing;
8+
using SmartFormat.Core.Settings;
9+
using SmartFormat.Tests.TestUtils;
910

1011
namespace SmartFormat.Tests.Core;
1112

@@ -66,9 +67,9 @@ public void Parser_Throws_Exceptions(string format)
6667
Assert.Throws<ParsingErrors>(() => formatter.Test(format, args, "Error"));
6768
}
6869

69-
[TestCase("{V(LU)}")] // braces are illegal
70-
[TestCase("{V LU }")] // blanks are illegal
71-
[TestCase("{VĀLUĒ}")] // 0x100 and 0x112 are illegal chars
70+
[TestCase("{V(LU)}")] // braces are not allowed
71+
[TestCase("{V LU\\}")] // escape char is not allowed
72+
[TestCase("{V?LU,}")] // ? and , are allowed chars
7273
public void Parser_Throws_On_Illegal_Selector_Chars(string format)
7374
{
7475
var parser = GetRegularParser();
@@ -81,9 +82,9 @@ public void Parser_Throws_On_Illegal_Selector_Chars(string format)
8182
{
8283
Assert.Multiple(() =>
8384
{
84-
// Throws, because selector contains 2 illegal characters
85+
// Throws, because selector contains disallowed characters
8586
Assert.That(e, Is.InstanceOf<ParsingErrors>());
86-
Assert.That(((ParsingErrors) e).Issues, Has.Count.EqualTo(2));
87+
Assert.That(((ParsingErrors) e).Issues, Has.Count.GreaterThanOrEqualTo(1));
8788
});
8889
}
8990
}
@@ -202,13 +203,14 @@ public void Parser_Error_Action_MaintainTokens(string invalidTemplate, bool last
202203
public void Parser_Error_Action_OutputErrorInResult()
203204
{
204205
// | Literal | Erroneous |
206+
// ▼ Selector must not contain {
205207
var invalidTemplate = "Hello, I'm {Name from {City}";
206208

207209
var parser = GetRegularParser(new SmartSettings {Parser = new ParserSettings {ErrorAction = ParseErrorAction.OutputErrorInResult}});
208210
using var parsed = parser.ParseFormat(invalidTemplate);
209211

210212
Assert.That(parsed.Items, Has.Count.EqualTo(1));
211-
Assert.That(parsed.Items[0].RawText, Does.StartWith("The format string has 3 issues"));
213+
Assert.That(parsed.Items[0].RawText, Does.StartWith("The format string has 1 issue"));
212214
}
213215

214216
[Test]
@@ -415,8 +417,8 @@ public void Parser_NotifyParsingError()
415417
var res = formatter.Format("{NoName {Other} {Same", default(object)!);
416418
Assert.Multiple(() =>
417419
{
418-
Assert.That(parsingError!.Issues, Has.Count.EqualTo(3));
419-
Assert.That(parsingError.Issues[2].Issue, Is.EqualTo(new Parser.ParsingErrorText()[SmartFormat.Core.Parsing.Parser.ParsingError.MissingClosingBrace]));
420+
Assert.That(parsingError!.Issues, Has.Count.EqualTo(2));
421+
Assert.That(parsingError.Issues[1].Issue, Is.EqualTo(new Parser.ParsingErrorText()[SmartFormat.Core.Parsing.Parser.ParsingError.MissingClosingBrace]));
420422
});
421423
}
422424

@@ -568,6 +570,29 @@ public void Selectors_With_Custom_Operator_Character(string formatString, char c
568570
});
569571
}
570572

573+
[TestCase("German |öäüßÖÄÜ!")]
574+
[TestCase("Russian абвгдеёжзийклмн")]
575+
[TestCase("French >éèêëçàùâîô")]
576+
[TestCase("Spanish <áéíóúñü¡¿")]
577+
[TestCase("Portuguese !ãõáâêéíóúç")]
578+
[TestCase("Chinese 汉字测试")]
579+
[TestCase("Arabic مرحبا بالعالم")]
580+
[TestCase("Turkish çğöşüİı")]
581+
[TestCase("Hindi नमस्ते दुनिया")]
582+
public void Selector_WorksWithAllUnicodeChars(string selector)
583+
{
584+
// See https://github.com/axuno/SmartFormat/issues/454
585+
586+
const string expected = "The Value";
587+
// The default formatter with default settings should be able to handle any
588+
// Unicode characters in selectors except the "magic" disallowed ones
589+
var formatter = Smart.CreateDefaultSmartFormat();
590+
// Use the Unicode string as a selector of the placeholder
591+
var template = $"{{{selector}}}";
592+
var result = formatter.Format(template, new Dictionary<string, string> { { selector, expected } });
593+
Assert.That(result, Is.EqualTo(expected));
594+
}
595+
571596
[TestCase("{A?.B}")]
572597
[TestCase("{Selector0?.Selector1}")]
573598
[TestCase("{A?[1].B}")]
@@ -681,30 +706,25 @@ public void ParseInputAsHtml(string input)
681706
Assert.That(literalText!.RawText, Is.EqualTo(input));
682707
}
683708

684-
[TestCase("<script>{Placeholder}</script>", false)] // should parse a placeholder
685-
[TestCase("<style>{Placeholder}</style>", false)] // should parse a placeholder
686-
[TestCase("Something <style>h1 { color : #000; }</style>! nice", true)] // illegal selector chars
687-
[TestCase("Something <script>{const a = '</script>';}</script>! nice", true)] // illegal selector chars
688-
public void ParseHtmlInput_Without_ParserSetting_IsHtml(string input, bool shouldThrow)
709+
[TestCase("<script>{Placeholder}</script>", "{Placeholder}")]
710+
[TestCase("<style>{Placeholder}</style>", "{Placeholder}")]
711+
[TestCase("Something <style>h1 { color : #000; }</style>! nice", "{ color : #000; }")]
712+
[TestCase("Something <script>{const a = '</script>';}</script>! nice", "{const a = '</script>';}")]
713+
public void ParseHtmlInput_Without_ParserSetting_IsHtml(string input, string selector)
689714
{
690715
var parser = GetRegularParser(new SmartSettings
691716
{
692717
StringFormatCompatibility = false,
693718
Parser = new ParserSettings { ErrorAction = ParseErrorAction.ThrowError, ParseInputAsHtml = false }
694719
});
695720

696-
switch (shouldThrow)
721+
var result = parser.ParseFormat(input);
722+
Assert.Multiple(() =>
697723
{
698-
case true:
699-
Assert.That(() => _ = parser.ParseFormat(input), Throws.TypeOf<ParsingErrors>());
700-
break;
701-
case false:
702-
{
703-
var result = parser.ParseFormat(input);
704-
Assert.That(result.Items, Has.Count.EqualTo(3));
705-
break;
706-
}
707-
}
724+
Assert.That(result.Items, Has.Count.EqualTo(3));
725+
Assert.That(((Placeholder) result.Items[1]).RawText, Is.EqualTo(selector));
726+
});
727+
708728
}
709729

710730
/// <summary>
@@ -807,29 +827,31 @@ function interpolationSearch(sortedArray, seekIndex) {
807827
[TestCase(true, false)]
808828
public void StyleTags_Can_Be_Parsed_Without_Failure(bool inputIsHtml, bool shouldFail)
809829
{
810-
var styles = @"
811-
<style type='text/css'>
812-
.media {
813-
display: grid;
814-
grid-template-columns: 1fr 3fr;
815-
}
830+
var styles = """
816831
817-
.media .content {
818-
font-size: .8rem;
819-
}
832+
<style type='text/css'>
833+
.media {
834+
display: grid;
835+
grid-template-columns: 1fr 3fr;
836+
}
820837
821-
.comment img {
822-
border: 1px solid grey;
823-
anything: 'xyz'
824-
}
838+
.media .content {
839+
font-size: .8rem;
840+
}
825841
826-
.list-item {
827-
border-bottom: 1px solid grey;
828-
}
829-
/* Comment: { which mixes up the parser without ParserSettings.ParseInputAsHtml = true */
830-
</style>
831-
<p>############### {TheVariable} ###############</p>
832-
";
842+
.comment img {
843+
border: 1px solid grey;
844+
anything: 'xyz'
845+
}
846+
847+
.list-item {
848+
border-bottom: 1px solid grey;
849+
}
850+
/* Comment: { which mixes up the parser without ParserSettings.ParseInputAsHtml = true */
851+
</style>
852+
<p>############### {TheVariable} ###############</p>
853+
854+
""";
833855
var parsingFailures = 0;
834856
var parser = GetRegularParser(new SmartSettings
835857
{

src/SmartFormat.Tests/Core/SettingsTests.cs

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,54 @@ public class SettingsTests
1111
public void TryingToAddDisallowedSelectorCharacters_Should_Throw()
1212
{
1313
var settings = new SmartSettings();
14-
Assert.That(() => settings.Parser.AddCustomSelectorChars(new[] {settings.Parser.PlaceholderBeginChar}),
14+
Assert.That(() => settings.Parser.AddCustomSelectorChars([settings.Parser.PlaceholderBeginChar]),
1515
Throws.ArgumentException.And.Message.Contains($"{settings.Parser.PlaceholderBeginChar}"));
1616
}
1717

1818
[Test]
1919
public void ExistingSelectorCharacter_Should_Not_Be_Added()
2020
{
2121
var settings = new SmartSettings();
22-
settings.Parser.AddCustomSelectorChars(new[] {'A', ' '});
23-
settings.Parser.AddCustomSelectorChars(new[] {' '});
22+
settings.Parser.AddCustomSelectorChars(['A', ' ']);
2423
Assert.Multiple(() =>
2524
{
2625
Assert.That(settings.Parser.CustomSelectorChars().Count(c => c == 'A'), Is.EqualTo(0));
27-
Assert.That(settings.Parser.CustomSelectorChars().Count(c => c == ' '), Is.EqualTo(1));
26+
Assert.That(settings.Parser.CustomSelectorChars().Count(c => c == ' '), Is.EqualTo(0));
27+
});
28+
}
29+
30+
[Test]
31+
public void ControlCharacters_Should_Be_Added_As_SelectorChars()
32+
{
33+
var settings = new SmartSettings();
34+
var controlChars = settings.Parser.ControlChars().ToList();
35+
settings.Parser.AddCustomSelectorChars(controlChars);
36+
37+
Assert.Multiple(() =>
38+
{
39+
Assert.That(settings.Parser.CustomSelectorChars().Count, Is.EqualTo(controlChars.Count));
40+
foreach (var c in settings.Parser.CustomSelectorChars())
41+
{
42+
Assert.That(settings.Parser.DisallowedSelectorChars(), Does.Not.Contain(c),
43+
$"Control char U+{(int)c:X4} should be allowed as selector char.");
44+
}
2845
});
2946
}
3047

3148
[Test]
3249
public void TryingToAddDisallowedOperatorCharacters_Should_Throw()
3350
{
3451
var settings = new SmartSettings();
35-
Assert.That(() => settings.Parser.AddCustomOperatorChars(new[] {settings.Parser.PlaceholderBeginChar}),
52+
Assert.That(() => settings.Parser.AddCustomOperatorChars([settings.Parser.PlaceholderBeginChar]),
3653
Throws.ArgumentException.And.Message.Contains($"{settings.Parser.PlaceholderBeginChar}"));
3754
}
3855

3956
[Test]
4057
public void ExistingOperatorCharacter_Should_Not_Be_Added()
4158
{
4259
var settings = new SmartSettings();
43-
settings.Parser.AddCustomOperatorChars(new[] {settings.Parser.OperatorChars()[0], '°'});
44-
settings.Parser.AddCustomOperatorChars(new[] {'°'});
60+
settings.Parser.AddCustomOperatorChars([settings.Parser.OperatorChars()[0], '°']);
61+
settings.Parser.AddCustomOperatorChars(['°']);
4562

4663
Assert.Multiple(() =>
4764
{
@@ -50,27 +67,41 @@ public void ExistingOperatorCharacter_Should_Not_Be_Added()
5067
});
5168
}
5269

53-
[TestCase('°')] // a custom char
54-
[TestCase('A')] // a standard selector char
55-
public void Add_CustomOperator_Used_As_Separator_Should_Throw(char operatorChar)
70+
[TestCase('{')]
71+
[TestCase('}')]
72+
[TestCase(':')]
73+
[TestCase('(')]
74+
[TestCase(')')]
75+
public void Add_Separators_As_Custom_Operator_Should_Throw(char operatorChar)
5676
{
5777
var settings = new SmartSettings();
58-
settings.Parser.AddCustomSelectorChars(new[] {operatorChar}); // reserve as selector char
5978

6079
// try to add the same char as operator
61-
Assert.That(() => settings.Parser.AddCustomOperatorChars(new[] {operatorChar}),
62-
Throws.ArgumentException.And.Message.Contains($"{operatorChar}"));
80+
Assert.That(() => settings.Parser.AddCustomOperatorChars([operatorChar]),
81+
Throws.ArgumentException.And.Message.Contains($"'{operatorChar}'"));
6382
}
6483

65-
[TestCase('°')] // a custom char
84+
[TestCase('°')] // a custom selector char
6685
[TestCase('.')] // a standard operator char
6786
public void Add_CustomSelector_Used_As_Operator_Should_Throw(char selectorChar)
6887
{
6988
var settings = new SmartSettings();
70-
settings.Parser.AddCustomOperatorChars(new[] {selectorChar}); // reserve as operator char
89+
settings.Parser.AddCustomOperatorChars([selectorChar]); // reserve as operator char
7190

7291
// try to add the same char as selector
73-
Assert.That(() => settings.Parser.AddCustomSelectorChars(new[] {selectorChar}),
92+
Assert.That(() => settings.Parser.AddCustomSelectorChars([selectorChar]),
7493
Throws.ArgumentException.And.Message.Contains($"{selectorChar}"));
7594
}
95+
96+
[TestCase((char) 127)] // a custom char
97+
[TestCase((char) 30)] // a standard operator char
98+
public void Add_CustomOperator_Used_As_Selector_Should_Throw(char operatorChar)
99+
{
100+
var settings = new SmartSettings();
101+
settings.Parser.AddCustomSelectorChars([operatorChar]); // reserve as operator char
102+
103+
// try to add the same char as selector
104+
Assert.That(() => settings.Parser.AddCustomOperatorChars([operatorChar]),
105+
Throws.ArgumentException.And.Message.Contains($"{operatorChar}"));
106+
}
76107
}

src/SmartFormat/Core/Parsing/Parser.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class Parser
3636
private readonly List<char> _operatorChars;
3737
private readonly List<char> _customOperatorChars;
3838
private readonly ParserSettings _parserSettings;
39-
private readonly List<char> _validSelectorChars;
39+
private readonly HashSet<char> _disallowedSelectorChars;
4040
private readonly List<char> _formatOptionsTerminatorChars;
4141

4242
#endregion
@@ -67,10 +67,7 @@ public Parser(SmartSettings? smartSettings = null)
6767
_customOperatorChars = _parserSettings.CustomOperatorChars();
6868
_formatOptionsTerminatorChars = _parserSettings.FormatOptionsTerminatorChars();
6969

70-
_validSelectorChars = new List<char>();
71-
_validSelectorChars.AddRange(_parserSettings.SelectorChars());
72-
_validSelectorChars.AddRange(_parserSettings.OperatorChars());
73-
_validSelectorChars.AddRange(_parserSettings.CustomSelectorChars());
70+
_disallowedSelectorChars = _parserSettings.DisallowedSelectorChars();
7471
}
7572

7673
#endregion
@@ -326,7 +323,7 @@ private void ProcessSelector(char inputChar, ParserState state, ParsingErrors pa
326323
else
327324
{
328325
// Ensure the selector characters are valid:
329-
if (!_validSelectorChars.Contains(inputChar))
326+
if (_disallowedSelectorChars.Contains(inputChar))
330327
parsingErrors.AddIssue(state.ResultFormat,
331328
$"'0x{Convert.ToUInt32(inputChar):X}': " +
332329
_parsingErrorText[ParsingError.InvalidCharactersInSelector],

0 commit comments

Comments
 (0)