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.

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