Skip to content

Commit c1df5c8

Browse files
LordLuceusclaude
andcommitted
feat: Add extensibility for text formatting and cleaning
- Add TextCleaner custom replacements (string and regex) with AddReplacement, AddRegexReplacement, and Clear methods - Extract BuildSpeakerPrefixedText as public method in SpeechManager for reuse in FormatTextOverride delegates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 66e5458 commit c1df5c8

2 files changed

Lines changed: 137 additions & 0 deletions

File tree

SpeechManager.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,18 @@ private static string FormatText(string speaker, string text, int textType)
167167
if (FormatTextOverride != null)
168168
return FormatTextOverride(speaker, text, textType);
169169

170+
return BuildSpeakerPrefixedText(speaker, text, textType);
171+
}
172+
173+
/// <summary>
174+
/// Builds formatted text with optional speaker prefix based on text type.
175+
/// </summary>
176+
/// <param name="speaker">The speaker name (can be null or empty)</param>
177+
/// <param name="text">The text content</param>
178+
/// <param name="textType">The type of text being formatted</param>
179+
/// <returns>Formatted text with speaker prefix for Dialogue type, or plain text otherwise</returns>
180+
public static string BuildSpeakerPrefixedText(string speaker, string text, int textType)
181+
{
170182
// Default: only Dialogue type gets speaker prefix
171183
if (textType == TextType.Dialogue && !Net35Extensions.IsNullOrWhiteSpace(speaker))
172184
return $"{speaker}: {text}";

TextCleaner.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.Text;
23
using System.Text.RegularExpressions;
34

@@ -6,9 +7,14 @@ namespace MelonAccessibilityLib
67
/// <summary>
78
/// Utility class to clean text for screen reader output.
89
/// Removes Unity rich text tags, normalizes whitespace, and handles escape sequences.
10+
/// Consumers can extend cleaning behavior via <see cref="AddReplacement"/> and <see cref="AddRegexReplacement(string, string, RegexOptions)"/>.
911
/// </summary>
1012
public static class TextCleaner
1113
{
14+
private static readonly object _lock = new object();
15+
private static readonly List<KeyValuePair<string, string>> _customReplacements = new List<KeyValuePair<string, string>>();
16+
private static readonly List<KeyValuePair<Regex, string>> _customRegexReplacements = new List<KeyValuePair<Regex, string>>();
17+
1218
// Unity rich text tags
1319
private static readonly Regex ColorTagRegex = new Regex(
1420
@"<color[^>]*>|</color>",
@@ -47,8 +53,99 @@ public static class TextCleaner
4753
RegexOptions.Compiled
4854
);
4955

56+
/// <summary>
57+
/// Adds a custom string replacement that will be applied during text cleaning.
58+
/// Replacements are applied in the order they are added, after Unity rich text tags are removed.
59+
/// </summary>
60+
/// <param name="find">The string to find.</param>
61+
/// <param name="replace">The string to replace it with.</param>
62+
public static void AddReplacement(string find, string replace)
63+
{
64+
if (string.IsNullOrEmpty(find))
65+
return;
66+
67+
lock (_lock)
68+
{
69+
_customReplacements.Add(new KeyValuePair<string, string>(find, replace ?? string.Empty));
70+
}
71+
}
72+
73+
/// <summary>
74+
/// Adds a custom regex replacement that will be applied during text cleaning.
75+
/// Regex replacements are applied in the order they are added, after string replacements.
76+
/// </summary>
77+
/// <param name="pattern">The regex pattern to match.</param>
78+
/// <param name="replace">The replacement string (supports regex substitutions like $1).</param>
79+
/// <param name="options">Optional regex options. Defaults to <see cref="RegexOptions.None"/>.</param>
80+
public static void AddRegexReplacement(string pattern, string replace, RegexOptions options = RegexOptions.None)
81+
{
82+
if (string.IsNullOrEmpty(pattern))
83+
return;
84+
85+
lock (_lock)
86+
{
87+
_customRegexReplacements.Add(new KeyValuePair<Regex, string>(
88+
new Regex(pattern, options),
89+
replace ?? string.Empty
90+
));
91+
}
92+
}
93+
94+
/// <summary>
95+
/// Adds a custom regex replacement that will be applied during text cleaning.
96+
/// Regex replacements are applied in the order they are added, after string replacements.
97+
/// </summary>
98+
/// <param name="regex">The compiled regex to use for matching.</param>
99+
/// <param name="replace">The replacement string (supports regex substitutions like $1).</param>
100+
public static void AddRegexReplacement(Regex regex, string replace)
101+
{
102+
if (regex == null)
103+
return;
104+
105+
lock (_lock)
106+
{
107+
_customRegexReplacements.Add(new KeyValuePair<Regex, string>(regex, replace ?? string.Empty));
108+
}
109+
}
110+
111+
/// <summary>
112+
/// Removes all custom string replacements (not regex replacements).
113+
/// </summary>
114+
public static void ClearReplacements()
115+
{
116+
lock (_lock)
117+
{
118+
_customReplacements.Clear();
119+
}
120+
}
121+
122+
/// <summary>
123+
/// Removes all custom regex replacements (not string replacements).
124+
/// </summary>
125+
public static void ClearRegexReplacements()
126+
{
127+
lock (_lock)
128+
{
129+
_customRegexReplacements.Clear();
130+
}
131+
}
132+
133+
/// <summary>
134+
/// Removes all custom replacements (both string and regex).
135+
/// </summary>
136+
public static void ClearAllCustomReplacements()
137+
{
138+
lock (_lock)
139+
{
140+
_customReplacements.Clear();
141+
_customRegexReplacements.Clear();
142+
}
143+
}
144+
50145
/// <summary>
51146
/// Clean text by removing Unity rich text tags and normalizing whitespace.
147+
/// Custom replacements registered via <see cref="AddReplacement"/> and <see cref="AddRegexReplacement(string, string, RegexOptions)"/>
148+
/// are applied after tag removal and before whitespace normalization.
52149
/// </summary>
53150
public static string Clean(string input)
54151
{
@@ -71,6 +168,9 @@ public static string Clean(string input)
71168
// Handle escape sequences
72169
cleaned = UnescapeText(cleaned);
73170

171+
// Apply custom replacements
172+
cleaned = ApplyCustomReplacements(cleaned);
173+
74174
// Normalize whitespace
75175
cleaned = MultipleSpacesRegex.Replace(cleaned, " ");
76176

@@ -94,6 +194,31 @@ private static string UnescapeText(string input)
94194
return sb.ToString();
95195
}
96196

197+
private static string ApplyCustomReplacements(string input)
198+
{
199+
if (string.IsNullOrEmpty(input))
200+
return input;
201+
202+
string result = input;
203+
204+
lock (_lock)
205+
{
206+
// Apply string replacements first
207+
foreach (var replacement in _customReplacements)
208+
{
209+
result = result.Replace(replacement.Key, replacement.Value);
210+
}
211+
212+
// Apply regex replacements
213+
foreach (var replacement in _customRegexReplacements)
214+
{
215+
result = replacement.Key.Replace(result, replacement.Value);
216+
}
217+
}
218+
219+
return result;
220+
}
221+
97222
/// <summary>
98223
/// Combine multiple lines into a single string, cleaning each line.
99224
/// </summary>

0 commit comments

Comments
 (0)