DocumentParsingManager.static.cs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. using System.Linq;
  2. using Content.Client.Guidebook.Controls;
  3. using Pidgin;
  4. using Robust.Client.UserInterface;
  5. using Robust.Client.UserInterface.Controls;
  6. using Robust.Shared.Utility;
  7. using static Pidgin.Parser;
  8. using static Pidgin.Parser<char>;
  9. using static Robust.Client.UserInterface.Control;
  10. using static Robust.Client.UserInterface.Controls.BoxContainer;
  11. namespace Content.Client.Guidebook;
  12. public sealed partial class DocumentParsingManager
  13. {
  14. private const string ListBullet = " › ";
  15. // Parser that consumes a - and then just parses normal rich text with some prefix text (a bullet point).
  16. private static readonly Parser<char, char> TryEscapedChar = Try(Char('\\')
  17. .Then(OneOf(
  18. Try(Char('<')),
  19. Try(Char('>')),
  20. Try(Char('\\')),
  21. Try(Char('-')),
  22. Try(Char('=')),
  23. Try(Char('"')),
  24. Try(Char(' ')),
  25. Try(Char('n')).ThenReturn('\n'),
  26. Try(Char('t')).ThenReturn('\t')
  27. )));
  28. private static readonly Parser<char, Unit> SkipNewline = Whitespace.SkipUntil(Char('\n'));
  29. private static readonly Parser<char, char> TrySingleNewlineToSpace =
  30. Try(SkipNewline).Then(SkipWhitespaces).ThenReturn(' ');
  31. private static readonly Parser<char, char> TextChar = OneOf(
  32. TryEscapedChar, // consume any backslashed being used to escape text
  33. TrySingleNewlineToSpace, // turn single newlines into spaces
  34. Any // just return the character.
  35. );
  36. private static readonly Parser<char, char> QuotedTextChar = OneOf(TryEscapedChar, Any);
  37. private static readonly Parser<char, string> QuotedText =
  38. Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
  39. private static readonly Parser<char, Unit> TryStartList =
  40. Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
  41. private static readonly Parser<char, Unit> TryStartTag = Try(Char('<')).Then(SkipWhitespaces);
  42. private static readonly Parser<char, Unit> TryStartParagraph =
  43. Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
  44. private static readonly Parser<char, Unit> TryLookTextEnd =
  45. Lookahead(OneOf(TryStartTag, TryStartList, TryStartParagraph, Try(Whitespace.SkipUntil(End))));
  46. private static readonly Parser<char, string> TextParser =
  47. TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
  48. private static readonly Parser<char, Control> TextControlParser = Try(Map<char, string, Control>(text =>
  49. {
  50. var rt = new RichTextLabel
  51. {
  52. HorizontalExpand = true,
  53. Margin = new Thickness(0, 0, 0, 15.0f)
  54. };
  55. var msg = new FormattedMessage();
  56. // THANK YOU RICHTEXT VERY COOL
  57. // (text doesn't default to white).
  58. msg.PushColor(Color.White);
  59. // If the parsing fails, don't throw an error and instead make an inline error message
  60. string? error;
  61. if (!msg.TryAddMarkup(text, out error))
  62. {
  63. Logger.GetSawmill("Guidebook").Error("Failed to parse RichText in Guidebook");
  64. return new GuidebookError(text, error);
  65. }
  66. msg.Pop();
  67. rt.SetMessage(msg);
  68. return rt;
  69. },
  70. TextParser)
  71. .Cast<Control>())
  72. .Labelled("richtext");
  73. private static readonly Parser<char, Control> HeaderControlParser = Try(Char('#'))
  74. .Then(SkipWhitespaces.Then(Map(text => new Label
  75. {
  76. Text = text,
  77. StyleClasses = { "LabelHeadingBigger" }
  78. },
  79. AnyCharExcept('\n').AtLeastOnceString())
  80. .Cast<Control>()))
  81. .Labelled("header");
  82. private static readonly Parser<char, Control> SubHeaderControlParser = Try(String("##"))
  83. .Then(SkipWhitespaces.Then(Map(text => new Label
  84. {
  85. Text = text,
  86. StyleClasses = { "LabelHeading" }
  87. },
  88. AnyCharExcept('\n').AtLeastOnceString())
  89. .Cast<Control>()))
  90. .Labelled("subheader");
  91. private static readonly Parser<char, Control> TertiaryHeaderControlParser = Try(String("###"))
  92. .Then(SkipWhitespaces.Then(Map(text => new Label
  93. {
  94. Text = text,
  95. StyleClasses = { "LabelKeyText" }
  96. },
  97. AnyCharExcept('\n').AtLeastOnceString())
  98. .Cast<Control>()))
  99. .Labelled("tertiaryheader");
  100. private static readonly Parser<char, Control> TryHeaderControl = OneOf(TertiaryHeaderControlParser, SubHeaderControlParser, HeaderControlParser);
  101. private static readonly Parser<char, Control> ListControlParser = Try(Char('-'))
  102. .Then(SkipWhitespaces)
  103. .Then(Map(
  104. control => new BoxContainer
  105. {
  106. Children = { new Label { Text = ListBullet, VerticalAlignment = VAlignment.Top }, control },
  107. Orientation = LayoutOrientation.Horizontal
  108. },
  109. TextControlParser)
  110. .Cast<Control>())
  111. .Labelled("list");
  112. #region Text Parsing
  113. #region Basic Text Parsing
  114. // Try look for an escaped character. If found, skip the escaping slash and return the character.
  115. // like TextChar, but not skipping whitespace around newlines
  116. // Quoted text
  117. #endregion
  118. #region rich text-end markers
  119. #endregion
  120. // parses text characters until it hits a text-end
  121. #endregion
  122. #region Headers
  123. #endregion
  124. #region Tag Parsing
  125. // closing brackets for tags
  126. private static readonly Parser<char, Unit> TagEnd = Char('>').Then(SkipWhitespaces);
  127. private static readonly Parser<char, Unit> ImmediateTagEnd = String("/>").Then(SkipWhitespaces);
  128. private static readonly Parser<char, Unit> TryLookTagEnd = Lookahead(OneOf(Try(TagEnd), Try(ImmediateTagEnd)));
  129. //parse tag argument key. any normal text character up until we hit a "="
  130. private static readonly Parser<char, string> TagArgKey =
  131. LetterOrDigit.Until(Char('=')).Select(string.Concat).Labelled("tag argument key");
  132. // parser for a singular tag argument. Note that each TryQuoteOrChar will consume a whole quoted block before the Until() looks for whitespace
  133. private static readonly Parser<char, (string, string)> TagArgParser =
  134. Map((key, value) => (key, value), TagArgKey, QuotedText).Before(SkipWhitespaces);
  135. // parser for all tag arguments
  136. private static readonly Parser<char, IEnumerable<(string, string)>> TagArgsParser =
  137. TagArgParser.Until(TryLookTagEnd);
  138. // parser for an opening tag.
  139. private static readonly Parser<char, string> TryOpeningTag =
  140. Try(Char('<'))
  141. .Then(SkipWhitespaces)
  142. .Then(TextChar.Until(OneOf(Whitespace.SkipAtLeastOnce(), TryLookTagEnd)))
  143. .Select(string.Concat)
  144. .Labelled("opening tag");
  145. private static Parser<char, Dictionary<string, string>> ParseTagArgs(string tag)
  146. {
  147. return TagArgsParser.Labelled($"{tag} arguments")
  148. .Select(x => x.ToDictionary(y => y.Item1, y => y.Item2))
  149. .Before(SkipWhitespaces);
  150. }
  151. private static Parser<char, Unit> TryTagTerminator(string tag)
  152. {
  153. return Try(String("</"))
  154. .Then(SkipWhitespaces)
  155. .Then(String(tag))
  156. .Then(SkipWhitespaces)
  157. .Then(TagEnd)
  158. .Labelled($"closing {tag} tag");
  159. }
  160. #endregion
  161. }