2020-05-15 00:34:04 +02:00
using System.Collections.Generic ;
using System.Linq ;
using System.Text.RegularExpressions ;
using Microsoft.Xna.Framework ;
using MLEM.Extensions ;
using MLEM.Font ;
using MLEM.Formatting.Codes ;
2020-05-15 14:22:33 +02:00
using MLEM.Misc ;
2020-05-15 00:34:04 +02:00
namespace MLEM.Formatting {
2020-05-21 12:53:42 +02:00
/// <summary>
/// A text formatter is used for drawing text using <see cref="GenericFont"/> 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 <see cref="Tokenize"/>.
/// </summary>
2020-05-15 14:22:33 +02:00
public class TextFormatter : GenericDataHolder {
2020-05-15 00:34:04 +02:00
2020-05-21 12:53:42 +02:00
/// <summary>
/// The formatting codes that this text formatter uses.
/// The <see cref="Regex"/> defines how the formatting code should be matched.
/// </summary>
2020-05-15 00:34:04 +02:00
public readonly Dictionary < Regex , Code . Constructor > Codes = new Dictionary < Regex , Code . Constructor > ( ) ;
2020-07-01 14:30:47 +02:00
/// <summary>
/// The macros that this text formatter uses.
/// A macro is a <see cref="Regex"/> that turns a snippet of text into another snippet of text.
/// Macros can resolve recursively and can resolve into formatting codes.
/// </summary>
public readonly Dictionary < Regex , Macro > Macros = new Dictionary < Regex , Macro > ( ) ;
2020-05-15 00:34:04 +02:00
2020-05-21 12:53:42 +02:00
/// <summary>
/// Creates a new text formatter with a set of default formatting codes.
/// </summary>
2020-05-17 00:10:29 +02:00
public TextFormatter ( ) {
2020-05-15 00:34:04 +02:00
// font codes
2020-05-17 00:10:29 +02:00
this . Codes . Add ( new Regex ( "<b>" ) , ( f , m , r ) = > new FontCode ( m , r , fnt = > fnt . Bold ) ) ;
this . Codes . Add ( new Regex ( "<i>" ) , ( f , m , r ) = > new FontCode ( m , r , fnt = > fnt . Italic ) ) ;
2020-11-04 23:44:41 +01:00
this . Codes . Add ( new Regex ( @"<s(?: #([0-9\w]{6,8}) (([+-.0-9]*)))?>" ) , ( 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 , out var offset ) ? offset : 2 ) ) ) ;
2020-05-15 22:15:24 +02:00
this . Codes . Add ( new Regex ( "<u>" ) , ( f , m , r ) = > new UnderlineCode ( m , r , 1 / 16F , 0.85F ) ) ;
this . Codes . Add ( new Regex ( "</(b|i|s|u|l)>" ) , ( f , m , r ) = > new FontCode ( m , r , null ) ) ;
2020-05-15 00:34:04 +02:00
// color codes
foreach ( var c in typeof ( Color ) . GetProperties ( ) ) {
if ( c . GetGetMethod ( ) . IsStatic ) {
var value = ( Color ) c . GetValue ( null ) ;
2020-05-15 19:55:59 +02:00
this . Codes . Add ( new Regex ( $"<c {c.Name}>" ) , ( f , m , r ) = > new ColorCode ( m , r , value ) ) ;
2020-05-15 00:34:04 +02:00
}
}
2020-11-04 23:44:41 +01:00
this . Codes . Add ( new Regex ( @"<c #([0-9\w]{6,8})>" ) , ( f , m , r ) = > new ColorCode ( m , r , ColorHelper . FromHexString ( m . Groups [ 1 ] . Value ) ) ) ;
2020-05-15 19:55:59 +02:00
this . Codes . Add ( new Regex ( "</c>" ) , ( f , m , r ) = > new ColorCode ( m , r , null ) ) ;
2020-05-15 14:22:33 +02:00
// animation codes
2020-05-15 19:55:59 +02:00
this . Codes . Add ( new Regex ( @"<a wobbly(?: ([+-.0-9]*) ([+-.0-9]*))?>" ) , ( f , m , r ) = > new WobblyCode ( m , r , float . TryParse ( m . Groups [ 1 ] . Value , out var mod ) ? mod : 5 , float . TryParse ( m . Groups [ 2 ] . Value , out var heightMod ) ? heightMod : 1 / 8F ) ) ;
this . Codes . Add ( new Regex ( "</a>" ) , ( f , m , r ) = > new AnimatedCode ( m , r ) ) ;
2020-07-01 14:30:47 +02:00
// macros
this . Macros . Add ( new Regex ( "~" ) , ( f , m , r ) = > GenericFont . Nbsp . ToCachedString ( ) ) ;
2021-03-09 17:45:49 +01:00
this . Macros . Add ( new Regex ( "<n>" ) , ( f , m , r ) = > '\n' . ToCachedString ( ) ) ;
2020-05-15 00:34:04 +02:00
}
2020-05-21 12:53:42 +02:00
/// <summary>
/// Tokenizes a string, returning a tokenized string that is ready for splitting, measuring and drawing.
/// </summary>
/// <param name="font">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.</param>
/// <param name="s">The string to tokenize</param>
/// <returns></returns>
2020-05-15 19:55:59 +02:00
public TokenizedString Tokenize ( GenericFont font , string s ) {
2020-07-01 14:30:47 +02:00
// resolve macros
s = this . ResolveMacros ( s ) ;
2020-05-15 00:34:04 +02:00
var tokens = new List < Token > ( ) ;
var codes = new List < Code > ( ) ;
2020-05-30 19:17:18 +02:00
// add the formatting code right at the start of the string
var firstCode = this . GetNextCode ( s , 0 , 0 ) ;
if ( firstCode ! = null )
codes . Add ( firstCode ) ;
2020-05-15 00:34:04 +02:00
var rawIndex = 0 ;
while ( rawIndex < s . Length ) {
2020-05-15 19:55:59 +02:00
var index = StripFormatting ( font , s . Substring ( 0 , rawIndex ) , tokens . SelectMany ( t = > t . AppliedCodes ) ) . Length ;
2020-05-15 00:34:04 +02:00
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 ) ;
2020-05-15 19:55:59 +02:00
tokens . Add ( new Token ( codes . ToArray ( ) , index , rawIndex , StripFormatting ( font , sub , codes ) , sub ) ) ;
2020-05-15 00:34:04 +02:00
break ;
}
// create a new token for the content up to the next code
var ret = s . Substring ( rawIndex , next . Match . Index - rawIndex ) ;
2020-05-15 19:55:59 +02:00
tokens . Add ( new Token ( codes . ToArray ( ) , index , rawIndex , StripFormatting ( font , ret , codes ) , ret ) ) ;
2020-05-15 00:34:04 +02:00
// move to the start of the next code
rawIndex = next . Match . Index ;
// remove all codes that are incompatible with the next one and apply it
codes . RemoveAll ( c = > c . EndsHere ( next ) ) ;
codes . Add ( next ) ;
}
2020-05-17 00:10:29 +02:00
return new TokenizedString ( font , s , StripFormatting ( font , s , tokens . SelectMany ( t = > t . AppliedCodes ) ) , tokens . ToArray ( ) ) ;
2020-05-15 00:34:04 +02:00
}
2020-07-01 14:30:47 +02:00
/// <summary>
/// Resolves the macros in the given string recursively, until no more macros can be resolved.
/// This method is used by <see cref="Tokenize"/>, meaning that it does not explicitly have to be called when using text formatting.
/// </summary>
/// <param name="s">The string to resolve macros for</param>
/// <returns>The final, recursively resolved string</returns>
public string ResolveMacros ( string s ) {
// resolve macros that resolve into macros
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 ) ;
} ) ;
}
} while ( matched ) ;
return s ;
}
2020-05-30 19:17:18 +02:00
private Code GetNextCode ( string s , int index , int maxIndex = int . MaxValue ) {
2020-05-15 19:55:59 +02:00
var ( c , m , r ) = this . Codes
. Select ( kv = > ( c : kv . Value , m : kv . Key . Match ( s , index ) , r : kv . Key ) )
2020-05-30 19:17:18 +02:00
. Where ( kv = > kv . m . Success & & kv . m . Index < = maxIndex )
2020-05-15 00:34:04 +02:00
. OrderBy ( kv = > kv . m . Index )
. FirstOrDefault ( ) ;
2020-05-15 19:55:59 +02:00
return c ? . Invoke ( this , m , r ) ;
}
private static string StripFormatting ( GenericFont font , string s , IEnumerable < Code > codes ) {
foreach ( var code in codes )
s = code . Regex . Replace ( s , code . GetReplacementString ( font ) ) ;
return s ;
2020-05-15 00:34:04 +02:00
}
2020-07-01 14:30:47 +02:00
/// <summary>
/// Represents a text formatting macro. Used by <see cref="TextFormatter.Macros"/>.
/// </summary>
/// <param name="formatter">The text formatter that created this macro</param>
/// <param name="match">The match for the macro's regex</param>
/// <param name="regex">The regex used to create this macro</param>
public delegate string Macro ( TextFormatter formatter , Match match , Regex regex ) ;
2020-05-15 00:34:04 +02:00
}
}