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
4 changes: 3 additions & 1 deletion se5/AmericanToBritish/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ and format as the Subtitle Edit 4 plugin, so an existing list keeps working:
```

A `Word` with the same American spelling as a built-in pair replaces it; `Ignore`
turns a built-in pair off.
turns a built-in pair off. An `Ignore` can also be a phrase, which is then left
untouched wherever it occurs (`<Ignore us="por favor" />` keeps "favor" there while
it is still converted elsewhere). A few such phrases are built in, e.g. "por favor".

First Subtitle Edit 5 plugin built on top of the shared
[`Plugin-Shared`](../Plugin-Shared/) library — Avalonia app boot, theme passing,
Expand Down
4 changes: 2 additions & 2 deletions se5/AmericanToBritish/WordListWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,13 @@
Opacity="0.7"
TextWrapping="Wrap"
Margin="0,0,0,8"
Text="American words from the built-in list that should never be converted." />
Text="American words from the built-in list that should never be converted, or whole phrases to leave untouched (e.g. &quot;por favor&quot;)." />
<Grid DockPanel.Dock="Bottom"
ColumnDefinitions="*,Auto,Auto"
Margin="0,8,0,0">
<TextBox Grid.Column="0"
x:Name="IgnoreBox"
Watermark="American word"
Watermark="American word or phrase"
Margin="0,0,6,0" />
<Button Grid.Column="1"
Content="Add"
Expand Down
97 changes: 97 additions & 0 deletions se5/Plugin-Shared/EnglishVariantConverter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;

Expand All @@ -16,6 +18,8 @@ public enum EnglishVariantDirection
/// (~1850 pairs), optionally extended/filtered by a <see cref="LocalWordList"/>.
/// Each pair becomes three case-aware regexes: lowercase, UPPERCASE, and Titlecase,
/// all matched as whole words. Picks direction via <see cref="EnglishVariantDirection"/>.
/// Keep phrases (built-in &lt;Keep text="..."/&gt; plus the local list's ignored entries) are left
/// untouched, e.g. the Spanish "por favor" must not become "por favour".
/// </summary>
public sealed class EnglishVariantConverter
{
Expand All @@ -29,6 +33,13 @@ public sealed class EnglishVariantConverter

private readonly EnglishVariantDirection _direction;

private readonly List<string> _keepPhrases = new();
private Regex? _keepRegex;

/// <summary>Private use area chars stand in for kept phrases while the rules run.</summary>
private const char PlaceholderFirst = '\uE000';
private const char PlaceholderLast = '\uF8FF';

public EnglishVariantConverter(EnglishVariantDirection direction, LocalWordList? localWordList = null)
{
_direction = direction;
Expand All @@ -49,10 +60,21 @@ public EnglishVariantConverter(EnglishVariantDirection direction, LocalWordList?
foreach (var us in localWordList.Ignored)
{
ignored.Add(us);
_keepPhrases.Add(us);
}
}

LoadBuiltInWordList(ignored);

if (_keepPhrases.Count > 0)
{
// Longest first so the longer of two overlapping phrases wins.
var alternatives = _keepPhrases
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderByDescending(p => p.Length)
.Select(p => Regex.Escape(p).Replace("\\ ", "\\s+"));
_keepRegex = new Regex("\\b(?:" + string.Join("|", alternatives) + ")\\b", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
}
}

public int RuleCount => _rules.Count;
Expand All @@ -64,16 +86,82 @@ public string Convert(string text)
return text;
}

var kept = MaskKeepPhrases(ref text);

var candidates = CollectCandidateRules(text);
foreach (var index in candidates)
{
var (pattern, replacement) = _rules[index];
text = pattern.Replace(text, replacement);
}

text = UnmaskKeepPhrases(text, kept);
return RevertFontColorAttribute(text);
}

/// <summary>
/// Replaces each kept phrase with one private use char (not a word char, so no rule can
/// match it or across it). Skipped for text that already contains such chars.
/// </summary>
private List<string>? MaskKeepPhrases(ref string text)
{
if (_keepRegex == null || ContainsPlaceholderChar(text))
{
return null;
}

var kept = new List<string>();
text = _keepRegex.Replace(text, m =>
{
if (kept.Count > PlaceholderLast - PlaceholderFirst)
{
return m.Value;
}

kept.Add(m.Value);
return ((char)(PlaceholderFirst + kept.Count - 1)).ToString();
});

return kept.Count > 0 ? kept : null;
}

private static string UnmaskKeepPhrases(string text, List<string>? kept)
{
if (kept == null)
{
return text;
}

var sb = new StringBuilder(text.Length + 16);
foreach (var c in text)
{
var index = c - PlaceholderFirst;
if (index >= 0 && index < kept.Count)
{
sb.Append(kept[index]);
}
else
{
sb.Append(c);
}
}

return sb.ToString();
}

private static bool ContainsPlaceholderChar(string text)
{
foreach (var c in text)
{
if (c >= PlaceholderFirst && c <= PlaceholderLast)
{
return true;
}
}

return false;
}

public bool TryConvert(string text, out string converted)
{
converted = Convert(text);
Expand All @@ -92,6 +180,15 @@ private void LoadBuiltInWordList(HashSet<string> ignored)
return;
}

foreach (var element in xml.Root.Elements("Keep"))
{
var keep = element.Attribute("text")?.Value.Trim();
if (!string.IsNullOrEmpty(keep))
{
_keepPhrases.Add(keep!);
}
}

foreach (var element in xml.Root.Elements("Word"))
{
var us = element.Attribute("us")?.Value;
Expand Down
3 changes: 3 additions & 0 deletions se5/Plugin-Shared/WordList.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Words>
<!-- Keep: phrases that are never converted (foreign words that look like American spellings). -->
<Keep text="por favor" />
<Keep text="faz favor" />
<Word us="accouterment" br="accoutrement" />
<Word us="accouterments" br="accoutrements" />
<Word us="aerogram" br="aerogramme" />
Expand Down
Loading