using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using Microsoft.Xna.Framework; using MLEM.Extensions; using MLEM.Font; using MLEM.Formatting.Codes; using MLEM.Misc; namespace MLEM.Formatting { /// /// A text formatter is used for drawing text using that contains different colors, bold/italic sections and animations. /// To format a string of text, use the codes as specified in the constructor. To tokenize and render a formatted string, use . /// public class TextFormatter : GenericDataHolder { /// /// The formatting codes that this text formatter uses. /// The defines how the formatting code should be matched. /// public readonly Dictionary Codes = new Dictionary(); /// /// The macros that this text formatter uses. /// A macro is a that turns a snippet of text into another snippet of text. /// Macros can resolve recursively and can resolve into formatting codes. /// public readonly Dictionary Macros = new Dictionary(); /// /// Creates a new text formatter with a set of default formatting codes. /// public TextFormatter() { // font codes this.Codes.Add(new Regex(""), (f, m, r) => new FontCode(m, r, fnt => fnt.Bold)); this.Codes.Add(new Regex(""), (f, m, r) => new FontCode(m, r, fnt => fnt.Italic)); this.Codes.Add(new Regex(@""), (f, m, r) => new ShadowCode(m, r, m.Groups[1].Success ? ColorHelper.FromHexString(m.Groups[1].Value) : Color.Black, new Vector2(float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var offset) ? offset : 2))); this.Codes.Add(new Regex(""), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.85F)); this.Codes.Add(new Regex(""), (f, m, r) => new UnderlineCode(m, r, 1 / 16F, 0.55F)); // color codes foreach (var c in typeof(Color).GetProperties()) { if (c.GetGetMethod().IsStatic) { var value = (Color) c.GetValue(null); this.Codes.Add(new Regex($""), (f, m, r) => new ColorCode(m, r, value)); } } this.Codes.Add(new Regex(@""), (f, m, r) => new ColorCode(m, r, ColorHelper.FromHexString(m.Groups[1].Value))); // animation codes this.Codes.Add(new Regex(@""), (f, m, r) => new WobblyCode(m, r, float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var mod) ? mod : 5, float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var heightMod) ? heightMod : 1 / 8F)); // control codes this.Codes.Add(new Regex(@""), (f, m, r) => new SimpleEndCode(m, r, m.Groups[1].Value)); // macros this.Macros.Add(new Regex("~"), (f, m, r) => GenericFont.Nbsp.ToString()); this.Macros.Add(new Regex(""), (f, m, r) => '\n'.ToString()); } /// /// Tokenizes a string, returning a tokenized string that is ready for splitting, measuring and drawing. /// /// The font to use for tokenization. Note that this font needs to be the same that will later be used for splitting, measuring and/or drawing. /// The string to tokenize /// The text alignment that should be used. This alignment can later be changed using . /// The tokenized string. public TokenizedString Tokenize(GenericFont font, string s, TextAlignment alignment = TextAlignment.Left) { // resolve macros s = this.ResolveMacros(s); var tokens = new List(); var codes = new List(); // add the formatting code right at the start of the string var firstCode = this.GetNextCode(s, 0, 0); if (firstCode != null) codes.Add(firstCode); var index = 0; var rawIndex = 0; while (rawIndex < s.Length) { var next = this.GetNextCode(s, rawIndex + 1); // if we've reached the end of the string if (next == null) { var sub = s.Substring(rawIndex, s.Length - rawIndex); tokens.Add(new Token(codes.ToArray(), index, rawIndex, TextFormatter.StripFormatting(font, sub, codes), sub)); break; } // create a new token for the content up to the next code var ret = s.Substring(rawIndex, next.Match.Index - rawIndex); var strippedRet = TextFormatter.StripFormatting(font, ret, codes); tokens.Add(new Token(codes.ToArray(), index, rawIndex, strippedRet, ret)); // move to the start of the next code rawIndex = next.Match.Index; index += strippedRet.Length; // remove all codes that are incompatible with the next one and apply it codes.RemoveAll(c => c.EndsHere(next) || next.EndsOther(c)); codes.Add(next); } return new TokenizedString(font, alignment, s, TextFormatter.StripFormatting(font, s, tokens.SelectMany(t => t.AppliedCodes)), tokens.ToArray()); } /// /// Resolves the macros in the given string recursively, until no more macros can be resolved. /// This method is used by , meaning that it does not explicitly have to be called when using text formatting. /// /// The string to resolve macros for /// The final, recursively resolved string public string ResolveMacros(string s) { // resolve macros that resolve into macros var rec = 0; bool matched; do { matched = false; foreach (var macro in this.Macros) { s = macro.Key.Replace(s, m => { // if the match evaluator was queried, then we know we matched something matched = true; return macro.Value(this, m, macro.Key); }); } rec++; if (rec >= 16) throw new ArithmeticException($"A string resolved macros recursively too many times. Does it contain any conflicting macros?\n{s}"); } while (matched); return s; } private Code GetNextCode(string s, int index, int maxIndex = int.MaxValue) { var (c, m, r) = this.Codes .Select(kv => (c: kv.Value, m: kv.Key.Match(s, index), r: kv.Key)) .Where(kv => kv.m.Success && kv.m.Index <= maxIndex) .OrderBy(kv => kv.m.Index) .FirstOrDefault(); return c?.Invoke(this, m, r); } private static string StripFormatting(GenericFont font, string s, IEnumerable codes) { foreach (var code in codes) { #pragma warning disable CS0618 s = code.Regex.Replace(s, code.GetReplacementString(font)); #pragma warning restore CS0618 } return s; } /// /// Represents a text formatting macro. Used by . /// /// The text formatter that created this macro /// The match for the macro's regex /// The regex used to create this macro public delegate string Macro(TextFormatter formatter, Match match, Regex regex); } }