using System; using System.Collections.Generic; using System.Text.RegularExpressions; using MLEM.Ui.Elements; 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. /// /// /// Note that this parser is rather rudimentary and doesn't deal well with very complex Markdown documents. Missing features are as follows: /// /// Lines that end without a double space are still converted to distinct lines rather than being merged with the next line /// Better list handling, including nested lists /// Horizontal rules /// Tables /// /// 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) : base(applyDefaultStyling) {} /// protected override IEnumerable<(ElementType, Element)> ParseUnstyled(string raw) { var inCodeBlock = false; foreach (var line in raw.Split('\n')) { // code blocks if (line.Trim().StartsWith("```")) { inCodeBlock = !inCodeBlock; continue; } // code block content if (inCodeBlock) { yield return (ElementType.CodeBlock, new Paragraph(Anchor.AutoLeft, 1, $"{line}")); continue; } // quotes if (line.StartsWith(">")) { yield return (ElementType.Blockquote, new Paragraph(Anchor.AutoLeft, 1, this.ParseParagraph(line.Substring(1).Trim()))); continue; } // vertical space (empty lines) if (line.Trim().Length <= 0) { yield return (ElementType.VerticalSpace, new VerticalSpace(0)); continue; } // images var imageMatch = Regex.Match(line, @"!\[\]\(([^)]+)\)"); if (imageMatch.Success) { yield return (ElementType.Image, this.ParseImage(imageMatch.Groups[1].Value)); continue; } // headers var parsedHeader = false; for (var h = 6; h >= 1; h--) { if (line.StartsWith(new string('#', h))) { 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; } } if (parsedHeader) continue; // parse everything else as a paragraph (with formatting) yield return (ElementType.Paragraph, new Paragraph(Anchor.AutoLeft, 1, this.ParseParagraph(line))); } } private string ParseParagraph(string par) { // replace links par = Regex.Replace(par, @"<([^>]+)>", "$1"); par = Regex.Replace(par, @"\[([^\]]+)\]\(([^)]+)\)", "$1"); // replace formatting par = Regex.Replace(par, @"\*\*([^\*]+)\*\*", "$1"); par = Regex.Replace(par, @"__([^_]+)__", "$1"); par = Regex.Replace(par, @"\*([^\*]+)\*", "$1"); par = Regex.Replace(par, @"_([^_]+)_", "$1"); par = Regex.Replace(par, @"~~([^~]+)~~", "$1"); // replace inline code with custom code font par = Regex.Replace(par, @"`([^`]+)`", $"$1"); return par; } } }