diff --git a/se5/AmericanToBritish/README.md b/se5/AmericanToBritish/README.md
index 76be9ef..12dc409 100644
--- a/se5/AmericanToBritish/README.md
+++ b/se5/AmericanToBritish/README.md
@@ -1,7 +1,7 @@
# American to British (Subtitle Edit 5 plugin)
Converts American English spellings to British English in the subtitle, using a
-bundled word list (~1000 pairs). Shows a checkable preview of every proposed
+bundled word list (~1850 pairs). Shows a checkable preview of every proposed
change so you can review and toggle individual conversions before applying.
First Subtitle Edit 5 plugin built on top of the shared
diff --git a/se5/BritishToAmerican/README.md b/se5/BritishToAmerican/README.md
index dc3243d..3c4aee7 100644
--- a/se5/BritishToAmerican/README.md
+++ b/se5/BritishToAmerican/README.md
@@ -1,7 +1,7 @@
# British to American (Subtitle Edit 5 plugin)
Mirror of the [American to British](../AmericanToBritish/) plugin in the
-opposite direction. Reuses the bundled word list (~1000 pairs) and the shared
+opposite direction. Reuses the bundled word list (~1850 pairs) and the shared
`EnglishVariantConverter` from [`Plugin-Shared`](../Plugin-Shared/) with
`Direction = BrToUs`.
diff --git a/se5/Plugin-Shared/EnglishVariantConverter.cs b/se5/Plugin-Shared/EnglishVariantConverter.cs
index 47d1a56..f02a468 100644
--- a/se5/Plugin-Shared/EnglishVariantConverter.cs
+++ b/se5/Plugin-Shared/EnglishVariantConverter.cs
@@ -13,13 +13,20 @@ public enum EnglishVariantDirection
///
/// Converts between American and British English using the bundled WordList.xml
-/// (~1000 pairs). Each pair becomes three case-aware regexes: lowercase,
+/// (~1850 pairs). Each pair becomes three case-aware regexes: lowercase,
/// UPPERCASE, and Titlecase, all matched as whole words. Picks direction via
/// .
///
public sealed class EnglishVariantConverter
{
private readonly List<(Regex Pattern, string Replacement)> _rules = new();
+
+ ///
+ /// Rule indexes keyed by the first word of the pattern, so a line only pays for the
+ /// handful of rules whose word it actually contains instead of all ~5500.
+ ///
+ private readonly Dictionary> _rulesByFirstWord = new(StringComparer.Ordinal);
+
private readonly EnglishVariantDirection _direction;
public EnglishVariantConverter(EnglishVariantDirection direction)
@@ -37,12 +44,11 @@ public string Convert(string text)
return text;
}
- foreach (var (pattern, replacement) in _rules)
+ var candidates = CollectCandidateRules(text);
+ foreach (var index in candidates)
{
- if (pattern.IsMatch(text))
- {
- text = pattern.Replace(text, replacement);
- }
+ var (pattern, replacement) = _rules[index];
+ text = pattern.Replace(text, replacement);
}
return RevertFontColorAttribute(text);
@@ -85,9 +91,74 @@ private void LoadBuiltInWordList()
private void AddRule(string from, string to)
{
- _rules.Add((new Regex("\\b" + Regex.Escape(from) + "\\b", RegexOptions.ExplicitCapture | RegexOptions.Compiled), to));
+ var index = _rules.Count;
+ _rules.Add((new Regex("\\b" + Regex.Escape(from) + "\\b", RegexOptions.ExplicitCapture), to));
+
+ var firstWord = FirstWord(from);
+ if (!_rulesByFirstWord.TryGetValue(firstWord, out var indexes))
+ {
+ indexes = new List();
+ _rulesByFirstWord.Add(firstWord, indexes);
+ }
+
+ indexes.Add(index);
}
+ ///
+ /// Rules whose first word occurs in , in rule order. A pattern
+ /// starts with "\b", so its first word can only match a whole word of the input - a line
+ /// without that word can never match the pattern and does not need to run it.
+ ///
+ private SortedSet CollectCandidateRules(string text)
+ {
+ var candidates = new SortedSet();
+ var i = 0;
+ while (i < text.Length)
+ {
+ if (!IsWordChar(text[i]))
+ {
+ i++;
+ continue;
+ }
+
+ var start = i;
+ while (i < text.Length && IsWordChar(text[i]))
+ {
+ i++;
+ }
+
+ if (_rulesByFirstWord.TryGetValue(text.Substring(start, i - start), out var indexes))
+ {
+ foreach (var index in indexes)
+ {
+ candidates.Add(index);
+ }
+ }
+ }
+
+ return candidates;
+ }
+
+ private static string FirstWord(string s)
+ {
+ var start = 0;
+ while (start < s.Length && !IsWordChar(s[start]))
+ {
+ start++;
+ }
+
+ var end = start;
+ while (end < s.Length && IsWordChar(s[end]))
+ {
+ end++;
+ }
+
+ return s.Substring(start, end - start);
+ }
+
+ /// Matches what "\w" (and therefore "\b") considers a word character.
+ private static bool IsWordChar(char c) => char.IsLetterOrDigit(c) || c == '_';
+
///
/// "color" inside <font color="..."> is HTML attribute syntax and must not be Britishized
/// by the word-list pass — that would corrupt the tag. Undo "colour" back to "color" inside
diff --git a/se5/Plugin-Shared/WordList.xml b/se5/Plugin-Shared/WordList.xml
index 0d7ff89..b469cd6 100644
--- a/se5/Plugin-Shared/WordList.xml
+++ b/se5/Plugin-Shared/WordList.xml
@@ -281,7 +281,6 @@
-
@@ -400,6 +399,8 @@
+
+
@@ -511,8 +512,10 @@
-
+
+
@@ -1244,4 +1247,613 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+