using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Glass.Typesetting;
using UnitTestSharp;
using Conformance = Glass.UnitTests.TextLayoutTests.WordWrapUax14Conformance;
using GlyphDefinition = Glass.SpriteFontDefinition.GlyphDefinition;
namespace Glass.UnitTests.Typesetting
{
public class WordMeasurerTests : TestFixture
{
public const char SoftHyphen = LineBreakIterator.SoftHyphen;
public const char LineSeparator = (char)0x2028;
public const int LetterWidth = 10;
public const int SpaceWidth = 4;
public const int HyphenWidth = 3;
public const int InvalidGlyphWidth = 7;
public static readonly string Emoji = char.ConvertFromUtf32(0x1F600);
public static readonly string LanguageTag = char.ConvertFromUtf32(0xE0001);
public static (short, short) Pair(char left, char right) => ((short)left, (short)right);
///
/// Lowercase letters, a space and a hyphen, with a few kerning pairs whose amounts all differ.
///
public static SpriteFontDefinition TestFont(Dictionary glyphs = null,
short kerningBetweenSpaces = 0)
{
glyphs ??= new Dictionary();
for (char letter = 'a'; letter <= 'z'; ++letter)
{
glyphs[letter] = new GlyphDefinition { Width = LetterWidth };
}
glyphs[' '] = new GlyphDefinition { Width = SpaceWidth };
glyphs['-'] = new GlyphDefinition { Width = HyphenWidth };
var kernings = new Dictionary<(short, short), short>
{
[Pair('a', 'b')] = -1,
[Pair('b', ' ')] = -2,
[Pair(' ', 'c')] = -3,
[Pair('b', '-')] = -1,
[Pair('a', 'c')] = -5,
};
if (kerningBetweenSpaces != 0)
{
kernings[Pair(' ', ' ')] = kerningBetweenSpaces;
}
return new SpriteFontDefinition(new SpriteFontDefinition.Data
{
GlyphDefinitions = glyphs,
InvalidGlyph = new GlyphDefinition { Width = InvalidGlyphWidth },
Kernings = kernings,
});
}
public static string Describe(int start, in MeasuredWord word, in WordEndMetrics ends)
{
return $"{start}..{word.ContentEnd}..{word.End}{(word.IsMandatory ? "!" : "")} " +
$"lead {word.LeadingKerning} content {word.ContentWidth} trailing {ends.TrailingWidthOf(word)} " +
$"hyphen {ends.HyphenWidthOf(word)}";
}
public static List Words(StringView text, IFontMetrics font = null)
{
font ??= TestFont();
var ends = new WordEndMetrics(font);
var returnMe = new List();
var measurer = new WordMeasurer(font, new LineBreakIterator(text));
while (measurer.MoveNext())
{
returnMe.Add(Describe(measurer.CurrentStart, measurer.Current, ends));
}
return returnMe;
}
///
/// Printable ASCII in assorted widths, with kerning between the characters LineBreakTest.txt uses most.
///
public static SpriteFontDefinition ConformanceFont()
{
var glyphs = new Dictionary();
for (char character = ' '; character <= '~'; ++character)
{
glyphs[character] = new GlyphDefinition { Width = character % 7 + 3 };
}
var kernings = new Dictionary<(short, short), short>();
const string kerned = " #%()0,-.a";
foreach (char left in kerned)
{
foreach (char right in kerned)
{
kernings[Pair(left, right)] = (short)((left * 3 + right) % 5 - 2);
}
}
return new SpriteFontDefinition(new SpriteFontDefinition.Data
{
GlyphDefinitions = glyphs,
InvalidGlyph = new GlyphDefinition { Width = InvalidGlyphWidth },
Kernings = kernings,
});
}
///
/// Whether the old engine measures `text` the way words are, so their widths should add up to its width.
///
public static bool OldEngineMeasuresAlike(string text)
{
return !text.Any(character => LineBreakIteratorTests.IsHardBreak(character) || character == SoftHyphen
|| char.IsSurrogate(character));
}
public class CtorTests : TestFixture
{
public void NullFont_Throws()
{
CheckThrow(typeof(ArgumentNullException));
new WordMeasurer(null, new LineBreakIterator("ab"));
}
public void KeepsFontAndText()
{
SpriteFontDefinition font = TestFont();
var measurer = new WordMeasurer(font, new LineBreakIterator("ab cd"));
CheckEqual(font, measurer.Font);
CheckEqual("ab cd", measurer.Text.ToString());
}
public void ExposesTheFontsAlreadyLookedUpMetricsKerningTableAndSpace()
{
SpriteFontDefinition font = TestFont();
var measurer = new WordMeasurer(font, new LineBreakIterator("ab"));
CheckEqual(font.MetricsTable, measurer.MetricsTable);
CheckEqual(font.KerningTable, measurer.KerningTable);
CheckEqual(font.MetricsTable[' '], measurer.Space);
}
}
public class MoveNextTests : TestFixture
{
[CpuTimeout(3000)]
public void OnLineBreakTest_EndsAtEveryBreakAndAddsUpToTheOldEnginesWidth()
{
SpriteFontDefinition font = ConformanceFont();
var endMetrics = new WordEndMetrics(font);
var mismatches = new List();
foreach (string line in File.ReadLines(Conformance.TestFilePath))
{
if (line.Length == 0 || line[0] == '#')
{
continue;
}
string text = Conformance.Parse(line).Text;
var ends = new List();
int width = 0;
var measurer = new WordMeasurer(font, new LineBreakIterator(text));
while (measurer.MoveNext())
{
ends.Add(measurer.Current.End);
width += measurer.Current.LeadingKerning + measurer.Current.ContentWidth
+ endMetrics.TrailingWidthOf(measurer.Current);
}
bool endsMatch = ends.SequenceEqual(LineBreakIteratorTests.PositionsIn(text));
bool widthMatches = !OldEngineMeasuresAlike(text)
|| width == (int)TextLayout.CalculateTextWidth(font, text);
if ((!endsMatch || !widthMatches) && mismatches.Count < 10)
{
mismatches.Add($"[{LineBreakIteratorTests.CodePointsOf(text)}] " +
$"ends {string.Join(",", ends)}, width {width}");
}
}
CheckEqual("", string.Join("; ", mismatches));
}
public void EmptyText_HasNoWords()
{
CheckEqual(new string[0], Words(""));
}
public void OneWord_IsMandatoryAndKeepsItsInteriorKerning()
{
CheckEqual(new[] { "0..2..2! lead 0 content 19 trailing 0 hyphen 0" }, Words("ab"));
}
public void Space_HangsWithTheKerningIntoIt()
{
CheckEqual("0..2..3 lead 0 content 19 trailing 2 hyphen 0", Words("ab c")[0]);
}
public void KerningAcrossABreak_IsTheNextWordsLeadingKerning()
{
CheckEqual("3..4..4! lead -3 content 10 trailing 0 hyphen 0", Words("ab c")[1]);
}
public void SeveralSpaces_AllHang()
{
CheckEqual("0..1..4 lead 0 content 10 trailing 12 hyphen 0", Words("a b")[0]);
}
public void SeveralSpaces_KernBetweenEachOther()
{
CheckEqual("0..1..4 lead 0 content 10 trailing 10 hyphen 0",
Words("a b", TestFont(kerningBetweenSpaces: -1))[0]);
}
public void SpacesAtTheStart_AreAWordWithNoContent()
{
CheckEqual(new[]
{
"0..0..2 lead 0 content 0 trailing 8 hyphen 0",
"2..4..4! lead 0 content 19 trailing 0 hyphen 0",
},
Words(" ab"));
}
public void LineFeed_HangsWithNoWidth_AndTheNextWordHasNoLeadingKerning()
{
CheckEqual(new[]
{
"0..1..3! lead 0 content 10 trailing 2 hyphen 0",
"3..4..4! lead 0 content 10 trailing 0 hyphen 0",
},
Words("b \nc"));
}
public void CarriageReturnLineFeed_EndsOneWord()
{
CheckEqual("0..1..3! lead 0 content 10 trailing 0 hyphen 0", Words("a\r\nb")[0]);
}
public void LineSeparator_IsAHardBreak()
{
CheckEqual("0..1..2! lead 0 content 10 trailing 0 hyphen 0", Words($"a{LineSeparator}b")[0]);
}
public void SoftHyphen_TakesNoWidthAndKerningCarriesAcrossIt()
{
CheckEqual(new[]
{
"0..2..2 lead 0 content 10 trailing 0 hyphen 3",
"2..3..3! lead -5 content 10 trailing 0 hyphen 0",
},
Words($"a{SoftHyphen}c"));
}
public void SoftHyphen_HyphenWidthIncludesTheKerningIntoTheHyphen()
{
CheckEqual("0..2..2 lead 0 content 10 trailing 0 hyphen 2", Words($"b{SoftHyphen}c")[0]);
}
public void SoftHyphenEndingTheText_NeedsNoHyphen()
{
CheckEqual(new[] { "0..3..3! lead 0 content 19 trailing 0 hyphen 0" }, Words($"ab{SoftHyphen}"));
}
public void SoftHyphenBeforeASpace_NeedsNoHyphen()
{
CheckEqual("0..2..3 lead 0 content 10 trailing 4 hyphen 0", Words($"a{SoftHyphen} b")[0]);
}
public void AstralCharacter_MeasuresAsOneInvalidGlyph()
{
CheckEqual(new[] { "0..2..2! lead 0 content 7 trailing 0 hyphen 0" }, Words(Emoji));
}
public void AstralDefaultIgnorable_TakesNoWidthAndKerningCarriesAcrossIt()
{
CheckEqual(new[] { "0..4..4! lead 0 content 15 trailing 0 hyphen 0" }, Words($"a{LanguageTag}c"));
}
public void LoneSurrogate_MeasuresAsTheInvalidGlyph()
{
CheckEqual(new[] { "0..1..1! lead 0 content 7 trailing 0 hyphen 0" },
Words(WordMeasurer.LoneSurrogate.ToString()));
}
public void View_PositionsAreRelativeToItsStart()
{
CheckEqual("0..2..3 lead 0 content 19 trailing 2 hyphen 0", Words(((StringView)"xxab cd")[2, 6])[0]);
}
}
public class MeasureAllTests : TestFixture
{
public const string ThreeWords = "ab cd\nef";
///
/// One-letter words, far more of them than the first guess at how many a text this long holds.
///
public static readonly string ManyShortWords = string.Join(" ", new string('a', 40).ToCharArray());
public void HoldsWhatMoveNextYields()
{
CheckEqual(Words(ThreeWords),
MeasuredWordsTests.ReadAll(WordMeasurer.MeasureAll(TestFont(), ThreeWords)));
}
public void MoreWordsThanFirstGuessed_AreAllKept()
{
CheckEqual(Words(ManyShortWords),
MeasuredWordsTests.ReadAll(WordMeasurer.MeasureAll(TestFont(), ManyShortWords)));
}
public void EmptyText_HasNoWords()
{
CheckEqual(0, WordMeasurer.MeasureAll(TestFont(), "").Count);
}
public void KeepsTheFontAndText()
{
SpriteFontDefinition font = TestFont();
MeasuredWords words = WordMeasurer.MeasureAll(font, ThreeWords);
CheckEqual(font, words.Font);
CheckEqual(ThreeWords, words.Text.ToString());
}
}
}
}