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
25 changes: 16 additions & 9 deletions src/libse/Common/FixCasing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,26 +159,29 @@ private static string FixEnglishAloneILowerToUpper(string input)
return text;
}

private static readonly string[] CasingTitles = { "Mrs.", "Miss.", "Mr.", "Ms.", "Dr." };
private static readonly string[] CasingNotChangeWords = { "does", "has", "will", "is", "and", "for", "but", "or", "of" };

private string FixCasingAfterTitles(string input)
{
var text = input;
var titles = new[] { "Mrs.", "Miss.", "Mr.", "Ms.", "Dr." };
var notChangeWords = new[] { "does", "has", "will", "is", "and", "for", "but", "or", "of" };
for (int i = 0; i < text.Length - 4; i++)
{
var start = text.Substring(i);
foreach (var title in titles)
// Compare against the tail in place - taking a Substring here allocated the
// rest of the line for every character position (quadratic on long lines).
var start = text.AsSpan(i);
foreach (var title in CasingTitles)
{
if (start.StartsWith(title, StringComparison.OrdinalIgnoreCase))
if (start.StartsWith(title.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
var idx = i + title.Length;
if (idx < text.Length - 2 && text[idx] == ' ')
{
idx++;
var words = text.Substring(idx).Split(' ', '\r', '\n', ',', '"', '?', '!', '.', '\'');
if (words.Length > 0 && !notChangeWords.Contains(words[0]))
if (words.Length > 0 && !CasingNotChangeWords.Contains(words[0]))
{
var upper = text[idx].ToString().ToUpperInvariant();
var upper = char.ToUpperInvariant(text[idx]).ToString();
text = text.Remove(idx, 1).Insert(idx, upper);
}
}
Expand Down Expand Up @@ -256,9 +259,13 @@ private string Fix(string original, string lastLine, List<string> nameList, Cult
var text = original;
if (FixNormal)
{
if (FixNormalOnlyAllUppercase && HtmlUtil.RemoveHtmlTags(text, true) != HtmlUtil.RemoveHtmlTags(text, true).ToUpper(subtitleCulture))
if (FixNormalOnlyAllUppercase)
{
return text;
var noTags = HtmlUtil.RemoveHtmlTags(text, true);
if (noTags != noTags.ToUpper(subtitleCulture))
{
return text;
}
}

if (text.Length > 1)
Expand Down
42 changes: 37 additions & 5 deletions src/libse/Common/StrippableText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,36 @@ namespace Nikse.SubtitleEdit.Core.Common
{
public class StrippableText
{
/// <summary>
/// Characters allowed to follow a name. Built once - the name loop below tested this
/// per candidate match, and concatenating the literal with Environment.NewLine there
/// allocated a fresh string every time.
/// </summary>
private static readonly string NameEndChars = @" ,.!?:;')]- <”""" + Environment.NewLine;

/// <summary>
/// Suffix test that does not copy the whole builder - the casing loop below asks this
/// once per character, and sb.ToString() there allocated the accumulated line each time.
/// </summary>
private static bool EndsWith(StringBuilder sb, string value)
{
if (sb.Length < value.Length)
{
return false;
}

var offset = sb.Length - value.Length;
for (var i = 0; i < value.Length; i++)
{
if (sb[offset + i] != value[i])
{
return false;
}
}

return true;
}

public string Pre { get; set; }
public string Post { get; set; }
public string StrippedText { get; set; }
Expand Down Expand Up @@ -138,14 +168,16 @@ private void ReplaceNames1Remove(IEnumerable<string> nameList, List<string> repl
int idName = 0;
foreach (string name in nameList)
{
int start = lower.IndexOf(name.ToLowerInvariant(), StringComparison.Ordinal);
// "lower" is already lower case, so an ignore-case search finds the same
// positions as the lower-cased name did - without allocating one string per name.
int start = lower.IndexOf(name, StringComparison.OrdinalIgnoreCase);
while (start >= 0 && start < lower.Length)
{
bool startOk = (start == 0) || (lower[start - 1] == ' ') || (lower[start - 1] == '-') ||
(lower[start - 1] == '"') || (lower[start - 1] == '\'') || (lower[start - 1] == '>') || (lower[start - 1] == '[') || (lower[start - 1] == '“') ||
Environment.NewLine.EndsWith(lower[start - 1]);

if (startOk && string.CompareOrdinal(name, "Don") == 0 && lower.Substring(start).StartsWith("don't", StringComparison.Ordinal))
if (startOk && string.CompareOrdinal(name, "Don") == 0 && lower.AsSpan(start).StartsWith("don't".AsSpan(), StringComparison.Ordinal))
{
startOk = false;
}
Expand All @@ -156,7 +188,7 @@ private void ReplaceNames1Remove(IEnumerable<string> nameList, List<string> repl
bool endOk = end <= lower.Length;
if (endOk)
{
endOk = end == lower.Length || (@" ,.!?:;')]- <”""" + Environment.NewLine).Contains(lower[end]);
endOk = end == lower.Length || NameEndChars.Contains(lower[end]);
}

if (endOk && StrippedText.Length >= start + name.Length)
Expand Down Expand Up @@ -268,15 +300,15 @@ public void FixCasing(IEnumerable<string> nameList, bool changeNameCases, bool m
{
sb.Append(s);
}
else if ((sb.EndsWith('<') || sb.ToString().EndsWith("</", StringComparison.Ordinal)) && i + 1 < StrippedText.Length && StrippedText[i + 1] == '>')
else if ((sb.EndsWith('<') || EndsWith(sb, "</")) && i + 1 < StrippedText.Length && StrippedText[i + 1] == '>')
{ // tags
sb.Append(s);
}
else if (sb.EndsWith('<') && s == '/' && i + 2 < StrippedText.Length && StrippedText[i + 2] == '>')
{ // tags
sb.Append(s);
}
else if (sb.ToString().EndsWith("... ", StringComparison.Ordinal))
else if (EndsWith(sb, "... "))
{
sb.Append(s);
lastWasBreak = false;
Expand Down
28 changes: 17 additions & 11 deletions src/libse/Common/TextLengthCalculator/CalcCJK.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,26 @@ public decimal CountCharacters(string text, bool forCps)
@"\p{IsCJKUnifiedIdeographs}|" +
@"\p{IsHangulSyllables}|" +
@"\p{IsCJKCompatibilityForms}", RegexOptions.Compiled);
/// <summary>
/// True for the Unicode blocks <see cref="CjkCharRegex"/> matches, plus Hiragana.
/// This runs once per character of every line the CJK length calculators measure (the
/// subtitle grid re-reads those on each repaint), so it tests the block ranges directly
/// instead of allocating a one-character string and running the regex over it.
/// CalcCjkTest.IsCjk_MatchesRegexForEveryChar pins it to the regex for all 65536 chars.
/// </summary>
public static bool IsCjk(char c)
{
var v = (int)c;
if (v >= 0x3040 && v <= 0x309F) // Hiragana
{
return true;
}

if (v >= 0x4E00 && v <= 0x9FAF) // Common and uncommon kanji
{
return true;
}

return CjkCharRegex.IsMatch(c.ToString());
return v >= 0x1100 && v <= 0x11FF || // Hangul Jamo
v >= 0x2E80 && v <= 0x2EFF || // CJK Radicals Supplement
v >= 0x3000 && v <= 0x303F || // CJK Symbols and Punctuation
v >= 0x3040 && v <= 0x309F || // Hiragana
v >= 0x3200 && v <= 0x32FF || // Enclosed CJK Letters and Months
v >= 0x3300 && v <= 0x33FF || // CJK Compatibility
v >= 0x3400 && v <= 0x4DBF || // CJK Unified Ideographs Extension A
v >= 0x4E00 && v <= 0x9FFF || // CJK Unified Ideographs
v >= 0xAC00 && v <= 0xD7AF || // Hangul Syllables
v >= 0xFE30 && v <= 0xFE4F; // CJK Compatibility Forms
}
}
}
9 changes: 7 additions & 2 deletions src/libse/Common/TextLengthCalculator/CalcNoSpaceCpsOnly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@
{
public class CalcNoSpaceCpsOnly : ICalcLength
{
// Both are stateless; CalcFactory memoizes the strategy lookup, so allocating a fresh
// one per call - on every grid repaint and keystroke - threw that away.
private static readonly CalcNoSpace NoSpace = new CalcNoSpace();
private static readonly CalcAll All = new CalcAll();

/// <summary>
/// Calculate all text excluding space (tags are not counted).
/// </summary>
public decimal CountCharacters(string text, bool forCps)
{
if (forCps)
{
return new CalcNoSpace().CountCharacters(text, true);
return NoSpace.CountCharacters(text, true);
}

return new CalcAll().CountCharacters(text, false);
return All.CountCharacters(text, false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
{
public class CalcNoSpaceOrPunctuationCpsOnly : ICalcLength
{
// Both are stateless; CalcFactory memoizes the strategy lookup, so allocating a fresh
// one per call - on every grid repaint and keystroke - threw that away.
private static readonly CalcNoSpaceOrPunctuation NoSpaceOrPunctuation = new CalcNoSpaceOrPunctuation();
private static readonly CalcAll All = new CalcAll();

/// <summary>
/// Calculate all text except punctuation or space (tags are not counted) for cps only.
/// Line length calc all characters.
Expand All @@ -10,10 +15,10 @@ public decimal CountCharacters(string text, bool forCps)
{
if (forCps)
{
return new CalcNoSpaceOrPunctuation().CountCharacters(text, false);
return NoSpaceOrPunctuation.CountCharacters(text, false);
}

return new CalcAll().CountCharacters(text, false);
return All.CountCharacters(text, false);
}
}
}
48 changes: 41 additions & 7 deletions src/libse/Common/Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -486,9 +486,8 @@ public static string AutoBreakLineMoreThanTwoLines(string text, int maximumLengt
foreach (var item in list)
{
index += item;
if (htmlTags.ContainsKey(index))
if (htmlTags.TryGetValue(index, out var v))
{
var v = htmlTags[index];
if (v.StartsWith("</", StringComparison.Ordinal))
{
v = Environment.NewLine + v;
Expand Down Expand Up @@ -861,15 +860,17 @@ private static string ReInsertHtmlTags(string s, Dictionary<int, string> htmlTag
int six = 0;
foreach (var letter in s)
{
if (Environment.NewLine.Contains(letter))
if (letter == '\r' || letter == '\n')
{
sb.Append(letter);
}
else
{
if (htmlTags.ContainsKey(six))
// One probe per character instead of two - auto-break runs this per line
// on every split/merge, and per keystroke with auto-break while typing.
if (htmlTags.TryGetValue(six, out var tag))
{
sb.Append(htmlTags[six]);
sb.Append(tag);
}
sb.Append(letter);
six++;
Expand All @@ -878,9 +879,9 @@ private static string ReInsertHtmlTags(string s, Dictionary<int, string> htmlTag

for (int i = 0; i < 15; i++)
{
if (htmlTags.ContainsKey(six + i))
if (htmlTags.TryGetValue(six + i, out var tag))
{
sb.Append(htmlTags[six + i]);
sb.Append(tag);
}
}

Expand All @@ -889,6 +890,39 @@ private static string ReInsertHtmlTags(string s, Dictionary<int, string> htmlTag
return s;
}

/// <summary>
/// Same answer as <c>s == s.ToUpperInvariant()</c>, without allocating the uppercased
/// copy. The hearing-impaired and casing rules ask this several times per subtitle line.
/// </summary>
public static bool IsAllUppercase(string s)
{
for (var i = 0; i < s.Length; i++)
{
if (char.ToUpperInvariant(s[i]) != s[i])
{
return false;
}
}

return true;
}

/// <summary>
/// Same answer as <c>s != s.ToLowerInvariant()</c>, without allocating the lowercased copy.
/// </summary>
public static bool HasUppercase(string s)
{
for (var i = 0; i < s.Length; i++)
{
if (char.ToLowerInvariant(s[i]) != s[i])
{
return true;
}
}

return false;
}

public static string UnbreakLine(string text)
{
var lines = text.SplitToLines();
Expand Down
20 changes: 17 additions & 3 deletions src/libse/Forms/FixCommonErrors/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,8 @@ public static string FixHyphensRemoveForSingleLine(Subtitle subtitle, string inp
else if (text.StartsWith("<font ", StringComparison.Ordinal))
{
var prev = subtitle.GetParagraphOrDefault(i - 1);
if (prev == null || !HtmlUtil.RemoveHtmlTags(prev.Text).TrimEnd().EndsWith('-') || HtmlUtil.RemoveHtmlTags(prev.Text).TrimEnd().EndsWith("--", StringComparison.Ordinal))
var prevNoTags = prev == null ? null : HtmlUtil.RemoveHtmlTags(prev.Text).TrimEnd();
if (prevNoTags == null || !prevNoTags.EndsWith('-') || prevNoTags.EndsWith("--", StringComparison.Ordinal))
{
var st = new StrippableText(text);
if (st.Pre.EndsWith('-') || st.Pre.EndsWith("- ", StringComparison.Ordinal))
Expand All @@ -421,10 +422,23 @@ public static string FixHyphensRemoveForSingleLine(Subtitle subtitle, string inp
private static string FixDash(Subtitle subtitle, int i, string text, string dash)
{
var prev = subtitle.GetParagraphOrDefault(i - 1);
if (prev == null || !HtmlUtil.RemoveHtmlTags(prev.Text).TrimEnd().EndsWith(dash, StringComparison.Ordinal) || HtmlUtil.RemoveHtmlTags(prev.Text).TrimEnd().EndsWith(dash + dash, StringComparison.Ordinal))
// RemoveHtmlTags strips SSA tags too, so it is not free - run it once per call
// instead of twice (this is asked for every paragraph by the dash rules).
var prevNoTags = prev == null ? null : HtmlUtil.RemoveHtmlTags(prev.Text).TrimEnd();
if (prevNoTags == null || !prevNoTags.EndsWith(dash, StringComparison.Ordinal) || prevNoTags.EndsWith(dash + dash, StringComparison.Ordinal))
{
var noTagLines = HtmlUtil.RemoveHtmlTags(text, true).SplitToLines();
var startHyphenCount = noTagLines.Count(line => line.TrimStart().StartsWith(dash, StringComparison.Ordinal));
// Counting at most three lines - the LINQ form allocated a closure, a delegate
// and a TrimStart() string per line, per paragraph.
var startHyphenCount = 0;
foreach (var line in noTagLines)
{
if (line.AsSpan().TrimStart().StartsWith(dash.AsSpan(), StringComparison.Ordinal))
{
startHyphenCount++;
}
}

if (startHyphenCount == 1)
{
var remove = true;
Expand Down
Loading
Loading