using System.Collections.Generic; using System.Globalization; using Amphora; using Glass.Typesetting; using UnitTestSharp; using LineBreakTestFile = Glass.UnitTests.Typesetting.LineBreakIteratorTests.LineBreakTestFile; namespace Glass.UnitTests.Typesetting { public partial class LineWrapperTests { public partial class MoveNextTests { /// /// Glyphs are 2 wide, except '-' and the 'z' that pads out a lead-in, which are 1. A hyphen narrower than /// any glyph is what makes a break at a soft hyphen observable: were they equal, the room reserved for /// the hyphen would fit the next glyph instead, and the line rightly wouldn't break. /// public class ProbeFont : OnePixelTestFont { public override GlyphMetrics MetricsFor(char glyph) => new GlyphMetrics { Width = glyph == '-' || glyph == 'z' ? 1 : 2 }; } public static readonly IFontMetrics LineBreakTestFont = new ProbeFont(); /// /// Whether the boundary at `index` follows a hard line break, which UAX #14 requires to break no matter /// how much room is left. CR followed by LF breaks once, after the LF. /// public static bool FollowsHardBreak(string text, int index) { char previous = text[index - 1]; return previous == '\r' ? text[index] != '\n' : IsHardBreak(previous); } /// /// Whitespace that hangs past the margin rather than wrapping. A break before it can never be seen, /// since there's always room for it, so those boundaries go untested. /// public static bool HangsAtLineEnd(char character) { if (IsHardBreak(character)) { return false; } switch (character) { case (char)0x00A0: case (char)0x2007: case (char)0x202F: return false; default: return char.IsWhiteSpace(character); } } /// /// Whether nothing but soft hyphens follows `index` before the line ends. They take no room, so a break /// before them can never be seen either. /// public static bool OnlySoftHyphensFollow(string text, int index) { for (int i = index; i < text.Length && !IsHardBreak(text[i]); ++i) { if (text[i] != LineBreakIterator.SoftHyphen) { return false; } } return true; } /// /// Whether a line ending at `index` shows a hyphen: it follows a soft hyphen, along with any combining /// marks the soft hyphen carries. /// public static bool ShowsAHyphenBefore(string text, int index) { int i = index - 1; while (i > 0 && CharUnicodeInfo.GetUnicodeCategory(text[i]) == UnicodeCategory.NonSpacingMark) { --i; } return text[i] == LineBreakIterator.SoftHyphen; } public static List LineStarts(string text, int maxWidth) { LineWrapper wrapper = Wrap(text, maxWidth, LineBreakTestFont); var returnMe = new List { 0 }; while (wrapper.MoveNext()) { returnMe.Add(wrapper.Current.End); } returnMe.RemoveAt(returnMe.Count - 1); return returnMe; } public static int WidthOf(string text, int length) => TextWidth.Calculate(LineBreakTestFont, new StringView(text, 0, length)); /// /// Whether a line breaks at `index` when it's the last spot that could hold one. /// public static bool BreaksAt(string text, int index) { if (FollowsHardBreak(text, index)) { return LineStarts(text, int.MaxValue).Contains(index); } // Only what follows the last hard break shares a line with `index`, and a hard break leaves the same // context behind as the start of text. int lineStart = 0; for (int candidate = index - 1; candidate > 0; --candidate) { if (FollowsHardBreak(text, candidate)) { lineStart = candidate; break; } } string line = text.Substring(lineStart); int lineIndex = index - lineStart; int room = WidthOf(line, lineIndex); if (ShowsAHyphenBefore(line, lineIndex)) { room += LineBreakTestFont.MetricsTable[WordMeasurer.HyphenGlyph].Width; } // A lead-in leaves exactly `room`, so the line ends at `index` only if that's a break. Its inner // space is a fallback break close enough to the end that whatever follows it fits a line, so nothing // is cut at the margin; its trailing space gives start-of-text context. int maxWidth = WidthOf(line, line.Length) + 8; int fillerWidth = maxWidth - room - WidthOf(" y ", 3); string leadIn = new string('y', fillerWidth / 2) + (fillerWidth % 2 == 1 ? "z" : "") + " y "; List starts = LineStarts(leadIn + line, maxWidth); return starts.Count > 1 && starts[1] - leadIn.Length == lineIndex; } [CpuTimeout(3000)] public void OnLineBreakTest_BreaksExactlyWhereTheFileSays() { var firstMismatches = new List(); foreach (LineBreakTestFile.Case testCase in LineBreakTestFile.Cases) { foreach (LineBreakTestFile.Boundary boundary in testCase.Boundaries) { if (HangsAtLineEnd(testCase.Text[boundary.Index]) || OnlySoftHyphensFollow(testCase.Text, boundary.Index)) { continue; } if (BreaksAt(testCase.Text, boundary.Index) != boundary.Breaks && firstMismatches.Count < 10) { firstMismatches.Add($"[{LineBreakIteratorTests.CodePointsOf(testCase.Text)}] " + $"at {boundary.Index} should {(boundary.Breaks ? "" : "not ")}break"); } } } CheckEqual("", string.Join("; ", firstMismatches)); } } } }