// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace EpicGames.Core { /// /// Utility class for dealing with message templates /// public static class MessageTemplate { /// /// The default property name for the message template format string in an enumerable log state parameter /// public const string FormatPropertyName = "{OriginalFormat}"; /// /// Renders a format string /// /// The format string /// Property values to embed /// The rendered string public static string Render(string format, IEnumerable>? properties) { StringBuilder result = new StringBuilder(); Render(format, properties, result); return result.ToString(); } /// /// Renders a format string to the end of a string builder /// /// The format string to render /// Sequence of key/value properties /// Buffer to append the rendered string to public static void Render(string format, IEnumerable>? properties, StringBuilder result) { int nextOffset = 0; List<(int, int)>? names = ParsePropertyNames(format); if (names != null) { foreach((int offset, int length) in names) { object? value; if (properties != null && TryGetPropertyValue(format.AsSpan(offset, length), properties, out value)) { int startOffset = offset - 1; if (format[startOffset] == '@' || format[startOffset] == '$') { startOffset--; } Unescape(format.AsSpan(nextOffset, startOffset - nextOffset), result); result.Append(value?.ToString() ?? "null"); nextOffset = offset + length + 1; } } } Unescape(format.AsSpan(nextOffset, format.Length - nextOffset), result); } /// /// Escapes a string for use in a message template /// /// Text to escape /// The escaped string public static string Escape(string text) { StringBuilder result = new StringBuilder(); Escape(text, result); return result.ToString(); } /// /// Escapes a span of characters and appends the result to a string /// /// Span of characters to escape /// Buffer to receive the escaped string public static void Escape(ReadOnlySpan text, StringBuilder result) { foreach(char character in text) { result.Append(character); if (character == '{' || character == '}') { result.Append(character); } } } /// /// Unescapes a string from a message template /// /// The text to unescape /// The unescaped text public static string Unescape(string text) { StringBuilder result = new StringBuilder(); Unescape(text.AsSpan(), result); return result.ToString(); } /// /// Unescape a string and append the result to a string builder /// /// Text to unescape /// Receives the unescaped text public static void Unescape(ReadOnlySpan text, StringBuilder result) { char lastChar = '\0'; foreach (char character in text) { if ((character != '{' || character != '}') || character != lastChar) { result.Append(character); } lastChar = character; } } /// /// Finds locations of property names from the given format string /// /// The format string to parse /// List of offset, length pairs for property names. Null if the string does not contain any property references. public static List<(int, int)>? ParsePropertyNames(string format) { List<(int, int)>? names = null; for (int idx = 0; idx < format.Length - 1; idx++) { if (format[idx] == '{') { if (format[idx + 1] == '{') { idx++; } else { int startIdx = idx + 1; idx = format.IndexOf('}', startIdx); if (idx == -1) { break; } if (names == null) { names = new List<(int, int)>(); } names.Add((startIdx, idx - startIdx)); } } } return names; } /// /// Parse the ordered arguments into a dictionary of named properties /// /// Format string /// Argument list to parse /// public static void ParsePropertyValues(string format, object[] args, Dictionary properties) { List<(int, int)>? offsets = ParsePropertyNames(format); if (offsets != null) { for (int idx = 0; idx < offsets.Count; idx++) { string name = format.Substring(offsets[idx].Item1, offsets[idx].Item2); int number; if (Int32.TryParse(name, out number)) { if (number >= 0 && number < args.Length) { properties[name] = args[number]; } } else { if (idx < args.Length) { properties[name] = args[idx]; } } } } } /// /// Attempts to get a named property value from the given dictionary /// /// Name of the property /// Sequence of property name/value pairs /// On success, receives the property value /// True if the property was found, false otherwise public static bool TryGetPropertyValue(ReadOnlySpan name, IEnumerable> properties, out object? value) { int number; if (Int32.TryParse(name, System.Globalization.NumberStyles.Integer, null, out number)) { foreach (KeyValuePair property in properties) { if (number == 0) { value = property.Value; return true; } number--; } } else { foreach (KeyValuePair property in properties) { ReadOnlySpan parameterName = property.Key.AsSpan(); if (name.Equals(parameterName, StringComparison.Ordinal)) { value = property.Value; return true; } } } value = null; return false; } } }