2020-05-15 00:34:04 +02:00
using System ;
using System.Collections.Generic ;
using System.Collections.ObjectModel ;
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-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-05-15 19:55:59 +02: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 ? ColorExtensions . FromHex ( 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-05-15 19:55:59 +02:00
this . Codes . Add ( new Regex ( @"<c #([0-9\w]{6,8})>" ) , ( f , m , r ) = > new ColorCode ( m , r , ColorExtensions . FromHex ( m . Groups [ 1 ] . Value ) ) ) ;
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-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-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-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
}
}
}