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(); } } }