Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion se5/AmericanToBritish/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion se5/BritishToAmerican/README.md
Original file line number Diff line number Diff line change
@@ -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`.

Expand Down
85 changes: 78 additions & 7 deletions se5/Plugin-Shared/EnglishVariantConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@ public enum EnglishVariantDirection

/// <summary>
/// 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
/// <see cref="EnglishVariantDirection"/>.
/// </summary>
public sealed class EnglishVariantConverter
{
private readonly List<(Regex Pattern, string Replacement)> _rules = new();

/// <summary>
/// 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.
/// </summary>
private readonly Dictionary<string, List<int>> _rulesByFirstWord = new(StringComparer.Ordinal);

private readonly EnglishVariantDirection _direction;

public EnglishVariantConverter(EnglishVariantDirection direction)
Expand All @@ -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);
Expand Down Expand Up @@ -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<int>();
_rulesByFirstWord.Add(firstWord, indexes);
}

indexes.Add(index);
}

/// <summary>
/// Rules whose first word occurs in <paramref name="text"/>, 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.
/// </summary>
private SortedSet<int> CollectCandidateRules(string text)
{
var candidates = new SortedSet<int>();
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);
}

/// <summary>Matches what "\w" (and therefore "\b") considers a word character.</summary>
private static bool IsWordChar(char c) => char.IsLetterOrDigit(c) || c == '_';

/// <summary>
/// "color" inside &lt;font color="..."&gt; 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
Expand Down
Loading
Loading