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.

212 lines
8.4 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using UnityEngine;
  7. using CommonMark;
  8. using CommonMark.Syntax;
  9. using UnityEngine.UI;
  10. using TMPro;
  11. using CustomUI.BeatSaber;
  12. namespace BSIPA_ModList.UI.ViewControllers
  13. {
  14. [RequireComponent(/*typeof(ScrollRect),*/ typeof(RectTransform))]
  15. public class MarkdownView : MonoBehaviour
  16. {
  17. private class TagTypeComponent : MonoBehaviour
  18. {
  19. internal BlockTag Tag;
  20. }
  21. private string markdown = "";
  22. public string Markdown
  23. {
  24. get => markdown;
  25. set
  26. {
  27. markdown = value;
  28. UpdateMd();
  29. }
  30. }
  31. public RectTransform rectTransform => GetComponent<RectTransform>();
  32. //private ScrollRect view;
  33. private RectTransform content;
  34. private CommonMarkSettings settings;
  35. public MarkdownView()
  36. {
  37. settings = CommonMarkSettings.Default.Clone();
  38. settings.AdditionalFeatures = CommonMarkAdditionalFeatures.All;
  39. settings.RenderSoftLineBreaksAsLineBreaks = false;
  40. settings.UriResolver = ResolveUri;
  41. }
  42. public Func<string, bool> HasEmbeddedImage;
  43. private string ResolveUri(string arg)
  44. {
  45. var name = arg.Substring(3);
  46. if (!arg.StartsWith("!::") && !arg.StartsWith("w::"))
  47. { // !:: means embedded, w:: means web
  48. // this block is for when neither is specified
  49. Logger.md.Debug($"Resolving nonspecific URI {arg}");
  50. // check if its embedded
  51. if (HasEmbeddedImage != null && HasEmbeddedImage(arg))
  52. return "!::" + arg;
  53. else
  54. return "w::" + arg;
  55. }
  56. Logger.md.Debug($"Resolved specific URI {arg}");
  57. return arg;
  58. }
  59. protected void Awake()
  60. {
  61. content = new GameObject("Content Wrapper").AddComponent<RectTransform>();
  62. content.SetParent(transform);
  63. content.localPosition = Vector2.zero;
  64. content.anchorMin = Vector2.zero;
  65. content.anchorMax = Vector2.one;
  66. var contentLayout = content.gameObject.AddComponent<LayoutElement>();
  67. var contentFitter = content.gameObject.AddComponent<ContentSizeFitter>();
  68. contentFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
  69. contentFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained;
  70. contentLayout.preferredWidth = 100f; // to be adjusted
  71. content.sizeDelta = new Vector2(100f,100f);
  72. content.gameObject.AddComponent<TagTypeComponent>();
  73. /*view = GetComponent<ScrollRect>();
  74. view.content = content;
  75. view.vertical = true;
  76. view.horizontal = false;
  77. view.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide;
  78. view.horizontalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide;*/
  79. }
  80. private void UpdateMd()
  81. {
  82. Clear();
  83. var doc = CommonMarkConverter.Parse(markdown, settings);
  84. Stack<RectTransform> layout = new Stack<RectTransform>();
  85. layout.Push(content);
  86. TextMeshProUGUI currentText = null;
  87. foreach (var node in doc.AsEnumerable())
  88. {
  89. Logger.md.Debug($"node {node}");
  90. if (node.Block != null)
  91. {
  92. var block = node.Block;
  93. void BlockNode(string name, float spacing, bool isVertical, bool isDoc = false)
  94. {
  95. var type = isVertical ? typeof(VerticalLayoutGroup) : typeof(HorizontalLayoutGroup);
  96. if (node.IsOpening)
  97. {
  98. Logger.md.Debug($"Creating block container {name}");
  99. currentText = null;
  100. var go = new GameObject(name, typeof(RectTransform), type);
  101. var vlayout = go.GetComponent<RectTransform>();
  102. vlayout.SetParent(layout.Peek());
  103. //if (isDoc)
  104. vlayout.anchoredPosition = Vector2.zero;
  105. go.AddComponent<TagTypeComponent>().Tag = block.Tag;
  106. layout.Push(vlayout);
  107. if (isVertical)
  108. {
  109. var vl = go.GetComponent<VerticalLayoutGroup>();
  110. vl.childControlHeight = vl.childControlWidth =
  111. vl.childForceExpandHeight = vl.childForceExpandWidth = false;
  112. vl.spacing = spacing;
  113. }
  114. else
  115. {
  116. var hl = go.GetComponent<HorizontalLayoutGroup>();
  117. hl.childControlHeight = hl.childControlWidth =
  118. hl.childForceExpandHeight = hl.childForceExpandWidth = false;
  119. hl.spacing = spacing;
  120. }
  121. }
  122. else if (node.IsClosing)
  123. {
  124. currentText = null;
  125. layout.Pop();
  126. }
  127. }
  128. switch (block.Tag)
  129. {
  130. case BlockTag.Document:
  131. BlockNode("DocumentRoot", .2f, true, true);
  132. break;
  133. case BlockTag.SetextHeading:
  134. BlockNode("Heading1", .1f, false);
  135. break;
  136. case BlockTag.AtxHeading:
  137. BlockNode("Heading2", .1f, false);
  138. break;
  139. case BlockTag.Paragraph:
  140. BlockNode("Paragraph", .1f, false);
  141. break;
  142. // TODO: add the rest of the tag types
  143. }
  144. }
  145. else if (node.Inline != null)
  146. { // inline element
  147. var inl = node.Inline;
  148. switch (inl.Tag)
  149. {
  150. case InlineTag.String:
  151. if (currentText == null)
  152. {
  153. Logger.md.Debug($"Adding new text element");
  154. var btt = layout.Peek().gameObject.GetComponent<TagTypeComponent>().Tag;
  155. currentText = BeatSaberUI.CreateText(layout.Peek(), "", Vector2.zero);
  156. //var le = currentText.gameObject.AddComponent<LayoutElement>();
  157. switch (btt)
  158. {
  159. case BlockTag.List:
  160. case BlockTag.ListItem:
  161. case BlockTag.Paragraph:
  162. currentText.fontSize = 3.5f;
  163. currentText.enableWordWrapping = true;
  164. break;
  165. case BlockTag.AtxHeading:
  166. currentText.fontSize = 4f;
  167. currentText.enableWordWrapping = true;
  168. break;
  169. case BlockTag.SetextHeading:
  170. currentText.fontSize = 4.5f;
  171. currentText.enableWordWrapping = true;
  172. break;
  173. // TODO: add other relevant types
  174. }
  175. }
  176. Logger.md.Debug($"Appending '{inl.LiteralContent}' to current element");
  177. currentText.text += inl.LiteralContent;
  178. break;
  179. }
  180. }
  181. }
  182. }
  183. private void Clear()
  184. {
  185. foreach (Transform child in content)
  186. Destroy(child.gameObject);
  187. }
  188. }
  189. }