Add BBCode-like markup parser for FormattedMessage.

This commit is contained in:
Pieter-Jan Briers
2019-10-13 22:47:51 +02:00
parent 08c4ef1ccf
commit 5e37c7a746
4 changed files with 132 additions and 1 deletions

View File

@@ -18,6 +18,7 @@
<PackageReference Include="Nett" Version="0.13.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="NGettext" Version="0.6.4" />
<PackageReference Include="Pidgin" Version="2.2.0" />
<PackageReference Include="SharpZipLib" Version="1.2.0" />
<PackageReference Condition="'$(TargetFramework)' == 'net472'" Include="System.Memory" Version="4.5.3" />
<PackageReference Include="YamlDotNet" Version="6.1.2" />

View File

@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using Pidgin;
using Robust.Shared.Maths;
using static Pidgin.Parser;
using static Pidgin.Parser<char>;
namespace Robust.Shared.Utility
{
public partial class FormattedMessage
{
// wtf I love parser combinators now.
private const char TagBegin = '[';
private const char TagEnd = ']';
private static readonly Parser<char, char> ParseEscapeSequence =
Char('\\').Then(OneOf(
Char('\\'),
Char(TagBegin)));
private static readonly Parser<char, TagText> ParseTagText =
ParseEscapeSequence.Or(Token(c => c != TagBegin && c != '\\'))
.AtLeastOnceString()
.Select(s => new TagText(s));
private static readonly Parser<char, TagColor> ParseTagColor =
String("color")
.Then(Char('='))
.Then(Token(ValidColorNameContents).AtLeastOnceString()
.Select(s =>
{
if (Color.TryFromName(s, out var color))
{
return new TagColor(color);
}
return new TagColor(Color.FromHex(s));
}));
private static readonly Parser<char, TagPop> ParseTagPop =
Char('/')
.Then(String("color"))
.ThenReturn(TagPop.Instance);
private static readonly Parser<char, Tag> ParseTagContents =
ParseTagColor.Cast<Tag>().Or(ParseTagPop.Cast<Tag>());
private static readonly Parser<char, Tag> ParseEnclosedTag =
ParseTagContents.Between(Char(TagBegin), Char(TagEnd));
private static readonly Parser<char, IEnumerable<Tag>> Parse =
ParseTagText.Cast<Tag>().Or(ParseEnclosedTag).Many();
public void AddMarkup(ReadOnlySpan<char> markup)
{
_tags.AddRange(Parse.ParseOrThrow(markup));
}
private static bool ValidColorNameContents(char c)
{
// Match contents of valid color name.
if (c == '#')
{
return true;
}
if (c >= 'a' && c <= 'z')
{
return true;
}
if (c >= 'A' && c <= 'Z')
{
return true;
}
if (c >= '0' && c <= '9')
{
return true;
}
return false;
}
}
}

View File

@@ -14,7 +14,7 @@ namespace Robust.Shared.Utility
/// </summary>
[PublicAPI]
[Serializable, NetSerializable]
public sealed class FormattedMessage
public sealed partial class FormattedMessage
{
public TagList Tags => new TagList(_tags);
private readonly List<Tag> _tags;
@@ -29,6 +29,13 @@ namespace Robust.Shared.Utility
_tags = new List<Tag>(capacity);
}
public static FormattedMessage FromMarkup(ReadOnlySpan<char> markup)
{
var msg = new FormattedMessage();
msg.AddMarkup(markup);
return msg;
}
/// <summary>
/// Create a new <c>FormattedMessage</c> by copying another one.
/// </summary>
@@ -109,6 +116,7 @@ namespace Robust.Shared.Utility
[Serializable, NetSerializable]
public class TagPop : Tag
{
public static readonly TagPop Instance = new TagPop();
}
public readonly struct TagList : IReadOnlyList<Tag>

View File

@@ -0,0 +1,38 @@
using NUnit.Framework;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.UnitTesting.Shared.Utility
{
[Parallelizable(ParallelScope.All)]
[TestFixture]
[TestOf(typeof(FormattedMessage))]
public class FormattedMessage_Test
{
[Test]
public static void TestParseMarkup()
{
var msg = FormattedMessage.FromMarkup("foo[color=#aabbcc]bar[/color]baz");
Assert.That(msg.Tags.Count, Is.EqualTo(5));
Assert.That(((FormattedMessage.TagText) msg.Tags[0]).Text, Is.EqualTo("foo"));
Assert.That(((FormattedMessage.TagColor) msg.Tags[1]).Color, Is.EqualTo(Color.FromHex("#aabbcc")));
Assert.That(((FormattedMessage.TagText) msg.Tags[2]).Text, Is.EqualTo("bar"));
Assert.That(msg.Tags[3], Is.InstanceOf<FormattedMessage.TagPop>());
Assert.That(((FormattedMessage.TagText) msg.Tags[4]).Text, Is.EqualTo("baz"));
}
[Test]
public static void TestParseMarkupColorName()
{
var msg = FormattedMessage.FromMarkup("foo[color=orange]bar[/color]baz");
Assert.That(msg.Tags.Count, Is.EqualTo(5));
Assert.That(((FormattedMessage.TagText) msg.Tags[0]).Text, Is.EqualTo("foo"));
Assert.That(((FormattedMessage.TagColor) msg.Tags[1]).Color, Is.EqualTo(Color.Orange));
Assert.That(((FormattedMessage.TagText) msg.Tags[2]).Text, Is.EqualTo("bar"));
Assert.That(msg.Tags[3], Is.InstanceOf<FormattedMessage.TagPop>());
Assert.That(((FormattedMessage.TagText) msg.Tags[4]).Text, Is.EqualTo("baz"));
}
}
}