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.

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