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(); /// /// The line thickness used by this text formatter, which determines how the default -based formatting codes are drawn. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public float LineThickness = 1 / 16F; /// /// The underline offset used by this text formatter, which determines how the default is drawn. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public float UnderlineOffset = 0.85F; /// /// The strikethrough offset used by this text formatter, which determines how the default 's strikethrough variant is drawn. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public float StrikethroughOffset = 0.55F; /// /// The default subscript offset used by this text formatter, which determines how the default is drawn if no custom value is used. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public float DefaultSubOffset = 0.15F; /// /// The default superscript offset used by this text formatter, which determines how the default is drawn if no custom value is used. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public float DefaultSupOffset = -0.25F; /// /// The default shadow color used by this text formatter, which determines how the default is drawn if no custom value is used. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public Color DefaultShadowColor = Color.Black; /// /// The default shadow offset used by this text formatter, which determines how the default is drawn if no custom value is used. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public Vector2 DefaultShadowOffset = new Vector2(2); /// /// The default wobbly modifier used by this text formatter, which determines how the default is drawn if no custom value is used. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public float DefaultWobblyModifier = 5; /// /// The default wobbly modifier used by this text formatter, which determines how the default is drawn if no custom value is used. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public float DefaultWobblyHeight = 1 / 8F; /// /// The default outline thickness used by this text formatter, which determines how the default is drawn if no custom value is used. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public float DefaultOutlineThickness = 2; /// /// The default outline color used by this text formatter, which determines how the default is drawn if no custom value is used. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public Color DefaultOutlineColor = Color.Black; /// /// Whether the default outline used by this text formatter should also draw outlines diagonally, which determines how the default is drawn if no custom value is used. Non-diagonally drawn outlines might generally look better when using a pixelart font. /// Note that this value only has an effect on the default formatting codes created through the constructor. /// public bool OutlineDiagonals = true; /// /// Creates a new text formatter with an optional set of default formatting codes. /// /// Whether default font modifier codes should be added, including bold, italic, strikethrough, shadow, subscript, and more. /// Whether default color codes should be added, including all values and the ability to use custom colors. /// Whether default animation codes should be added, namely the wobbly animation. /// Whether default macros should be added, including TeX's ~ non-breaking space and more. public TextFormatter(bool hasFontModifiers = true, bool hasColors = true, bool hasAnimations = true, bool hasMacros = true) { // general font modifier codes if (hasFontModifiers) { 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, ColorHelper.TryFromHexString(m.Groups[1].Value, out var color) ? color : this.DefaultShadowColor, float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var offset) ? new Vector2(offset) : this.DefaultShadowOffset)); this.Codes.Add(new Regex(""), (f, m, r) => new UnderlineCode(m, r, this.LineThickness, this.UnderlineOffset)); this.Codes.Add(new Regex(""), (f, m, r) => new UnderlineCode(m, r, this.LineThickness, this.StrikethroughOffset)); this.Codes.Add(new Regex(@""), (f, m, r) => new SubSupCode(m, r, float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var off) ? off : this.DefaultSubOffset)); this.Codes.Add(new Regex(@""), (f, m, r) => new SubSupCode(m, r, float.TryParse(m.Groups[1].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var off) ? -off : this.DefaultSupOffset)); this.Codes.Add(new Regex(@""), (f, m, r) => new OutlineCode(m, r, ColorHelper.TryFromHexString(m.Groups[1].Value, out var color) ? color : this.DefaultOutlineColor, float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var thickness) ? thickness : this.DefaultOutlineThickness, this.OutlineDiagonals)); } // color codes if (hasColors) { 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.TryFromHexString(m.Groups[1].Value, out var color) ? color : Color.Red)); } // animation codes if (hasAnimations) { 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 : this.DefaultWobblyModifier, float.TryParse(m.Groups[2].Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var heightMod) ? heightMod : this.DefaultWobblyHeight)); } // control codes this.Codes.Add(new Regex(@""), (f, m, r) => new SimpleEndCode(m, r, m.Groups[1].Value)); // macros if (hasMacros) { 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; var ret = s; bool matched; do { matched = false; foreach (var macro in this.Macros) { ret = macro.Key.Replace(ret, 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 >= 64) throw new ArithmeticException($"A string resolved macros recursively too many times. Does it contain any conflicting macros?\nOriginal: {s}\nCurrent: {ret}"); } while (matched); return ret; } /// /// Strips all formatting codes from the given string, causing a string without any formatting codes to be returned. /// Note that, if a has already been created using , it is more efficient to use or . /// /// The string to strip formatting codes from. /// The stripped string. public string StripAllFormatting(string s) { foreach (var regex in this.Codes.Keys) s = regex.Replace(s, string.Empty); return s; } private Code GetNextCode(string s, int index, int maxIndex = int.MaxValue) { var (constructor, match, regex) = this.Codes .Select(kv => (Constructor: kv.Value, Match: kv.Key.Match(s, index), Regex: kv.Key)) .Where(kv => kv.Match.Success && kv.Match.Index <= maxIndex) .OrderBy(kv => kv.Match.Index) .FirstOrDefault(); return constructor?.Invoke(this, match, regex); } private static string StripFormatting(GenericFont font, string s, IEnumerable codes) { foreach (var code in codes) { #pragma warning disable CS0618 // this can be combined with StripAllFormatting (which was added after GetReplacementString was deprecated) once GetReplacementString is removed // (just make this method accept a set of regular expressions, and then call it with all code keys in StripAllFormatting, and the applied codes' regexes in Tokenize) 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); } }