diff --git a/src/Test/FixCommonErrors/FixCommonErrorsTest.cs b/src/Test/FixCommonErrors/FixCommonErrorsTest.cs index 234afef17..85d4a6578 100644 --- a/src/Test/FixCommonErrors/FixCommonErrorsTest.cs +++ b/src/Test/FixCommonErrors/FixCommonErrorsTest.cs @@ -8,6 +8,7 @@ using Nikse.SubtitleEdit.Logic; using System; using System.Collections.Generic; using System.IO; +using System.Linq; namespace Test.FixCommonErrors { @@ -3532,5 +3533,55 @@ namespace Test.FixCommonErrors Assert.AreEqual("It is I this illustrious illiteration. It's this...", _subtitle.Paragraphs[0].Text); } } + + [TestMethod] + public void FixMissingOpenBracketOneTest() + { + var engine = new FixMissingOpenBracket(); + var sub = GetGenericSub(); + sub.Paragraphs.First().Text = "Hey, FOO)."; + engine.Fix(sub, new EmptyFixCallback()); + Assert.AreEqual("(Hey, FOO).", sub.Paragraphs.First().Text); + } + + [TestMethod] + public void FixMissingOpenBracketTwoTest() + { + var engine = new FixMissingOpenBracket(); + var sub = GetGenericSub(); + sub.Paragraphs.First().Text = "Reaper, hostiles, 100 meters\neast. Two hundred meters south)."; + engine.Fix(sub, new EmptyFixCallback()); + Assert.AreEqual("(Reaper, hostiles, 100 meters\neast. Two hundred meters south).", sub.Paragraphs.First().Text); + } + + [TestMethod] + public void FixMissingOpenBracketThreeTest() + { + var engine = new FixMissingOpenBracket(); + var sub = GetGenericSub(); + sub.Paragraphs.First().Text = "- Foobar bar zzz).\n- Foo bar Zz"; + engine.Fix(sub, new EmptyFixCallback()); + Assert.AreEqual( "- (Foobar bar zzz).\n- Foo bar Zz", sub.Paragraphs.First().Text); + } + [TestMethod] + public void FixMissingOpenBracketFourTest() + { + var engine = new FixMissingOpenBracket(); + var sub = GetGenericSub(); + sub.Paragraphs.First().Text = "Foobar THIS IS A NOISE)"; + engine.Fix(sub, new EmptyFixCallback()); + Assert.AreEqual( "Foobar (THIS IS A NOISE)", sub.Paragraphs.First().Text); + } + + private static Subtitle GetGenericSub() + { + return new Subtitle() + { + Paragraphs = + { + new Paragraph("Hello World!", 1000, 2000) + } + }; + } } } diff --git a/src/libse/Forms/FixCommonErrors/FixMissingOpenBracket.cs b/src/libse/Forms/FixCommonErrors/FixMissingOpenBracket.cs index fd80939ab..4603be7b3 100644 --- a/src/libse/Forms/FixCommonErrors/FixMissingOpenBracket.cs +++ b/src/libse/Forms/FixCommonErrors/FixMissingOpenBracket.cs @@ -1,6 +1,7 @@ using Nikse.SubtitleEdit.Core.Common; using Nikse.SubtitleEdit.Core.Interfaces; using System; +using System.Linq; namespace Nikse.SubtitleEdit.Core.Forms.FixCommonErrors { @@ -11,96 +12,131 @@ namespace Nikse.SubtitleEdit.Core.Forms.FixCommonErrors public static string FixMissingOpenBracket { get; set; } = "Fix missing [ or ( in line"; } - private static string Fix(string text, string openB) - { - string pre = string.Empty; - string closeB = openB == "(" ? ")" : "]"; + private static bool IsIgnorable(char ch) => char.IsWhiteSpace(ch) || ch == '-'; - if (text.Contains(" " + closeB)) + private static char GetOpeningPair(char tag) => tag == ')' ? '(' : '['; + + private static int CalcInsertPositionFromBeginning(string input) + { + var len = input.Length; + var i = 0; + + // skip asa tag + while (i < len && input[i] == '{') { - openB = openB + " "; + i = input.IndexOf('}', i + 1) + 1; + if (i == 0) break; } - do - { - if (text.Length > 1 && text.StartsWith('-')) - { - pre += "- "; - if (text[1] == ' ') - { - text = text.Substring(2); - } - else - { - text = text.Substring(1); - } - } - if (text.Length > 3 && text.StartsWith("", StringComparison.OrdinalIgnoreCase)) - { - pre += ""; - if (text[3] == ' ') - { - text = text.Substring(4); - } - else - { - text = text.Substring(3); - } - } - if (text.Length > 1 && (text[0] == ' ' || text[0] == '.')) - { - pre += text[0] == '.' ? '.' : ' '; - text = text.Substring(1); - while (text.Length > 0 && text[0] == '.') - { - pre += "."; - text = text.Substring(1); - } - text = text.TrimStart(' '); - } - } while (text.StartsWith("", StringComparison.Ordinal) || text.StartsWith('-')); + while (i < len && IsIgnorable(input[i])) i++; - text = pre + openB + text; - return text; + // skip html tags + while (i < len && input[i] == '<') + { + i = input.IndexOf('>', i + 1) + 1; + if (i == 0) break; + } + + // skip anything that is not a letter or digit + while (i < len && !char.IsLetterOrDigit(input[i])) i++; + + return i; } + private static string RestoreMissingOpenParenthesis(string input) + { + var len = input.Length; + + // empty string + if (len == 0) return input; + + var closeTags = new[] { ']', ')' }; + // ignore line if contains opening + if (input.Any(ch => ch == '(' || ch == '[')) return input; + + var ci = input.IndexOfAny(closeTags); + // invalid position + if (ci < 1) return input; + + var k = ci - 1; + // jump backward if uppercase or any one of the ignorable chars + while (k > 0 && char.IsUpper(input[k]) || IsIgnorable(input[k])) k--; + + // note if we have case like: "Hey, FOO)." then we want to insert the open before the "Hey" + if (k > 0 && input[k] == ',') + { + k--; + while (k > 0 && char.IsLetterOrDigit(input[k])) k--; + } + + // try landing on white-space char + if (k >= 0 && k + 1 < len && input[k] != ' ' && input[k + 1] == ' ') k++; + // try finding first valid char (this is used to not insert '(' or '[') in to left of a white-space + while (k < ci && char.IsWhiteSpace(input[k])) k++; + + // FO) => (FO) + if (ci - k > 1) + { + input = input.Insert(k, GetOpeningPair(input[ci]).ToString()); + } + else + { + // recalculate value of k from beginning + k = CalcInsertPositionFromBeginning(input); + if (k < ci) + { + input = input.Insert(k, GetOpeningPair(input[ci]).ToString()); + } + } + + return input; + } + + private static bool IsPerLineRestoration(string[] lines) => lines.Length == 1 || lines.All(l => l.HasSentenceEnding()); + public void Fix(Subtitle subtitle, IFixCallbacks callbacks) { - string fixAction = Language.FixMissingOpenBracket; - int fixCount = 0; - for (int i = 0; i < subtitle.Paragraphs.Count; i++) + var fixAction = Language.FixMissingOpenBracket; + var fixCount = 0; + for (var i = 0; i < subtitle.Paragraphs.Count; i++) { var p = subtitle.Paragraphs[i]; if (callbacks.AllowFix(p, fixAction)) { - var hit = false; - string oldText = p.Text; - var openIdx = p.Text.IndexOf('('); - var closeIdx = p.Text.IndexOf(')'); - if (closeIdx >= 0 && (closeIdx < openIdx || openIdx < 0)) + var oldText = p.Text; + var text = p.Text; + + // split only if both lines are closed + var lines = p.Text.SplitToLines().ToArray(); + // logic to perform for when line is/are closed. + if (IsPerLineRestoration(lines)) { - p.Text = Fix(p.Text, "("); - hit = true; + var count = lines.Length; + for (var j = 0; j < count; j++) + { + lines[j] = RestoreMissingOpenParenthesis(lines[j]); + } + + // rebuild the text + text = count > 1 ? string.Join(Environment.NewLine, lines) : lines[0]; + } + else + { + // handles 2+ lines even if the their adjacent is not closed + text = RestoreMissingOpenParenthesis(text); } - openIdx = p.Text.IndexOf('['); - closeIdx = p.Text.IndexOf(']'); - if (closeIdx >= 0 && (closeIdx < openIdx || openIdx < 0)) - { - p.Text = Fix(p.Text, "["); - hit = true; - } - - if (hit) + if (oldText.Length != text.Length) { fixCount++; + p.Text = text; callbacks.AddFixToListView(p, fixAction, oldText, p.Text); } } } + callbacks.UpdateFixStatus(fixCount, Language.FixMissingOpenBracket); } - } -} +} \ No newline at end of file