using System; using UnityEngine; using UnityEngine.UIElements; using System.Xml.Linq; using System.Text.RegularExpressions; using System.Collections.Generic; using System.Linq; using System.Net; namespace Unity.Tutorials.Core.Editor { /// /// Creates UIToolkit elements from a rich text. /// public static class RichTextParser { // Tries to parse text to XDocument word by word - outputs the longest successful string before failing static string ShowContentWithError(string errorContent) { string longestString = ""; string previousLongestString = ""; string[] lines = errorContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); foreach (string line in lines) { string[] words = line.Split(new[] { " " }, StringSplitOptions.None); foreach (string word in words) { longestString += word + " "; try { XDocument.Parse("" + longestString + ""); } catch { continue; } previousLongestString = longestString; } longestString += "\r\n"; } return previousLongestString; } /// /// Preprocess rich text - add space around tags. /// /// Text with tags /// Text with space around tags static string PreProcessRichText(string inputText) { string processed = inputText; processed = processed.Replace("", " "); processed = processed.Replace("", " "); processed = processed.Replace("", " "); processed = processed.Replace("", " "); processed = processed.Replace("", " "); processed = processed.Replace("", " "); processed = processed.Replace("", " "); processed = processed.Replace("", " "); return processed; } /// /// Helper function to detect if the string contains any characters in /// the Unicode range reserved for Chinese, Japanese and Korean characters. /// /// String to check for CJK letters. /// True if it contains Chinese, Japanese or Korean characters. static bool NeedSymbolWrapping(string textLine) { // Unicode range for CJK letters. // Range chosen from StackOverflow: https://stackoverflow.com/a/42411925 // Validated from sources: // https://www.unicode.org/faq/han_cjk.html // https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) return textLine.Any(c => (uint)c >= 0x4E00 && (uint)c <= 0x2FA1F); } /// /// Adds a new wrapping word Label to the target visualElement. Type can be BoldLabel, ItalicLabel or Label /// /// /// The text inside the word label. /// Redundant storage, mostly used for automated testing. /// Parent container for the word Label. static Label AddLabel(string textToAdd, List elementList, VisualElement addToVisualElement) where T : Label { Label wordLabel = null; Type LabelType = typeof(T); if (LabelType == typeof(ItalicLabel)) { wordLabel = new ItalicLabel(textToAdd); } else if (LabelType == typeof(BoldLabel)) { wordLabel = new BoldLabel(textToAdd); } else if (LabelType == typeof(TextLabel)) { wordLabel = new TextLabel(textToAdd); } if (wordLabel == null) { Debug.LogError("Error: Unsupported Label type used. Use TextLabel, BoldLabel or ItalicLabel."); return null; } elementList.Add(wordLabel); addToVisualElement.Add(wordLabel); return wordLabel; } /// /// Transforms HTML tags to word element labels with different styles to enable rich text. /// /// /// /// The following need to set for the container's style: /// flex-direction: row; /// flex-wrap: wrap; /// /// List of VisualElements made from the parsed text. public static List RichTextToVisualElements(string htmlText, VisualElement targetContainer) { bool addError = false; string errorText = ""; try { XDocument.Parse("" + htmlText + ""); } catch (Exception e) { targetContainer.Clear(); errorText = e.Message; htmlText = ShowContentWithError(htmlText); addError = true; } List elements = new List(); targetContainer.Clear(); bool boldOn = false; // sets this on sets off bool italicOn = false; // bool forceWordWrap = false; bool linkOn = false; string linkURL = ""; string styleClass = ""; bool addStyle = false; bool firstLine = true; bool lastLineHadText = false; // start streaming text per word to elements while retaining current style for each word block string[] lines = htmlText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); foreach (string line in lines) { // Check if the line begins with whitespace and turn that into corresponding Label string initialWhiteSpaces = ""; foreach (char singleCharacter in line) { if (singleCharacter == ' ' || singleCharacter == '\t') { initialWhiteSpaces += singleCharacter; } else { break; } } string processedLine = PreProcessRichText(line); // Separate the line into words string[] words = processedLine.Split(new[] { " ", "\t" }, StringSplitOptions.RemoveEmptyEntries); if (!lastLineHadText) { if (!firstLine) elements.Add(AddParagraphToElement(targetContainer)); if (initialWhiteSpaces.Length > 0) { WhiteSpaceLabel indentationLabel = new WhiteSpaceLabel(initialWhiteSpaces); targetContainer.Add(indentationLabel); elements.Add(indentationLabel); } } if (!firstLine && lastLineHadText) { elements.Add(AddLinebreakToElement(targetContainer)); lastLineHadText = false; if (initialWhiteSpaces.Length > 0) { WhiteSpaceLabel indentationLabel = new WhiteSpaceLabel(initialWhiteSpaces); targetContainer.Add(indentationLabel); elements.Add(indentationLabel); } } foreach (string word in words) { // Wrap every character instead of word in case of Chinese and Japanese // Note: override with Force word wrapping here if (word == "" || word == " " || word == " ") continue; lastLineHadText = true; string strippedWord = word; bool removeBold = false; bool removeItalic = false; bool addParagraph = false; bool removeLink = false; bool removeWordWrap = false; bool removeStyle = false; strippedWord = strippedWord.Trim(); if (strippedWord.Contains("")) { strippedWord = strippedWord.Replace("", ""); boldOn = true; } if (strippedWord.Contains("")) { strippedWord = strippedWord.Replace("", ""); italicOn = true; } if (strippedWord.Contains("")) { strippedWord = strippedWord.Replace("", ""); forceWordWrap = true; } if (strippedWord.Contains("", ""); } if (addStyle && strippedWord.Contains("class=")) { strippedWord = strippedWord.Replace("class=", ""); int styleFrom = strippedWord.IndexOf("\"", StringComparison.Ordinal) + 1; int styleTo = strippedWord.LastIndexOf("\"", StringComparison.Ordinal); styleClass = strippedWord.Substring(styleFrom, styleTo - styleFrom); strippedWord = strippedWord.Substring(styleTo + 2, (strippedWord.Length - 2) - styleTo); strippedWord.Replace("\">", ""); } if (strippedWord.Contains("&")) { strippedWord = WebUtility.HtmlDecode(strippedWord); } if (strippedWord.Contains("")) { strippedWord = strippedWord.Replace("", ""); removeLink = true; } if (strippedWord.Contains("")) { strippedWord = strippedWord.Replace("", ""); removeStyle = true; } if (strippedWord.Contains("
")) { strippedWord = strippedWord.Replace("
", ""); addParagraph = true; } if (strippedWord.Contains("
")) { strippedWord = strippedWord.Replace("
", ""); removeBold = true; } if (strippedWord.Contains("")) { strippedWord = strippedWord.Replace("", ""); removeItalic = true; } if (strippedWord.Contains("")) { strippedWord = strippedWord.Replace("", ""); removeWordWrap = true; } if (boldOn && strippedWord != "") { if (wrapCharacters) { foreach (char character in strippedWord) { AddLabel(character.ToString(), elements, targetContainer); } } else { AddLabel(strippedWord, elements, targetContainer); } } else if (italicOn && strippedWord != "") { if (wrapCharacters) { foreach (char character in strippedWord) { AddLabel(character.ToString(), elements, targetContainer); } } else { AddLabel(strippedWord, elements, targetContainer); } } else if (addParagraph) { elements.Add(AddParagraphToElement(targetContainer)); } else if (linkOn && !string.IsNullOrEmpty(linkURL)) { var label = new HyperlinkLabel { text = strippedWord, tooltip = linkURL }; label.RegisterCallback( (evt, linkurl) => { TutorialEditorUtils.OpenUrl(linkurl); }, linkURL ); targetContainer.Add(label); elements.Add(label); } else { if (strippedWord != "") { if (wrapCharacters) { foreach (char character in strippedWord) { Label newLabel = AddLabel(character.ToString(), elements, targetContainer); if (addStyle && !string.IsNullOrEmpty(styleClass)) { newLabel.AddToClassList(styleClass); } } } else { Label newLabel = AddLabel(strippedWord, elements, targetContainer); if (addStyle && !string.IsNullOrEmpty(styleClass)) { newLabel.AddToClassList(styleClass); } } } } if (removeBold) boldOn = false; if (removeItalic) italicOn = false; if (removeLink) { linkOn = false; linkURL = ""; } if (removeStyle) addStyle = false; if (removeWordWrap) forceWordWrap = false; } firstLine = false; } if (addError) { var label = new ParseErrorLabel() { text = Localization.Tr("PARSE ERROR"), tooltip = Localization.Tr("Click here to see more information in the console.") }; label.RegisterCallback((e) => Debug.LogError(errorText)); targetContainer.Add(label); elements.Add(label); } return elements; } static VisualElement AddLinebreakToElement(VisualElement elementTo) { Label wordLabel = new Label(" "); wordLabel.style.flexDirection = FlexDirection.Row; wordLabel.style.flexGrow = 1f; wordLabel.style.width = 3000f; wordLabel.style.height = 0f; elementTo.Add(wordLabel); return wordLabel; } static VisualElement AddParagraphToElement(VisualElement elementTo) { Label wordLabel = new Label(" "); wordLabel.style.flexDirection = FlexDirection.Row; wordLabel.style.flexGrow = 1f; wordLabel.style.width = 3000f; elementTo.Add(wordLabel); return wordLabel; } // Dummy classes so that we can customize the styles from a USS file. /// /// Label for the red parser error displayed where the parsing fails /// public class ParseErrorLabel : Label {} /// /// Text label for links /// public class HyperlinkLabel : Label {} /// /// Text label for text that wraps per word /// public class TextLabel : Label { /// /// Constructs with text. /// /// public TextLabel(string text) : base(text) { } } /// /// Text label for white space used to indent lines /// public class WhiteSpaceLabel : Label { /// /// Constructs with text. /// /// public WhiteSpaceLabel(string text) : base(text) { } } /// /// Text label with bold style /// public class BoldLabel : Label { /// /// Constructs with text. /// /// public BoldLabel(string text) : base(text) { } } /// /// Text label with italic style /// public class ItalicLabel : Label { /// /// Constructs with text. /// /// public ItalicLabel(string text) : base(text) { } } } }