* implement a fix for tags being found in code blocks still needs polish, consider this a rough draft * refactor to reuse a local function uses CheckWrappedInCode to check that there are no code blocks that surround the tag being parsed * Add more test coverage of MessageHelper.ParseTags * reset indexes for @ here mention * add a test case to catch error fixed from prev commit * wip commit of most test cases working * fix the Enclosed in block util method * code cleanup * lint whitespace * lint brackets for single line if blocks * move messagehelpertests to the new unit test dir * expose internals to the unit test project this seems to have been breaking the build, since CI would build the merged branch, where rest wasn't exposed to the unit tests
This commit is contained in:
committed by
Christopher F
parent
e8cb031704
commit
c977f2ec9c
@@ -1,3 +1,4 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ using System.Runtime.CompilerServices;
|
|||||||
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")]
|
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")]
|
||||||
[assembly: InternalsVisibleTo("Discord.Net.Webhook")]
|
[assembly: InternalsVisibleTo("Discord.Net.Webhook")]
|
||||||
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
|
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
||||||
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
|
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Runtime.CompilerServices;
|
|||||||
[assembly: InternalsVisibleTo("Discord.Net.Webhook")]
|
[assembly: InternalsVisibleTo("Discord.Net.Webhook")]
|
||||||
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
|
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
|
||||||
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
|
||||||
|
|
||||||
[assembly: TypeForwardedTo(typeof(Discord.Embed))]
|
[assembly: TypeForwardedTo(typeof(Discord.Embed))]
|
||||||
[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))]
|
[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Model = Discord.API.Message;
|
using Model = Discord.API.Message;
|
||||||
|
|
||||||
@@ -108,14 +109,56 @@ namespace Discord.Rest
|
|||||||
public static ImmutableArray<ITag> ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection<IUser> userMentions)
|
public static ImmutableArray<ITag> ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection<IUser> userMentions)
|
||||||
{
|
{
|
||||||
var tags = ImmutableArray.CreateBuilder<ITag>();
|
var tags = ImmutableArray.CreateBuilder<ITag>();
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
|
var codeIndex = 0;
|
||||||
|
|
||||||
|
var inlineRegex = new Regex(@"[^\\]?(`).+?[^\\](`)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline);
|
||||||
|
var blockRegex = new Regex(@"[^\\]?(```).+?[^\\](```)", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.Singleline);
|
||||||
|
|
||||||
|
// checks if the tag being parsed is wrapped in code blocks
|
||||||
|
bool CheckWrappedCode()
|
||||||
|
{
|
||||||
|
// util to check if the index of a tag is within the bounds of the codeblock
|
||||||
|
bool EnclosedInBlock(Match m)
|
||||||
|
=> m.Groups[1].Index < index && index < m.Groups[2].Index;
|
||||||
|
|
||||||
|
// loop through all code blocks that are before the start of the tag
|
||||||
|
while (codeIndex < index)
|
||||||
|
{
|
||||||
|
var blockMatch = blockRegex.Match(text, codeIndex);
|
||||||
|
if (blockMatch.Success)
|
||||||
|
{
|
||||||
|
if (EnclosedInBlock(blockMatch))
|
||||||
|
return true;
|
||||||
|
// continue if the end of the current code was before the start of the tag
|
||||||
|
codeIndex += blockMatch.Groups[2].Index + blockMatch.Groups[2].Length;
|
||||||
|
if (codeIndex < index)
|
||||||
|
continue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var inlineMatch = inlineRegex.Match(text, codeIndex);
|
||||||
|
if (inlineMatch.Success)
|
||||||
|
{
|
||||||
|
if (EnclosedInBlock(inlineMatch))
|
||||||
|
return true;
|
||||||
|
// continue if the end of the current code was before the start of the tag
|
||||||
|
codeIndex += inlineMatch.Groups[2].Index + inlineMatch.Groups[2].Length;
|
||||||
|
if (codeIndex < index)
|
||||||
|
continue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
index = text.IndexOf('<', index);
|
index = text.IndexOf('<', index);
|
||||||
if (index == -1) break;
|
if (index == -1) break;
|
||||||
int endIndex = text.IndexOf('>', index + 1);
|
int endIndex = text.IndexOf('>', index + 1);
|
||||||
if (endIndex == -1) break;
|
if (endIndex == -1) break;
|
||||||
|
if (CheckWrappedCode()) break;
|
||||||
string content = text.Substring(index, endIndex - index + 1);
|
string content = text.Substring(index, endIndex - index + 1);
|
||||||
|
|
||||||
if (MentionUtils.TryParseUser(content, out ulong id))
|
if (MentionUtils.TryParseUser(content, out ulong id))
|
||||||
@@ -158,10 +201,12 @@ namespace Discord.Rest
|
|||||||
}
|
}
|
||||||
|
|
||||||
index = 0;
|
index = 0;
|
||||||
|
codeIndex = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
index = text.IndexOf("@everyone", index);
|
index = text.IndexOf("@everyone", index);
|
||||||
if (index == -1) break;
|
if (index == -1) break;
|
||||||
|
if (CheckWrappedCode()) break;
|
||||||
var tagIndex = FindIndex(tags, index);
|
var tagIndex = FindIndex(tags, index);
|
||||||
if (tagIndex.HasValue)
|
if (tagIndex.HasValue)
|
||||||
tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.EveryoneMention, index, "@everyone".Length, 0, guild?.EveryoneRole));
|
tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.EveryoneMention, index, "@everyone".Length, 0, guild?.EveryoneRole));
|
||||||
@@ -169,10 +214,12 @@ namespace Discord.Rest
|
|||||||
}
|
}
|
||||||
|
|
||||||
index = 0;
|
index = 0;
|
||||||
|
codeIndex = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
index = text.IndexOf("@here", index);
|
index = text.IndexOf("@here", index);
|
||||||
if (index == -1) break;
|
if (index == -1) break;
|
||||||
|
if (CheckWrappedCode()) break;
|
||||||
var tagIndex = FindIndex(tags, index);
|
var tagIndex = FindIndex(tags, index);
|
||||||
if (tagIndex.HasValue)
|
if (tagIndex.HasValue)
|
||||||
tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.HereMention, index, "@here".Length, 0, guild?.EveryoneRole));
|
tags.Insert(tagIndex.Value, new Tag<IRole>(TagType.HereMention, index, "@here".Length, 0, guild?.EveryoneRole));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Discord.Net.Relay")]
|
[assembly: InternalsVisibleTo("Discord.Net.Relay")]
|
||||||
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
|
||||||
|
|||||||
118
test/Discord.Net.Tests.Unit/MessageHelperTests.cs
Normal file
118
test/Discord.Net.Tests.Unit/MessageHelperTests.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using Xunit;
|
||||||
|
using Discord.Rest;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for <see cref="MessageHelper"/> parsing.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageHelperTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that no tags are parsed while in code blocks
|
||||||
|
/// or inline code.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("`@everyone`")]
|
||||||
|
[InlineData("`<@163184946742034432>`")]
|
||||||
|
[InlineData("```@everyone```")]
|
||||||
|
[InlineData("```cs \n @everyone```")]
|
||||||
|
[InlineData("```cs <@163184946742034432> ```")]
|
||||||
|
[InlineData("``` test ``` ```cs <@163184946742034432> ```")]
|
||||||
|
[InlineData("`<:test:537920404019216384>`")]
|
||||||
|
[InlineData("``` @everyone `")] // discord client handles these weirdly
|
||||||
|
[InlineData("``` @everyone ``")]
|
||||||
|
[InlineData("` @here `")]
|
||||||
|
[InlineData("` @everyone @here <@163184946742034432> <@&163184946742034432> <#163184946742034432> <:test:537920404019216384> `")]
|
||||||
|
public void ParseTagsInCode(string testData)
|
||||||
|
{
|
||||||
|
// don't care that I'm passing in null channels/guilds/users
|
||||||
|
// as they shouldn't be required
|
||||||
|
var result = MessageHelper.ParseTags(testData, null, null, null);
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Tests parsing tags that surround inline code or a code block. </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("`` <@&163184946742034432>")]
|
||||||
|
[InlineData("``` code block 1 ``` ``` code block 2 ``` <@&163184946742034432>")]
|
||||||
|
[InlineData("` code block 1 ``` ` code block 2 ``` <@&163184946742034432>")]
|
||||||
|
[InlineData("<@&163184946742034432> ``` code block 1 ```")]
|
||||||
|
[InlineData("``` code ``` ``` code ``` @here ``` code ``` ``` more ```")]
|
||||||
|
[InlineData("``` code ``` @here ``` more ```")]
|
||||||
|
public void ParseTagsAroundCode(string testData)
|
||||||
|
{
|
||||||
|
// don't care that I'm passing in null channels/guilds/users
|
||||||
|
// as they shouldn't be required
|
||||||
|
var result = MessageHelper.ParseTags(testData, null, null, null);
|
||||||
|
Assert.NotEmpty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(@"\` @everyone \`")]
|
||||||
|
[InlineData(@"\`\`\` @everyone \`\`\`")]
|
||||||
|
[InlineData(@"hey\`\`\`@everyone\`\`\`!!")]
|
||||||
|
public void IgnoreEscapedCodeBlocks(string testData)
|
||||||
|
{
|
||||||
|
var result = MessageHelper.ParseTags(testData, null, null, null);
|
||||||
|
Assert.NotEmpty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cannot test parsing a user, as it uses the ReadOnlyCollection<IUser> arg.
|
||||||
|
// this could be done if mocked entities are merged in PR #1290
|
||||||
|
|
||||||
|
/// <summary> Tests parsing a mention of a role. </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("<@&163184946742034432>")]
|
||||||
|
[InlineData("**<@&163184946742034432>**")]
|
||||||
|
[InlineData("__<@&163184946742034432>__")]
|
||||||
|
[InlineData("<><@&163184946742034432>")]
|
||||||
|
public void ParseRole(string roleTag)
|
||||||
|
{
|
||||||
|
var result = MessageHelper.ParseTags(roleTag, null, null, null);
|
||||||
|
Assert.Contains(result, x => x.Type == TagType.RoleMention);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Tests parsing a channel. </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("<#429115823748284417>")]
|
||||||
|
[InlineData("**<#429115823748284417>**")]
|
||||||
|
[InlineData("<><#429115823748284417>")]
|
||||||
|
public void ParseChannel(string channelTag)
|
||||||
|
{
|
||||||
|
var result = MessageHelper.ParseTags(channelTag, null, null, null);
|
||||||
|
Assert.Contains(result, x => x.Type == TagType.ChannelMention);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Tests parsing an emoji. </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("<:test:537920404019216384>")]
|
||||||
|
[InlineData("**<:test:537920404019216384>**")]
|
||||||
|
[InlineData("<><:test:537920404019216384>")]
|
||||||
|
public void ParseEmoji(string emoji)
|
||||||
|
{
|
||||||
|
var result = MessageHelper.ParseTags(emoji, null, null, null);
|
||||||
|
Assert.Contains(result, x => x.Type == TagType.Emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Tests parsing a mention of @everyone. </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("@everyone")]
|
||||||
|
[InlineData("**@everyone**")]
|
||||||
|
public void ParseEveryone(string everyone)
|
||||||
|
{
|
||||||
|
var result = MessageHelper.ParseTags(everyone, null, null, null);
|
||||||
|
Assert.Contains(result, x => x.Type == TagType.EveryoneMention);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Tests parsing a mention of @here. </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("@here")]
|
||||||
|
[InlineData("**@here**")]
|
||||||
|
public void ParseHere(string here)
|
||||||
|
{
|
||||||
|
var result = MessageHelper.ParseTags(here, null, null, null);
|
||||||
|
Assert.Contains(result, x => x.Type == TagType.HereMention);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user