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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +