using System; using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Formatting; using MLEM.Textures; using MLEM.Ui.Elements; using MLEM.Ui.Style; #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER using System.Net.Http; #else using System.Net; #endif 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 = #if NET6_0_OR_GREATER Enum.GetValues(); #else (ElementType[]) Enum.GetValues(typeof(ElementType)); #endif /// /// 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 similarly to . /// /// 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 rather than replacing 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. /// An action that is invoked with the loaded image once it is fetched. Note that this action will be invoked synchronously. /// The loaded image. /// Thrown if is null, or if there is an loading the image and is unset. protected Image ParseImage(string path, Action onImageFetched = null) { if (this.GraphicsDevice == null) throw new NullReferenceException("A UI parser requires a GraphicsDevice for parsing images"); var bytesLock = new object(); byte[] bytes = null; TextureRegion image = null; return new Image(Anchor.AutoLeft, Vector2.One, _ => { if (image == null) { bool bytesNull; lock (bytesLock) bytesNull = bytes == null; if (!bytesNull) { Texture2D tex = null; lock (bytesLock) { try { using (var stream = new MemoryStream(bytes)) { using (var read = Texture2D.FromStream(this.GraphicsDevice, stream)) tex = read.PremultipliedCopy(); } } catch (Exception e) { CatchOrRethrow(e); } bytes = null; } if (tex != null) { image = new TextureRegion(tex); onImageFetched?.Invoke(image); } } } return image; }) { SetHeightBasedOnAspect = true, OnAddedToUi = e => { if (image == null) { bool bytesNull; lock (bytesLock) bytesNull = bytes == null; if (bytesNull) LoadImageStream(); } }, OnRemovedFromUi = e => { lock (bytesLock) bytes = null; image?.Texture.Dispose(); image = null; } }; async void LoadImageStream() { // only apply the base path for relative files if (this.ImageBasePath != null && !path.StartsWith("http") && !Path.IsPathRooted(path)) path = $"{this.ImageBasePath}/{path}"; try { byte[] src; if (path.StartsWith("http")) { #if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER using (var client = new HttpClient()) src = await client.GetByteArrayAsync(path); #else using (var client = new WebClient()) src = await client.DownloadDataTaskAsync(path); #endif } else { using (var fileStream = Path.IsPathRooted(path) ? File.OpenRead(path) : TitleContainer.OpenStream(path)) { using (var memStream = new MemoryStream()) { await fileStream.CopyToAsync(memStream); src = memStream.ToArray(); } } } lock (bytesLock) bytes = src; } catch (Exception e) { CatchOrRethrow(e); } } void CatchOrRethrow(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 } } }