From 8b42c1dd46e3c242fef55bf82f9927c2c4a70e4a Mon Sep 17 00:00:00 2001
From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Date: Tue, 28 May 2024 10:40:29 +1200
Subject: [PATCH] Improve FormattedMessage exception handling (#5170)
* Improve FormattedMessage exception handling
* comments
---
RELEASE-NOTES.md | 2 +-
.../Utility/FormattedMessage.MarkupParser.cs | 68 +++++++++++++---
Robust.Shared/Utility/FormattedMessage.cs | 79 ++++++++++++++++---
3 files changed, 128 insertions(+), 21 deletions(-)
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 3615ea308e..53f3366ac7 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -39,7 +39,7 @@ END TEMPLATE-->
### New features
-*None yet*
+* Added several new `FormattedMessage` methods for better exception tolerance when parsing markup. Several existing methods have been marked as obsolete, with new renamed methods taking their place.
### Bugfixes
diff --git a/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs b/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs
index f26ee8b1b0..400cf9f30c 100644
--- a/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs
+++ b/Robust.Shared/Utility/FormattedMessage.MarkupParser.cs
@@ -1,4 +1,6 @@
+using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using Pidgin;
using Robust.Shared.Maths;
using static Pidgin.Parser;
@@ -15,29 +17,51 @@ public sealed partial class FormattedMessage
/// true if the markup is valid
public static bool ValidMarkup(string markup)
{
- return ParseResult(markup).Success;
+ return TryParse(markup, out _, out _);
}
+ ///
+ /// Attempts to add markup. If an error occurs, it will do nothing and return an error message.
+ /// This method does NOT fall back to using the permissive parser (which parses invalid markup as text).
+ ///
+ public bool TryAddMarkup(string markup, [NotNullWhen(false)] out string? error)
+ {
+ if (!TryParse(markup, out var nodes, out error))
+ return false;
+
+ _nodes.AddRange(nodes);
+ return true;
+ }
+
+ [Obsolete("Use AddMarkupOrThrow or TryAddMarkup")]
+ public void AddMarkup(string markup) => AddMarkupOrThrow(markup);
+
///
/// Parses the given markup and adds the resulting nodes to this formatted message
///
/// The markup to parse
- public void AddMarkup(string markup)
+ /// Thrown when an error occurs while trying to parse the markup.
+ public void AddMarkupOrThrow(string markup)
{
- _nodes.AddRange(Parse(markup));
+ _nodes.AddRange(ParseOrThrow(markup));
}
///
- /// Same as but will parse invalid markup tags as text.
+ /// Same as but will attempt to parse invalid markup tags as text.
///
- public void AddMarkupPermissive(string markup)
+ /// Thrown when an error occurs even when using the permissive parser.
+ public void AddMarkupPermissive(string markup, out string? error)
{
- _nodes.AddRange(ParseSafe(markup));
+ _nodes.AddRange(ParsePermissive(markup, out error));
}
+ ///
+ public void AddMarkupPermissive(string markup) => AddMarkupPermissive(markup, out _);
+
///
/// Same as but adds a newline too.
///
+ [Obsolete]
public void PushMarkup(string markup)
{
AddMarkup(markup);
@@ -52,13 +76,37 @@ public sealed partial class FormattedMessage
/// This parser doesn't use backtracking by chaining pidgins parsers in such a way that branches that don't apply
/// always fail on the first character
///
- private static IEnumerable Parse(string input) => ParseNodes.ParseOrThrow(input);
+ private static List ParseOrThrow(string input) => ParseNodes.ParseOrThrow(input);
///
- /// Same as but uses backtracking once to ensure invalid markup just gets parsed as text
+ /// Attempt to parse the given input. Returns an error message if it fails. Does not fall back to the permissive parser
///
- private static IEnumerable ParseSafe(string input) => ParseNodesSafe.ParseOrThrow(input);
- private static Result> ParseResult(string input) => ParseNodes.Parse(input);
+ public static bool TryParse(string input, [NotNullWhen(true)] out List? nodes, [NotNullWhen(false)] out string? error)
+ {
+ var result = ParseNodes.Parse(input);
+ if (result.Success)
+ {
+ nodes = result.Value;
+ error = null;
+ return true;
+ }
+
+ error = result.Error!.RenderErrorMessage();
+ nodes = null;
+ return false;
+ }
+
+ ///
+ /// Variant of that falls back to using the permissive parser if an error occurs.
+ ///
+ /// Thrown when an error occurs even when using the permissive parser.
+ public static List ParsePermissive(string input, out string? error)
+ {
+ if (TryParse(input, out var nodes, out error))
+ return nodes;
+
+ return ParseNodesSafe.ParseOrThrow(input);
+ }
//TODO: Make Begin and End a cvar
// Parser definitions for reserved characters
diff --git a/Robust.Shared/Utility/FormattedMessage.cs b/Robust.Shared/Utility/FormattedMessage.cs
index d9a80bad0e..b6051eaf04 100644
--- a/Robust.Shared/Utility/FormattedMessage.cs
+++ b/Robust.Shared/Utility/FormattedMessage.cs
@@ -1,8 +1,10 @@
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.Serialization;
@@ -49,30 +51,72 @@ public sealed partial class FormattedMessage
/// The message to copy.
public FormattedMessage(FormattedMessage toCopy)
{
- _nodes = Extensions.ShallowClone(toCopy._nodes);
+ _nodes = toCopy._nodes.ShallowClone();
}
- public static FormattedMessage FromMarkup(string markup)
+ 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.AddMarkup(markup);
+ msg.AddMarkupOrThrow(markup);
return msg;
}
- public static FormattedMessage FromUnformatted(string markup)
+ [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(markup);
+ msg.AddText(text);
return msg;
}
- public static FormattedMessage FromMarkupPermissive(string markup)
+ ///
+ /// 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);
+ 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.
///
@@ -82,13 +126,28 @@ public sealed partial class FormattedMessage
}
///
- /// Remove all markup, leaving only the basic text content behind.
+ /// Remove all markup, leaving only the basic text content behind. Throws if it fails to parse the markup tags.
///
- public static string RemoveMarkup(string text)
+ /// Thrown when an error occurs while trying to parse the markup.
+ public static string RemoveMarkupOrThrow(string markup)
{
- return FromMarkup(text).ToString();
+ 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 .