Skip to content

Commit

Permalink
Fix an issue with random grawlix being interpreted as GitHub math exp…
Browse files Browse the repository at this point in the history
…ressions.
  • Loading branch information
IEvangelist committed Feb 16, 2024
1 parent 2f2c4e2 commit ac3f793
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 8 deletions.
27 changes: 27 additions & 0 deletions src/ProfanityFilter.Services/Extensions/RandomExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace ProfanityFilter.Services.Extensions;

internal static class RandomExtensions
{
/// <summary>
/// Gets a random subset of items from the source array, with the
/// specified length, while respecting the limits.
/// </summary>
internal static TItem[] RandomItemsWithLimitToOne<TItem>(
this TItem[] array,
int length,
TItem limit) where TItem : notnull
{
var choices = array.Except([limit]).ToArray();

var items = Random.Shared.GetItems(choices, length);

var index = Random.Shared.Next(0, items.Length);

items[index] = limit;

return items;
}
}
22 changes: 17 additions & 5 deletions src/ProfanityFilter.Services/Filters/Symbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,31 @@ namespace ProfanityFilter.Services.Filters;

internal static class Symbols
{
/// <summary>
/// Only permit a single <c>"\$"</c> string within an entire grawlix
/// replacement, to avoid GitHub from rendering it as a Math expression.
/// </summary>
internal const string LimitGrawlixToOne = $"\\{UnescapedLimitGrawlixToOne}";

/// <summary>
/// Only permit a single <c>"$"</c> string within an entire grawlix
/// replacement, to avoid GitHub from rendering it as a Math expression.
/// </summary>
internal const string UnescapedLimitGrawlixToOne = "$";

/// <summary>
/// An array of hand-selected grawlix replacements for profane words.
/// See <a href="https://english.stackexchange.com/a/86840/446108"></a>
/// </summary>
internal static string[] Grawlixes =
[
new Symbol('#', true),
new Symbol('#'),
new Symbol('$'),
new Symbol('@'),
new Symbol('!', true),
new Symbol('!'),
new Symbol('%'),
new Symbol('&'),
new Symbol('*', true),
new Symbol('*'),
new Symbol('+'),
new Symbol('?'),
new Symbol('^'),
Expand All @@ -41,7 +53,7 @@ internal static class Symbols
];
}

file readonly record struct Symbol(char Value, bool RequiresEscaping = false)
file readonly record struct Symbol(char Value, bool Escape = true)
{
/// <summary>
/// Implicitly converts a <see cref="char"/> to a <see cref="Symbol"/>.
Expand All @@ -55,6 +67,6 @@ file readonly record struct Symbol(char Value, bool RequiresEscaping = false)

public override string ToString()
{
return RequiresEscaping ? $"\\{Value}" : $"{Value}";
return Escape ? $"\\{Value}" : $"{Value}";
}
}
4 changes: 4 additions & 0 deletions src/ProfanityFilter.Services/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
global using System.Collections.Concurrent;
global using System.Collections.Frozen;
global using System.Diagnostics.CodeAnalysis;
global using System.Runtime.InteropServices;
global using System.Text;
global using System.Text.RegularExpressions;

global using Microsoft.Extensions.Caching.Memory;
global using Microsoft.Extensions.DependencyInjection;

global using Pathological.Globbing.Extensions;
global using Pathological.Globbing.Options;

global using ProfanityFilter.Services.Extensions;
global using ProfanityFilter.Services.Filters;
global using ProfanityFilter.Services.Results;
Expand Down
13 changes: 11 additions & 2 deletions src/ProfanityFilter.Services/Internals/MatchEvaluators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ internal static MatchEvaluator GrawlixEvaluator(FilterParameters parameters) =>

var grawlixes = isNotTitle ? Symbols.Grawlixes : Symbols.UnescapedGrawlixes;

var limit = isNotTitle ? Symbols.LimitGrawlixToOne : Symbols.UnescapedLimitGrawlixToOne;

var randomGrawlix = grawlixes.RandomItemsWithLimitToOne(match.Length, limit);

var result = string.Join(
"", Random.Shared.GetItems(grawlixes, match.Length));
"", randomGrawlix);

return result;
};
Expand All @@ -38,9 +42,14 @@ internal static MatchEvaluator BoldGrawlixEvaluator(FilterParameters parameters)

var grawlixes = isNotTitle ? Symbols.Grawlixes : Symbols.UnescapedGrawlixes;

var limit = isNotTitle ? Symbols.LimitGrawlixToOne : Symbols.UnescapedLimitGrawlixToOne;

var randomGrawlix = grawlixes.RandomItemsWithLimitToOne(match.Length, limit);

var result = string.Join(
"", Random.Shared.GetItems(grawlixes, match.Length));
"", randomGrawlix);

// Titles cannot be bolded
return isNotTitle ? $"__{result}__" : result;
};

Expand Down
4 changes: 3 additions & 1 deletion tests/ProfanityFilter.Services.Tests/MatchEvaluatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void RandomAsteriskEvaluator_Returns_Expected_Result()
[Fact]
public void GrawlixEvaluator_Returns_Expected_Result()
{
var input = "swear";
var input = "someReallyLongSwearWordOrPhrase";
var regex = TestRegex();
var match = regex.Match(input);
var sut = MatchEvaluators.GrawlixEvaluator;
Expand All @@ -50,6 +50,7 @@ public void GrawlixEvaluator_Returns_Expected_Result()
var actual = sut(parameters).Invoke(match);

Assert.NotEqual(input, actual);
Assert.Single(actual.Where(@char => @char is '$'));
}

[Fact]
Expand All @@ -64,6 +65,7 @@ public void BoldGrawlixEvaluator_Returns_Expected_Result()
var actual = sut(parameters).Invoke(match);

Assert.NotEqual(input, actual);
Assert.Single(actual.Where(@char => @char is '$'));
}

[Fact]
Expand Down
51 changes: 51 additions & 0 deletions tests/ProfanityFilter.Services.Tests/RandomExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) David Pine. All rights reserved.
// Licensed under the MIT License.

namespace ProfanityFilter.Services.Tests;

public class RandomExtensionsTests
{
[Fact]
public void RandomItemsWithLimitsReturnsRandomSubsetOfItems()
{
// Arrange
var source = new[] { "a", "b", "c", "d", "e" };

// Act
var result = source.RandomItemsWithLimitToOne(3, "");

// Assert
Assert.Equal(3, result.Length);
}

[Fact]
public void RandomItemsWithLimitsRespectsLimits()
{
// Arrange
var source = new[] { "a", "b", "c", "d", "e" };

// Act
var result = source.RandomItemsWithLimitToOne(3, "a");

// Assert
Assert.Equal(3, result.Length);
Assert.Single(result.Where(str => str is "a"));
}

[Fact]
public void RandomItemsWithLimitsReturnsHugeArrayWithOnlyOneLimitedValue()
{
// Arrange
var source = new[] { "a", "b", "c", "d", "e" };

// Act
var result = source.RandomItemsWithLimitToOne(100_000, "c");

// Assert
Assert.Contains(result, str => str is "a");
Assert.Contains(result, str => str is "b");
Assert.Single(result.Where(str => str is "c"));
Assert.Contains(result, str => str is "d");
Assert.Contains(result, str => str is "e");
}
}

0 comments on commit ac3f793

Please sign in to comment.