diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76ea7ac..f50598c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,7 @@ Additions
Improvements
- Allow elements to auto-adjust their size even when their children are aligned oddly
- Close other dropdowns when opening a dropdown
+- Generified UiMarkdownParser by adding abstract UiParser
Fixes
- Fixed parents of elements that prevent spill not being notified properly
diff --git a/MLEM.Ui/Parsers/UiMarkdownParser.cs b/MLEM.Ui/Parsers/UiMarkdownParser.cs
index b47a2b9..b8f26ec 100644
--- a/MLEM.Ui/Parsers/UiMarkdownParser.cs
+++ b/MLEM.Ui/Parsers/UiMarkdownParser.cs
@@ -1,20 +1,12 @@
using System;
using System.Collections.Generic;
-using System.IO;
-using System.Net.Http;
using System.Text.RegularExpressions;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
-using MLEM.Formatting;
-using MLEM.Misc;
-using MLEM.Textures;
using MLEM.Ui.Elements;
-using MLEM.Ui.Style;
namespace MLEM.Ui.Parsers {
///
- /// A class for parsing Markdown strings into a set of MLEM.Ui elements with styling for each individual .
- /// To parse, use or . To style the parsed output, use before parsing.
+ /// A class for parsing Markdown strings into a set of MLEM.Ui elements with styling for each individual .
+ /// To parse, use or . To style the parsed output, use before parsing.
///
///
/// Note that this parser is rather rudimentary and doesn't deal well with very complex Markdown documents. Missing features are as follows:
@@ -25,106 +17,18 @@ namespace MLEM.Ui.Parsers {
/// - Tables
///
///
- public class UiMarkdownParser {
-
- private static readonly ElementType[] ElementTypes = EnumHelper.GetValues();
-
- ///
- /// The base path for markdown images, which is prepended to the image link.
- ///
- public string ImageBasePath;
- ///
- /// An action that is invoked when an image fails to load while parsing.
- /// This action receives the expected location of the image, as well as the that occured.
- ///
- public Action ImageExceptionHandler;
- ///
- /// The graphics device that should be used when loading images and other graphics-dependent content.
- ///
- public GraphicsDevice GraphicsDevice;
- ///
- /// The name of the font used for inline code as well as code blocks.
- /// This only has an effect if a font with this name is added to the used 's .
- /// This defaults to "Monospaced" if default styling is applied in .
- ///
- public string CodeFont;
-
- private readonly Dictionary> elementStyles = new Dictionary>();
+ public class UiMarkdownParser : UiParser {
///
/// Creates a new UI markdown parser and optionally initializes some default style settings.
///
/// Whether default style settings should be applied.
- public UiMarkdownParser(bool applyDefaultStyling = true) {
- if (applyDefaultStyling) {
- this.CodeFont = "Monospaced";
- this.Style(ElementType.VerticalSpace, v => v.Size = new Vector2(1, 5));
- for (var i = 0; i < 6; i++) {
- var level = i;
- this.Style(UiMarkdownParser.ElementTypes[Array.IndexOf(UiMarkdownParser.ElementTypes, ElementType.Header1) + i], p => {
- p.Alignment = TextAlignment.Center;
- p.TextScaleMultiplier = 2 - level * 0.15F;
- });
- }
- }
- }
+ public UiMarkdownParser(bool applyDefaultStyling = true) : base(applyDefaultStyling) {}
- ///
- /// Parses the given markdown string into a set of elements (using ) and adds them as children to the givem .
- /// During this process, the element stylings specified using are also applied.
- ///
- /// The markdown to parse.
- /// The element to add the parsed elements to.
- /// The , for chaining.
- public Element ParseInto(string markdown, Element element) {
- foreach (var (_, e) in this.Parse(markdown))
- element.AddChild(e);
- return element;
- }
-
- ///
- /// Parses the given markdown string into a set of elements and returns them along with their .
- /// During this process, the element stylings specified using are also applied.
- ///
- /// The markdown to parse.
- /// The parsed elements.
- public IEnumerable<(ElementType, Element)> Parse(string markdown) {
- foreach (var (t, e) in this.ParseUnstyled(markdown)) {
- if (this.elementStyles.TryGetValue(t, out var style))
- style.Invoke(e);
- yield return (t, e);
- }
- }
-
- ///
- /// Specifies an action to be invoked when a new element with the given is parsed in or .
- /// These actions can be used to modify the style properties of the created elements.
- ///
- /// The element types that should be styled. Can be a combined flag.
- /// The action that styles the elements with the given element type.
- /// Whether the function should be added to the existing style settings, or replace them.
- /// The type of elements that the given flags are expected to be.
- /// This parser, for chaining.
- public UiMarkdownParser Style(ElementType types, Action style, bool add = false) where T : Element {
- foreach (var type in UiMarkdownParser.ElementTypes) {
- if (types.HasFlag(type)) {
- if (add && this.elementStyles.ContainsKey(type)) {
- this.elementStyles[type] += Action;
- } else {
- this.elementStyles[type] = Action;
- }
- }
- }
- return this;
-
- void Action(Element e) {
- style.Invoke(e as T ?? throw new ArgumentException($"Expected {typeof(T)} for style action but got {e.GetType()}"));
- }
- }
-
- private IEnumerable<(ElementType, Element)> ParseUnstyled(string markdown) {
+ ///
+ protected override IEnumerable<(ElementType, Element)> ParseUnstyled(string raw) {
var inCodeBlock = false;
- foreach (var line in markdown.Split('\n')) {
+ foreach (var line in raw.Split('\n')) {
// code blocks
if (line.Trim().StartsWith("```")) {
inCodeBlock = !inCodeBlock;
@@ -151,45 +55,7 @@ namespace MLEM.Ui.Parsers {
// images
var imageMatch = Regex.Match(line, @"!\[\]\(([^)]+)\)");
if (imageMatch.Success) {
- if (this.GraphicsDevice == null)
- throw new NullReferenceException("A markdown parser requires a GraphicsDevice for parsing images");
-
- TextureRegion image = null;
- LoadImageAsync();
- yield return (ElementType.Image, new Image(Anchor.AutoLeft, new Vector2(1, -1), _ => image) {
- OnDisposed = e => image?.Texture.Dispose()
- });
-
- async void LoadImageAsync() {
- var loc = imageMatch.Groups[1].Value;
- // only apply the base path for relative files
- if (this.ImageBasePath != null && !loc.StartsWith("http") && !Path.IsPathRooted(loc))
- loc = $"{this.ImageBasePath}/{loc}";
- try {
- Texture2D tex;
- if (loc.StartsWith("http")) {
- using (var client = new HttpClient()) {
- using (var src = await client.GetStreamAsync(loc)) {
- using (var memory = new MemoryStream()) {
- // download the full stream before passing it to texture
- await src.CopyToAsync(memory);
- tex = Texture2D.FromStream(this.GraphicsDevice, memory);
- }
- }
- }
- } else {
- using (var stream = Path.IsPathRooted(loc) ? File.OpenRead(loc) : TitleContainer.OpenStream(loc))
- tex = Texture2D.FromStream(this.GraphicsDevice, stream);
- }
- image = new TextureRegion(tex);
- } catch (Exception e) {
- if (this.ImageExceptionHandler != null) {
- this.ImageExceptionHandler.Invoke(loc, e);
- } else {
- throw new Exception($"Couldn't parse image {loc}, and no ImageExceptionHandler was set", e);
- }
- }
- }
+ yield return (ElementType.Image, this.ParseImage(imageMatch.Groups[1].Value));
continue;
}
@@ -197,7 +63,7 @@ namespace MLEM.Ui.Parsers {
var parsedHeader = false;
for (var h = 6; h >= 1; h--) {
if (line.StartsWith(new string('#', h))) {
- var type = UiMarkdownParser.ElementTypes[Array.IndexOf(UiMarkdownParser.ElementTypes, ElementType.Header1) + h - 1];
+ var type = UiParser.ElementTypes[Array.IndexOf(UiParser.ElementTypes, ElementType.Header1) + h - 1];
yield return (type, new Paragraph(Anchor.AutoLeft, 1, this.ParseParagraph(line.Substring(h).Trim())));
parsedHeader = true;
break;
@@ -226,75 +92,5 @@ namespace MLEM.Ui.Parsers {
return par;
}
- ///
- /// A flags enumeration used by that contains the types of elements that can be parsed and returned in or .
- /// This is a flags enumeration so that can have multiple element types being styled at the same time.
- ///
- [Flags]
- public enum ElementType {
-
- ///
- /// A blockquote.
- /// This element type is a .
- ///
- Blockquote = 1,
- ///
- /// A vertical space, which is a gap between multiple markdown paragraphs.
- /// This element type is a .
- ///
- VerticalSpace = 2,
- ///
- /// An image.
- /// This element type is an .
- ///
- Image = 4,
- ///
- /// A header with header level 1.
- /// This element type is a .
- ///
- Header1 = 8,
- ///
- /// A header with header level 2.
- /// This element type is a .
- ///
- Header2 = 16,
- ///
- /// A header with header level 3.
- /// This element type is a .
- ///
- Header3 = 32,
- ///
- /// A header with header level 4.
- /// This element type is a .
- ///
- Header4 = 64,
- ///
- /// A header with header level 5.
- /// This element type is a .
- ///
- Header5 = 128,
- ///
- /// A header with header level 6.
- /// This element type is a .
- ///
- Header6 = 256,
- ///
- /// A combined flag that contains through .
- /// This element type is a .
- ///
- Headers = ElementType.Header1 | ElementType.Header2 | ElementType.Header3 | ElementType.Header4 | ElementType.Header5 | ElementType.Header6,
- ///
- /// A paragraph, which is one line of markdown text.
- /// This element type is a .
- ///
- Paragraph = 512,
- ///
- /// A single line of a code block.
- /// This element type is a .
- ///
- CodeBlock = 1024
-
- }
-
}
}
diff --git a/MLEM.Ui/Parsers/UiParser.cs b/MLEM.Ui/Parsers/UiParser.cs
new file mode 100644
index 0000000..570d8e6
--- /dev/null
+++ b/MLEM.Ui/Parsers/UiParser.cs
@@ -0,0 +1,246 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using MLEM.Formatting;
+using MLEM.Misc;
+using MLEM.Textures;
+using MLEM.Ui.Elements;
+using MLEM.Ui.Style;
+
+namespace MLEM.Ui.Parsers {
+ ///
+ /// A base class for parsing various types of formatted strings into a set of MLEM.Ui elements with styling for each individual .
+ /// The only parser currently implemented is .
+ ///
+ public abstract class UiParser {
+
+ ///
+ /// An array containing all of the enum values.
+ ///
+ public static readonly ElementType[] ElementTypes = EnumHelper.GetValues();
+
+ ///
+ /// The base path for images, which is prepended to the image link.
+ ///
+ public string ImageBasePath;
+ ///
+ /// An action that is invoked when an image fails to load while parsing.
+ /// This action receives the expected location of the image, as well as the that occured.
+ ///
+ public Action ImageExceptionHandler;
+ ///
+ /// The graphics device that should be used when loading images and other graphics-dependent content.
+ ///
+ public GraphicsDevice GraphicsDevice;
+ ///
+ /// The name of the font used for inline code as well as code blocks.
+ /// This only has an effect if a font with this name is added to the used 's .
+ /// This defaults to "Monospaced" if default styling is applied in .
+ ///
+ public string CodeFont;
+
+ private readonly Dictionary> elementStyles = new Dictionary>();
+
+ ///
+ /// Creates a new UI parser and optionally initializes some default style settings.
+ ///
+ /// Whether default style settings should be applied.
+ protected UiParser(bool applyDefaultStyling) {
+ if (applyDefaultStyling) {
+ this.CodeFont = "Monospaced";
+ this.Style(ElementType.VerticalSpace, v => v.Size = new Vector2(1, 5));
+ for (var i = 0; i < 6; i++) {
+ var level = i;
+ this.Style(UiParser.ElementTypes[Array.IndexOf(UiParser.ElementTypes, UiParser.ElementType.Header1) + i], p => {
+ p.Alignment = TextAlignment.Center;
+ p.TextScaleMultiplier = 2 - level * 0.15F;
+ });
+ }
+ }
+ }
+
+ ///
+ /// Parses the given raw formatted string into a set of elements and returns them along with their .
+ /// This method is used by implementors to parse specific text, and it is used by and .
+ ///
+ /// The raw string to parse.
+ /// The parsed elements, without styling.
+ protected abstract IEnumerable<(ElementType, Element)> ParseUnstyled(string raw);
+
+ ///
+ /// Parses the given raw formatted string into a set of elements and returns them along with their .
+ /// During this process, the element stylings specified using are also applied.
+ ///
+ /// The raw string to parse.
+ /// The parsed elements.
+ public IEnumerable<(ElementType, Element)> Parse(string raw) {
+ foreach (var (t, e) in this.ParseUnstyled(raw)) {
+ if (this.elementStyles.TryGetValue(t, out var style))
+ style.Invoke(e);
+ yield return (t, e);
+ }
+ }
+
+ ///
+ /// Parses the given raw formatted string into a set of elements (using ) and adds them as children to the givem .
+ /// During this process, the element stylings specified using are also applied.
+ ///
+ /// The raw string to parse.
+ /// The element to add the parsed elements to.
+ /// The , for chaining.
+ public Element ParseInto(string raw, Element element) {
+ foreach (var (_, e) in this.Parse(raw))
+ element.AddChild(e);
+ return element;
+ }
+
+ ///
+ /// Specifies an action to be invoked when a new element with the given is parsed in or .
+ /// These actions can be used to modify the style properties of the created elements.
+ ///
+ /// The element types that should be styled. Can be a combined flag.
+ /// The action that styles the elements with the given element type.
+ /// Whether the function should be added to the existing style settings, or replace them.
+ /// The type of elements that the given flags are expected to be.
+ /// This parser, for chaining.
+ public UiParser Style(ElementType types, Action style, bool add = false) where T : Element {
+ foreach (var type in UiParser.ElementTypes) {
+ if (types.HasFlag(type)) {
+ if (add && this.elementStyles.ContainsKey(type)) {
+ this.elementStyles[type] += Action;
+ } else {
+ this.elementStyles[type] = Action;
+ }
+ }
+ }
+ return this;
+
+ void Action(Element e) {
+ style.Invoke(e as T ?? throw new ArgumentException($"Expected {typeof(T)} for style action but got {e.GetType()}"));
+ }
+ }
+
+ ///
+ /// Parses the given path into a element by loading it from disk or downloading it from the internet.
+ /// Note that, for a that doesn't start with http and isn't rooted, the is prepended automatically.
+ /// This method invokes an asynchronouns action, meaning the 's will likely not have loaded in when this method returns.
+ ///
+ /// The absolute, relative or web path to the image.
+ /// The loaded image.
+ /// Thrown if is null, or if there is an loading the image and is unset.
+ protected Image ParseImage(string path) {
+ if (this.GraphicsDevice == null)
+ throw new NullReferenceException("A UI parser requires a GraphicsDevice for parsing images");
+
+ TextureRegion image = null;
+ LoadImageAsync();
+ return new Image(Anchor.AutoLeft, new Vector2(1, -1), _ => image) {
+ OnDisposed = e => image?.Texture.Dispose()
+ };
+
+ async void LoadImageAsync() {
+ // only apply the base path for relative files
+ if (this.ImageBasePath != null && !path.StartsWith("http") && !Path.IsPathRooted(path))
+ path = $"{this.ImageBasePath}/{path}";
+ try {
+ Texture2D tex;
+ if (path.StartsWith("http")) {
+ using (var client = new HttpClient()) {
+ using (var src = await client.GetStreamAsync(path)) {
+ using (var memory = new MemoryStream()) {
+ // download the full stream before passing it to texture
+ await src.CopyToAsync(memory);
+ tex = Texture2D.FromStream(this.GraphicsDevice, memory);
+ }
+ }
+ }
+ } else {
+ using (var stream = Path.IsPathRooted(path) ? File.OpenRead(path) : TitleContainer.OpenStream(path))
+ tex = Texture2D.FromStream(this.GraphicsDevice, stream);
+ }
+ image = new TextureRegion(tex);
+ } catch (Exception e) {
+ if (this.ImageExceptionHandler != null) {
+ this.ImageExceptionHandler.Invoke(path, e);
+ } else {
+ throw new NullReferenceException($"Couldn't parse image {path}, and no ImageExceptionHandler was set", e);
+ }
+ }
+ }
+ }
+
+ ///
+ /// A flags enumeration used by that contains the types of elements that can be parsed and returned in or .
+ /// This is a flags enumeration so that can have multiple element types being styled at the same time.
+ ///
+ [Flags]
+ public enum ElementType {
+
+ ///
+ /// A blockquote.
+ /// This element type is a .
+ ///
+ Blockquote = 1,
+ ///
+ /// A vertical space, which is a gap between multiple paragraphs.
+ /// This element type is a .
+ ///
+ VerticalSpace = 2,
+ ///
+ /// An image.
+ /// This element type is an .
+ ///
+ Image = 4,
+ ///
+ /// A header with header level 1.
+ /// This element type is a .
+ ///
+ Header1 = 8,
+ ///
+ /// A header with header level 2.
+ /// This element type is a .
+ ///
+ Header2 = 16,
+ ///
+ /// A header with header level 3.
+ /// This element type is a .
+ ///
+ Header3 = 32,
+ ///
+ /// A header with header level 4.
+ /// This element type is a .
+ ///
+ Header4 = 64,
+ ///
+ /// A header with header level 5.
+ /// This element type is a .
+ ///
+ Header5 = 128,
+ ///
+ /// A header with header level 6.
+ /// This element type is a .
+ ///
+ Header6 = 256,
+ ///
+ /// A combined flag that contains through .
+ /// This element type is a .
+ ///
+ Headers = ElementType.Header1 | ElementType.Header2 | ElementType.Header3 | ElementType.Header4 | ElementType.Header5 | ElementType.Header6,
+ ///
+ /// A paragraph, which is one line (or non-vertically spaced section) of text.
+ /// This element type is a .
+ ///
+ Paragraph = 512,
+ ///
+ /// A single line of a code block.
+ /// This element type is a .
+ ///
+ CodeBlock = 1024
+
+ }
+
+ }
+}