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)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
StringBuilder output = new StringBuilder(input.Length * 3);
foreach (char c in input)
{
string s = c.ToString();
if (ReplacementRules.TryGetValue(s, out string replacement))
{
_ = output.Append('\v');
_ = output.Append(replacement);
_ = output.Append('\v');
}
else
{
_ = output.Append('\v');
_ = output.Append(c);
_ = output.Append('\v');
}
}
return output.ToString();
}
///
/// 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)
{
if (string.IsNullOrEmpty(input) || (!input.Contains('\v') && !input.Contains('\x01')))
{
return input;
}
string stripped = input.Replace("\v", string.Empty);
if (!stripped.Contains('\x01'))
{
return stripped;
}
StringBuilder output = new StringBuilder(stripped.Length);
for (int i = 0; i < stripped.Length; i++)
{
if (stripped[i] == '\x01' && i + 4 < stripped.Length && stripped[i + 4] == '\x01')
{
string code = stripped.Substring(i, 5);
if (reverseReplacementRules.TryGetValue(code, out string value))
{
_ = output.Append(value);
i += 4;
continue;
}
}
_ = output.Append(stripped[i]);
}
return output.ToString();
}
///
/// 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)
{
if (input.IndexOf(',') != -1)
{
input = input.Replace(',', '.');
}
return double.TryParse(input, 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;
}
}