Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constrain custom selectors and operators #172

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
afd5b77
Update issue templates
axunonb Apr 17, 2021
1d22508
Inserted a link to merged version/v3 changes
axunonb Apr 27, 2021
11c3033
Updated README
axunonb Apr 27, 2021
d1779f9
Merge remote-tracking branch 'upstream/main' into version/v3.0
axunonb Apr 29, 2021
cb5559a
Make parser fully covered with unit tests
axunonb Apr 29, 2021
e327de5
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb Apr 29, 2021
cab97df
Updated CHANGES.md
axunonb Apr 29, 2021
e8d9c93
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb Apr 29, 2021
294f810
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb Apr 29, 2021
792c391
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 3, 2021
4f45a84
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 9, 2021
e49db1e
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 9, 2021
2c562b5
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 9, 2021
2ba8c5f
Updated .editorconfig
axunonb May 9, 2021
7c3a12e
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 25, 2021
bda63e0
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 25, 2021
ef58744
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 26, 2021
67b2119
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
974224b
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
3e6e8ae
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
6219b47
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
34f2821
Merge remote-tracking branch 'upstream/version/v3.0' into version/v3.0
axunonb May 27, 2021
17edb59
Add custom selector and operator constraints
axunonb May 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/SmartFormat.Tests/Core/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -617,5 +617,55 @@ public void Parse_Unicode(string formatString, string unicodeLiteral, int itemIn
else
Assert.That(() => literal.ToString(), Throws.ArgumentException.And.Message.Contains(unicodeLiteral));
}

[TestCase("{A }", ' ')]
[TestCase("{B§}", '§')]
[TestCase("{?C}", '?')]
public void Selector_With_Custom_Selector_Character(string formatString, char customChar)
{
var parser = GetRegularParser();
parser.Settings.Parser.AddCustomSelectorChars(new[]{customChar});
var result = parser.ParseFormat(formatString, new[] {"d"});

var placeholder = result.Items[0] as Placeholder;
Assert.That(placeholder, Is.Not.Null);
Assert.That(placeholder.Selectors.Count, Is.EqualTo(1));
Assert.That(placeholder.Selectors[0].ToString(), Is.EqualTo(formatString.Substring(1,2)));
}

[TestCase("{a b}", ' ')]
[TestCase("{a°b}", '°')]
public void Selectors_With_Custom_Operator_Character(string formatString, char customChar)
{
var parser = GetRegularParser();
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
var result = parser.ParseFormat(formatString, new[] {"d"});

var placeholder = result.Items[0] as Placeholder;
Assert.That(placeholder, Is.Not.Null);
Assert.That(placeholder.Selectors.Count, Is.EqualTo(2));
Assert.That(placeholder.Selectors[0].ToString(), Is.EqualTo(formatString.Substring(1,1)));
Assert.That(placeholder.Selectors[1].ToString(), Is.EqualTo(formatString.Substring(3, 1)));
Assert.That(placeholder.Selectors[1].Operator, Is.EqualTo(formatString.Substring(2,1)));
}

[TestCase("{C?.D}", '?')]
[TestCase("{C..D}", '.')]
public void Selector_With_Contiguous_Operator_Characters(string formatString, char customChar)
{
// contiguous operators '?.' are parsed as ONE

var parser = GetRegularParser();
// adding '.' is ignored, as it's the standard operator
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
var result = parser.ParseFormat(formatString, new[] {"d"});

var placeholder = result.Items[0] as Placeholder;
Assert.That(placeholder, Is.Not.Null);
Assert.That(placeholder.Selectors.Count, Is.EqualTo(2));
Assert.That(placeholder.Selectors[0].ToString(), Is.EqualTo(formatString.Substring(1,1)));
Assert.That(placeholder.Selectors[1].ToString(), Is.EqualTo(formatString.Substring(4,1)));
Assert.That(placeholder.Selectors[1].Operator, Is.EqualTo(formatString.Substring(2,2)));
}
}
}
104 changes: 104 additions & 0 deletions src/SmartFormat.Tests/Core/SettingsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using SmartFormat.Core.Settings;

namespace SmartFormat.Tests.Core
{
[TestFixture]
public class SettingsTests
{
[Test]
public void TryingToAddDisallowedSelectorCharacters_Should_Throw()
{
var settings = new SmartSettings();
Assert.That(() => settings.Parser.AddCustomSelectorChars(new[] {settings.Parser.PlaceholderBeginChar}),
Throws.ArgumentException.And.Message.Contains($"{settings.Parser.PlaceholderBeginChar}"));
}

[Test]
public void ExistingSelectorCharacter_Should_Not_Be_Added()
{
var settings = new SmartSettings();
settings.Parser.AddCustomSelectorChars(new[] {'A', ' '});
settings.Parser.AddCustomSelectorChars(new[] {' '});

Assert.That(settings.Parser.CustomSelectorChars.Count(c => c == 'A'), Is.EqualTo(0));
Assert.That(settings.Parser.CustomSelectorChars.Count(c => c == ' '), Is.EqualTo(1));
}

[Test]
public void TryingToAddDisallowedOperatorCharacters_Should_Throw()
{
var settings = new SmartSettings();
Assert.That(() => settings.Parser.AddCustomOperatorChars(new[] {settings.Parser.PlaceholderBeginChar}),
Throws.ArgumentException.And.Message.Contains($"{settings.Parser.PlaceholderBeginChar}"));
}

[Test]
public void ExistingOperatorCharacter_Should_Not_Be_Added()
{
var settings = new SmartSettings();
settings.Parser.AddCustomOperatorChars(new[] {settings.Parser.OperatorChars[0], '°'});
settings.Parser.AddCustomOperatorChars(new[] {'°'});

Assert.That(settings.Parser.CustomOperatorChars.Count(c => c == settings.Parser.OperatorChars[0]), Is.EqualTo(0));
Assert.That(settings.Parser.CustomOperatorChars.Count(c => c == '°'), Is.EqualTo(1));
}

[Test]
public void GetCaseSensitivityComparison()
{
var settings = new SmartSettings();
foreach (var name in Enum.GetNames(typeof(CaseSensitivityType)))
{
settings.CaseSensitivity = (CaseSensitivityType) Enum.Parse(typeof(CaseSensitivityType), name);
Assert.DoesNotThrow(() => settings.GetCaseSensitivityComparison());
}

settings.CaseSensitivity = (CaseSensitivityType) int.MaxValue;
Assert.Throws<InvalidOperationException>(() => settings.GetCaseSensitivityComparison());
}

[Test]
public void CaseSensitivityComparer()
{
var settings = new SmartSettings();
foreach (var name in Enum.GetNames(typeof(CaseSensitivityType)))
{
settings.CaseSensitivity = (CaseSensitivityType) Enum.Parse(typeof(CaseSensitivityType), name);
Assert.DoesNotThrow(() => settings.GetCaseSensitivityComparer());
}

settings.CaseSensitivity = (CaseSensitivityType) int.MaxValue;
Assert.Throws<InvalidOperationException>(() => settings.GetCaseSensitivityComparer());
}

[TestCase('°')] // a custom char
[TestCase('A')] // a standard selector char
public void Add_CustomOperator_Used_As_Seperator_Should_Throw(char operatorChar)
{
var settings = new SmartSettings();
settings.Parser.AddCustomSelectorChars(new[] {operatorChar}); // reserve as selector char

// try to add the same char as operator
Assert.That(() => settings.Parser.AddCustomOperatorChars(new[] {operatorChar}),
Throws.ArgumentException.And.Message.Contains($"{operatorChar}"));
}

[TestCase('°')] // a custom char
[TestCase('.')] // a standard operator char
public void Add_CustomSelector_Used_As_Operator_Should_Throw(char selectorChar)
{
var settings = new SmartSettings();
settings.Parser.AddCustomOperatorChars(new[] {selectorChar}); // reserve as operator char

// try to add the same char as selector
Assert.That(() => settings.Parser.AddCustomSelectorChars(new[] {selectorChar}),
Throws.ArgumentException.And.Message.Contains($"{selectorChar}"));
}
}
}
19 changes: 8 additions & 11 deletions src/SmartFormat/Core/Parsing/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ internal Parser(SmartSettings smartSettings)
/// <summary>
/// Includes a-z and A-Z in the list of allowed selector chars.
/// </summary>
[Obsolete("Use 'ParserSettings.AllowAlphanumericSelectors' instead.")]
[Obsolete("Alphanumeric selectors are always enabled")]
public void AddAlphanumericSelectors()
{
_parserSettings.AllowAlphanumericSelectors = true;
// Do nothing - this is the standard behavior
}

/// <summary>
Expand Down Expand Up @@ -116,8 +116,7 @@ public void UseBraceEscaping()
[Obsolete("This feature has been removed", true)]
public void UseAlternativeBraces(char opening, char closing)
{
_parserSettings.PlaceholderBeginChar = opening;
_parserSettings.PlaceholderEndChar = opening;
throw new NotImplementedException("This feature has been removed");
}

#endregion
Expand Down Expand Up @@ -565,7 +564,7 @@ private void ParseSelector(string inputFormat, ref Format currentFormat, ref Ind
}

var inputChar = inputFormat[index.Current];
if (_parserSettings.OperatorChars.Contains(inputChar))
if (_parserSettings.OperatorChars.Contains(inputChar) || _parserSettings.CustomOperatorChars.Contains(inputChar))
{
// Add the selector:
if (index.Current != index.LastEnd)
Expand Down Expand Up @@ -675,8 +674,7 @@ private void ParseFormatOptions(string inputFormat, ref IndexContainer index)
}

/// <summary>
/// Checks whether the selector character is valid,
/// depending on <see cref="ParserSettings.AllowAlphanumericSelectors"/> and <see cref="ParserSettings.CustomSelectorChars"/>.
/// Checks whether the selector character is valid.
/// </summary>
/// <param name="c">The character to check.</param>
/// <returns><see langword="true"/> if the character is valid, else <see langword="false"/>.</returns>
Expand All @@ -685,11 +683,10 @@ internal bool IsValidSelectorChar(char c)
{
// C# variables: LetterOrDigit and "_"
// Required for Alignment: "-" and ","
return _parserSettings.AllowAlphanumericSelectors
? _parserSettings.AlphanumericSelectorChars.Contains(c) || _parserSettings.OperatorChars.Contains(c) || _parserSettings.CustomSelectorChars.Contains(c)
: _parserSettings.NumericSelectorChars.Contains(c) || _parserSettings.OperatorChars.Contains(c);
return _parserSettings.AlphanumericSelectorChars.Contains(c) || _parserSettings.OperatorChars.Contains(c) ||
_parserSettings.CustomSelectorChars.Contains(c);
}

private static bool FormatterNameExists(string name, string[] formatterExtensionNames)
{
return formatterExtensionNames.Any(n => n == name);
Expand Down
76 changes: 41 additions & 35 deletions src/SmartFormat/Core/Settings/ParserSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Licensed under the MIT license.
//

using System;
using System.Collections.Generic;
using System.Linq;
using SmartFormat.Core.Parsing;
Expand All @@ -15,7 +16,6 @@ namespace SmartFormat.Core.Settings
public class ParserSettings
{
private readonly IList<char> _alphanumericSelectorChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-".ToCharArray();
private readonly IList<char> _numericSelectorChars = "0123456789-".ToCharArray();

private readonly IList<char> _customSelectorChars = new List<char>();
private readonly IList<char> _customOperatorChars = new List<char>();
Expand All @@ -26,64 +26,69 @@ public class ParserSettings
/// </summary>
public ParseErrorAction ErrorAction { get; set; } = ParseErrorAction.ThrowError;

/// <summary>
/// If <see langword="true"/>, selectors can be alpha-numeric: They may contain
/// letters, digits, and the underscore character (_), like it is for C# variable names.
/// If <see langword="false"/>, only digits are allowed as selectors. Default is <see langword="true"/>.
/// </summary>
public bool AllowAlphanumericSelectors { get; set; } = true;

/// <summary>
/// The list of alphanumeric selector characters.
/// </summary>
public IReadOnlyList<char> AlphanumericSelectorChars => (IReadOnlyList<char>) _alphanumericSelectorChars;

/// <summary>
/// The list of numeric selector characters.
/// Gets a read-only list of the custom selector characters, which were set with <see cref="AddCustomSelectorChars"/>.
/// </summary>
public IReadOnlyList<char> NumericSelectorChars => (IReadOnlyList<char>) _numericSelectorChars;
public IReadOnlyList<char> CustomSelectorChars => (IReadOnlyList<char>) _customSelectorChars;

/// <summary>
/// Gets a read-only list of the custom selector characters, which were set with <see cref="AddCustomSelectorChars"/>.
/// Gets a list of characters which are not allowed in a selector.
/// </summary>
public IReadOnlyList<char> CustomSelectorChars => (IReadOnlyList<char>) _customSelectorChars;
public IReadOnlyList<char> DisallowedSelectorChars
{
get
{
var chars = new List<char> {
CharLiteralEscapeChar, FormatterNameSeparator, AlignmentOperator, SelectorOperator,
PlaceholderBeginChar, PlaceholderEndChar, FormatterOptionsBeginChar, FormatterOptionsEndChar
};
chars.AddRange(OperatorChars);
return chars;
}
}

/// <summary>
/// Gets a read-only list of the custom operator characters, which were set with <see cref="AddCustomSelectorChars"/>.
/// Contiguous operator characters are parsed as one operator (e.g. '?.').
/// </summary>
public IReadOnlyList<char> CustomOperatorChars => (IReadOnlyList<char>) _customOperatorChars;

/// <summary>
/// Add a list of allowable selector characters on top of the <see cref="AllowAlphanumericSelectors"/> setting.
/// Add a list of allowable selector characters on top of the <see cref="AlphanumericSelectorChars"/> setting.
/// This can be useful to support additional selector syntax such as math.
/// </summary>
/// Characters in <see cref="DisallowedSelectorChars"/> cannot be added.
/// Operator chars and selector chars must be different.
/// </summary>
public void AddCustomSelectorChars(IList<char> characters)
{
if (AllowAlphanumericSelectors)
{
foreach (var c in characters)
{
if (!_customSelectorChars.Contains(c) && !_alphanumericSelectorChars.Contains(c))
_customSelectorChars.Add(c);
}
}
else
foreach (var c in characters)
{
foreach (var c in characters)
{
if (!_customSelectorChars.Contains(c) && !_numericSelectorChars.Contains(c))
_customSelectorChars.Add(c);
}
if (DisallowedSelectorChars.Contains(c) || _customOperatorChars.Contains(c))
throw new ArgumentException($"Cannot add '{c}' as a custom selector character. It is disallowed or in use as an operator.");

if (!_customSelectorChars.Contains(c) && !_alphanumericSelectorChars.Contains(c))
_customSelectorChars.Add(c);
}
}

/// <summary>
/// Add a list of allowable operator characters on top of the standard <see cref="OperatorChars"/> setting.
/// Characters in <see cref="DisallowedSelectorChars"/> cannot be added.
/// Operator chars and selector chars must be different.
/// </summary>
public void AddCustomOperatorChars(IList<char> characters)
{
foreach (var c in characters)
{
if (DisallowedSelectorChars.Where(_ => OperatorChars.All(ch => ch != c)).Contains(c) ||
_alphanumericSelectorChars.Contains(c) || _customSelectorChars.Contains(c))
throw new ArgumentException($"Cannot add '{c}' as a custom operator character. It is disallowed or in use as a selector.");

if (!OperatorChars.Contains(c) && !_customOperatorChars.Contains(c))
_customOperatorChars.Add(c);
}
Expand Down Expand Up @@ -114,42 +119,43 @@ public void AddCustomOperatorChars(IList<char> characters)
/// The character which separates the formatter name (if any exists) from other parts of the placeholder.
/// E.g.: {Variable:FormatterName:argument} or {Variable:FormatterName}
/// </summary>
internal char FormatterNameSeparator { get; set; } = ':';
internal char FormatterNameSeparator { get; } = ':';

/// <summary>
/// The standard operator characters.
/// Contiguous operator characters are parsed as one operator (e.g. '?.').
/// </summary>
internal IReadOnlyList<char> OperatorChars => new List<char> {SelectorOperator, AlignmentOperator, '[', ']'};

/// <summary>
/// The character which separates the selector for alignment. <c>E.g.: Smart.Format("Name: {name,10}")</c>
/// </summary>
internal char AlignmentOperator { get; set; } = ',';
internal char AlignmentOperator { get; } = ',';

/// <summary>
/// The character which separates two or more selectors <c>E.g.: "First.Second.Third"</c>
/// </summary>
internal char SelectorOperator { get; set; } = '.';
internal char SelectorOperator { get; } = '.';

/// <summary>
/// Gets the character indicating the start of a <see cref="Placeholder"/>.
/// </summary>
public char PlaceholderBeginChar { get; internal set; } = '{';
public char PlaceholderBeginChar { get; } = '{';

/// <summary>
/// Gets the character indicating the end of a <see cref="Placeholder"/>.
/// </summary>
public char PlaceholderEndChar { get; internal set; } = '}';
public char PlaceholderEndChar { get; } = '}';

/// <summary>
/// Gets the character indicating the begin of formatter options.
/// </summary>
public char FormatterOptionsBeginChar { get; internal set; } = '(';
public char FormatterOptionsBeginChar { get; } = '(';

/// <summary>
/// Gets the character indicating the end of formatter options.
/// </summary>
public char FormatterOptionsEndChar { get; internal set; } = ')';
public char FormatterOptionsEndChar { get; } = ')';

/// <summary>
/// Characters which terminate parsing of format options.
Expand Down
2 changes: 1 addition & 1 deletion src/SmartFormat/Extensions/ListFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class ListFormatter : IFormatter, ISource

public ListFormatter(SmartFormatter formatter)
{
formatter.Parser.AddOperators("[]()");
formatter.Parser.AddOperators("[]");
_smartSettings = formatter.Settings;
}

Expand Down