Skip to content

Commit

Permalink
🚸 Better support quoted strings and escaped quotes in strings +semver…
Browse files Browse the repository at this point in the history
…:minor fix #22
  • Loading branch information
decaprime committed May 18, 2024
1 parent 297b440 commit d432c15
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 22 deletions.
68 changes: 52 additions & 16 deletions VCF.Core/Common/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,68 @@ namespace VampireCommandFramework.Common;
internal static class Utility
{

// also todo: maybe bad code to rewrite, look later
internal static IEnumerable<string> GetParts(string input)

/// <summary>
/// This method splits the input string into parts based on spaces, removing whitespace,
/// but preserving quoted strings as literal parts.
/// </summary>
/// <remarks>
/// This should support escaping quotes with \
/// </remarks>
internal static List<string> GetParts(string input)
{
var parts = input.Split(" ", StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < parts.Length; i++)
var parts = new List<string>();
if (string.IsNullOrWhiteSpace(input)) return parts;

bool inQuotes = false;
var sb = new StringBuilder();

for (int i = 0; i < input.Length; i++)
{
if (parts[i].StartsWith('"'))
char ch = input[i];

// Handle escaped quotes
if (ch == '\\' && i + 1 < input.Length)
{
parts[i] = parts[i].TrimStart('"');
for (var start = i++; i < parts.Length; i++)
char nextChar = input[i + 1];
if (nextChar == '"')
{
if (parts[i].EndsWith('"'))
{
parts[i] = parts[i].TrimEnd('"');
yield return string.Join(" ", parts[start..(i + 1)]);
break;
}
sb.Append(nextChar);
i++; // Skip the escaped quote
continue;
}
}

if (ch == '"')
{
inQuotes = !inQuotes;
continue;
}

if (ch == ' ' && !inQuotes)
{
if (sb.Length > 0)
{
parts.Add(sb.ToString());
sb.Clear();
}
}
else
{
yield return parts[i];
sb.Append(ch);
}
}

if (sb.Length > 0)
{
parts.Add(sb.ToString());
}

return parts;
}



internal static void InternalError(this ICommandContext ctx) => ctx.SysReply("An internal error has occurred.");

internal static void SysReply(this ICommandContext ctx, string input) => ctx.Reply($"[vcf] ".Color(Color.Primary) + input.Color(Color.White));
Expand Down Expand Up @@ -70,7 +106,7 @@ internal static string[] SplitIntoPages(string rawText, int pageSize = MAX_MESSA
var page = new StringBuilder();
var rawLines = rawText.Split(Environment.NewLine); // todo: does this work on both platofrms?
var lines = new List<string>();

// process rawLines -> lines of length <= pageSize
foreach (var line in rawLines)
{
Expand All @@ -97,7 +133,7 @@ internal static string[] SplitIntoPages(string rawText, int pageSize = MAX_MESSA
lines.Add(line);
}
}

// batch as many lines together into pageSize
foreach (var line in lines)
{
Expand Down
62 changes: 56 additions & 6 deletions VCF.Tests/GetPartsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,72 @@ namespace VCF.Tests;
public class GetPartsTests
{
[Test]
public void GetParts_No_Quotes()
public void GetParts_Empty()
{
var empty = Array.Empty<string>();
Assert.That(Utility.GetParts("blah blah"), Is.EqualTo(new[] { "blah", "blah" }));
Assert.That(Utility.GetParts(""), Is.EqualTo(empty));
Assert.That(Utility.GetParts(" "), Is.EqualTo(empty));
Assert.That(Utility.GetParts(null), Is.EqualTo(empty));
}

[Test]
public void GetParts_No_Quotes()
{
Assert.That(Utility.GetParts("blah blah"), Is.EqualTo(new[] { "blah", "blah" }));
Assert.That(Utility.GetParts("a "), Is.EqualTo(new[] { "a" }));
Assert.That(Utility.GetParts(" a"), Is.EqualTo(new[] { "a" }));
Assert.That(Utility.GetParts(" a b c "), Is.EqualTo(new[] { "a", "b", "c" }));
}

[Test]
public void GetParts_Quotes()
public void GetParts_Quotes_Preserves_Spacing()
{
Assert.That(Utility.GetParts(" a \" b c \""), Is.EqualTo(new[] { "a", " b c " }));
}

[Test]
public void GetParts_Quotes_Many()
{
Assert.That(Utility.GetParts(" \"a\" \" b c \" not quoted "), Is.EqualTo(new[] { "a", " b c ", "not", "quoted" }));
}

[Test]
public void GetParts_Quote_SingleTerm()
{
Assert.That(Utility.GetParts("a \"b c\""), Is.EqualTo(new[] { "a", "b c" }));
// TODO: Consider if this should be the result, it fails now
// Assert.That(CommandRegistry.GetParts(" a \" b c \""), Is.EqualTo(new[] { "a", " b c " }));
Assert.That(Utility.GetParts("a"), Is.EqualTo(new[] { "a" }));
Assert.That(Utility.GetParts("\"a\""), Is.EqualTo(new[] { "a" }));
Assert.That(Utility.GetParts("\"abc\""), Is.EqualTo(new[] { "abc" }));
}

[Test]
public void GetParts_Quote_SingleTerm_Positional()
{
Assert.That(Utility.GetParts("a \"b\" c"), Is.EqualTo(new[] { "a", "b", "c" }));
Assert.That(Utility.GetParts("\"a\" b c"), Is.EqualTo(new[] { "a", "b", "c" }));
Assert.That(Utility.GetParts("a b \"c\""), Is.EqualTo(new[] { "a", "b", "c" }));
}

[Test]
public void GetParts_Escape_Quotes()
{
Assert.That(Utility.GetParts("\"I'm like \\\"O'rly?\\\", they're like \\\"ya rly\\\"\""), Is.EqualTo(new[] { "I'm like \"O'rly?\", they're like \"ya rly\"" }));
Assert.That(Utility.GetParts(@"a\b\\""c\"""), Is.EqualTo(new[] { @"a\b\""c""" }));
}

[Test]
public void GetParts_Escape_Slashliteral()
{
Assert.That(Utility.GetParts(@"\"), Is.EqualTo(new[] { @"\" }));
Assert.That(Utility.GetParts(@"\\\"), Is.EqualTo(new[] { @"\\\" }));

Assert.That(Utility.GetParts("\\a"), Is.EqualTo(new[] { "\\a" }));
Assert.That(Utility.GetParts("\\\""), Is.EqualTo(new[] { "\"" }));
}

[Test]
public void GetParts_Escape_Grouping()
{
Assert.That(Utility.GetParts(@"\ \"), Is.EqualTo(new[] { @"\", @"\" }));
Assert.That(Utility.GetParts(@"a\b \ c"), Is.EqualTo(new[] { @"a\b", @"\", "c" }));
}
}

0 comments on commit d432c15

Please sign in to comment.