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));
}
}
}
}