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)
{
}
}
}
}