using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using System.Text.RegularExpressions; using Developpez.Dotnet.Collections; using Developpez.Dotnet.Properties; using CH = Developpez.Dotnet.CompatibilityHelper; namespace Developpez.Dotnet.Text { /// /// Représente un modèle de chaine de caractères, où on peut utiliser des noms plutôt que des index /// pour les valeurs à remplacer. /// public class StringTemplate { private static readonly Regex _regex = new Regex(@"(?{+)(?\w+)(?:[^}]+)?(?}+)", CH.RegexOptions.Compiled); private readonly string _template; private readonly string _templateWithIndexes; private readonly IList _placeholders; /// /// Initialise une nouvelle instance de StringTemplate avec le template à utiliser /// /// Le template à utiliser. Le format est identique à celui accepté par String.Format, /// mais avec des noms à la place des index pour les valeurs à remplacer public StringTemplate(string template) : this(template, true) { } private StringTemplate(string template, bool lookupInCache) { template.CheckArgumentNull("template"); _template = template; if (lookupInCache) { var cached = GetTemplateFromCache(template, false); if (cached != null) { _placeholders = cached._placeholders; _templateWithIndexes = cached._templateWithIndexes; return; } } ParseTemplate(out _templateWithIndexes, out _placeholders); } private void ParseTemplate(out string templateWithIndexes, out IList placeholders) { var tmp = new List(); MatchEvaluator evaluator = m => { string open = m.Groups["open"].Value; string close = m.Groups["close"].Value; string key = m.Groups["key"].Value; string format = m.Groups["format"].Value; if (open.Length % 2 == 0) return m.Value; open = RemoveLastChar(open); close = RemoveLastChar(close); if (!tmp.Contains(key)) { tmp.Add(key); } int index = tmp.IndexOf(key); return string.Format("{0}{{{1}{2}}}{3}", open, index, format, close); }; templateWithIndexes = _regex.Replace(_template, evaluator); placeholders = tmp.AsReadOnly(); } private static string RemoveLastChar(string str) { if (str.Length > 1) return str.Substring(0, str.Length - 1); return string.Empty; } /// /// Convertit une chaine en StringTemplate /// /// La chaine à convertir /// Un StringTemplate utilisant la chaine convertie comme template public static implicit operator StringTemplate(string s) { return GetTemplateFromCache(s, true); } /// /// Renvoie une chaine représentant cette instance de StringTemplate. /// /// Le template utilisé par ce StringTemplate public override string ToString() { return _template; } /// /// Remplace les placeholders du template par les valeurs fournies dans le dictionnaire spécifié /// /// Le dictionnaire contenant les valeurs pour les placeholders /// La chaine formatée /// Aucune valeur n'a été trouvée pour /// un placeholder du template public string Format(IDictionary values) { return Format(values, true); } /// /// Remplace les placeholders du template par les valeurs fournies dans le dictionnaire spécifié /// /// Le dictionnaire contenant les valeurs pour les placeholders /// Indique si une exception doit être levée quand aucune valeur /// n'est trouvée pour un placeholder du template. Si ce paramètre vaut false, le placeholder est laissé /// tel quel dans la chaine formatée. /// La chaine formatée /// Le paramètre throwOnMissingValue /// vaut true et aucune valeur n'a été trouvée pour un placeholder du template public string Format(IDictionary values, bool throwOnMissingValue) { values.CheckArgumentNull("values"); object[] array = new object[_placeholders.Count]; for(int i = 0; i < _placeholders.Count; i++) { string key = _placeholders[i]; object value; if (!values.TryGetValue(key, out value)) { if (throwOnMissingValue) throw new KeyNotFoundException(string.Format(ExceptionMessages.TemplateKeyNotFound, key)); value = string.Format("{{{0}}}", key); } array[i] = value; } return string.Format(_templateWithIndexes, array); } /// /// Remplace les placeholders du template par les valeurs fournies dans l'objet spécifié /// /// L'objet contenant les valeurs pour les placeholders. Chaque propriété de /// l'objet correspond à un placeholder du template /// La chaine formatée /// Aucune valeur n'a été trouvée pour /// un placeholder du template /// Cette méthode s'utilise typiquement avec un objet de type anonyme, créé avec la syntaxe /// new { nom1 = valeur1, nom2 = valeur2 } public string Format(object values) { return Format(values, true); } /// /// Remplace les placeholders du template par les valeurs fournies dans l'objet spécifié /// /// L'objet contenant les valeurs pour les placeholders. Chaque propriété de /// l'objet correspond à un placeholder du template /// Indique si une exception doit être levée quand aucune valeur /// n'est trouvée pour un placeholder du template. Si ce paramètre vaut false, le placeholder est laissé /// tel quel dans la chaine formatée. /// La chaine formatée /// Le paramètre throwOnMissingValue /// vaut true et aucune valeur n'a été trouvée pour un placeholder du template /// Cette méthode s'utilise typiquement avec un objet de type anonyme, créé avec la syntaxe /// new { nom1 = valeur1, nom2 = valeur2 } public string Format(object values, bool throwOnMissingValue) { values.CheckArgumentNull("values"); return Format(MakeDictionary(values), throwOnMissingValue); } private IDictionary MakeDictionary(object obj) { Dictionary dict = new Dictionary(); foreach (string name in _placeholders) { object value; if (TryGetMemberValue(obj, name, out value)) dict.Add(name, value); } return dict; } private static bool TryGetMemberValue(object obj, string memberName, out object value) { Type type = obj.GetType(); Func getter = GetGetterFromCache(type, memberName); if (getter != null) { value = getter(obj); return true; } value = null; return false; } /// /// Remplace les placeholders du template spécifié par les valeurs fournies dans le dictionnaire spécifié /// /// Le template à utiliser /// Le dictionnaire contenant les valeurs pour les placeholders /// La chaine formatée /// Aucune valeur n'a été trouvée pour /// un placeholder du template public static string Format(string template, IDictionary values) { return GetTemplateFromCache(template, true).Format(values); } /// /// Remplace les placeholders du template spécifié par les valeurs fournies dans le dictionnaire spécifié /// /// Le template à utiliser /// Le dictionnaire contenant les valeurs pour les placeholders /// Indique si une exception doit être levée quand aucune valeur /// n'est trouvée pour un placeholder du template. Si ce paramètre vaut false, le placeholder est laissé /// tel quel dans la chaine formatée. /// La chaine formatée /// Le paramètre throwOnMissingValue /// vaut true et aucune valeur n'a été trouvée pour un placeholder du template public static string Format(string template, IDictionary values, bool throwOnMissingValue) { return GetTemplateFromCache(template, true).Format(values, throwOnMissingValue); } /// /// Remplace les placeholders du template par les valeurs fournies dans l'objet spécifié /// /// Le template à utiliser /// L'objet contenant les valeurs pour les placeholders. Chaque placeholder du /// template est remplacé par la valeur de la propriété ou du champ de même nom /// La chaine formatée /// Cette méthode s'utilise typiquement avec un objet de type anonyme, créé avec la syntaxe /// new { nom1 = valeur1, nom2 = valeur2 } /// Aucune valeur n'a été trouvée pour /// un placeholder du template public static string Format(string template, object values) { return GetTemplateFromCache(template, true).Format(values); } /// /// Remplace les placeholders du template par les valeurs fournies dans l'objet spécifié /// /// Le template à utiliser /// L'objet contenant les valeurs pour les placeholders. Chaque placeholder du /// template est remplacé par la valeur de la propriété ou du champ de même nom /// Indique si une exception doit être levée quand aucune valeur /// n'est trouvée pour un placeholder du template. Si ce paramètre vaut false, le placeholder est laissé /// tel quel dans la chaine formatée. /// La chaine formatée /// Cette méthode s'utilise typiquement avec un objet de type anonyme, créé avec la syntaxe /// new { nom1 = valeur1, nom2 = valeur2 } /// Le paramètre throwOnMissingValue /// vaut true et aucune valeur n'a été trouvée pour un placeholder du template public static string Format(string template, object values, bool throwOnMissingValue) { return GetTemplateFromCache(template, true).Format(values, throwOnMissingValue); } #region Cache private static readonly Dictionary _templateCache = new Dictionary(); private static StringTemplate GetTemplateFromCache(string template, bool create) { StringTemplate stringTemplate; if (!_templateCache.TryGetValue(template, out stringTemplate) && create) { stringTemplate = new StringTemplate(template, false); _templateCache[template] = stringTemplate; } return stringTemplate; } private static readonly Dictionary>> _gettersCache = new Dictionary>>(); private static Func GetGetterFromCache(Type type, string memberName) { Dictionary> typeGetters; if (!_gettersCache.TryGetValue(type, out typeGetters)) { typeGetters = new Dictionary>(); _gettersCache[type] = typeGetters; } Func getter; if (!typeGetters.TryGetValue(memberName, out getter)) { getter = CreateGetter(type, memberName); typeGetters[memberName] = getter; } return getter; } private static Func CreateGetter(Type type, string memberName) { MemberInfo member = null; while (type != null) { #if NETFX_CORE var prop = type.GetTypeInfo().GetDeclaredProperty(memberName); #else var prop = type.GetProperty(memberName, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance); #endif if (prop != null && prop.CanRead) { member = prop; break; } #if NETFX_CORE var field = type.GetTypeInfo().GetDeclaredField(memberName); #else var field = type.GetField(memberName, BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance); #endif if (field != null) { member = field; break; } #if NETFX_CORE type = type.GetTypeInfo().BaseType; #else type = type.BaseType; #endif } if (member == null) return null; var param = Expression.Parameter(typeof (object), "x"); var memberAccess = Expression.MakeMemberAccess(Expression.Convert(param, type), member); Expression body = memberAccess; #if NETFX_CORE if (memberAccess.Type.GetTypeInfo().IsValueType) #else if (memberAccess.Type.IsValueType) #endif body = Expression.Convert(memberAccess, typeof (object)); var expr = Expression.Lambda>(body, param); return expr.Compile(); } #endregion } }