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 SOH-delimited three-letter codes /// (e.g. space → \x01spc\x01, newline → \x01nli\x01). These codes use the /// non-printable SOH character (U+0001) as a sentinel. Written via string concatenation /// ("\x01" + "spc" + "\x01") to avoid C#'s greedy \x hex escape absorbing /// following hex-digit letters. U+0001 cannot appear in normal user source, preventing raw /// source text from being accidentally decoded. /// 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() { {"~", "\x01" + "til" + "\x01" }, {" ", "\x01" + "spc" + "\x01" }, {"%", "\x01" + "per" + "\x01" }, {"<", "\x01" + "let" + "\x01" }, {">", "\x01" + "grt" + "\x01" }, {",", "\x01" + "com" + "\x01" }, {"!", "\x01" + "exm" + "\x01" }, {"|", "\x01" + "pip" + "\x01" }, {"\n", "\x01" + "nli" + "\x01" }, {"\r", "\x01" + "ret" + "\x01" }, {"\t", "\x01" + "tab" + "\x01" }, {"\b", "\x01" + "bac" + "\x01" }, {"\f", "\x01" + "for" + "\x01" }, {"\a", "\x01" + "ale" + "\x01" }, {"", "\x01" + "emp" + "\x01" }, }; 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) { // \x01emp\x01/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, StringComparison.Ordinal)); if (!matches.Any()) { return input; } KeyValuePair match = matches.First(); int startIndex = input.IndexOf(match.Key, StringComparison.Ordinal); 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; } }