--- /dev/null
+<BoxContainer xmlns="https://spacestation14.io"
+ xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+ Margin="5 5 5 5"
+ MinHeight="200">
+ <PanelContainer HorizontalExpand="True">
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BorderThickness="2" BorderColor="White" />
+ </PanelContainer.PanelOverride>
+ <BoxContainer Orientation="Vertical">
+
+ <PanelContainer>
+ <PanelContainer.PanelOverride>
+ <gfx:StyleBoxFlat BorderThickness="0 0 0 1" BackgroundColor="DarkRed" BorderColor="Black" />
+ </PanelContainer.PanelOverride>
+ <Label Margin="5" StyleClasses="bold" Text="{Loc 'guidebook-parser-error'}" />
+ </PanelContainer>
+
+ <OutputPanel Margin="5" MinHeight="75" VerticalExpand="True" Name="Original">
+ <OutputPanel.StyleBoxOverride>
+ <gfx:StyleBoxFlat BorderThickness="0 0 0 1" BorderColor="Gray"
+ ContentMarginLeftOverride="3" ContentMarginRightOverride="3"
+ ContentMarginBottomOverride="3" ContentMarginTopOverride="3" />
+ </OutputPanel.StyleBoxOverride>
+ </OutputPanel>
+
+ <Collapsible Margin="5" MinHeight="75" VerticalExpand="True">
+ <CollapsibleHeading Title="{Loc 'guidebook-error-message' }" />
+ <CollapsibleBody VerticalExpand="True">
+ <OutputPanel Name="Error" VerticalExpand="True" MinHeight="100">
+ <OutputPanel.StyleBoxOverride>
+ <gfx:StyleBoxFlat
+ ContentMarginLeftOverride="3" ContentMarginRightOverride="3"
+ ContentMarginBottomOverride="3" ContentMarginTopOverride="3" />
+ </OutputPanel.StyleBoxOverride>
+ </OutputPanel>
+ </CollapsibleBody>
+ </Collapsible>
+
+ </BoxContainer>
+ </PanelContainer>
+</BoxContainer>
--- /dev/null
+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);
+ }
+}
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;
[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<ProtoId<GuideEntryPrototype>, GuideEntry> _entries = new();
+ private readonly ISawmill _sawmill;
+
public GuidebookWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+ _sawmill = Logger.GetSawmill("Guidebook");
Tree.OnSelectedItemChanged += OnSelectionChanged;
};
}
+ 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)
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}.");
}
}
entry.Children = sortedChildren;
}
+
entries.ExceptWith(entry.Children);
}
+
rootEntries = entries.ToList();
}
.ThenBy(rootEntry => Loc.GetString(rootEntry.Name));
}
- private void RepopulateTree(List<ProtoId<GuideEntryPrototype>>? roots = null, ProtoId<GuideEntryPrototype>? forcedRoot = null)
+ private void RepopulateTree(List<ProtoId<GuideEntryPrototype>>? roots = null,
+ ProtoId<GuideEntryPrototype>? forcedRoot = null)
{
Tree.Clear();
HashSet<ProtoId<GuideEntryPrototype>> 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<GuideEntryPrototype> id, TreeItem? parent, HashSet<ProtoId<GuideEntryPrototype>> addedEntries)
+ private TreeItem? AddEntry(ProtoId<GuideEntryPrototype> id,
+ TreeItem? parent,
+ HashSet<ProtoId<GuideEntryPrototype>> addedEntries)
{
if (!_entries.TryGetValue(id, out var entry))
return null;
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;
element.SetHiddenState(true, SearchBar.Text.Trim());
}
}
-
}
}
using System.Linq;
+using Content.Client.Guidebook.Controls;
using Content.Client.Guidebook.Richtext;
using Content.Shared.Guidebook;
using Pidgin;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Sandboxing;
+using Robust.Shared.Utility;
using static Pidgin.Parser;
namespace Content.Client.Guidebook;
[Dependency] private readonly ISandboxHelper _sandboxHelper = default!;
private readonly Dictionary<string, Parser<char, Control>> _tagControlParsers = new();
- private Parser<char, Control> _tagParser = default!;
private Parser<char, Control> _controlParser = default!;
+
+ private ISawmill _sawmill = default!;
+ private Parser<char, Control> _tagParser = default!;
public Parser<char, IEnumerable<Control>> ControlParser = default!;
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<IDocumentTag>())
{
}
ControlParser = SkipWhitespaces.Then(_controlParser.Many());
+
+ _sawmill = Logger.GetSawmill("Guidebook");
}
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, 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<char, Control> 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<char, Control> 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<char, IEnumerable<Control>> TagContentParser(string tag) =>
- OneOf(
- Try(ImmediateTagEnd).ThenReturn(Enumerable.Empty<Control>()),
- TagEnd.Then(_controlParser.Until(TryTagTerminator(tag)).Labelled($"{tag} children"))
- );
+ private Parser<char, IEnumerable<Control>> TagContentParser(string tag)
+ {
+ return OneOf(
+ Try(ImmediateTagEnd).ThenReturn(Enumerable.Empty<Control>()),
+ TagEnd.Then(_controlParser.Until(TryTagTerminator(tag)).Labelled($"{tag} children"))
+ );
+ }
}
using System.Linq;
+using Content.Client.Guidebook.Controls;
using Pidgin;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
{
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<char, char> 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<char, char> 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<char, Unit> SkipNewline = Whitespace.SkipUntil(Char('\n'));
- private static readonly Parser<char, char> TrySingleNewlineToSpace = Try(SkipNewline).Then(SkipWhitespaces).ThenReturn(' ');
+ private static readonly Parser<char, char> TrySingleNewlineToSpace =
+ Try(SkipNewline).Then(SkipWhitespaces).ThenReturn(' ');
private static readonly Parser<char, char> 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<char, char> QuotedTextChar = OneOf(TryEscapedChar, Any);
+ private static readonly Parser<char, string> QuotedText =
+ Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
+
+ private static readonly Parser<char, Unit> TryStartList =
+ Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
+
+ private static readonly Parser<char, Unit> TryStartTag = Try(Char('<')).Then(SkipWhitespaces);
+
+ private static readonly Parser<char, Unit> TryStartParagraph =
+ Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
+
+ private static readonly Parser<char, Unit> TryLookTextEnd =
+ Lookahead(OneOf(TryStartTag, TryStartList, TryStartParagraph, Try(Whitespace.SkipUntil(End))));
+
+ private static readonly Parser<char, string> TextParser =
+ TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
+
+ private static readonly Parser<char, Control> TextControlParser = Try(Map<char, string, Control>(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<Control>())
+ .Labelled("richtext");
+
+ private static readonly Parser<char, Control> HeaderControlParser = Try(Char('#'))
+ .Then(SkipWhitespaces.Then(Map(text => new Label
+ {
+ Text = text,
+ StyleClasses = { "LabelHeadingBigger" }
+ },
+ AnyCharExcept('\n').AtLeastOnceString())
+ .Cast<Control>()))
+ .Labelled("header");
+
+ private static readonly Parser<char, Control> SubHeaderControlParser = Try(String("##"))
+ .Then(SkipWhitespaces.Then(Map(text => new Label
+ {
+ Text = text,
+ StyleClasses = { "LabelHeading" }
+ },
+ AnyCharExcept('\n').AtLeastOnceString())
+ .Cast<Control>()))
+ .Labelled("subheader");
+
+ private static readonly Parser<char, Control> TryHeaderControl = OneOf(SubHeaderControlParser, HeaderControlParser);
+
+ private static readonly Parser<char, Control> ListControlParser = Try(Char('-'))
+ .Then(SkipWhitespaces)
+ .Then(Map(
+ control => new BoxContainer
+ {
+ Children = { new Label { Text = ListBullet, VerticalAlignment = VAlignment.Top }, control },
+ Orientation = LayoutOrientation.Horizontal
+ },
+ TextControlParser)
+ .Cast<Control>())
+ .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<char, string> QuotedText = Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
+
#endregion
#region rich text-end markers
- private static readonly Parser<char, Unit> TryStartList = Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
- private static readonly Parser<char, Unit> TryStartTag = Try(Char('<')).Then(SkipWhitespaces);
- private static readonly Parser<char, Unit> TryStartParagraph = Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
- private static readonly Parser<char, Unit> TryLookTextEnd = Lookahead(OneOf(TryStartTag, TryStartList, TryStartParagraph, Try(Whitespace.SkipUntil(End))));
+
#endregion
// parses text characters until it hits a text-end
- private static readonly Parser<char, string> TextParser = TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
- private static readonly Parser<char, Control> 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<Control>()).Labelled("richtext");
#endregion
#region Headers
- private static readonly Parser<char, Control> HeaderControlParser = Try(Char('#')).Then(SkipWhitespaces.Then(Map(text => new Label()
- {
- Text = text,
- StyleClasses = { "LabelHeadingBigger" }
- }, AnyCharExcept('\n').AtLeastOnceString()).Cast<Control>())).Labelled("header");
- private static readonly Parser<char, Control> SubHeaderControlParser = Try(String("##")).Then(SkipWhitespaces.Then(Map(text => new Label()
- {
- Text = text,
- StyleClasses = { "LabelHeading" }
- }, AnyCharExcept('\n').AtLeastOnceString()).Cast<Control>())).Labelled("subheader");
-
- private static readonly Parser<char, Control> 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<char, Control> ListControlParser = Try(Char('-')).Then(SkipWhitespaces).Then(Map(
- control => new BoxContainer()
- {
- Children = { new Label() { Text = ListBullet, VerticalAlignment = VAlignment.Top, }, control },
- Orientation = LayoutOrientation.Horizontal,
- }, TextControlParser).Cast<Control>()).Labelled("list");
-
#region Tag Parsing
+
// closing brackets for tags
private static readonly Parser<char, Unit> TagEnd = Char('>').Then(SkipWhitespaces);
private static readonly Parser<char, Unit> ImmediateTagEnd = String("/>").Then(SkipWhitespaces);
private static readonly Parser<char, Unit> TryLookTagEnd = Lookahead(OneOf(Try(TagEnd), Try(ImmediateTagEnd)));
//parse tag argument key. any normal text character up until we hit a "="
- private static readonly Parser<char, string> TagArgKey = LetterOrDigit.Until(Char('=')).Select(string.Concat).Labelled("tag argument key");
+ private static readonly Parser<char, string> 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<char, (string, string)> TagArgParser = Map((key, value) => (key, value), TagArgKey, QuotedText).Before(SkipWhitespaces);
+ private static readonly Parser<char, (string, string)> TagArgParser =
+ Map((key, value) => (key, value), TagArgKey, QuotedText).Before(SkipWhitespaces);
// parser for all tag arguments
- private static readonly Parser<char, IEnumerable<(string, string)>> TagArgsParser = TagArgParser.Until(TryLookTagEnd);
+ private static readonly Parser<char, IEnumerable<(string, string)>> TagArgsParser =
+ TagArgParser.Until(TryLookTagEnd);
// parser for an opening tag.
private static readonly Parser<char, string> 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<char, Dictionary<string, string>> ParseTagArgs(string tag)
{
.Then(TagEnd)
.Labelled($"closing {tag} tag");
}
+
#endregion
}
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