using System; using System.Collections.Generic; using System.Linq; using UnityEngine.Localization.SmartFormat.Core.Extensions; using UnityEngine.Localization.SmartFormat.Core.Formatting; using UnityEngine.Localization.SmartFormat.Core.Output; using UnityEngine.Localization.SmartFormat.Core.Parsing; using UnityEngine.Localization.SmartFormat.Core.Settings; namespace UnityEngine.Localization.SmartFormat { /// /// This class contains the Format method that constructs /// the composite string by invoking each extension. /// [Serializable] public class SmartFormatter : ISerializationCallbackReceiver { [SerializeReference] SmartSettings m_Settings; [SerializeReference] Parser m_Parser; [SerializeReference] List m_Sources; [SerializeReference] List m_Formatters; List m_NotEmptyFormatterExtensionNames; static readonly object[] k_Empty = { null }; /// /// Event raising, if an error occurs during formatting. /// public event EventHandler OnFormattingFailure; /// /// Gets the list of source extensions. /// public List SourceExtensions { get => m_Sources; } /// /// Gets the list of formatter extensions. /// public List FormatterExtensions => m_Formatters; /// /// Creates a new instance of SmartFormatter. /// public SmartFormatter() { m_Settings = new SmartSettings(); m_Parser = new Parser(m_Settings); m_Sources = new List(); m_Formatters = new List(); } /// /// Gets all names of registered formatter extensions which are not empty. /// /// public List GetNotEmptyFormatterExtensionNames() { if (m_NotEmptyFormatterExtensionNames != null) return m_NotEmptyFormatterExtensionNames; m_NotEmptyFormatterExtensionNames = new List(); foreach (var extension in FormatterExtensions) { if (extension?.Names != null) { foreach (var name in extension.Names) { if (!string.IsNullOrEmpty(name)) m_NotEmptyFormatterExtensionNames.Add(name); } } } return m_NotEmptyFormatterExtensionNames; } /// /// Adds each extensions to this formatter. /// Each extension must implement ISource. /// /// public void AddExtensions(params ISource[] sourceExtensions) { SourceExtensions.InsertRange(0, sourceExtensions); } /// /// Adds each extensions to this formatter. /// Each extension must implement IFormatter. /// /// public void AddExtensions(params IFormatter[] formatterExtensions) { m_NotEmptyFormatterExtensionNames = null; FormatterExtensions.InsertRange(0, formatterExtensions); } /// /// Searches for a Source Extension of the given type, and returns it. /// This can be used to easily find and configure extensions. /// Returns null if the type cannot be found. /// /// /// public T GetSourceExtension() where T : class, ISource { return SourceExtensions.OfType().FirstOrDefault(); } /// /// Searches for a Formatter Extension of the given type, and returns it. /// This can be used to easily find and configure extensions. /// Returns null if the type cannot be found. /// /// /// public T GetFormatterExtension() where T : class, IFormatter { return FormatterExtensions.OfType().FirstOrDefault(); } /// /// Gets or set the instance of the /// public Parser Parser { get => m_Parser; set => m_Parser = value; } /// /// Get the for Smart.Format /// public SmartSettings Settings { get => m_Settings; set => m_Settings = value; } /// /// Replaces one or more format items in a specified string with the string representation of a specific object. /// /// A composite format string. /// The object to format. /// Returns the formatted input with items replaced with their string representation. public string Format(string format, params object[] args) { return Format(null, args, format); } /// /// Replaces one or more format items in a specified string with the string representation of a specific object. /// /// The list of objects to format. /// A composite format string. /// Returns the formatted input with items replaced with their string representation. public string Format(IList args, string format) { return Format(null, args, format); } /// /// Replaces one or more format items in a specified string with the string representation of a specific object. /// /// The to use. /// A composite format string. /// The object to format. /// Returns the formatted input with items replaced with their string representation. public string Format(IFormatProvider provider, string format, params object[] args) { return Format(provider, args, format); } /// /// Replaces one or more format items in a specified string with the string representation of a specific object. /// /// The to use. /// The object to format. /// A composite format string. /// Returns the formatted input with items replaced with their string representation. public string Format(IFormatProvider provider, IList args, string format) { args = args ?? k_Empty; using (StringOutputPool.Get(format.Length + args.Count * 8, out var output)) { var formatParsed = Parser.ParseFormat(format, GetNotEmptyFormatterExtensionNames()); var current = args.Count > 0 ? args[0] : args; // The first item is the default. var formatDetails = FormatDetailsPool.Get(this, formatParsed, args, null, provider, output); Format(formatDetails, formatParsed, current); FormatDetailsPool.Release(formatDetails); FormatItemPool.ReleaseFormat(formatParsed); return output.ToString(); } } /// /// Writes the formatting result into an instance. /// /// The where the result is written to. /// The format string. /// The objects to format. public void FormatInto(IOutput output, string format, params object[] args) { args = args ?? k_Empty; var formatParsed = Parser.ParseFormat(format, GetNotEmptyFormatterExtensionNames()); var current = args.Length > 0 ? args[0] : args; // The first item is the default. var formatDetails = FormatDetailsPool.Get(this, formatParsed, args, null, null, output); Format(formatDetails, formatParsed, current); FormatDetailsPool.Release(formatDetails); FormatItemPool.ReleaseFormat(formatParsed); } /// /// Replaces one or more format items in a specified string with the string representation of a specific object, /// using the . /// /// The to use. /// A composite format string. /// The objects to format. /// Returns the formatted input with items replaced with their string representation. public string FormatWithCache(ref FormatCache cache, string format, IList args) { return FormatWithCache(ref cache, format, null, args); } /// /// Replaces one or more format items in a specified string with the string representation of a specific object, /// using the . /// /// /// /// /// /// public string FormatWithCache(ref FormatCache cache, string format, IFormatProvider formatProvider, IList args) { args = args ?? k_Empty; using (StringOutputPool.Get(format.Length + args.Count * 8, out var output)) { if (cache == null) cache = FormatCachePool.Get(Parser.ParseFormat(format, GetNotEmptyFormatterExtensionNames())); var current = args.Count > 0 ? args[0] : args; // The first item is the default. var formatDetails = FormatDetailsPool.Get(this, cache.Format, args, cache, formatProvider, output); Format(formatDetails, cache.Format, current); FormatDetailsPool.Release(formatDetails); return output.ToString(); } } /// /// Writes the formatting result into an instance, using the . /// /// The to use. /// The where the result is written to. /// The format string. /// The objects to format. public void FormatWithCacheInto(ref FormatCache cache, IOutput output, string format, params object[] args) { args = args ?? k_Empty; if (cache == null) cache = FormatCachePool.Get(Parser.ParseFormat(format, GetNotEmptyFormatterExtensionNames())); var current = args.Length > 0 ? args[0] : args; // The first item is the default. var formatDetails = FormatDetailsPool.Get(this, cache.Format, args, cache, null, output); Format(formatDetails, cache.Format, current); FormatDetailsPool.Release(formatDetails); } void Format(FormatDetails formatDetails, Format format, object current) { var formattingInfo = FormattingInfoPool.Get(formatDetails, format, current); Format(formattingInfo); FormattingInfoPool.Release(formattingInfo); } /// /// Format the argument. /// /// public virtual void Format(FormattingInfo formattingInfo) { if (formattingInfo.Format is null) return; // Before we start, make sure we have at least one source extension and one formatter extension: CheckForExtensions(); foreach (var item in formattingInfo.Format.Items) { if (item is LiteralText literalItem) { formattingInfo.Write(literalItem.ToString()); continue; } // Otherwise, the item must be a placeholder. var placeholder = (Placeholder)item; var childFormattingInfo = formattingInfo.CreateChild(placeholder); try { EvaluateSelectors(childFormattingInfo); } catch (DataNotReadyException ex) { // Handle async data not being ready (LOC-1087) if (!string.IsNullOrEmpty(ex.Text)) formattingInfo.Write(ex.Text); continue; } catch (Exception ex) { // An error occurred while evaluation selectors var errorIndex = placeholder.Format?.startIndex ?? placeholder.Selectors.Last().endIndex; FormatError(item, ex, errorIndex, childFormattingInfo); continue; } try { EvaluateFormatters(childFormattingInfo); } catch (Exception ex) { // An error occurred while evaluating formatters var errorIndex = placeholder.Format?.startIndex ?? placeholder.Selectors.Last().endIndex; FormatError(item, ex, errorIndex, childFormattingInfo); } } } private void FormatError(FormatItem errorItem, Exception innerException, int startIndex, FormattingInfo formattingInfo) { OnFormattingFailure?.Invoke(this, new FormattingErrorEventArgs(errorItem.RawText, startIndex, Settings.FormatErrorAction != ErrorAction.ThrowError)); switch (Settings.FormatErrorAction) { case ErrorAction.Ignore: return; case ErrorAction.ThrowError: throw innerException as FormattingException ?? new FormattingException(errorItem, innerException, startIndex); case ErrorAction.OutputErrorInResult: formattingInfo.FormatDetails.FormattingException = innerException as FormattingException ?? new FormattingException(errorItem, innerException, startIndex); formattingInfo.Write(innerException.Message); formattingInfo.FormatDetails.FormattingException = null; break; case ErrorAction.MaintainTokens: formattingInfo.Write(formattingInfo.Placeholder.RawText); break; } } private void CheckForExtensions() { if (SourceExtensions.Count == 0) throw new InvalidOperationException( "No source extensions are available. Please add at least one source extension, such as the DefaultSource."); if (FormatterExtensions.Count == 0) throw new InvalidOperationException( "No formatter extensions are available. Please add at least one formatter extension, such as the DefaultFormatter."); } private void EvaluateSelectors(FormattingInfo formattingInfo) { if (formattingInfo.Placeholder == null) return; var firstSelector = true; foreach (var selector in formattingInfo.Placeholder.Selectors) { formattingInfo.Selector = selector; formattingInfo.Result = null; var handled = InvokeSourceExtensions(formattingInfo); if (handled) formattingInfo.CurrentValue = formattingInfo.Result; if (firstSelector) { firstSelector = false; // Handle "nested scopes" by traversing the stack: var parentFormattingInfo = formattingInfo; while (!handled && parentFormattingInfo.Parent != null) { parentFormattingInfo = parentFormattingInfo.Parent; parentFormattingInfo.Selector = selector; parentFormattingInfo.Result = null; handled = InvokeSourceExtensions(parentFormattingInfo); if (handled) formattingInfo.CurrentValue = parentFormattingInfo.Result; } } if (!handled) throw formattingInfo.FormattingException($"Could not evaluate the selector \"{selector.RawText}\"", selector); } } private bool InvokeSourceExtensions(FormattingInfo formattingInfo) { foreach (var sourceExtension in SourceExtensions) { var handled = sourceExtension.TryEvaluateSelector(formattingInfo); if (handled) return true; } return false; } /// /// Try to get a suitable formatter. /// /// /// private void EvaluateFormatters(FormattingInfo formattingInfo) { var handled = InvokeFormatterExtensions(formattingInfo); if (!handled) throw formattingInfo.FormattingException("No suitable Formatter could be found", formattingInfo.Format); } /// /// First check whether the named formatter name exist in of the , /// next check whether the named formatter is able to process the format. /// /// /// True if an FormatterExtension was found, else False. private bool InvokeFormatterExtensions(FormattingInfo formattingInfo) { if (formattingInfo.Placeholder == null) return false; var formatterName = formattingInfo.Placeholder.FormatterName; // Evaluate the named formatter (or, evaluate all "" formatters) foreach (var formatterExtension in FormatterExtensions) { if (!formatterExtension.Names.Contains(formatterName)) continue; var handled = formatterExtension.TryEvaluateFormat(formattingInfo); if (handled) return true; } return false; } public void OnBeforeSerialize() { m_NotEmptyFormatterExtensionNames = null; } public void OnAfterDeserialize() { m_NotEmptyFormatterExtensionNames = null; } } }