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.

832 lines
38 KiB

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