using System; using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; using Xunit; namespace Avalonia.Base.UnitTests.Media.TextFormatting; /// /// Direct coverage for β€” surrogate-pair decoding via /// , the small /// helper properties / methods, and the bitmask-based IsWhiteSpace path /// that depends on every used value fitting in 64 /// bits. /// public class CodepointTests { [Theory] [InlineData("a", 0, (uint)'a', 1)] [InlineData("abc", 1, (uint)'b', 1)] [InlineData("abc", 2, (uint)'c', 1)] public void ReadAt_BmpScalar_ReturnsCharAndAdvancesByOne(string text, int index, uint expectedValue, int expectedCount) { var cp = Codepoint.ReadAt(text.AsSpan(), index, out var count); Assert.Equal(expectedValue, cp.Value); Assert.Equal(expectedCount, count); } [Fact] public void ReadAt_HighSurrogate_AtStart_DecodesPair() { // U+1F600 GRINNING FACE β€” high surrogate at index 0. const string text = "πŸ˜€"; var cp = Codepoint.ReadAt(text.AsSpan(), 0, out var count); Assert.Equal(0x1F600u, cp.Value); Assert.Equal(2, count); } [Fact] public void ReadAt_LowSurrogate_ScansBackToHighSurrogate() { // Reading at the low surrogate position should still return the full // supplementary codepoint by looking one index back. const string text = "πŸ˜€"; var cp = Codepoint.ReadAt(text.AsSpan(), 1, out var count); Assert.Equal(0x1F600u, cp.Value); Assert.Equal(2, count); } [Fact] public void ReadAt_HighSurrogate_WithoutFollowingLow_ReturnsReplacement() { // Lone high surrogate at end of string. const string text = "a\uD83D"; var cp = Codepoint.ReadAt(text.AsSpan(), 1, out var count); Assert.Equal(Codepoint.ReplacementCodepoint.Value, cp.Value); Assert.Equal(1, count); } [Fact] public void ReadAt_HighSurrogate_FollowedByNonLow_ReturnsReplacement() { // High surrogate followed by a regular BMP character (invalid pair). const string text = "\uD83Da"; var cp = Codepoint.ReadAt(text.AsSpan(), 0, out var count); Assert.Equal(Codepoint.ReplacementCodepoint.Value, cp.Value); Assert.Equal(1, count); } [Fact] public void ReadAt_LoneLowSurrogate_AtStart_ReturnsReplacement() { // Lone low surrogate with nothing before it. const string text = "\uDE00b"; var cp = Codepoint.ReadAt(text.AsSpan(), 0, out var count); Assert.Equal(Codepoint.ReplacementCodepoint.Value, cp.Value); Assert.Equal(1, count); } [Fact] public void ReadAt_LowSurrogate_NotPrecededByHigh_ReturnsReplacement() { // Low surrogate at index 1, but index 0 is a regular char (not a high surrogate). const string text = "a\uDE00"; var cp = Codepoint.ReadAt(text.AsSpan(), 1, out var count); Assert.Equal(Codepoint.ReplacementCodepoint.Value, cp.Value); Assert.Equal(1, count); } [Fact] public void ReadAt_IndexPastLength_ReturnsReplacement() { const string text = "abc"; var cp = Codepoint.ReadAt(text.AsSpan(), 5, out var count); Assert.Equal(Codepoint.ReplacementCodepoint.Value, cp.Value); Assert.Equal(1, count); } [Fact] public void ReadAt_IndexAtLength_ReturnsReplacement() { const string text = "abc"; var cp = Codepoint.ReadAt(text.AsSpan(), 3, out var count); Assert.Equal(Codepoint.ReplacementCodepoint.Value, cp.Value); Assert.Equal(1, count); } [Fact] public void ReadAt_EmptySpan_ReturnsReplacement() { var cp = Codepoint.ReadAt(ReadOnlySpan.Empty, 0, out var count); Assert.Equal(Codepoint.ReplacementCodepoint.Value, cp.Value); Assert.Equal(1, count); } [Fact] public void CodepointEnumerator_DecodesMixedBmpAndSupplementaryText() { // 'a' + πŸ˜€ (U+1F600) + 'b' + βœ“ (U+2713) + πŸš€ (U+1F680). const string text = "aπŸ˜€bβœ“πŸš€"; var expected = new uint[] { 'a', 0x1F600, 'b', 0x2713, 0x1F680 }; var enumerator = new CodepointEnumerator(text.AsSpan()); var actual = new List(); while (enumerator.MoveNext(out var cp)) { actual.Add(cp.Value); } Assert.Equal(expected, actual); } /// /// uses a bitmask trick that assumes every /// value used in the mask fits in 64 bits. /// If Control, Format, or SpaceSeparator ever moves past /// position 63 in the enum, the mask silently produces wrong results. This /// guards against that. /// [Fact] public void IsWhiteSpace_AllMaskedGeneralCategoriesFitInBitmask() { Assert.True((int)GeneralCategory.Control < 64); Assert.True((int)GeneralCategory.Format < 64); Assert.True((int)GeneralCategory.SpaceSeparator < 64); } [Theory] [InlineData(0x0020u, true)] // SPACE [InlineData(0x0009u, true)] // TAB (Control) [InlineData(0x000Au, true)] // LF (Control) [InlineData(0x000Du, true)] // CR (Control) [InlineData(0x00A0u, true)] // NBSP (SpaceSeparator) [InlineData(0x200Bu, true)] // ZWSP (Format) [InlineData(0x0061u, false)] // 'a' [InlineData(0x0030u, false)] // '0' [InlineData(0x002Eu, false)] // '.' public void IsWhiteSpace_KnownCodepoints(uint value, bool expected) { Assert.Equal(expected, new Codepoint(value).IsWhiteSpace); } [Theory] [InlineData(0x000Au, true)] // LF [InlineData(0x000Bu, true)] // VT [InlineData(0x000Cu, true)] // FF [InlineData(0x000Du, true)] // CR [InlineData(0x0085u, true)] // NEL [InlineData(0x2028u, true)] // LINE SEPARATOR [InlineData(0x2029u, true)] // PARAGRAPH SEPARATOR [InlineData(0x0020u, false)] [InlineData(0x0061u, false)] [InlineData(0x0009u, false)] // TAB is not a "break" char in Avalonia's sense public void IsBreakChar_KnownCodepoints(uint value, bool expected) { Assert.Equal(expected, new Codepoint(value).IsBreakChar); } [Theory] [InlineData(0x0061u, false)] // 'a' [InlineData(0x4E2Du, true)] // 'δΈ­' Wide [InlineData(0xFF21u, true)] // 'οΌ‘' Fullwidth [InlineData(0xFF71u, true)] // 'ο½±' Halfwidth [InlineData(0x03B1u, false)] // 'Ξ±' Ambiguous (not east asian per IsEastAsian) [InlineData(0x0020u, false)] // ' ' Narrow public void IsEastAsian_KnownCodepoints(uint value, bool expected) { Assert.Equal(expected, new Codepoint(value).IsEastAsian); } [Theory] [InlineData(0x3008u, 0x2329u)] // γ€ˆ β†’ ⟨ [InlineData(0x3009u, 0x232Au)] // 〉 β†’ ⟩ [InlineData(0x0061u, 0x0061u)] // 'a' β†’ 'a' (unchanged) [InlineData(0x0028u, 0x0028u)] // '(' β†’ '(' (unchanged) public void GetCanonicalType_MapsKnownCodepoints(uint input, uint expected) { // GetCanonicalType is internal; reachable here via InternalsVisibleTo. var actual = Codepoint.GetCanonicalType(new Codepoint(input)); Assert.Equal(expected, actual.Value); } [Theory] [InlineData(0x0028u, true, 0x0029u)] // '(' β†’ ')' [InlineData(0x0029u, true, 0x0028u)] // ')' β†’ '(' [InlineData(0x005Bu, true, 0x005Du)] // '[' β†’ ']' [InlineData(0x005Du, true, 0x005Bu)] // ']' β†’ '[' [InlineData(0x0061u, false, 0u)] // 'a' has no pair [InlineData(0x0020u, false, 0u)] // ' ' has no pair public void TryGetPairedBracket_KnownCodepoints(uint codepoint, bool expectedSuccess, uint expectedPair) { var result = new Codepoint(codepoint).TryGetPairedBracket(out var pair); Assert.Equal(expectedSuccess, result); if (expectedSuccess) { Assert.Equal(expectedPair, pair.Value); } } [Theory] [InlineData(0x00ADu, true)] // SOFT HYPHEN [InlineData(0x200Du, true)] // ZERO WIDTH JOINER [InlineData(0x034Fu, true)] // COMBINING GRAPHEME JOINER [InlineData(0xFE0Fu, true)] // VARIATION SELECTOR-16 [InlineData(0xFEFFu, true)] // ZERO WIDTH NO-BREAK SPACE [InlineData(0xE0100u, true)] // VARIATION SELECTOR-17 [InlineData(0x0301u, false)] // COMBINING ACUTE ACCENT [InlineData(0x20E3u, false)] // COMBINING ENCLOSING KEYCAP [InlineData(0x0041u, false)] // 'A' [InlineData(0x0020u, false)] // ' ' public void IsDefaultIgnorable_KnownCodepoints(uint value, bool expected) { Assert.Equal(expected, new Codepoint(value).IsDefaultIgnorable); } [Theory] [InlineData(0x1F600u, true)] // πŸ˜€ GRINNING FACE [InlineData(0x1F4AFu, true)] // πŸ’― HUNDRED POINTS SYMBOL [InlineData(0x2764u, true)] // ❀ HEAVY BLACK HEART (text presentation by default) [InlineData(0x0023u, true)] // '#' (an emoji only as part of a keycap sequence) [InlineData(0x0030u, true)] // '0' [InlineData(0xFE0Fu, false)] // VARIATION SELECTOR-16 is Emoji_Component, not Emoji [InlineData(0x0041u, false)] // 'A' public void IsEmoji_KnownCodepoints(uint value, bool expected) { Assert.Equal(expected, new Codepoint(value).IsEmoji); } [Theory] [InlineData(0x1F600u, true)] // πŸ˜€ defaults to emoji presentation [InlineData(0x1F4AFu, true)] // πŸ’― defaults to emoji presentation [InlineData(0x231Au, true)] // ⌚ WATCH defaults to emoji presentation [InlineData(0x2764u, false)] // ❀ needs U+FE0F to be presented as emoji [InlineData(0x0023u, false)] // '#' [InlineData(0x0030u, false)] // '0' [InlineData(0x0041u, false)] // 'A' public void HasEmojiPresentation_KnownCodepoints(uint value, bool expected) { Assert.Equal(expected, new Codepoint(value).HasEmojiPresentation); } [Fact] public void ImplicitConversions_RoundTripValue() { var cp = new Codepoint(0x1F600u); int asInt = cp; uint asUint = cp; Assert.Equal(0x1F600, asInt); Assert.Equal(0x1F600u, asUint); } [Fact] public void IsInRangeInclusive_BoundsAreInclusive() { Assert.True(Codepoint.IsInRangeInclusive(new Codepoint(0x10u), 0x10u, 0x20u)); Assert.True(Codepoint.IsInRangeInclusive(new Codepoint(0x20u), 0x10u, 0x20u)); Assert.True(Codepoint.IsInRangeInclusive(new Codepoint(0x15u), 0x10u, 0x20u)); Assert.False(Codepoint.IsInRangeInclusive(new Codepoint(0x0Fu), 0x10u, 0x20u)); Assert.False(Codepoint.IsInRangeInclusive(new Codepoint(0x21u), 0x10u, 0x20u)); } }