using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using CommonMark; using CommonMark.Syntax; using UnityEngine.UI; using TMPro; using CustomUI.BeatSaber; using IPA.Utilities; using System.Reflection; using UnityEngine.EventSystems; using System.Diagnostics; using System.Collections; using System.IO; using System.Text; using System.Text.RegularExpressions; namespace BSIPA_ModList.UI.ViewControllers { /// /// A UI component that renders Markdown in-game. /// [RequireComponent(typeof(RectTransform))] public class MarkdownView : MonoBehaviour { private class TagTypeComponent : MonoBehaviour { internal BlockTag Tag; internal HeadingData hData; internal ListData lData; internal int listCount; internal int listIndex; } private string markdown = ""; private bool mdDirty = false; public string Markdown { get => markdown; set { markdown = value; mdDirty = true; } } /// /// A convenience property to access the on the this is on. /// /// the associated with this component public RectTransform rectTransform => GetComponent(); private ScrollView scrView; private RectTransform content; private RectTransform viewport; private CommonMarkSettings settings; /// /// Creates a new . Should never be called correctly. Instead, use . /// public MarkdownView() { settings = CommonMarkSettings.Default.Clone(); settings.AdditionalFeatures = CommonMarkAdditionalFeatures.All; settings.RenderSoftLineBreaksAsLineBreaks = false; settings.UriResolver = ResolveUri; } /// /// This function will be called whenever attempting to resolve an image URI, to ensure that the image exists in the embedded assembly. /// /// a delegate for the function to call 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; } private static string GetLinkUri(string uri) { if (uri[0] == '!') { Logger.md.Error($"Cannot link to embedded resource in mod description"); return null; } else return uri.Substring(3); } private static Stream ConsolasAssetBundleFontStream => Assembly.GetExecutingAssembly().GetManifestResourceStream("BSIPA_ModList.Bundles.consolas.font"); private static AssetBundleCreateRequest _bundleRequest; private static AssetBundle _bundle; private static AssetBundle Bundle { get { if (_bundle == null && _bundleRequest != null) throw new InvalidOperationException("Asset bundle is being loaded asynchronously; please wait for that to complete"); if (_bundle == null) _bundle = AssetBundle.LoadFromStream(ConsolasAssetBundleFontStream); return _bundle; } } private static AssetBundleRequest _consolasRequest; private static TMP_FontAsset _unsetConsolas; private static TMP_FontAsset _consolas; private static TMP_FontAsset Consolas { get { if (_unsetConsolas == null && _consolasRequest != null) throw new InvalidOperationException("Asset is being loaded asynchronously; please wait for that to complete"); if (_unsetConsolas == null) _unsetConsolas = Bundle?.LoadAsset("CONSOLAS"); if (_consolas == null && _unsetConsolas != null) _consolas = SetupFont(_unsetConsolas); return _consolas; } } private static TMP_FontAsset SetupFont(TMP_FontAsset f) { var originalFont = Resources.FindObjectsOfTypeAll().Last(f2 => f2.name == "Teko-Medium SDF No Glow"); var matCopy = Instantiate(originalFont.material); matCopy.mainTexture = f.material.mainTexture; matCopy.mainTextureOffset = f.material.mainTextureOffset; matCopy.mainTextureScale = f.material.mainTextureScale; f.material = matCopy; f = Instantiate(f); MaterialReferenceManager.AddFontAsset(f); return f; } internal static void StartLoadResourcesAsync() { SharedCoroutineStarter.instance.StartCoroutine(LoadResourcesAsync()); } private static IEnumerator LoadResourcesAsync() { Logger.md.Debug("Starting to load resources"); _bundleRequest = AssetBundle.LoadFromStreamAsync(ConsolasAssetBundleFontStream); yield return _bundleRequest; _bundle = _bundleRequest.assetBundle; Logger.md.Debug("Bundle loaded"); _consolasRequest = _bundle.LoadAssetAsync("CONSOLAS"); yield return _consolasRequest; _unsetConsolas = _consolasRequest.asset as TMP_FontAsset; Logger.md.Debug("Font loaded"); } internal void Awake() { if (Consolas == null) Logger.md.Error($"Loading of Consolas font failed"); gameObject.SetActive(false); var vpgo = new GameObject("Viewport"); viewport = vpgo.AddComponent(); viewport.SetParent(transform); viewport.localPosition = Vector2.zero; viewport.anchorMin = Vector2.zero; viewport.anchorMax = Vector2.one; viewport.anchoredPosition = new Vector2(.5f, .5f); viewport.sizeDelta = Vector2.zero; var vpmask = vpgo.AddComponent(); var vpim = vpgo.AddComponent(); // supposedly Mask needs an Image? vpmask.showMaskGraphic = false; vpim.color = Color.white; vpim.sprite = WhitePixel; vpim.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial; content = new GameObject("Content Wrapper").AddComponent(); content.SetParent(viewport); content.gameObject.AddComponent(); content.localPosition = Vector2.zero; content.anchorMin = new Vector2(0f, 1f); content.anchorMax = new Vector2(1f, 1f); content.anchoredPosition = Vector2.zero; var contentLayout = content.gameObject.AddComponent(); var contentFitter = content.gameObject.AddComponent(); contentFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; contentLayout.childControlHeight = true; contentLayout.childControlWidth = false; contentLayout.childForceExpandHeight = false; contentLayout.childForceExpandWidth = true; contentLayout.childAlignment = TextAnchor.UpperCenter; contentLayout.spacing = 0f; var pageUp = Instantiate(Resources.FindObjectsOfTypeAll