diff --git a/Extensions.Test/StringExtensionsTests.cs b/Extensions.Test/StringExtensionsTests.cs index 1b72f96..ba216c5 100644 --- a/Extensions.Test/StringExtensionsTests.cs +++ b/Extensions.Test/StringExtensionsTests.cs @@ -590,4 +590,47 @@ public void NominalWordWrapKeepsAVisibleHyphenFollowedByASoftHyphenOnOneLineWhen List result = [.. input.NominalWordWrap(1000f, 10f)]; CollectionAssert.AreEqual(new List { "well-known" }, result); } + + [TestMethod] + public void NominalWordWrapKeepsADoubleHyphenDashTogether() + { + // 12 chars per line; "It was late-" would fit, but the break must not fall between the two hyphens. + string input = "It was late--too late to call"; + List result = [.. input.NominalWordWrap(12f, 1f)]; + CollectionAssert.AreEqual(new List { "It was", "late--too", "late to call" }, result); + } + + [TestMethod] + public void NominalWordWrapBreaksAfterTheLastHyphenOfARun() + { + string input = "It was late--too"; + List result = [.. input.NominalWordWrap(13f, 1f)]; + CollectionAssert.AreEqual(new List { "It was late--", "too" }, result); + } + + [TestMethod] + public void NominalWordWrapDoesNotBreakAfterAWordInitialHyphen() + { + List number = [.. "ab -5".NominalWordWrap(4f, 1f)]; + CollectionAssert.AreEqual(new List { "ab", "-5" }, number); + + List flag = [.. "Use the --force flag".NominalWordWrap(10f, 1f)]; + CollectionAssert.AreEqual(new List { "Use the", "--force", "flag" }, flag); + } + + [TestMethod] + public void NominalWordWrapStillBreaksAfterAHyphenThatFollowsALeadingOne() + { + // "-5-6": the second hyphen has a digit before it, so it is still a break opportunity. + List result = [.. "-5-6".NominalWordWrap(3f, 1f)]; + CollectionAssert.AreEqual(new List { "-5-", "6" }, result); + } + + [TestMethod] + public void NominalWordWrapDoesNotAddAHyphenAtASoftHyphenAfterALeadingDash() + { + // The soft hyphen after "--" is a break opportunity, but the visible dash already ends the upper line. + List result = [.. "ab --\u00ADforce".NominalWordWrap(5f, 1f)]; + CollectionAssert.AreEqual(new List { "ab --", "force" }, result); + } } diff --git a/Extensions/StringExtensions.cs b/Extensions/StringExtensions.cs index bbcb745..79dca59 100644 --- a/Extensions/StringExtensions.cs +++ b/Extensions/StringExtensions.cs @@ -364,19 +364,25 @@ private static IEnumerable NominalWordWrapIterator(string text, int maxC { // Split a whitespace-delimited word into break-units at hyphenation opportunities: // after a visible hyphen (kept on the left chunk) or at a soft hyphen (removed unless a break lands there). + // A visible hyphen is a break opportunity only when it ends its run of hyphens and some other character + // comes before it in the word, so "--" stays together and "-5" or "--force" is never split after the dash. List<(string Text, bool HyphenBefore)> chunks = []; StringBuilder current = new(); bool hyphenBefore = false; + bool seenNonHyphen = false; - foreach (char ch in word) + for (int i = 0; i < word.Length; i++) { + char ch = word[i]; if (ch == SoftHyphen) { if (current.Length > 0) { + // A visible hyphen that did not end the chunk (a leading "--" before a soft hyphen) already shows at a break here. + bool endsInHyphen = current[^1] == '-'; chunks.Add((current.ToString(), hyphenBefore)); current.Clear(); - hyphenBefore = true; + hyphenBefore = !endsInHyphen; } else if (chunks.Count > 0 && chunks[^1].Text[^1] != '-') { @@ -390,7 +396,11 @@ private static IEnumerable NominalWordWrapIterator(string text, int maxC } current.Append(ch); - if (ch == '-') + if (ch != '-') + { + seenNonHyphen = true; + } + else if (seenNonHyphen && (i + 1 >= word.Length || word[i + 1] != '-')) { chunks.Add((current.ToString(), hyphenBefore)); current.Clear();