diff --git a/Content.Client/Guidebook/Controls/GuidebookError.xaml b/Content.Client/Guidebook/Controls/GuidebookError.xaml
new file mode 100644
index 000000000000..b84d527ea0a2
--- /dev/null
+++ b/Content.Client/Guidebook/Controls/GuidebookError.xaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Guidebook/Controls/GuidebookError.xaml.cs b/Content.Client/Guidebook/Controls/GuidebookError.xaml.cs
new file mode 100644
index 000000000000..461f196c8389
--- /dev/null
+++ b/Content.Client/Guidebook/Controls/GuidebookError.xaml.cs
@@ -0,0 +1,23 @@
+using JetBrains.Annotations;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Guidebook.Controls;
+
+[UsedImplicitly] [GenerateTypedNameReferences]
+public sealed partial class GuidebookError : BoxContainer
+{
+ public GuidebookError()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public GuidebookError(string original, string? error) : this()
+ {
+ Original.AddText(original);
+
+ if (error is not null)
+ Error.AddText(error);
+ }
+}
diff --git a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
index c904a9c78986..469b0ed22245 100644
--- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
+++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml.cs
@@ -4,12 +4,10 @@
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Controls.FancyTree;
using Content.Client.UserInterface.Systems.Info;
-using Content.Shared.CCVar;
using Content.Shared.Guidebook;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;
@@ -18,15 +16,18 @@ namespace Content.Client.Guidebook.Controls;
[GenerateTypedNameReferences]
public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
{
- [Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
+ [Dependency] private readonly IResourceManager _resourceManager = default!;
private Dictionary, GuideEntry> _entries = new();
+ private readonly ISawmill _sawmill;
+
public GuidebookWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+ _sawmill = Logger.GetSawmill("Guidebook");
Tree.OnSelectedItemChanged += OnSelectionChanged;
@@ -36,6 +37,20 @@ public GuidebookWindow()
};
}
+ public void HandleClick(string link)
+ {
+ if (!_entries.TryGetValue(link, out var entry))
+ return;
+
+ if (Tree.TryGetIndexFromMetadata(entry, out var index))
+ {
+ Tree.ExpandParentEntries(index.Value);
+ Tree.SetSelectedIndex(index);
+ }
+ else
+ ShowGuide(entry);
+ }
+
private void OnSelectionChanged(TreeItem? item)
{
if (item != null && item.Metadata is GuideEntry entry)
@@ -71,8 +86,9 @@ private void ShowGuide(GuideEntry entry)
if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd()))
{
- EntryContainer.AddChild(new Label() { Text = "ERROR: Failed to parse document." });
- Logger.Error($"Failed to parse contents of guide document {entry.Id}.");
+ // The guidebook will automatically display the in-guidebook error if it fails
+
+ _sawmill.Error($"Failed to parse contents of guide document {entry.Id}.");
}
}
@@ -124,8 +140,10 @@ private IEnumerable GetSortedEntries(List GetSortedEntries(List Loc.GetString(rootEntry.Name));
}
- private void RepopulateTree(List>? roots = null, ProtoId? forcedRoot = null)
+ private void RepopulateTree(List>? roots = null,
+ ProtoId? forcedRoot = null)
{
Tree.Clear();
HashSet> addedEntries = new();
- TreeItem? parent = forcedRoot == null ? null : AddEntry(forcedRoot.Value, null, addedEntries);
+ var parent = forcedRoot == null ? null : AddEntry(forcedRoot.Value, null, addedEntries);
foreach (var entry in GetSortedEntries(roots))
{
AddEntry(entry.Id, parent, addedEntries);
}
+
Tree.SetAllExpanded(true);
}
- private TreeItem? AddEntry(ProtoId id, TreeItem? parent, HashSet> addedEntries)
+ private TreeItem? AddEntry(ProtoId id,
+ TreeItem? parent,
+ HashSet> addedEntries)
{
if (!_entries.TryGetValue(id, out var entry))
return null;
@@ -179,22 +201,6 @@ private void RepopulateTree(List>? roots = null, Pr
return item;
}
- public void HandleClick(string link)
- {
- if (!_entries.TryGetValue(link, out var entry))
- return;
-
- if (Tree.TryGetIndexFromMetadata(entry, out var index))
- {
- Tree.ExpandParentEntries(index.Value);
- Tree.SetSelectedIndex(index);
- }
- else
- {
- ShowGuide(entry);
- }
- }
-
private void HandleFilter()
{
var emptySearch = SearchBar.Text.Trim().Length == 0;
@@ -208,6 +214,5 @@ private void HandleFilter()
element.SetHiddenState(true, SearchBar.Text.Trim());
}
}
-
}
}
diff --git a/Content.Client/Guidebook/DocumentParsingManager.cs b/Content.Client/Guidebook/DocumentParsingManager.cs
index e8a0743b9e05..857ae552024f 100644
--- a/Content.Client/Guidebook/DocumentParsingManager.cs
+++ b/Content.Client/Guidebook/DocumentParsingManager.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using Content.Client.Guidebook.Controls;
using Content.Client.Guidebook.Richtext;
using Content.Shared.Guidebook;
using Pidgin;
@@ -7,6 +8,7 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Sandboxing;
+using Robust.Shared.Utility;
using static Pidgin.Parser;
namespace Content.Client.Guidebook;
@@ -22,8 +24,10 @@ public sealed partial class DocumentParsingManager
[Dependency] private readonly ISandboxHelper _sandboxHelper = default!;
private readonly Dictionary> _tagControlParsers = new();
- private Parser _tagParser = default!;
private Parser _controlParser = default!;
+
+ private ISawmill _sawmill = default!;
+ private Parser _tagParser = default!;
public Parser> ControlParser = default!;
public void Initialize()
@@ -32,7 +36,8 @@ public void Initialize()
.Assert(_tagControlParsers.ContainsKey, tag => $"unknown tag: {tag}")
.Bind(tag => _tagControlParsers[tag]);
- _controlParser = OneOf(_tagParser, TryHeaderControl, ListControlParser, TextControlParser).Before(SkipWhitespaces);
+ _controlParser = OneOf(_tagParser, TryHeaderControl, ListControlParser, TextControlParser)
+ .Before(SkipWhitespaces);
foreach (var typ in _reflectionManager.GetAllChildren())
{
@@ -40,6 +45,8 @@ public void Initialize()
}
ControlParser = SkipWhitespaces.Then(_controlParser.Many());
+
+ _sawmill = Logger.GetSawmill("Guidebook");
}
public bool TryAddMarkup(Control control, ProtoId entryId, bool log = true)
@@ -68,37 +75,57 @@ public bool TryAddMarkup(Control control, string text, bool log = true)
}
catch (Exception e)
{
- if (log)
- Logger.Error($"Encountered error while generating markup controls: {e}");
+ _sawmill.Error($"Encountered error while generating markup controls: {e}");
+
+ control.AddChild(new GuidebookError(text, e.ToStringBetter()));
+
return false;
}
return true;
}
- private Parser CreateTagControlParser(string tagId, Type tagType, ISandboxHelper sandbox) => Map(
- (args, controls) =>
- {
- var tag = (IDocumentTag) sandbox.CreateInstance(tagType);
- if (!tag.TryParseTag(args, out var control))
- {
- Logger.Error($"Failed to parse {tagId} args");
- return new Control();
- }
+ private Parser CreateTagControlParser(string tagId, Type tagType, ISandboxHelper sandbox)
+ {
+ return Map(
+ (args, controls) =>
+ {
+ try
+ {
+ var tag = (IDocumentTag) sandbox.CreateInstance(tagType);
+ if (!tag.TryParseTag(args, out var control))
+ {
+ _sawmill.Error($"Failed to parse {tagId} args");
+ return new GuidebookError(args.ToString() ?? tagId, $"Failed to parse {tagId} args");
+ }
- foreach (var child in controls)
- {
- control.AddChild(child);
- }
- return control;
- },
- ParseTagArgs(tagId),
- TagContentParser(tagId)).Labelled($"{tagId} control");
+ foreach (var child in controls)
+ {
+ control.AddChild(child);
+ }
+
+ return control;
+ }
+ catch (Exception e)
+ {
+ var output = args.Aggregate(string.Empty,
+ (current, pair) => current + $"{pair.Key}=\"{pair.Value}\" ");
+
+ _sawmill.Error($"Tag: {tagId} \n Arguments: {output}/>");
+ return new GuidebookError($"Tag: {tagId}\nArguments: {output}", e.ToString());
+ }
+ },
+ ParseTagArgs(tagId),
+ TagContentParser(tagId))
+ .Labelled($"{tagId} control");
+ }
// Parse a bunch of controls until we encounter a matching closing tag.
- private Parser> TagContentParser(string tag) =>
- OneOf(
- Try(ImmediateTagEnd).ThenReturn(Enumerable.Empty()),
- TagEnd.Then(_controlParser.Until(TryTagTerminator(tag)).Labelled($"{tag} children"))
- );
+ private Parser> TagContentParser(string tag)
+ {
+ return OneOf(
+ Try(ImmediateTagEnd).ThenReturn(Enumerable.Empty()),
+ TagEnd.Then(_controlParser.Until(TryTagTerminator(tag)).Labelled($"{tag} children"))
+ );
+ }
}
diff --git a/Content.Client/Guidebook/DocumentParsingManager.static.cs b/Content.Client/Guidebook/DocumentParsingManager.static.cs
index ab38fcb1546d..5d25d8f64525 100644
--- a/Content.Client/Guidebook/DocumentParsingManager.static.cs
+++ b/Content.Client/Guidebook/DocumentParsingManager.static.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using Content.Client.Guidebook.Controls;
using Pidgin;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -14,92 +15,142 @@ public sealed partial class DocumentParsingManager
{
private const string ListBullet = " › ";
- #region Text Parsing
- #region Basic Text Parsing
- // Try look for an escaped character. If found, skip the escaping slash and return the character.
- private static readonly Parser TryEscapedChar = Try(Char('\\').Then(OneOf(
- Try(Char('<')),
- Try(Char('>')),
- Try(Char('\\')),
- Try(Char('-')),
- Try(Char('=')),
- Try(Char('"')),
- Try(Char(' ')),
- Try(Char('n')).ThenReturn('\n'),
- Try(Char('t')).ThenReturn('\t')
- )));
+ // Parser that consumes a - and then just parses normal rich text with some prefix text (a bullet point).
+ private static readonly Parser TryEscapedChar = Try(Char('\\')
+ .Then(OneOf(
+ Try(Char('<')),
+ Try(Char('>')),
+ Try(Char('\\')),
+ Try(Char('-')),
+ Try(Char('=')),
+ Try(Char('"')),
+ Try(Char(' ')),
+ Try(Char('n')).ThenReturn('\n'),
+ Try(Char('t')).ThenReturn('\t')
+ )));
private static readonly Parser SkipNewline = Whitespace.SkipUntil(Char('\n'));
- private static readonly Parser TrySingleNewlineToSpace = Try(SkipNewline).Then(SkipWhitespaces).ThenReturn(' ');
+ private static readonly Parser TrySingleNewlineToSpace =
+ Try(SkipNewline).Then(SkipWhitespaces).ThenReturn(' ');
private static readonly Parser TextChar = OneOf(
TryEscapedChar, // consume any backslashed being used to escape text
TrySingleNewlineToSpace, // turn single newlines into spaces
Any // just return the character.
- );
+ );
- // like TextChar, but not skipping whitespace around newlines
private static readonly Parser QuotedTextChar = OneOf(TryEscapedChar, Any);
+ private static readonly Parser QuotedText =
+ Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
+
+ private static readonly Parser TryStartList =
+ Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
+
+ private static readonly Parser TryStartTag = Try(Char('<')).Then(SkipWhitespaces);
+
+ private static readonly Parser TryStartParagraph =
+ Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
+
+ private static readonly Parser TryLookTextEnd =
+ Lookahead(OneOf(TryStartTag, TryStartList, TryStartParagraph, Try(Whitespace.SkipUntil(End))));
+
+ private static readonly Parser TextParser =
+ TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
+
+ private static readonly Parser TextControlParser = Try(Map(text =>
+ {
+ var rt = new RichTextLabel
+ {
+ HorizontalExpand = true,
+ Margin = new Thickness(0, 0, 0, 15.0f)
+ };
+
+ var msg = new FormattedMessage();
+ // THANK YOU RICHTEXT VERY COOL
+ // (text doesn't default to white).
+ msg.PushColor(Color.White);
+
+ // If the parsing fails, don't throw an error and instead make an inline error message
+ string? error;
+ if (!msg.TryAddMarkup(text, out error))
+ {
+ Logger.GetSawmill("Guidebook").Error("Failed to parse RichText in Guidebook");
+
+ return new GuidebookError(text, error);
+ }
+
+ msg.Pop();
+ rt.SetMessage(msg);
+ return rt;
+ },
+ TextParser)
+ .Cast())
+ .Labelled("richtext");
+
+ private static readonly Parser HeaderControlParser = Try(Char('#'))
+ .Then(SkipWhitespaces.Then(Map(text => new Label
+ {
+ Text = text,
+ StyleClasses = { "LabelHeadingBigger" }
+ },
+ AnyCharExcept('\n').AtLeastOnceString())
+ .Cast()))
+ .Labelled("header");
+
+ private static readonly Parser SubHeaderControlParser = Try(String("##"))
+ .Then(SkipWhitespaces.Then(Map(text => new Label
+ {
+ Text = text,
+ StyleClasses = { "LabelHeading" }
+ },
+ AnyCharExcept('\n').AtLeastOnceString())
+ .Cast()))
+ .Labelled("subheader");
+
+ private static readonly Parser TryHeaderControl = OneOf(SubHeaderControlParser, HeaderControlParser);
+
+ private static readonly Parser ListControlParser = Try(Char('-'))
+ .Then(SkipWhitespaces)
+ .Then(Map(
+ control => new BoxContainer
+ {
+ Children = { new Label { Text = ListBullet, VerticalAlignment = VAlignment.Top }, control },
+ Orientation = LayoutOrientation.Horizontal
+ },
+ TextControlParser)
+ .Cast())
+ .Labelled("list");
+
+ #region Text Parsing
+
+ #region Basic Text Parsing
+
+ // Try look for an escaped character. If found, skip the escaping slash and return the character.
+
+
+ // like TextChar, but not skipping whitespace around newlines
+
+
// Quoted text
- private static readonly Parser QuotedText = Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
+
#endregion
#region rich text-end markers
- private static readonly Parser TryStartList = Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
- private static readonly Parser TryStartTag = Try(Char('<')).Then(SkipWhitespaces);
- private static readonly Parser TryStartParagraph = Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
- private static readonly Parser TryLookTextEnd = Lookahead(OneOf(TryStartTag, TryStartList, TryStartParagraph, Try(Whitespace.SkipUntil(End))));
+
#endregion
// parses text characters until it hits a text-end
- private static readonly Parser TextParser = TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
- private static readonly Parser TextControlParser = Try(Map(text =>
- {
- var rt = new RichTextLabel()
- {
- HorizontalExpand = true,
- Margin = new Thickness(0, 0, 0, 15.0f),
- };
-
- var msg = new FormattedMessage();
- // THANK YOU RICHTEXT VERY COOL
- // (text doesn't default to white).
- msg.PushColor(Color.White);
- msg.AddMarkup(text);
- msg.Pop();
- rt.SetMessage(msg);
- return rt;
- }, TextParser).Cast()).Labelled("richtext");
#endregion
#region Headers
- private static readonly Parser HeaderControlParser = Try(Char('#')).Then(SkipWhitespaces.Then(Map(text => new Label()
- {
- Text = text,
- StyleClasses = { "LabelHeadingBigger" }
- }, AnyCharExcept('\n').AtLeastOnceString()).Cast())).Labelled("header");
- private static readonly Parser SubHeaderControlParser = Try(String("##")).Then(SkipWhitespaces.Then(Map(text => new Label()
- {
- Text = text,
- StyleClasses = { "LabelHeading" }
- }, AnyCharExcept('\n').AtLeastOnceString()).Cast())).Labelled("subheader");
-
- private static readonly Parser TryHeaderControl = OneOf(SubHeaderControlParser, HeaderControlParser);
#endregion
- // Parser that consumes a - and then just parses normal rich text with some prefix text (a bullet point).
- private static readonly Parser ListControlParser = Try(Char('-')).Then(SkipWhitespaces).Then(Map(
- control => new BoxContainer()
- {
- Children = { new Label() { Text = ListBullet, VerticalAlignment = VAlignment.Top, }, control },
- Orientation = LayoutOrientation.Horizontal,
- }, TextControlParser).Cast()).Labelled("list");
-
#region Tag Parsing
+
// closing brackets for tags
private static readonly Parser TagEnd = Char('>').Then(SkipWhitespaces);
private static readonly Parser ImmediateTagEnd = String("/>").Then(SkipWhitespaces);
@@ -107,20 +158,24 @@ public sealed partial class DocumentParsingManager
private static readonly Parser TryLookTagEnd = Lookahead(OneOf(Try(TagEnd), Try(ImmediateTagEnd)));
//parse tag argument key. any normal text character up until we hit a "="
- private static readonly Parser TagArgKey = LetterOrDigit.Until(Char('=')).Select(string.Concat).Labelled("tag argument key");
+ private static readonly Parser TagArgKey =
+ LetterOrDigit.Until(Char('=')).Select(string.Concat).Labelled("tag argument key");
// parser for a singular tag argument. Note that each TryQuoteOrChar will consume a whole quoted block before the Until() looks for whitespace
- private static readonly Parser TagArgParser = Map((key, value) => (key, value), TagArgKey, QuotedText).Before(SkipWhitespaces);
+ private static readonly Parser TagArgParser =
+ Map((key, value) => (key, value), TagArgKey, QuotedText).Before(SkipWhitespaces);
// parser for all tag arguments
- private static readonly Parser> TagArgsParser = TagArgParser.Until(TryLookTagEnd);
+ private static readonly Parser> TagArgsParser =
+ TagArgParser.Until(TryLookTagEnd);
// parser for an opening tag.
private static readonly Parser TryOpeningTag =
Try(Char('<'))
- .Then(SkipWhitespaces)
- .Then(TextChar.Until(OneOf(Whitespace.SkipAtLeastOnce(), TryLookTagEnd)))
- .Select(string.Concat).Labelled($"opening tag");
+ .Then(SkipWhitespaces)
+ .Then(TextChar.Until(OneOf(Whitespace.SkipAtLeastOnce(), TryLookTagEnd)))
+ .Select(string.Concat)
+ .Labelled("opening tag");
private static Parser> ParseTagArgs(string tag)
{
@@ -138,5 +193,6 @@ private static Parser TryTagTerminator(string tag)
.Then(TagEnd)
.Labelled($"closing {tag} tag");
}
+
#endregion
}
diff --git a/Resources/Locale/en-US/guidebook/guidebook.ftl b/Resources/Locale/en-US/guidebook/guidebook.ftl
index f7f37948f5a4..b6c99766c606 100644
--- a/Resources/Locale/en-US/guidebook/guidebook.ftl
+++ b/Resources/Locale/en-US/guidebook/guidebook.ftl
@@ -3,6 +3,8 @@ guidebook-placeholder-text = Select an entry.
guidebook-placeholder-text-2 = If you're new, head over to "New? Start here!"
guidebook-filter-placeholder-text = Filter items
+guidebook-parser-error = Parser Error
+guidebook-error-message = Error Message
guidebook-monkey-unspin = Unspin Monkey
guidebook-monkey-disco = Disco Monkey