using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; namespace YesNt.Interpreter.Utilities; /// /// Extension methods for string manipulation used throughout the interpreter. /// /// /// /// YesNt uses a "safe string" encoding to pass values through the interpreter pipeline without /// accidentally triggering keyword matching. Special characters (spaces, operators, punctuation, /// control characters) are replaced with tilde-prefixed three-letter codes /// (e.g. space → ~spc, newline → ~nli). The mapping is defined in /// . Use to encode and /// to decode. /// /// public static class StringExtensions { private static readonly Dictionary reverseReplacementRules; /// /// Gets the table that maps special characters to their safe-string escape codes. /// Keys are the original characters; values are the three-letter tilde codes. /// public static Dictionary ReplacementRules { get; } = new() { {"~", "~til" }, {" ", "~spc" }, {"%", "~per" }, {"<", "~let" }, {">", "~grt" }, {",", "~com" }, {"!", "~exm" }, {"|", "~pip" }, {"\n","~nli" }, {"\r","~ret" }, {"\t","~tab" }, {"\b","~bac" }, {"\f","~for" }, {"\a","~ale" }, {"", "~emp" }, }; static StringExtensions() { reverseReplacementRules = ReplacementRules.ToDictionary(x => x.Value, x => x.Key); } /// /// Encodes a string into safe-string format so that special characters cannot accidentally /// trigger interpreter keyword matching. Each character is wrapped with vertical-tab sentinels /// before rule substitution so that multi-character replacements do not overlap. /// /// The plain string to encode. /// The safe-string encoded representation. public static string ToSafeString(this string input) { StringBuilder output = new StringBuilder(); foreach (char c in input) { _ = output.Append($"\v{c}\v"); } return ReplaceOnce(output.ToString(), ReplacementRules); } /// /// Decodes a safe-string back to its original plain-text form. /// /// A safe-string encoded string. /// The decoded plain string. public static string FromSafeString(this string input) { return ReplaceOnce(input.Replace("\v", string.Empty), reverseReplacementRules); } /// /// Tries to parse the string as a , first decoding safe-string encoding /// and normalising decimal separators (comma → period). /// /// The string to parse (may be safe-string encoded). /// When this method returns, contains the parsed value if successful. /// if parsing succeeded; otherwise . public static bool ToStandardizedNumber(this string input, out double result) { return double.TryParse(input.FromSafeString().Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCulture, out result); } /// Replaces only the first occurrence of in the string. /// The source string. /// The substring to find. /// The replacement value. /// A new string with the first occurrence replaced. public static string ReplaceFirstOccurrence(this string input, string oldValue, string newValue) { int place = input.IndexOf(oldValue); return input.Remove(place, oldValue.Length).Insert(place, newValue); } /// Replaces only the last occurrence of in the string. /// The source string. /// The substring to find. /// The replacement value. /// A new string with the last occurrence replaced. public static string ReplaceLastOccurrence(this string input, string oldValue, string newValue) { int place = input.LastIndexOf(oldValue); return input.Remove(place, Math.Min(oldValue.Length, input.Length - place)).Insert(place, newValue); } /// Counts the number of trailing whitespace characters in the string. /// The source string. /// The number of whitespace characters at the end of the string. public static int WhiteSpaceAtEnd(this string input) { int count = 0; int index = input.Length - 1; while (index >= 0 && char.IsWhiteSpace(input[index--])) { count++; } return count; } private static string ReplaceOnce(string input, Dictionary replacementRules) { // ~emp/string.Empty is a special case, it is used to represent empty strings and won't work with the normal rules because an empty string always matches and causes an infinite loop 3 letter abbreviation IEnumerable> matches = replacementRules.Where(rule => rule.Key != string.Empty && input.Contains(rule.Key)); if (!matches.Any()) { return input; } KeyValuePair match = matches.First(); int startIndex = input.IndexOf(match.Key); int endIndex = startIndex + match.Key.Length; string before = ReplaceOnce(input[..startIndex], replacementRules); string replaced = match.Value; string after = ReplaceOnce(input[endIndex..], replacementRules); return before + replaced + after; } }