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(@"(\w+)>"), (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 applied = new List();
var allCodes = new List();
// add the formatting code right at the start of the string
var firstCode = this.GetNextCode(s, 0, 0);
if (firstCode != null)
applied.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(applied.ToArray(), index, rawIndex, TextFormatter.StripFormatting(font, sub, applied), sub));
break;
}
allCodes.Add(next);
// 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, applied);
tokens.Add(new Token(applied.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
applied.RemoveAll(c => c.EndsHere(next) || next.EndsOther(c));
applied.Add(next);
}
return new TokenizedString(font, alignment, s, TextFormatter.StripFormatting(font, s, allCodes), tokens.ToArray(), allCodes.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);
}
}