You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

824 lines
38 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. using CommonMark;
  6. using CommonMark.Syntax;
  7. using UnityEngine.UI;
  8. using TMPro;
  9. using CustomUI.BeatSaber;
  10. using IPA.Utilities;
  11. using System.Reflection;
  12. using UnityEngine.EventSystems;
  13. using System.Diagnostics;
  14. using System.Collections;
  15. using System.IO;
  16. using System.Text;
  17. using System.Text.RegularExpressions;
  18. namespace BSIPA_ModList.UI.ViewControllers
  19. {
  20. /// <summary>
  21. /// A UI component that renders Markdown in-game.
  22. /// </summary>
  23. [RequireComponent(typeof(RectTransform))]
  24. public class MarkdownView : MonoBehaviour
  25. {
  26. private class TagTypeComponent : MonoBehaviour
  27. {
  28. internal BlockTag Tag;
  29. internal HeadingData hData;
  30. internal ListData lData;
  31. internal int listCount;
  32. internal int listIndex;
  33. }
  34. private string markdown = "";
  35. private bool mdDirty = false;
  36. public string Markdown
  37. {
  38. get => markdown;
  39. set
  40. {
  41. markdown = value;
  42. mdDirty = true;
  43. }
  44. }
  45. /// <summary>
  46. /// A convenience property to access the <see cref="RectTransform"/> on the <see cref="GameObject"/> this is on.
  47. /// </summary>
  48. /// <value>the <see cref="RectTransform"/> associated with this component</value>
  49. public RectTransform rectTransform => GetComponent<RectTransform>();
  50. private ScrollView scrView;
  51. private RectTransform content;
  52. private RectTransform viewport;
  53. private CommonMarkSettings settings;
  54. /// <summary>
  55. /// Creates a new <see cref="MarkdownView"/>. Should never be called correctly. Instead, use <see cref="GameObject.AddComponent{T}"/>.
  56. /// </summary>
  57. public MarkdownView()
  58. {
  59. settings = CommonMarkSettings.Default.Clone();
  60. settings.AdditionalFeatures = CommonMarkAdditionalFeatures.All;
  61. settings.RenderSoftLineBreaksAsLineBreaks = false;
  62. settings.UriResolver = ResolveUri;
  63. }
  64. /// <summary>
  65. /// This function will be called whenever attempting to resolve an image URI, to ensure that the image exists in the embedded assembly.
  66. /// </summary>
  67. /// <value>a delegate for the function to call</value>
  68. public Func<string, bool> HasEmbeddedImage;
  69. private string ResolveUri(string arg)
  70. {
  71. var name = arg.Substring(3);
  72. if (!arg.StartsWith("!::") && !arg.StartsWith("w::"))
  73. { // !:: means embedded, w:: means web
  74. // this block is for when neither is specified
  75. Logger.md.Debug($"Resolving nonspecific URI {arg}");
  76. // check if its embedded
  77. if (HasEmbeddedImage != null && HasEmbeddedImage(arg))
  78. return "!::" + arg;
  79. else
  80. return "w::" + arg;
  81. }
  82. Logger.md.Debug($"Resolved specific URI {arg}");
  83. return arg;
  84. }
  85. private static string GetLinkUri(string uri)
  86. {
  87. if (uri[0] == '!')
  88. {
  89. Logger.md.Error($"Cannot link to embedded resource in mod description");
  90. return null;
  91. }
  92. else
  93. return uri.Substring(3);
  94. }
  95. private static Stream ConsolasAssetBundleFontStream => Assembly.GetExecutingAssembly().GetManifestResourceStream("BSIPA_ModList.Bundles.consolas.font");
  96. private static AssetBundleCreateRequest _bundleRequest;
  97. private static AssetBundle _bundle;
  98. private static AssetBundle Bundle
  99. {
  100. get
  101. {
  102. if (_bundle == null && _bundleRequest != null)
  103. throw new InvalidOperationException("Asset bundle is being loaded asynchronously; please wait for that to complete");
  104. if (_bundle == null)
  105. _bundle = AssetBundle.LoadFromStream(ConsolasAssetBundleFontStream);
  106. return _bundle;
  107. }
  108. }
  109. private static AssetBundleRequest _consolasRequest;
  110. private static TMP_FontAsset _unsetConsolas;
  111. private static TMP_FontAsset _consolas;
  112. private static TMP_FontAsset Consolas
  113. {
  114. get
  115. {
  116. if (_unsetConsolas == null && _consolasRequest != null)
  117. throw new InvalidOperationException("Asset is being loaded asynchronously; please wait for that to complete");
  118. if (_unsetConsolas == null)
  119. _unsetConsolas = Bundle?.LoadAsset<TMP_FontAsset>("CONSOLAS");
  120. if (_consolas == null && _unsetConsolas != null)
  121. _consolas = SetupFont(_unsetConsolas);
  122. return _consolas;
  123. }
  124. }
  125. private static TMP_FontAsset SetupFont(TMP_FontAsset f)
  126. {
  127. var originalFont = Resources.FindObjectsOfTypeAll<TMP_FontAsset>().Last(f2 => f2.name == "Teko-Medium SDF No Glow");
  128. var matCopy = Instantiate(originalFont.material);
  129. matCopy.mainTexture = f.material.mainTexture;
  130. matCopy.mainTextureOffset = f.material.mainTextureOffset;
  131. matCopy.mainTextureScale = f.material.mainTextureScale;
  132. f.material = matCopy;
  133. f = Instantiate(f);
  134. MaterialReferenceManager.AddFontAsset(f);
  135. return f;
  136. }
  137. internal static void StartLoadResourcesAsync()
  138. {
  139. SharedCoroutineStarter.instance.StartCoroutine(LoadResourcesAsync());
  140. }
  141. private static IEnumerator LoadResourcesAsync()
  142. {
  143. Logger.md.Debug("Starting to load resources");
  144. _bundleRequest = AssetBundle.LoadFromStreamAsync(ConsolasAssetBundleFontStream);
  145. yield return _bundleRequest;
  146. _bundle = _bundleRequest.assetBundle;
  147. Logger.md.Debug("Bundle loaded");
  148. _consolasRequest = _bundle.LoadAssetAsync<TMP_FontAsset>("CONSOLAS");
  149. yield return _consolasRequest;
  150. _unsetConsolas = _consolasRequest.asset as TMP_FontAsset;
  151. Logger.md.Debug("Font loaded");
  152. }
  153. internal void Awake()
  154. {
  155. if (Consolas == null)
  156. Logger.md.Error($"Loading of Consolas font failed");
  157. gameObject.SetActive(false);
  158. var vpgo = new GameObject("Viewport");
  159. viewport = vpgo.AddComponent<RectTransform>();
  160. viewport.SetParent(transform);
  161. viewport.localPosition = Vector2.zero;
  162. viewport.anchorMin = Vector2.zero;
  163. viewport.anchorMax = Vector2.one;
  164. viewport.anchoredPosition = new Vector2(.5f, .5f);
  165. viewport.sizeDelta = Vector2.zero;
  166. var vpmask = vpgo.AddComponent<Mask>();
  167. var vpim = vpgo.AddComponent<Image>(); // supposedly Mask needs an Image?
  168. vpmask.showMaskGraphic = false;
  169. vpim.color = Color.white;
  170. vpim.sprite = WhitePixel;
  171. vpim.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  172. content = new GameObject("Content Wrapper").AddComponent<RectTransform>();
  173. content.SetParent(viewport);
  174. content.gameObject.AddComponent<TagTypeComponent>();
  175. content.localPosition = Vector2.zero;
  176. content.anchorMin = new Vector2(0f, 1f);
  177. content.anchorMax = new Vector2(1f, 1f);
  178. content.anchoredPosition = Vector2.zero;
  179. var contentLayout = content.gameObject.AddComponent<VerticalLayoutGroup>();
  180. var contentFitter = content.gameObject.AddComponent<ContentSizeFitter>();
  181. contentFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
  182. contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
  183. contentLayout.childControlHeight = true;
  184. contentLayout.childControlWidth = false;
  185. contentLayout.childForceExpandHeight = false;
  186. contentLayout.childForceExpandWidth = true;
  187. contentLayout.childAlignment = TextAnchor.UpperCenter;
  188. contentLayout.spacing = 0f;
  189. var pageUp = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageUpButton"), rectTransform, false);
  190. var pageDown = Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last((Button x) => x.name == "PageDownButton"), rectTransform, false);
  191. {
  192. var pup_rt = pageUp.transform as RectTransform;
  193. var pup_sof = pup_rt.sizeDelta.y;
  194. var pup_xoff = (rectTransform.sizeDelta.x / 2) + (pup_sof / 2);
  195. pup_rt.anchoredPosition = new Vector2(pup_xoff, pup_rt.anchoredPosition.y);
  196. var pup_bg_rt = pup_rt.Find("BG") as RectTransform;
  197. pup_bg_rt.sizeDelta = new Vector2(pup_bg_rt.sizeDelta.y, pup_bg_rt.sizeDelta.y);
  198. // fix hitbox
  199. pup_rt.anchorMin = new Vector2(.5f, pup_rt.anchorMin.y);
  200. pup_rt.anchorMax = new Vector2(.5f, pup_rt.anchorMax.y);
  201. pup_rt.sizeDelta = new Vector2(pup_rt.sizeDelta.y, pup_rt.sizeDelta.y);
  202. }
  203. {
  204. var pdn_rt = pageDown.transform as RectTransform;
  205. var pdn_sof = pdn_rt.sizeDelta.y;
  206. var pdn_xoff = (rectTransform.sizeDelta.x / 2) + (pdn_sof / 2);
  207. pdn_rt.anchoredPosition = new Vector2(pdn_xoff, pdn_rt.anchoredPosition.y);
  208. var pdn_bg_rt = pdn_rt.Find("BG") as RectTransform;
  209. pdn_bg_rt.sizeDelta = new Vector2(pdn_bg_rt.sizeDelta.y, pdn_bg_rt.sizeDelta.y);
  210. // fix hitbox
  211. pdn_rt.anchorMin = new Vector2(.5f, pdn_rt.anchorMin.y);
  212. pdn_rt.anchorMax = new Vector2(.5f, pdn_rt.anchorMax.y);
  213. pdn_rt.sizeDelta = new Vector2(pdn_rt.sizeDelta.y, pdn_rt.sizeDelta.y);
  214. }
  215. scrView = gameObject.AddComponent<ScrollView>();
  216. scrView.SetPrivateField("_pageUpButton", pageUp);
  217. scrView.SetPrivateField("_pageDownButton", pageDown);
  218. scrView.SetPrivateField("_contentRectTransform", content);
  219. scrView.SetPrivateField("_viewport", viewport);
  220. gameObject.SetActive(true);
  221. }
  222. private static Sprite whitePixel;
  223. private static Sprite WhitePixel
  224. {
  225. get
  226. {
  227. if (whitePixel == null)
  228. whitePixel = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "WhitePixel");
  229. return whitePixel;
  230. }
  231. }
  232. private static Sprite blockQuoteBackground;
  233. private static Sprite BlockQuoteBackground
  234. {
  235. get
  236. {
  237. if (blockQuoteBackground == null)
  238. blockQuoteBackground = Resources.FindObjectsOfTypeAll<Sprite>().First(s => s.name == "RoundRectNormal");
  239. return blockQuoteBackground;
  240. }
  241. }
  242. private static readonly Color BlockQuoteColor = new Color( 30f / 255, 109f / 255, 178f / 255, .25f);
  243. private static readonly Color BlockCodeColor = new Color(135f / 255, 135f / 255, 135f / 255, .5f);
  244. #if DEBUG
  245. #if UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
  246. private byte tbreakSettings = 0;
  247. #endif
  248. #endif
  249. internal void Update()
  250. {
  251. #if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
  252. if (Input.GetKeyDown(KeyCode.K))
  253. {
  254. tbreakSettings = (byte)((tbreakSettings + 1) % 16);
  255. UpdateMd();
  256. Logger.md.Info(tbreakSettings.ToString());
  257. }
  258. #endif
  259. if (mdDirty)
  260. StartCoroutine(UpdateMd());
  261. }
  262. [Flags]
  263. private enum CurrentTextFlags
  264. {
  265. None = 0, Bold = 1, Italic = 2, Underline = 4, Strikethrough = 8,
  266. }
  267. private const string LinkDefaultColor = "#59B0F4";
  268. // private const string LinkHoverColor = "#009dff";
  269. private static readonly Regex linkRefTitleColorRegex = new Regex(@"\{\s*color:\s*#?([a-fA-F0-9]{6})\s*\}", RegexOptions.Compiled | RegexOptions.Singleline);
  270. const float PSize = 3.5f;
  271. const float BlockCodeSize = PSize - .5f;
  272. const float H1Size = 5.5f;
  273. const float HLevelDecrease = 0.5f;
  274. private IEnumerator UpdateMd()
  275. {
  276. mdDirty = false;
  277. Clear();
  278. // enable so it will set stuff up
  279. content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = true;
  280. var doc = CommonMarkConverter.Parse(markdown, settings);
  281. Stack<RectTransform> layout = new Stack<RectTransform>();
  282. layout.Push(content);
  283. TextMeshProUGUI currentText = null;
  284. List<TextMeshProUGUI> texts = new List<TextMeshProUGUI>();
  285. CurrentTextFlags textFlags = 0;
  286. foreach (var node in doc.AsEnumerable())
  287. {
  288. Logger.md.Debug($"node {node}");
  289. if (node.Block != null)
  290. {
  291. var block = node.Block;
  292. const float BreakHeight = .5f;
  293. const int TextInset = 1;
  294. const int BlockQuoteInset = TextInset * 2;
  295. const int BlockCodeInset = BlockQuoteInset;
  296. const int ListInset = TextInset;
  297. void Spacer(float size = 1.5f)
  298. {
  299. var go = new GameObject("Spacer", typeof(RectTransform));
  300. var vlayout = go.GetComponent<RectTransform>();
  301. vlayout.SetParent(layout.Peek());
  302. vlayout.anchorMin = new Vector2(.5f, .5f);
  303. vlayout.anchorMax = new Vector2(.5f, .5f);
  304. vlayout.localScale = Vector3.one;
  305. vlayout.localPosition = Vector3.zero;
  306. var l = go.AddComponent<LayoutElement>();
  307. l.minHeight = l.preferredHeight = size;
  308. }
  309. HorizontalOrVerticalLayoutGroup BlockNode(string name, float spacing, bool isVertical, Action<TagTypeComponent> apply = null, float? spacer = null, bool isDoc = false, bool matchWidth = false, float matchWidthDiff = 0f)
  310. {
  311. if (node.IsOpening)
  312. {
  313. Logger.md.Debug($"Creating block container {name}");
  314. currentText = null;
  315. var go = new GameObject(name, typeof(RectTransform));
  316. var vlayout = go.GetComponent<RectTransform>();
  317. vlayout.SetParent(layout.Peek());
  318. //vlayout.anchoredPosition = new Vector2(.5f, .5f);
  319. vlayout.anchorMin = new Vector2(.5f, .5f);
  320. vlayout.anchorMax = new Vector2(.5f, .5f);
  321. vlayout.localScale = Vector3.one;
  322. vlayout.localPosition = Vector3.zero;
  323. if (isDoc)
  324. {
  325. vlayout.sizeDelta = new Vector2(rectTransform.rect.width, 0f);
  326. vlayout.anchorMin = new Vector2(0f, 1f);
  327. vlayout.anchorMax = new Vector2(1f, 1f);
  328. }
  329. if (matchWidth)
  330. vlayout.sizeDelta = new Vector2(layout.Peek().rect.width-matchWidthDiff, 0f);
  331. var tt = go.AddComponent<TagTypeComponent>();
  332. tt.Tag = block.Tag;
  333. apply?.Invoke(tt);
  334. layout.Push(vlayout);
  335. HorizontalOrVerticalLayoutGroup l;
  336. if (isVertical)
  337. l = go.AddComponent<VerticalLayoutGroup>();
  338. else
  339. l = go.AddComponent<HorizontalLayoutGroup>();
  340. l.childControlHeight = l.childControlWidth = true;
  341. l.childForceExpandHeight = false;
  342. l.childForceExpandWidth = isDoc;
  343. l.spacing = spacing;
  344. if (isDoc)
  345. vlayout.anchoredPosition = new Vector2(0f, -vlayout.rect.height);
  346. return l;
  347. }
  348. else if (node.IsClosing)
  349. {
  350. currentText = null;
  351. layout.Pop();
  352. if (spacer.HasValue)
  353. Spacer(spacer.Value);
  354. }
  355. return null;
  356. }
  357. void ThematicBreak(float spacerSize = 1.5f)
  358. { // TODO: Fix positioning
  359. var go = new GameObject("ThematicBreak", typeof(RectTransform), typeof(HorizontalLayoutGroup));
  360. var vlayout = go.GetComponent<RectTransform>();
  361. vlayout.SetParent(layout.Peek());
  362. var l = go.GetComponent<HorizontalLayoutGroup>();
  363. #if DEBUG && UI_CONFIGURE_MARKDOWN_THEMATIC_BREAK
  364. l.childControlHeight = (tbreakSettings & 0b0001) != 0; // if set, not well behaved
  365. l.childControlWidth = (tbreakSettings & 0b0010) != 0;
  366. l.childForceExpandHeight = (tbreakSettings & 0b0100) != 0; // if set, not well behaved
  367. l.childForceExpandWidth = (tbreakSettings & 0b1000) != 0;
  368. #else
  369. l.childControlHeight = false;
  370. l.childControlWidth = false;
  371. l.childForceExpandHeight = false;
  372. l.childForceExpandWidth = false;
  373. #endif
  374. l.childAlignment = TextAnchor.UpperCenter;
  375. l.spacing = 0f;
  376. vlayout.localScale = Vector3.one;
  377. vlayout.anchoredPosition = Vector2.zero;
  378. vlayout.anchorMin = new Vector2(.5f, .5f);
  379. vlayout.anchorMax = new Vector2(.5f, .5f);
  380. vlayout.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);
  381. vlayout.localPosition = Vector3.zero;
  382. currentText = null;
  383. go = new GameObject("ThematicBreak Bar", typeof(RectTransform), typeof(Image), typeof(LayoutElement));
  384. var im = go.GetComponent<Image>();
  385. im.color = Color.white;
  386. // i think i need to copy the sprite because i'm using the same one for the mask
  387. im.sprite = Sprite.Create(WhitePixel.texture, WhitePixel.rect, Vector2.zero);
  388. im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  389. var rt = go.GetComponent<RectTransform>();
  390. rt.SetParent(vlayout);
  391. var le = go.GetComponent<LayoutElement>();
  392. le.minWidth = le.preferredWidth = layout.Peek().rect.width;
  393. le.minHeight = le.preferredHeight = BreakHeight;
  394. le.flexibleHeight = le.flexibleWidth = 1f;
  395. rt.localScale = Vector3.one;
  396. rt.localPosition = Vector3.zero;
  397. rt.anchoredPosition = Vector3.zero;
  398. rt.anchorMin = Vector2.zero;
  399. rt.anchorMax = Vector2.one;
  400. rt.sizeDelta = new Vector2(layout.Peek().rect.width, BreakHeight);
  401. if (spacerSize != 0f)
  402. Spacer(spacerSize);
  403. }
  404. switch (block.Tag)
  405. {
  406. case BlockTag.Document:
  407. BlockNode("DocumentRoot", .5f, true, isDoc: true);
  408. break;
  409. case BlockTag.SetextHeading:
  410. var l = BlockNode("SeHeading", 0f, false, t => t.hData = block.Heading, spacer: 0f);
  411. if (l)
  412. {
  413. l.childAlignment = TextAnchor.UpperCenter;
  414. l.padding = new RectOffset(TextInset, TextInset, 0, 0);
  415. }
  416. else
  417. ThematicBreak(2f);
  418. break;
  419. case BlockTag.AtxHeading:
  420. l = BlockNode("AtxHeading", .1f, false, t => t.hData = block.Heading);
  421. if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
  422. if (l && block.Heading.Level == 1)
  423. l.childAlignment = TextAnchor.UpperCenter;
  424. break;
  425. case BlockTag.Paragraph:
  426. l = BlockNode("Paragraph", .1f, false, spacer: 1.5f);
  427. if (l) l.padding = new RectOffset(TextInset, TextInset, 0, 0);
  428. break;
  429. case BlockTag.ThematicBreak:
  430. ThematicBreak();
  431. break;
  432. // TODO: add the rest of the tag types
  433. case BlockTag.BlockQuote:
  434. l = BlockNode("BlockQuote", .1f, true, matchWidth: true, matchWidthDiff: BlockQuoteInset*2, spacer: 1.5f);
  435. if (l)
  436. {
  437. l.childForceExpandWidth = true;
  438. l.padding = new RectOffset(BlockQuoteInset, BlockQuoteInset, BlockQuoteInset, 0);
  439. var go = l.gameObject;
  440. var im = go.AddComponent<Image>();
  441. im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  442. im.type = Image.Type.Sliced;
  443. im.sprite = Instantiate(BlockQuoteBackground);
  444. im.color = BlockQuoteColor;
  445. }
  446. break;
  447. case BlockTag.IndentedCode:
  448. case BlockTag.FencedCode:
  449. {
  450. currentText = null;
  451. var go = new GameObject("CodeBlock", typeof(RectTransform));
  452. var vlayout = go.GetComponent<RectTransform>();
  453. vlayout.SetParent(layout.Peek());
  454. vlayout.anchorMin = new Vector2(.5f, .5f);
  455. vlayout.anchorMax = new Vector2(.5f, .5f);
  456. vlayout.localScale = Vector3.one;
  457. vlayout.localPosition = Vector3.zero;
  458. vlayout.sizeDelta = new Vector2(layout.Peek().rect.width - BlockCodeInset * 2, 0f);
  459. var tt = go.AddComponent<TagTypeComponent>();
  460. tt.Tag = block.Tag;
  461. l = go.AddComponent<VerticalLayoutGroup>();
  462. l.childControlHeight = l.childControlWidth = true;
  463. l.childForceExpandHeight = false;
  464. l.childForceExpandWidth = true;
  465. l.spacing = 1.5f;
  466. l.padding = new RectOffset(BlockCodeInset, BlockCodeInset, BlockCodeInset, BlockCodeInset);
  467. var im = go.AddComponent<Image>();
  468. im.material = CustomUI.Utilities.UIUtilities.NoGlowMaterial;
  469. im.type = Image.Type.Sliced;
  470. im.sprite = Instantiate(BlockQuoteBackground);
  471. im.color = BlockCodeColor;
  472. var text = BeatSaberUI.CreateText(vlayout, $"<noparse>{block.StringContent}</noparse>", Vector2.zero);
  473. text.fontSize = BlockCodeSize;
  474. text.font = Consolas;
  475. }
  476. break;
  477. case BlockTag.List:
  478. l = BlockNode("List", .05f, true, t => t.lData = block.ListData, matchWidth: true, matchWidthDiff: ListInset * 2, spacer: 1.5f);
  479. if (l)
  480. {
  481. l.childForceExpandWidth = true;
  482. l.padding = new RectOffset(ListInset, ListInset, 0, 0);
  483. var go = l.gameObject;
  484. var tt = go.GetComponent<TagTypeComponent>();
  485. // count up children
  486. var count = 0;
  487. for (var c = block.FirstChild; c != null; c = c.NextSibling) count++;
  488. tt.listCount = count;
  489. tt.listIndex = 0;
  490. }
  491. break;
  492. case BlockTag.ListItem:
  493. l = BlockNode("ListItem", .05f, false, matchWidth: true, spacer: null);
  494. if (l)
  495. { // TODO: this is mega scuffed
  496. l.childForceExpandWidth = true;
  497. var go = l.gameObject;
  498. var rt = go.GetComponent<RectTransform>();
  499. var tt = go.GetComponent<TagTypeComponent>();
  500. var ptt = rt.parent.gameObject.GetComponent<TagTypeComponent>();
  501. var index = ptt.listIndex++;
  502. var listCount = ptt.listCount;
  503. var maxNum = listCount + ptt.lData.Start;
  504. var numChars = (int)Math.Floor(Math.Log10(maxNum) + 1);
  505. var cNum = index + ptt.lData.Start;
  506. var lt = ptt.lData.ListType;
  507. var id = lt == ListType.Bullet ? ptt.lData.BulletChar.ToString() : (cNum + (ptt.lData.Delimiter == ListDelimiter.Parenthesis ? ")" : "."));
  508. var ident = BeatSaberUI.CreateText(rt, $"<nobr>{id} </nobr>\n", Vector2.zero);
  509. if (lt == ListType.Ordered) // pad it out to fill space
  510. ident.text += $"<nobr><mspace=1em>{new string(' ', numChars + 1)}</mspace></nobr>";
  511. var contGo = new GameObject("Content", typeof(RectTransform));
  512. var vlayout = contGo.GetComponent<RectTransform>();
  513. vlayout.SetParent(rt);
  514. vlayout.anchorMin = new Vector2(.5f, .5f);
  515. vlayout.anchorMax = new Vector2(.5f, .5f);
  516. vlayout.localScale = Vector3.one;
  517. vlayout.localPosition = Vector3.zero;
  518. //vlayout.sizeDelta = new Vector2(rt.rect.width, 0f);
  519. var tt2 = contGo.AddComponent<TagTypeComponent>();
  520. tt2.Tag = block.Tag;
  521. var csf = contGo.AddComponent<ContentSizeFitter>();
  522. csf.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
  523. csf.verticalFit = ContentSizeFitter.FitMode.Unconstrained;
  524. l = contGo.AddComponent<VerticalLayoutGroup>();
  525. l.childAlignment = TextAnchor.UpperLeft;
  526. l.childControlHeight = l.childControlWidth = true;
  527. l.childForceExpandHeight = false;
  528. l.childForceExpandWidth = true;
  529. l.spacing = .5f;
  530. layout.Push(vlayout);
  531. }
  532. else
  533. layout.Pop(); // pop one more to clear content rect from stack
  534. break;
  535. case BlockTag.HtmlBlock:
  536. break;
  537. case BlockTag.ReferenceDefinition: // i have no idea what the state looks like here
  538. break;
  539. }
  540. }
  541. else if (node.Inline != null)
  542. { // inline element
  543. var inl = node.Inline;
  544. void Flag(CurrentTextFlags flag)
  545. {
  546. if (node.IsOpening)
  547. textFlags |= flag;
  548. else if (node.IsClosing)
  549. textFlags &= ~flag;
  550. }
  551. void EnsureText()
  552. {
  553. if (currentText == null)
  554. {
  555. Logger.md.Debug($"Adding new text element");
  556. var tt = layout.Peek().gameObject.GetComponent<TagTypeComponent>();
  557. currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero);
  558. currentText.gameObject.AddComponent<TextLinkDecoder>();
  559. currentText.enableWordWrapping = true;
  560. switch (tt.Tag)
  561. {
  562. case BlockTag.List:
  563. case BlockTag.ListItem:
  564. case BlockTag.Paragraph:
  565. currentText.fontSize = PSize;
  566. break;
  567. case BlockTag.AtxHeading:
  568. var size = H1Size;
  569. size -= HLevelDecrease * (tt.hData.Level - 1);
  570. currentText.fontSize = size;
  571. if (tt.hData.Level == 1)
  572. currentText.alignment = TextAlignmentOptions.Center;
  573. break;
  574. case BlockTag.SetextHeading:
  575. currentText.fontSize = H1Size;
  576. currentText.alignment = TextAlignmentOptions.Center;
  577. break;
  578. // TODO: add other relevant types
  579. }
  580. texts.Add(currentText);
  581. }
  582. }
  583. switch (inl.Tag)
  584. {
  585. case InlineTag.String:
  586. EnsureText();
  587. string head = "<noparse>", tail = "</noparse>";
  588. if (textFlags.HasFlag(CurrentTextFlags.Bold))
  589. { head = "<b>" + head; tail += "</b>"; }
  590. if (textFlags.HasFlag(CurrentTextFlags.Italic))
  591. { head = "<i>" + head; tail += "</i>"; }
  592. if (textFlags.HasFlag(CurrentTextFlags.Strikethrough))
  593. { head = "<s>" + head; tail += "</s>"; }
  594. if (textFlags.HasFlag(CurrentTextFlags.Underline))
  595. { head = "<u>" + head; tail += "</u>"; }
  596. currentText.text += head + inl.LiteralContent + tail;
  597. break;
  598. case InlineTag.Strong:
  599. Flag(CurrentTextFlags.Bold);
  600. break;
  601. case InlineTag.Strikethrough:
  602. Flag(CurrentTextFlags.Strikethrough);
  603. break;
  604. case InlineTag.Emphasis:
  605. Flag(CurrentTextFlags.Italic);
  606. break;
  607. case InlineTag.Code:
  608. EnsureText();
  609. currentText.text += $"<font=\"CONSOLAS\"><size=80%><mark=#A0A0C080><noparse>{inl.LiteralContent}</noparse></mark></size></font>";
  610. break;
  611. case InlineTag.SoftBreak:
  612. EnsureText();
  613. currentText.text += " "; // soft breaks translate to a space
  614. break;
  615. case InlineTag.LineBreak:
  616. EnsureText();
  617. currentText.text += "\n"; // line breaks translate to a new line
  618. break;
  619. case InlineTag.Link:
  620. EnsureText();
  621. Flag(CurrentTextFlags.Underline);
  622. var color = LinkDefaultColor;
  623. var targetUrl = ResolveUri(inl.TargetUrl);
  624. var m = linkRefTitleColorRegex.Match(inl.LiteralContent);
  625. if (m.Success)
  626. color = "#" + m.Groups[1].Value;
  627. if (node.IsOpening)
  628. currentText.text += $"<color={color}><link=\"{targetUrl}\">";
  629. else if (node.IsClosing)
  630. currentText.text += "</link></color>";
  631. break;
  632. case InlineTag.RawHtml:
  633. EnsureText();
  634. currentText.text += inl.LiteralContent;
  635. break;
  636. case InlineTag.Placeholder:
  637. break;
  638. }
  639. }
  640. }
  641. yield return null; // delay one frame
  642. scrView.Setup();
  643. // this is the bullshit I have to use to make it work properly
  644. content.gameObject.GetComponent<VerticalLayoutGroup>().enabled = false;
  645. var childRt = content.GetChild(0) as RectTransform;
  646. childRt.anchoredPosition = new Vector2(0f, childRt.anchoredPosition.y);
  647. }
  648. private class TextLinkDecoder : MonoBehaviour, IPointerClickHandler
  649. {
  650. private TextMeshProUGUI tmp;
  651. public void Awake()
  652. {
  653. tmp = GetComponent<TextMeshProUGUI>();
  654. }
  655. public void OnPointerClick(PointerEventData eventData)
  656. {
  657. // this may not actually get me what i want
  658. int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmp, eventData.pointerPressRaycast.worldPosition, null);
  659. if (linkIndex != -1)
  660. { // was a link clicked?
  661. TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];
  662. // open the link id as a url, which is the metadata we added in the text field
  663. var qualifiedUrl = linkInfo.GetLinkID();
  664. if (qualifiedUrl.StartsWith("$$"))
  665. return; // this means its used for something else
  666. Logger.md.Debug($"Link pressed {qualifiedUrl}");
  667. var uri = GetLinkUri(qualifiedUrl);
  668. if (uri != null)
  669. Process.Start(uri);
  670. }
  671. }
  672. private List<Color32[]> SetLinkToColor(int linkIndex, Color32 color)
  673. {
  674. TMP_LinkInfo linkInfo = tmp.textInfo.linkInfo[linkIndex];
  675. var oldVertColors = new List<Color32[]>(); // store the old character colors
  676. for (int i = 0; i < linkInfo.linkTextLength; i++)
  677. { // for each character in the link string
  678. int characterIndex = linkInfo.linkTextfirstCharacterIndex + i; // the character index into the entire text
  679. var charInfo = tmp.textInfo.characterInfo[characterIndex];
  680. int meshIndex = charInfo.materialReferenceIndex; // Get the index of the material / sub text object used by this character.
  681. int vertexIndex = charInfo.vertexIndex; // Get the index of the first vertex of this character.
  682. Color32[] vertexColors = tmp.textInfo.meshInfo[meshIndex].colors32; // the colors for this character
  683. oldVertColors.Add(vertexColors.ToArray());
  684. if (charInfo.isVisible)
  685. {
  686. vertexColors[vertexIndex + 0] = color;
  687. vertexColors[vertexIndex + 1] = color;
  688. vertexColors[vertexIndex + 2] = color;
  689. vertexColors[vertexIndex + 3] = color;
  690. }
  691. }
  692. // Update Geometry
  693. tmp.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
  694. return oldVertColors;
  695. }
  696. }
  697. private void Clear()
  698. {
  699. content.gameObject.SetActive(false);
  700. void Clear(Transform target)
  701. {
  702. foreach (Transform child in target)
  703. {
  704. Clear(child);
  705. Logger.md.Debug($"Destroying {child.name}");
  706. child.SetParent(null);
  707. Destroy(child.gameObject);
  708. }
  709. }
  710. Clear(content);
  711. content.gameObject.SetActive(true);
  712. }
  713. }
  714. }