using System.Text; namespace AcDream.App.UI.Layout; /// /// Exact port of retail's string-table escape codec /// (StringTableMetaLanguage::UnescapeString @ 0x0067BDC0 / /// EscapeString @ 0x0067BBC0 and their character tables /// GetUnEscapedChar @ 0x0067B750 / GetEscapedChar @ 0x0067B6C0). /// /// /// /// PLACEMENT (the systemic 2026-08-17 normalization round): retail decodes /// escapes at the string SOURCE, not per-widget. Every public /// StringInfo resolution runs the unescape unconditionally before any /// consumer sees the text — StringInfo::InqString @ 0x0042E490 tail /// and StringInfo::GetLiteralValue @ 0x0042CA50 both end in /// UnescapeString. The write side is the inverse: /// StringInfo::SetLiteralValue @ 0x0042C980 runs EscapeString /// when storing plain text (and StringInfo::AddVariable_String /// @ 0x0042E6C0 always stores variables that way), so stored text is /// escaped, resolved text is decoded, and variable content round-trips /// verbatim. acdream's equivalent source is ; /// widgets and controllers receive already-decoded strings and must not /// re-decode (a second pass corrupts an authored \\n — escaped /// backslash then 'n' — into a line break). /// /// /// The escape set (byte-verified against the PDB-paired 2013 binary; the /// metalanguage character-set literal at file offset 0x3FE178 is the ten /// characters []!{}#\|^$): /// \n → LF (0x0A), \t → TAB (0x09), \r → CR (0x0D), /// \q → '"' (0x22), and a backslash before any of the ten /// metalanguage characters yields that character itself. A backslash before /// anything else is NOT an escape — retail copies it through verbatim /// (GetUnEscapedChar returns 0 and UnescapeString's /// else-branch keeps the current character). /// /// public static class RetailStringEscapes { /// The ten metalanguage-significant characters that escape to /// themselves. Byte-decoded from the retail binary (see class remarks) — /// the same literal both character tables test with wcschr. private const string MetaCharacters = "[]!{}#\\|^$"; /// /// StringTableMetaLanguage::GetUnEscapedChar @ 0x0067B750: the /// character an escape pair \+ decodes /// to, or '\0' when the pair is not an escape. /// internal static char GetUnEscapedChar(char value) => value switch { 'n' => '\n', 'q' => '"', 'r' => '\r', 't' => '\t', not '\0' when MetaCharacters.Contains(value) => value, _ => '\0', }; /// /// StringTableMetaLanguage::GetEscapedChar @ 0x0067B6C0: the /// character that follows the backslash when /// must be stored escaped, or '\0' when it is stored verbatim. /// internal static char GetEscapedChar(char value) => value switch { '\t' => 't', '\n' => 'n', '\r' => 'r', '"' => 'q', not '\0' when MetaCharacters.Contains(value) => value, _ => '\0', }; /// /// StringTableMetaLanguage::UnescapeString @ 0x0067BDC0: decodes /// every two-character escape pair; all other characters (including a /// backslash that does not start a recognized pair, and a trailing /// backslash) copy through verbatim. /// public static string Unescape(string value) { ArgumentNullException.ThrowIfNull(value); // Fast path: a string with no backslash cannot contain an escape. int first = value.IndexOf('\\'); if (first < 0) return value; var result = new StringBuilder(value.Length); for (int i = 0; i < value.Length; i++) { char current = value[i]; // Retail reads the character AFTER the candidate backslash (the // terminator — never an escape — when at the end of the buffer). char next = i + 1 < value.Length ? value[i + 1] : '\0'; char unescaped = GetUnEscapedChar(next); if (current == '\\' && unescaped != '\0') { result.Append(unescaped); i++; // consume the pair } else if (current != '\0') { result.Append(current); } } return result.ToString(); } /// /// StringTableMetaLanguage::EscapeString @ 0x0067BBC0: the exact /// inverse — every character with a mapping /// is stored as \ + that mapping; everything else verbatim. /// Unescape(Escape(x)) == x for every — /// the round-trip retail relies on for template variables. /// public static string Escape(string value) { ArgumentNullException.ThrowIfNull(value); StringBuilder? result = null; for (int i = 0; i < value.Length; i++) { char current = value[i]; char escaped = GetEscapedChar(current); if (escaped != '\0') { result ??= new StringBuilder(value.Length + 4) .Append(value, 0, i); result.Append('\\').Append(escaped); } else if (current != '\0') { result?.Append(current); } } return result?.ToString() ?? value; } }