using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEngine; using CommonMark; using CommonMark.Syntax; using UnityEngine.UI; using TMPro; using CustomUI.BeatSaber; namespace BSIPA_ModList.UI.ViewControllers { [RequireComponent(/*typeof(ScrollRect),*/ typeof(RectTransform))] public class MarkdownView : MonoBehaviour { private class TagTypeComponent : MonoBehaviour { internal BlockTag Tag; } private string markdown = ""; public string Markdown { get => markdown; set { markdown = value; UpdateMd(); } } public RectTransform rectTransform => GetComponent(); //private ScrollRect view; private RectTransform content; private CommonMarkSettings settings; public MarkdownView() { settings = CommonMarkSettings.Default.Clone(); settings.AdditionalFeatures = CommonMarkAdditionalFeatures.All; settings.RenderSoftLineBreaksAsLineBreaks = false; settings.UriResolver = ResolveUri; } public Func HasEmbeddedImage; private string ResolveUri(string arg) { var name = arg.Substring(3); if (!arg.StartsWith("!::") && !arg.StartsWith("w::")) { // !:: means embedded, w:: means web // this block is for when neither is specified Logger.md.Debug($"Resolving nonspecific URI {arg}"); // check if its embedded if (HasEmbeddedImage != null && HasEmbeddedImage(arg)) return "!::" + arg; else return "w::" + arg; } Logger.md.Debug($"Resolved specific URI {arg}"); return arg; } protected void Awake() { content = new GameObject("Content Wrapper").AddComponent(); content.SetParent(transform); var contentLayout = content.gameObject.AddComponent(); var contentFitter = content.gameObject.AddComponent(); contentFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; contentFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained; contentLayout.preferredWidth = 100f; // to be adjusted content.sizeDelta = new Vector2(100f,100f); /*view = GetComponent(); view.content = content; view.vertical = true; view.horizontal = false; view.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide; view.horizontalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide;*/ } private void UpdateMd() { Clear(); var doc = CommonMarkConverter.Parse(markdown, settings); Stack layout = new Stack(); layout.Push(content); TextMeshProUGUI currentText = null; foreach (var node in doc.AsEnumerable()) { Logger.md.Debug($"node {node}"); if (node.Block != null) { var block = node.Block; void BlockNode(string name, float spacing, bool isVertical) { var type = isVertical ? typeof(VerticalLayoutGroup) : typeof(HorizontalLayoutGroup); if (node.IsOpening) { Logger.md.Debug($"Creating block container {name}"); currentText = null; var go = new GameObject(name, typeof(RectTransform), type); var vlayout = go.GetComponent(); vlayout.SetParent(layout.Peek()); go.AddComponent().Tag = block.Tag; layout.Push(vlayout); if (isVertical) { var vl = go.GetComponent(); vl.childControlHeight = vl.childControlWidth = vl.childForceExpandHeight = vl.childForceExpandWidth = false; vl.spacing = spacing; } else { var hl = go.GetComponent(); hl.childControlHeight = hl.childControlWidth = hl.childForceExpandHeight = hl.childForceExpandWidth = false; hl.spacing = spacing; } } else if (node.IsClosing) { currentText = null; layout.Pop(); } } switch (block.Tag) { case BlockTag.Document: BlockNode("DocumentRoot", 10f, true); break; case BlockTag.SetextHeading: BlockNode("Heading1", .1f, false); break; case BlockTag.AtxHeading: BlockNode("Heading2", .1f, false); break; case BlockTag.Paragraph: BlockNode("Paragraph", .1f, false); break; // TODO: add the rest of the tag types } } else if (node.Inline != null) { // inline element var inl = node.Inline; switch (inl.Tag) { case InlineTag.String: if (currentText == null) { Logger.md.Debug($"Adding new text element"); var btt = layout.Peek().gameObject.GetComponent().Tag; currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero); //var le = currentText.gameObject.AddComponent(); switch (btt) { case BlockTag.List: case BlockTag.ListItem: case BlockTag.Paragraph: currentText.fontSize = 3.5f; currentText.enableWordWrapping = true; break; case BlockTag.AtxHeading: currentText.fontSize = 4f; currentText.enableWordWrapping = true; break; case BlockTag.SetextHeading: currentText.fontSize = 4.5f; currentText.enableWordWrapping = true; break; // TODO: add other relevant types } } Logger.md.Debug($"Appending '{inl.LiteralContent}' to current element"); currentText.text += inl.LiteralContent; break; } } } } private void Clear() { foreach (Transform child in content) Destroy(child.gameObject); } } }