From 2d3d93c610c7b3efaa8e30058e45e3ef34565976 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Sun, 4 Sep 2022 12:26:55 +0200 Subject: [PATCH] Generified UiMarkdownParser by adding abstract UiParser --- CHANGELOG.md | 1 + MLEM.Ui/Parsers/UiMarkdownParser.cs | 222 +------------------------ MLEM.Ui/Parsers/UiParser.cs | 246 ++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+), 213 deletions(-) create mode 100644 MLEM.Ui/Parsers/UiParser.cs 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 + + } + + } +}