Create In-Guidebook Errors (#28942)
* Create in-guidebook errors * Localize client-facing parser error * Uncomment line * Missed another localization string
This commit is contained in:
41
Content.Client/Guidebook/Controls/GuidebookError.xaml
Normal file
41
Content.Client/Guidebook/Controls/GuidebookError.xaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<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>
|
||||||
23
Content.Client/Guidebook/Controls/GuidebookError.xaml.cs
Normal file
23
Content.Client/Guidebook/Controls/GuidebookError.xaml.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,10 @@ using Content.Client.UserInterface.ControlExtensions;
|
|||||||
using Content.Client.UserInterface.Controls;
|
using Content.Client.UserInterface.Controls;
|
||||||
using Content.Client.UserInterface.Controls.FancyTree;
|
using Content.Client.UserInterface.Controls.FancyTree;
|
||||||
using Content.Client.UserInterface.Systems.Info;
|
using Content.Client.UserInterface.Systems.Info;
|
||||||
using Content.Shared.CCVar;
|
|
||||||
using Content.Shared.Guidebook;
|
using Content.Shared.Guidebook;
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
using Robust.Client.UserInterface.XAML;
|
using Robust.Client.UserInterface.XAML;
|
||||||
using Robust.Shared.Configuration;
|
|
||||||
using Robust.Shared.ContentPack;
|
using Robust.Shared.ContentPack;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
@@ -18,15 +16,18 @@ namespace Content.Client.Guidebook.Controls;
|
|||||||
[GenerateTypedNameReferences]
|
[GenerateTypedNameReferences]
|
||||||
public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
|
||||||
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
|
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
|
||||||
|
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||||
|
|
||||||
private Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> _entries = new();
|
private Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> _entries = new();
|
||||||
|
|
||||||
|
private readonly ISawmill _sawmill;
|
||||||
|
|
||||||
public GuidebookWindow()
|
public GuidebookWindow()
|
||||||
{
|
{
|
||||||
RobustXamlLoader.Load(this);
|
RobustXamlLoader.Load(this);
|
||||||
IoCManager.InjectDependencies(this);
|
IoCManager.InjectDependencies(this);
|
||||||
|
_sawmill = Logger.GetSawmill("Guidebook");
|
||||||
|
|
||||||
Tree.OnSelectedItemChanged += OnSelectionChanged;
|
Tree.OnSelectedItemChanged += OnSelectionChanged;
|
||||||
|
|
||||||
@@ -36,6 +37,20 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private void OnSelectionChanged(TreeItem? item)
|
||||||
{
|
{
|
||||||
if (item != null && item.Metadata is GuideEntry entry)
|
if (item != null && item.Metadata is GuideEntry entry)
|
||||||
@@ -71,8 +86,9 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
|||||||
|
|
||||||
if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd()))
|
if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd()))
|
||||||
{
|
{
|
||||||
EntryContainer.AddChild(new Label() { Text = "ERROR: Failed to parse document." });
|
// The guidebook will automatically display the in-guidebook error if it fails
|
||||||
Logger.Error($"Failed to parse contents of guide document {entry.Id}.");
|
|
||||||
|
_sawmill.Error($"Failed to parse contents of guide document {entry.Id}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +140,10 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
|||||||
|
|
||||||
entry.Children = sortedChildren;
|
entry.Children = sortedChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.ExceptWith(entry.Children);
|
entries.ExceptWith(entry.Children);
|
||||||
}
|
}
|
||||||
|
|
||||||
rootEntries = entries.ToList();
|
rootEntries = entries.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,21 +153,25 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
|||||||
.ThenBy(rootEntry => Loc.GetString(rootEntry.Name));
|
.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();
|
Tree.Clear();
|
||||||
|
|
||||||
HashSet<ProtoId<GuideEntryPrototype>> addedEntries = new();
|
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))
|
foreach (var entry in GetSortedEntries(roots))
|
||||||
{
|
{
|
||||||
AddEntry(entry.Id, parent, addedEntries);
|
AddEntry(entry.Id, parent, addedEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
Tree.SetAllExpanded(true);
|
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))
|
if (!_entries.TryGetValue(id, out var entry))
|
||||||
return null;
|
return null;
|
||||||
@@ -179,22 +201,6 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
|||||||
return item;
|
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()
|
private void HandleFilter()
|
||||||
{
|
{
|
||||||
var emptySearch = SearchBar.Text.Trim().Length == 0;
|
var emptySearch = SearchBar.Text.Trim().Length == 0;
|
||||||
@@ -208,6 +214,5 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
|||||||
element.SetHiddenState(true, SearchBar.Text.Trim());
|
element.SetHiddenState(true, SearchBar.Text.Trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Client.Guidebook.Controls;
|
||||||
using Content.Client.Guidebook.Richtext;
|
using Content.Client.Guidebook.Richtext;
|
||||||
using Content.Shared.Guidebook;
|
using Content.Shared.Guidebook;
|
||||||
using Pidgin;
|
using Pidgin;
|
||||||
@@ -7,6 +8,7 @@ using Robust.Shared.ContentPack;
|
|||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Reflection;
|
using Robust.Shared.Reflection;
|
||||||
using Robust.Shared.Sandboxing;
|
using Robust.Shared.Sandboxing;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
using static Pidgin.Parser;
|
using static Pidgin.Parser;
|
||||||
|
|
||||||
namespace Content.Client.Guidebook;
|
namespace Content.Client.Guidebook;
|
||||||
@@ -22,8 +24,10 @@ public sealed partial class DocumentParsingManager
|
|||||||
[Dependency] private readonly ISandboxHelper _sandboxHelper = default!;
|
[Dependency] private readonly ISandboxHelper _sandboxHelper = default!;
|
||||||
|
|
||||||
private readonly Dictionary<string, Parser<char, Control>> _tagControlParsers = new();
|
private readonly Dictionary<string, Parser<char, Control>> _tagControlParsers = new();
|
||||||
private Parser<char, Control> _tagParser = default!;
|
|
||||||
private Parser<char, Control> _controlParser = 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 Parser<char, IEnumerable<Control>> ControlParser = default!;
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
@@ -32,7 +36,8 @@ public sealed partial class DocumentParsingManager
|
|||||||
.Assert(_tagControlParsers.ContainsKey, tag => $"unknown tag: {tag}")
|
.Assert(_tagControlParsers.ContainsKey, tag => $"unknown tag: {tag}")
|
||||||
.Bind(tag => _tagControlParsers[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>())
|
foreach (var typ in _reflectionManager.GetAllChildren<IDocumentTag>())
|
||||||
{
|
{
|
||||||
@@ -40,6 +45,8 @@ public sealed partial class DocumentParsingManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
ControlParser = SkipWhitespaces.Then(_controlParser.Many());
|
ControlParser = SkipWhitespaces.Then(_controlParser.Many());
|
||||||
|
|
||||||
|
_sawmill = Logger.GetSawmill("Guidebook");
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, bool log = true)
|
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, bool log = true)
|
||||||
@@ -68,37 +75,57 @@ public sealed partial class DocumentParsingManager
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
if (log)
|
_sawmill.Error($"Encountered error while generating markup controls: {e}");
|
||||||
Logger.Error($"Encountered error while generating markup controls: {e}");
|
|
||||||
|
control.AddChild(new GuidebookError(text, e.ToStringBetter()));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Parser<char, Control> CreateTagControlParser(string tagId, Type tagType, ISandboxHelper sandbox) => Map(
|
private Parser<char, Control> CreateTagControlParser(string tagId, Type tagType, ISandboxHelper sandbox)
|
||||||
|
{
|
||||||
|
return Map(
|
||||||
(args, controls) =>
|
(args, controls) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var tag = (IDocumentTag) sandbox.CreateInstance(tagType);
|
var tag = (IDocumentTag) sandbox.CreateInstance(tagType);
|
||||||
if (!tag.TryParseTag(args, out var control))
|
if (!tag.TryParseTag(args, out var control))
|
||||||
{
|
{
|
||||||
Logger.Error($"Failed to parse {tagId} args");
|
_sawmill.Error($"Failed to parse {tagId} args");
|
||||||
return new Control();
|
return new GuidebookError(args.ToString() ?? tagId, $"Failed to parse {tagId} args");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var child in controls)
|
foreach (var child in controls)
|
||||||
{
|
{
|
||||||
control.AddChild(child);
|
control.AddChild(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
return control;
|
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),
|
ParseTagArgs(tagId),
|
||||||
TagContentParser(tagId)).Labelled($"{tagId} control");
|
TagContentParser(tagId))
|
||||||
|
.Labelled($"{tagId} control");
|
||||||
|
}
|
||||||
|
|
||||||
// Parse a bunch of controls until we encounter a matching closing tag.
|
// Parse a bunch of controls until we encounter a matching closing tag.
|
||||||
private Parser<char, IEnumerable<Control>> TagContentParser(string tag) =>
|
private Parser<char, IEnumerable<Control>> TagContentParser(string tag)
|
||||||
OneOf(
|
{
|
||||||
|
return OneOf(
|
||||||
Try(ImmediateTagEnd).ThenReturn(Enumerable.Empty<Control>()),
|
Try(ImmediateTagEnd).ThenReturn(Enumerable.Empty<Control>()),
|
||||||
TagEnd.Then(_controlParser.Until(TryTagTerminator(tag)).Labelled($"{tag} children"))
|
TagEnd.Then(_controlParser.Until(TryTagTerminator(tag)).Labelled($"{tag} children"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Client.Guidebook.Controls;
|
||||||
using Pidgin;
|
using Pidgin;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
@@ -14,10 +15,9 @@ public sealed partial class DocumentParsingManager
|
|||||||
{
|
{
|
||||||
private const string ListBullet = " › ";
|
private const string ListBullet = " › ";
|
||||||
|
|
||||||
#region Text Parsing
|
// Parser that consumes a - and then just parses normal rich text with some prefix text (a bullet point).
|
||||||
#region Basic Text Parsing
|
private static readonly Parser<char, char> TryEscapedChar = Try(Char('\\')
|
||||||
// Try look for an escaped character. If found, skip the escaping slash and return the character.
|
.Then(OneOf(
|
||||||
private static readonly Parser<char, char> TryEscapedChar = Try(Char('\\').Then(OneOf(
|
|
||||||
Try(Char('<')),
|
Try(Char('<')),
|
||||||
Try(Char('>')),
|
Try(Char('>')),
|
||||||
Try(Char('\\')),
|
Try(Char('\\')),
|
||||||
@@ -31,7 +31,8 @@ public sealed partial class DocumentParsingManager
|
|||||||
|
|
||||||
private static readonly Parser<char, Unit> SkipNewline = Whitespace.SkipUntil(Char('\n'));
|
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(
|
private static readonly Parser<char, char> TextChar = OneOf(
|
||||||
TryEscapedChar, // consume any backslashed being used to escape text
|
TryEscapedChar, // consume any backslashed being used to escape text
|
||||||
@@ -39,67 +40,117 @@ public sealed partial class DocumentParsingManager
|
|||||||
Any // just return the character.
|
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, char> QuotedTextChar = OneOf(TryEscapedChar, Any);
|
||||||
|
|
||||||
// Quoted text
|
private static readonly Parser<char, string> QuotedText =
|
||||||
private static readonly Parser<char, string> QuotedText = Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
|
Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
|
||||||
#endregion
|
|
||||||
|
private static readonly Parser<char, Unit> TryStartList =
|
||||||
|
Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
|
||||||
|
|
||||||
#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> 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, Unit> TryStartParagraph =
|
||||||
private static readonly Parser<char, string> TextParser = TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
|
Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
|
||||||
|
|
||||||
private static readonly Parser<char, Control> TextControlParser = Try(Map(text =>
|
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()
|
var rt = new RichTextLabel
|
||||||
{
|
{
|
||||||
HorizontalExpand = true,
|
HorizontalExpand = true,
|
||||||
Margin = new Thickness(0, 0, 0, 15.0f),
|
Margin = new Thickness(0, 0, 0, 15.0f)
|
||||||
};
|
};
|
||||||
|
|
||||||
var msg = new FormattedMessage();
|
var msg = new FormattedMessage();
|
||||||
// THANK YOU RICHTEXT VERY COOL
|
// THANK YOU RICHTEXT VERY COOL
|
||||||
// (text doesn't default to white).
|
// (text doesn't default to white).
|
||||||
msg.PushColor(Color.White);
|
msg.PushColor(Color.White);
|
||||||
msg.AddMarkup(text);
|
|
||||||
|
// 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();
|
msg.Pop();
|
||||||
rt.SetMessage(msg);
|
rt.SetMessage(msg);
|
||||||
return rt;
|
return rt;
|
||||||
}, TextParser).Cast<Control>()).Labelled("richtext");
|
},
|
||||||
#endregion
|
TextParser)
|
||||||
|
.Cast<Control>())
|
||||||
|
.Labelled("richtext");
|
||||||
|
|
||||||
#region Headers
|
private static readonly Parser<char, Control> HeaderControlParser = Try(Char('#'))
|
||||||
private static readonly Parser<char, Control> HeaderControlParser = Try(Char('#')).Then(SkipWhitespaces.Then(Map(text => new Label()
|
.Then(SkipWhitespaces.Then(Map(text => new Label
|
||||||
{
|
{
|
||||||
Text = text,
|
Text = text,
|
||||||
StyleClasses = { "LabelHeadingBigger" }
|
StyleClasses = { "LabelHeadingBigger" }
|
||||||
}, AnyCharExcept('\n').AtLeastOnceString()).Cast<Control>())).Labelled("header");
|
},
|
||||||
|
AnyCharExcept('\n').AtLeastOnceString())
|
||||||
|
.Cast<Control>()))
|
||||||
|
.Labelled("header");
|
||||||
|
|
||||||
private static readonly Parser<char, Control> SubHeaderControlParser = Try(String("##")).Then(SkipWhitespaces.Then(Map(text => new Label()
|
private static readonly Parser<char, Control> SubHeaderControlParser = Try(String("##"))
|
||||||
|
.Then(SkipWhitespaces.Then(Map(text => new Label
|
||||||
{
|
{
|
||||||
Text = text,
|
Text = text,
|
||||||
StyleClasses = { "LabelHeading" }
|
StyleClasses = { "LabelHeading" }
|
||||||
}, AnyCharExcept('\n').AtLeastOnceString()).Cast<Control>())).Labelled("subheader");
|
},
|
||||||
|
AnyCharExcept('\n').AtLeastOnceString())
|
||||||
|
.Cast<Control>()))
|
||||||
|
.Labelled("subheader");
|
||||||
|
|
||||||
private static readonly Parser<char, Control> TryHeaderControl = OneOf(SubHeaderControlParser, HeaderControlParser);
|
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
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
// Parser that consumes a - and then just parses normal rich text with some prefix text (a bullet point).
|
#region rich text-end markers
|
||||||
private static readonly Parser<char, Control> ListControlParser = Try(Char('-')).Then(SkipWhitespaces).Then(Map(
|
|
||||||
control => new BoxContainer()
|
#endregion
|
||||||
{
|
|
||||||
Children = { new Label() { Text = ListBullet, VerticalAlignment = VAlignment.Top, }, control },
|
// parses text characters until it hits a text-end
|
||||||
Orientation = LayoutOrientation.Horizontal,
|
|
||||||
}, TextControlParser).Cast<Control>()).Labelled("list");
|
#endregion
|
||||||
|
|
||||||
|
#region Headers
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Tag Parsing
|
#region Tag Parsing
|
||||||
|
|
||||||
// closing brackets for tags
|
// closing brackets for tags
|
||||||
private static readonly Parser<char, Unit> TagEnd = Char('>').Then(SkipWhitespaces);
|
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> ImmediateTagEnd = String("/>").Then(SkipWhitespaces);
|
||||||
@@ -107,20 +158,24 @@ public sealed partial class DocumentParsingManager
|
|||||||
private static readonly Parser<char, Unit> TryLookTagEnd = Lookahead(OneOf(Try(TagEnd), Try(ImmediateTagEnd)));
|
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 "="
|
//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
|
// 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
|
// 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.
|
// parser for an opening tag.
|
||||||
private static readonly Parser<char, string> TryOpeningTag =
|
private static readonly Parser<char, string> TryOpeningTag =
|
||||||
Try(Char('<'))
|
Try(Char('<'))
|
||||||
.Then(SkipWhitespaces)
|
.Then(SkipWhitespaces)
|
||||||
.Then(TextChar.Until(OneOf(Whitespace.SkipAtLeastOnce(), TryLookTagEnd)))
|
.Then(TextChar.Until(OneOf(Whitespace.SkipAtLeastOnce(), TryLookTagEnd)))
|
||||||
.Select(string.Concat).Labelled($"opening tag");
|
.Select(string.Concat)
|
||||||
|
.Labelled("opening tag");
|
||||||
|
|
||||||
private static Parser<char, Dictionary<string, string>> ParseTagArgs(string tag)
|
private static Parser<char, Dictionary<string, string>> ParseTagArgs(string tag)
|
||||||
{
|
{
|
||||||
@@ -138,5 +193,6 @@ public sealed partial class DocumentParsingManager
|
|||||||
.Then(TagEnd)
|
.Then(TagEnd)
|
||||||
.Labelled($"closing {tag} tag");
|
.Labelled($"closing {tag} tag");
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-placeholder-text-2 = If you're new, head over to "New? Start here!"
|
||||||
guidebook-filter-placeholder-text = Filter items
|
guidebook-filter-placeholder-text = Filter items
|
||||||
|
|
||||||
|
guidebook-parser-error = Parser Error
|
||||||
|
guidebook-error-message = Error Message
|
||||||
|
|
||||||
guidebook-monkey-unspin = Unspin Monkey
|
guidebook-monkey-unspin = Unspin Monkey
|
||||||
guidebook-monkey-disco = Disco Monkey
|
guidebook-monkey-disco = Disco Monkey
|
||||||
|
|||||||
Reference in New Issue
Block a user