using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Xml.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
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
///
/// Content that contains a markdown error
///
static string ParseUntilError(string faultyContent)
{
string longestString = "";
string previousLongestString = "";
string[] lines = faultyContent.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 targetContainer)
where T : Label
{
Type labelType = typeof(T);
Func generator = null;
if (labelType == typeof(ItalicLabel))
{
generator = () => new ItalicLabel(textToAdd);
}
else if (labelType == typeof(BoldLabel))
{
generator = () => new BoldLabel(textToAdd);
}
else if (labelType == typeof(TextLabel))
{
generator = () => new TextLabel(textToAdd);
}
else if (labelType == typeof(WhiteSpaceLabel))
{
generator = () => new WhiteSpaceLabel(textToAdd);
}
if (generator == null)
{
Debug.LogError("Error: Unsupported Label type used. Use TextLabel, BoldLabel or ItalicLabel.");
return null;
}
return TrackAndAddGeneratedElement(generator, targetContainer, elementList) as Label;
}
static VisualElement GenerateParseErrorLabel(string errorText)
{
var label = new ParseErrorLabel()
{
text = Localization.Tr(LocalizationKeys.k_TutorialLabelParseError),
tooltip = Localization.Tr(LocalizationKeys.k_TutorialLabelParseErrorTooltip)
};
label.RegisterCallback((e) => Debug.LogError(errorText));
return label;
}
static VisualElement GenerateLinkLabel(string text, string url)
{
var label = new HyperlinkLabel
{
text = text,
tooltip = url
};
EventCallback linkCallback = null;
string actualPath = "";
// Link points to a relative directory
if (url[0] == '.')
{
if (url.Length == 1) //current folder
{
actualPath = Path.GetFullPath(url);
}
else if (url.StartsWith("./"))
{
if (url.Length == 2) //current folder
{
actualPath = Path.GetFullPath(url);
}
else //Folder within the current one, or parent folder
{
actualPath = Path.GetFullPath(url.Remove(0, 2));
}
}
else //local asset, assume the "." is there only because the user doesn't know about the "./" notation
{
actualPath = Path.GetFullPath(url.Remove(0, 1));
}
linkCallback = (evt, path) =>
{
EditorUtility.OpenWithDefaultApp(path);
};
}
else
{
actualPath = url;
linkCallback = (evt, path) =>
{
TutorialEditorUtils.OpenUrl(path);
};
}
label.RegisterCallback(linkCallback, actualPath);
return label;
}
///
/// 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)
{
targetContainer.Clear();
var generatedElements = new List();
// 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);
RenderLines(lines, targetContainer, generatedElements);
return generatedElements;
}
static void RenderLines(string[] lines, VisualElement targetContainer, List elements)
{
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;
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)
{
AddLabel(initialWhiteSpaces, elements, targetContainer);
}
}
if (!firstLine && lastLineHadText)
{
elements.Add(AddLinebreakToElement(targetContainer));
lastLineHadText = false;
if (initialWhiteSpaces.Length > 0)
{
AddLabel(initialWhiteSpaces, elements, targetContainer);
}
}
RenderWords(words, ref lastLineHadText, ref boldOn, ref italicOn, ref forceWordWrap, ref linkOn, ref addStyle, ref linkURL, ref styleClass, elements, targetContainer);
firstLine = false;
}
}
static void RenderWords(string[] words, ref bool lastLineHadText, ref bool boldOn,
ref bool italicOn, ref bool forceWordWrap, ref bool linkOn, ref bool addStyle, ref string linkURL,
ref string styleClass, List elements, VisualElement targetContainer)
{
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))
{
string url = linkURL;
TrackAndAddGeneratedElement(() => GenerateLinkLabel(strippedWord, url), targetContainer, elements);
}
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;
}
}
}
static VisualElement TrackAndAddGeneratedElement(Func generator, VisualElement targetContainer, List trackedElements)
{
VisualElement newElement = generator.Invoke();
targetContainer.Add(newElement);
trackedElements.Add(newElement);
return newElement;
}
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)
{
}
}
}
}