using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using JetBrains.Annotations;
using Nett.Parser;
using Robust.Shared.Maths;
using Robust.Shared.RichText;
using Robust.Shared.Serialization;
namespace Robust.Shared.Utility;
///
/// Represents a formatted message in the form of a list of "tags".
/// Does not do any concrete formatting, simply useful as an API surface.
///
///
[PublicAPI]
[Serializable, NetSerializable]
public sealed partial class FormattedMessage : IEquatable, IReadOnlyList
{
public static FormattedMessage Empty => new();
///
/// The list of nodes the formatted message is made out of
///
public IReadOnlyList Nodes => _nodes;
///
/// true if the formatted message doesn't contain any nodes
///
public bool IsEmpty => _nodes.Count == 0;
public int Count => _nodes.Count;
public MarkupNode this[int index] => _nodes[index];
private readonly List _nodes;
///
/// Used for inserting the correct closing node when calling
///
private Stack? _openNodeStack;
public FormattedMessage()
{
_nodes = new List();
}
public FormattedMessage(int capacity)
{
_nodes = new List(capacity);
}
///
/// Create a new FormattedMessage by copying another one.
///
/// The message to copy.
public FormattedMessage(FormattedMessage toCopy)
{
_nodes = toCopy._nodes.ShallowClone();
}
private FormattedMessage(List nodes)
{
_nodes = nodes;
}
///
/// Attempt to create a new formatted message from some markup text. Returns an error if it fails.
///
public static bool TryFromMarkup(string markup, [NotNullWhen(true)] out FormattedMessage? msg, [NotNullWhen(false)] out string? error)
{
if (!TryParse(markup, out var nodes, out error))
{
msg = null;
return false;
}
msg = new FormattedMessage(nodes);
return true;
}
///
/// Attempt to create a new formatted message from some markup text.
///
public static bool TryFromMarkup(string markup, [NotNullWhen(true)] out FormattedMessage? msg)
=> TryFromMarkup(markup, out msg, out _);
///
/// Attempt to create a new formatted message from some markup text. Throws if the markup is invalid.
///
/// Thrown when an error occurs while trying to parse the markup.
public static FormattedMessage FromMarkupOrThrow(string markup)
{
var msg = new FormattedMessage();
msg.AddMarkupOrThrow(markup);
return msg;
}
[Obsolete("Use FromMarkupOrThrow or TryFromMarkup")]
public static FormattedMessage FromMarkup(string markup) => FromMarkupOrThrow(markup);
public static FormattedMessage FromUnformatted(string text)
{
var msg = new FormattedMessage();
msg.AddText(text);
return msg;
}
///
/// Variant of that
/// attempts to fall back to using the permissive parser that interprets invalid markup tags as normal text.
/// This may still throw if the permissive parser fails.
///
/// Thrown when an error occurs while trying to parse the markup.
public static FormattedMessage FromMarkupPermissive(string markup, out string? error)
{
var msg = new FormattedMessage();
msg.AddMarkupPermissive(markup, out error);
return msg;
}
///
public static FormattedMessage FromMarkupPermissive(string markup) => FromMarkupPermissive(markup, out _);
///
/// Escape a string of text to be able to be formatted into markup.
///
public static string EscapeText(string text)
{
return text.Replace("\\", "\\\\").Replace("[", "\\[");
}
///
/// Escape a string parameter value to be able to be formatted into markup.
///
public static string EscapeStringParameter(string parameter)
{
return EscapeText(parameter).Replace("\"", "\\\"");
}
///
/// Remove all markup, leaving only the basic text content behind. Throws if it fails to parse the markup tags.
///
/// Thrown when an error occurs while trying to parse the markup.
public static string RemoveMarkupOrThrow(string markup)
{
return FromMarkupOrThrow(markup).ToString();
}
///
/// Attempts to remove all valid markup tags, leaving only the basic text content behind.
/// If this markup contains invalid tags that cannot be parsed, they will not be removed and will instead be trated
/// as normal text. Hence the output should probably only be parsed using try-parse the permissive parser.
///
/// Thrown when an error occurs while trying to fall back to the permissive parser.
public static string RemoveMarkupPermissive(string markup)
{
return FromMarkupPermissive(markup).ToString();
}
[Obsolete("Use RemoveMarkupOrThrow or RemoveMarkupPermissive")]
public static string RemoveMarkup(string markup) => RemoveMarkupOrThrow(markup);
///
/// Adds a text node.
/// This node doesn't need to be closed with .
///
/// The text to add
public void AddText(string text)
{
PushTag(new MarkupNode(text));
}
///
/// Adds an open color node. It needs to later be closed by calling
///
/// The color of the node to add
public void PushColor(Color color)
{
PushTag(new MarkupNode("color", new MarkupParameter(color), null));
}
///
/// Adds a newline as a text node.
/// This node doesn't need to be closed with .
///
public void PushNewline()
{
AddText("\n");
}
///
/// Removes extraneous whitespace from the end of the message.
///
public void TrimEnd()
{
while (_nodes.Count > 1)
{
var last = _nodes[^1];
if (last.Name == null && last.Value.TryGetString(out var text))
{
string trimmed = text.TrimEnd();
if (trimmed.Length == 0)
{
_nodes.Pop();
continue;
}
else if (trimmed != text)
{
_nodes[^1] = new MarkupNode(trimmed);
}
}
break;
}
}
///
/// Adds a new open node to the formatted message.
/// The method for inserting closed nodes: . It needs to be
/// called once for each inserted open node that isn't self closing.
///
/// The node to add
/// Whether the node is self closing or not.
/// Self closing nodes automatically insert a closing node after the open one
public void PushTag(MarkupNode markupNode, bool selfClosing = false)
{
_nodes.Add(markupNode);
if (markupNode.Name == null)
return;
if (selfClosing)
{
_nodes.Add(new MarkupNode(markupNode.Name, null, null, true));
return;
}
_openNodeStack ??= new Stack();
_openNodeStack.Push(markupNode);
}
///
/// Closes the last added node that wasn't self closing
///
public void Pop()
{
if (_openNodeStack == null || !_openNodeStack.TryPop(out var node))
return;
_nodes.Add(new MarkupNode(node.Name, null, null, true));
}
///
/// Adds a formatted message to this one.
///
/// The formatted message to be added
public void AddMessage(FormattedMessage other)
{
_nodes.AddRange(other._nodes);
}
///
/// Clears the formatted message
///
public void Clear()
{
_nodes.Clear();
}
///
/// Returns an enumerator that enumerates every rune for each text node contained in this formatted text instance.
///
public FormattedMessageRuneEnumerator EnumerateRunes()
{
return new FormattedMessageRuneEnumerator(this);
}
public NodeEnumerator GetEnumerator()
{
return new NodeEnumerator(_nodes.GetEnumerator());
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
///
public bool Equals(FormattedMessage? other)
{
if (_nodes.Count != other?._nodes.Count)
return false;
for (var i = 0; i < _nodes.Count; i++)
{
if (!_nodes[i].Equals(other?._nodes[i]))
return false;
}
return true;
}
///
public override int GetHashCode()
{
var hash = 0;
foreach (var node in _nodes)
{
hash = HashCode.Combine(hash, node.GetHashCode());
}
return hash;
}
/// The string without markup tags.
public override string ToString()
{
var builder = new StringBuilder();
foreach (var node in _nodes)
{
if (node.Name == null)
builder.Append(node.Value.StringValue);
}
return builder.ToString();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// The string without filtering out markup tags.
public string ToMarkup()
{
return string.Join("", _nodes);
}
public struct FormattedMessageRuneEnumerator : IEnumerable, IEnumerator
{
private readonly FormattedMessage _msg;
private List.Enumerator _tagEnumerator;
private StringRuneEnumerator _runeEnumerator;
internal FormattedMessageRuneEnumerator(FormattedMessage msg)
{
_msg = msg;
_tagEnumerator = msg._nodes.GetEnumerator();
// Rune enumerator will immediately give false on first iteration so I dont' need to special case anything.
_runeEnumerator = "".EnumerateRunes();
}
public IEnumerator GetEnumerator() => this;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public bool MoveNext()
{
while (!_runeEnumerator.MoveNext())
{
MarkupNode text;
while (true)
{
var result = _tagEnumerator.MoveNext();
if (!result)
return false;
if (_tagEnumerator.Current is not { Name: null, Value.StringValue: not null } nextText)
continue;
text = nextText;
break;
}
_runeEnumerator = text.Value.StringValue!.EnumerateRunes();
}
return true;
}
public void Reset()
{
_tagEnumerator = _msg._nodes.GetEnumerator();
_runeEnumerator = "".EnumerateRunes();
}
public Rune Current => _runeEnumerator.Current;
object IEnumerator.Current => Current;
void IDisposable.Dispose()
{
}
}
public struct NodeEnumerator : IEnumerator
{
private List.Enumerator _enumerator;
internal NodeEnumerator(List.Enumerator enumerator)
{
_enumerator = enumerator;
}
public bool MoveNext()
{
return _enumerator.MoveNext();
}
void IEnumerator.Reset()
{
((IEnumerator) _enumerator).Reset();
}
public MarkupNode Current => _enumerator.Current;
object IEnumerator.Current => Current;
public void Dispose()
{
_enumerator.Dispose();
}
}
}