ladybird/Tests/LibUnicode/TestSegmenter.cpp

453 lines
18 KiB
C++
Raw Permalink Normal View History

/*
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibTest/TestCase.h>
#include <AK/Array.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Utf16String.h>
#include <AK/Utf16View.h>
#include <AK/Vector.h>
#include <LibUnicode/Segmenter.h>
template<size_t N>
static void test_grapheme_segmentation(StringView string, size_t const (&expected_boundaries)[N])
{
Vector<size_t> boundaries;
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Grapheme);
segmenter->for_each_boundary(MUST(String::from_utf8(string)), [&](auto boundary) {
boundaries.append(boundary);
return IterationDecision::Continue;
});
EXPECT_EQ(boundaries, ReadonlySpan<size_t> { expected_boundaries });
}
TEST_CASE(grapheme_segmentation)
{
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Grapheme);
segmenter->for_each_boundary(String {}, [&](auto i) {
dbgln("{}", i);
VERIFY_NOT_REACHED();
return IterationDecision::Break;
});
test_grapheme_segmentation("a"sv, { 0u, 1u });
test_grapheme_segmentation("ab"sv, { 0u, 1u, 2u });
test_grapheme_segmentation("abc"sv, { 0u, 1u, 2u, 3u });
test_grapheme_segmentation("a\nb"sv, { 0u, 1u, 2u, 3u });
test_grapheme_segmentation("a\n\rb"sv, { 0u, 1u, 2u, 3u, 4u });
test_grapheme_segmentation("a\r\nb"sv, { 0u, 1u, 3u, 4u });
test_grapheme_segmentation("aᄀb"sv, { 0u, 1u, 4u, 5u });
test_grapheme_segmentation("aᄀᄀb"sv, { 0u, 1u, 7u, 8u });
test_grapheme_segmentation("aᄀᆢb"sv, { 0u, 1u, 7u, 8u });
test_grapheme_segmentation("aᄀ가b"sv, { 0u, 1u, 7u, 8u });
test_grapheme_segmentation("aᄀ각b"sv, { 0u, 1u, 7u, 8u });
test_grapheme_segmentation("a😀b"sv, { 0u, 1u, 5u, 6u });
test_grapheme_segmentation("a👨👩👧👦b"sv, { 0u, 1u, 26u, 27u });
test_grapheme_segmentation("a👩🏼👨🏻b"sv, { 0u, 1u, 29u, 30u });
}
TEST_CASE(grapheme_segmentation_indic_conjunct_break)
{
test_grapheme_segmentation("\u0915"sv, { 0u, 3u });
test_grapheme_segmentation("\u0915a"sv, { 0u, 3u, 4u });
test_grapheme_segmentation("\u0915\u0916"sv, { 0u, 3u, 6u });
test_grapheme_segmentation("\u0915\u094D\u0916"sv, { 0u, 9u });
test_grapheme_segmentation("\u0915\u09BC\u09CD\u094D\u0916"sv, { 0u, 15u });
test_grapheme_segmentation("\u0915\u094D\u09BC\u09CD\u0916"sv, { 0u, 15u });
test_grapheme_segmentation("\u0915\u09BC\u09CD\u094D\u09BC\u09CD\u0916"sv, { 0u, 21u });
test_grapheme_segmentation("\u0915\u09BC\u09CD\u09BC\u09CD\u094D\u0916"sv, { 0u, 21u });
test_grapheme_segmentation("\u0915\u094D\u09BC\u09CD\u09BC\u09CD\u0916"sv, { 0u, 21u });
test_grapheme_segmentation("\u0915\u09BC\u09CD\u09BC\u09CD\u094D\u09BC\u09CD\u0916"sv, { 0u, 27u });
test_grapheme_segmentation("\u0915\u09BC\u09CD\u094D\u09BC\u09CD\u09BC\u09CD\u0916"sv, { 0u, 27u });
test_grapheme_segmentation("\u0915\u09BC\u09CD\u09BC\u09CD\u094D\u09BC\u09CD\u09BC\u09CD\u0916"sv, { 0u, 33u });
}
template<size_t N>
static void test_word_segmentation(StringView string, size_t const (&expected_boundaries)[N])
{
Vector<size_t> boundaries;
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word);
segmenter->for_each_boundary(MUST(String::from_utf8(string)), [&](auto boundary) {
boundaries.append(boundary);
return IterationDecision::Continue;
});
EXPECT_EQ(boundaries, ReadonlySpan<size_t> { expected_boundaries });
}
TEST_CASE(word_segmentation)
{
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word);
segmenter->for_each_boundary(String {}, [&](auto) {
VERIFY_NOT_REACHED();
return IterationDecision::Break;
});
test_word_segmentation("a"sv, { 0u, 1u });
test_word_segmentation("ab"sv, { 0u, 2u });
test_word_segmentation("abc"sv, { 0u, 3u });
test_word_segmentation("ab cd"sv, { 0u, 2u, 3u, 5u });
test_word_segmentation("ab cd"sv, { 0u, 2u, 4u, 6u });
test_word_segmentation("ab\tcd"sv, { 0u, 2u, 3u, 5u });
test_word_segmentation("ab\ncd"sv, { 0u, 2u, 3u, 5u });
test_word_segmentation("ab\n\rcd"sv, { 0u, 2u, 3u, 4u, 6u });
test_word_segmentation("ab\r\ncd"sv, { 0u, 2u, 4u, 6u });
test_word_segmentation("a😀b"sv, { 0u, 1u, 5u, 6u });
test_word_segmentation("a👨👩👧👦b"sv, { 0u, 1u, 26u, 27u });
test_word_segmentation("a👩🏼👨🏻b"sv, { 0u, 1u, 29u, 30u });
test_word_segmentation("ab 12 cd"sv, { 0u, 2u, 3u, 5u, 6u, 8u });
test_word_segmentation("ab 1.2 cd"sv, { 0u, 2u, 3u, 6u, 7u, 9u });
test_word_segmentation("ab 12.34 cd"sv, { 0u, 2u, 3u, 8u, 9u, 11u });
test_word_segmentation("ab example.com cd"sv, { 0u, 2u, 3u, 14u, 15u, 17u });
test_word_segmentation("ab can't cd"sv, { 0u, 2u, 3u, 8u, 9u, 11u });
test_word_segmentation("ab \"can't\" cd"sv, { 0u, 2u, 3u, 4u, 9u, 10u, 11u, 13u });
test_word_segmentation(
"The quick (“brown”) fox cant jump 32.3 feet, right?"sv,
{ 0u, 3u, 4u, 9u, 10u, 11u, 14u, 19u, 22u, 23u, 24u, 27u, 28u, 35u, 36u, 40u, 41u, 45u, 46u, 50u, 51u, 52u, 57u, 58u });
}
template<size_t N>
static void test_line_segmentation(StringView string, size_t const (&expected_boundaries)[N])
{
Vector<size_t> boundaries;
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Line);
segmenter->for_each_boundary(MUST(String::from_utf8(string)), [&](auto boundary) {
boundaries.append(boundary);
return IterationDecision::Continue;
});
EXPECT_EQ(boundaries, ReadonlySpan<size_t> { expected_boundaries });
}
TEST_CASE(line_segmentation)
{
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Line);
segmenter->for_each_boundary(String {}, [&](auto) {
VERIFY_NOT_REACHED();
return IterationDecision::Break;
});
// Single characters.
test_line_segmentation("a"sv, { 0u, 1u });
// No break opportunities within a single word.
test_line_segmentation("abc"sv, { 0u, 3u });
// Break opportunity after whitespace.
test_line_segmentation("ab cd"sv, { 0u, 3u, 5u });
test_line_segmentation("ab cd"sv, { 0u, 4u, 6u });
test_line_segmentation("ab\tcd"sv, { 0u, 3u, 5u });
// Hard line breaks.
test_line_segmentation("ab\ncd"sv, { 0u, 3u, 5u });
test_line_segmentation("ab\r\ncd"sv, { 0u, 4u, 6u });
// CJK ideographs allow break between each character.
test_line_segmentation("你好"sv, { 0u, 3u, 6u });
test_line_segmentation("你好世界"sv, { 0u, 3u, 6u, 9u, 12u });
// Mixed ASCII and CJK.
test_line_segmentation("ab你好cd"sv, { 0u, 2u, 5u, 8u, 10u });
}
template<size_t N>
static void test_ascii_line_segmentation(Utf16String const& string, size_t const (&expected_boundaries)[N])
{
auto segmenter = Unicode::Segmenter::try_create_for_ascii_line(string.utf16_view());
VERIFY(segmenter);
Vector<size_t> boundaries;
segmenter->for_each_boundary(string.utf16_view(), [&](auto boundary) {
boundaries.append(boundary);
return IterationDecision::Continue;
});
EXPECT_EQ(boundaries, ReadonlySpan<size_t> { expected_boundaries });
}
static void expect_ascii_line_segmentation_matches_icu(Utf16String const& string, Unicode::Segmenter& icu_segmenter)
{
auto ascii_segmenter = Unicode::Segmenter::try_create_for_ascii_line(string.utf16_view());
VERIFY(ascii_segmenter);
Vector<size_t> icu_boundaries;
icu_segmenter.for_each_boundary(string.utf16_view(), [&](auto boundary) {
icu_boundaries.append(boundary);
return IterationDecision::Continue;
});
Vector<size_t> ascii_boundaries;
ascii_segmenter->for_each_boundary(string.utf16_view(), [&](auto boundary) {
ascii_boundaries.append(boundary);
return IterationDecision::Continue;
});
EXPECT_EQ(ascii_boundaries, icu_boundaries);
}
TEST_CASE(ascii_line_segmentation)
{
auto empty_segmenter = Unicode::Segmenter::try_create_for_ascii_line({});
VERIFY(empty_segmenter);
empty_segmenter->for_each_boundary(String {}, [&](auto) {
VERIFY_NOT_REACHED();
return IterationDecision::Break;
});
// Single characters and atomic words.
test_ascii_line_segmentation("a"_utf16, { 0u, 1u });
test_ascii_line_segmentation("abc"_utf16, { 0u, 3u });
// Break opportunity after whitespace.
test_ascii_line_segmentation("ab cd"_utf16, { 0u, 3u, 5u });
test_ascii_line_segmentation("ab cd"_utf16, { 0u, 4u, 6u });
test_ascii_line_segmentation("ab\tcd"_utf16, { 0u, 3u, 5u });
// Hard line breaks.
test_ascii_line_segmentation("ab\ncd"_utf16, { 0u, 3u, 5u });
test_ascii_line_segmentation("ab\r\ncd"_utf16, { 0u, 4u, 6u });
test_ascii_line_segmentation("ab\rcd"_utf16, { 0u, 3u, 5u });
// Alphanumerics are atomic across letter/digit boundaries.
test_ascii_line_segmentation("abc123"_utf16, { 0u, 6u });
test_ascii_line_segmentation("123abc"_utf16, { 0u, 6u });
test_ascii_line_segmentation("a 1 b"_utf16, { 0u, 2u, 4u, 5u });
// Printable ASCII punctuation follows UAX#14 line-breaking rules.
test_ascii_line_segmentation("example.com"_utf16, { 0u, 11u }); // LB15d (× IS) and LB29 (IS × AL) keep the dotted name atomic.
test_ascii_line_segmentation("hello, world"_utf16, { 0u, 7u, 12u }); // Break after the space (LB18).
test_ascii_line_segmentation("a/b/c"_utf16, { 0u, 2u, 4u, 5u }); // LB13 × SY, default ÷ after SY.
test_ascii_line_segmentation("http://a/b"_utf16, { 0u, 7u, 9u, 10u });
test_ascii_line_segmentation("\"ab\""_utf16, { 0u, 4u }); // LB19 around QU.
test_ascii_line_segmentation("$100"_utf16, { 0u, 4u }); // LB25 PR × NU and NU × NU.
test_ascii_line_segmentation("5%"_utf16, { 0u, 2u }); // LB25 NU × PO.
test_ascii_line_segmentation("a+$b"_utf16, { 0u, 2u, 4u }); // LB24 AL × PR; PR × PR has no rule.
test_ascii_line_segmentation("x%20y"_utf16, { 0u, 5u }); // LB24 AL × PO; LB25 PO × NU; LB23 NU × AL.
test_ascii_line_segmentation(" .23"_utf16, { 0u, 1u, 4u }); // LB15c forces a break before a leading decimal point.
test_ascii_line_segmentation("1/2"_utf16, { 0u, 3u }); // LB25 NU × SY × NU.
test_ascii_line_segmentation("1)$"_utf16, { 0u, 3u }); // LB25 NU × CP and CP × PR.
test_ascii_line_segmentation("(ab)"_utf16, { 0u, 4u }); // LB14 OP × ; LB13 × CP.
test_ascii_line_segmentation(") ("_utf16, { 0u, 2u, 3u }); // LB18 break after SP.
test_ascii_line_segmentation("a(b)c"_utf16, { 0u, 5u }); // LB30 AL × OP and CP × AL.
test_ascii_line_segmentation("a-\"b"_utf16, { 0u, 4u }); // LB21 × HY and LB19 around QU.
test_ascii_line_segmentation("what?yes"_utf16, { 0u, 5u, 8u }); // LB13 × EX, default ÷ after EX.
test_ascii_line_segmentation("what?\"yes\""_utf16, { 0u, 10u }); // LB19 keeps the quoted segment atomic.
// U+007C VERTICAL LINE is UAX#14 class BA: no break before, break after.
test_ascii_line_segmentation("a|b"_utf16, { 0u, 2u, 3u });
test_ascii_line_segmentation("aaa|bbb"_utf16, { 0u, 4u, 7u });
test_ascii_line_segmentation("a||b"_utf16, { 0u, 3u, 4u });
// Hyphen behavior follows UAX#14: × HY before, ÷ after when not in a numeric expression.
// LB20a additionally glues a leading hyphen to the following letter.
test_ascii_line_segmentation("-foo"_utf16, { 0u, 4u });
test_ascii_line_segmentation("x -foo"_utf16, { 0u, 2u, 6u });
test_ascii_line_segmentation("word-break"_utf16, { 0u, 5u, 10u });
// LB25 keeps numeric expressions atomic across hyphens (HY × NU).
test_ascii_line_segmentation("-2"_utf16, { 0u, 2u });
test_ascii_line_segmentation("foo-2"_utf16, { 0u, 5u });
test_ascii_line_segmentation("ABCD-1234"_utf16, { 0u, 9u });
test_ascii_line_segmentation("-#tag"_utf16, { 0u, 5u });
}
TEST_CASE(ascii_line_segmentation_matches_icu_for_context_independent_text)
{
// The fast path implements UAX#14 for ASCII inputs and should agree with ICU on every input it accepts.
Array test_strings = {
"a"_utf16,
"abc"_utf16,
"ab cd"_utf16,
"ab cd"_utf16,
"ab\tcd"_utf16,
"ab\ncd"_utf16,
"ab\r\ncd"_utf16,
"abc123"_utf16,
"a 1 b 2"_utf16,
"The quick brown fox jumps over the lazy dog"_utf16,
"example.com"_utf16,
"hello, world"_utf16,
"a/b/c"_utf16,
"http://a/b"_utf16,
"\"ab\""_utf16,
"$100"_utf16,
"5%"_utf16,
"a+$b"_utf16,
"x%20y"_utf16,
" .23"_utf16,
"1/2"_utf16,
"1)$"_utf16,
"(ab)"_utf16,
") ("_utf16,
"a(b)c"_utf16,
"a-\"b"_utf16,
"what?yes"_utf16,
"what?\"yes\""_utf16,
"a|b"_utf16,
"-foo"_utf16,
"word-break"_utf16,
"foo-2"_utf16,
"ABCD-1234"_utf16,
"a[b]c"_utf16,
"$(100)"_utf16,
"10/%"_utf16,
"/0"_utf16,
")%"_utf16,
"x -foo"_utf16,
"\"[0-9]{8,19}\""_utf16,
};
auto icu_segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Line);
for (auto const& string : test_strings) {
expect_ascii_line_segmentation_matches_icu(string, *icu_segmenter);
}
}
TEST_CASE(try_create_for_ascii_line)
{
auto eligible = [](StringView text) {
auto string = Utf16String::from_utf8(text);
return Unicode::Segmenter::try_create_for_ascii_line(string.utf16_view()) != nullptr;
};
// Letters, digits, and whitespace are admitted.
EXPECT(eligible(""sv));
EXPECT(eligible("hello"sv));
EXPECT(eligible("hello world"sv));
EXPECT(eligible("123"sv));
EXPECT(eligible("abc 123\nxyz"sv));
EXPECT(eligible("a\tb\r\nc"sv));
// Printable ASCII punctuation is admitted.
EXPECT(eligible("hello, world"sv));
EXPECT(eligible("a/b"sv));
EXPECT(eligible("example.com"sv));
EXPECT(eligible("word-break"sv));
EXPECT(eligible("$100"sv));
EXPECT(eligible("\"quoted\""sv));
EXPECT(eligible("1/2"sv));
EXPECT(eligible(" .23"sv));
EXPECT(eligible("-#tag"sv));
EXPECT(eligible("(ab)"sv));
EXPECT(eligible("[ab]"sv));
EXPECT(eligible("{ab}"sv));
// Unsupported controls and non-ASCII are rejected so that ICU handles them.
EXPECT(!eligible("\x1b"sv));
EXPECT(!eligible("你好"sv));
}
TEST_CASE(ascii_line_segmenter_clone_independence)
{
auto utf16_a = "ab cd"_utf16;
auto segmenter_a = Unicode::Segmenter::try_create_for_ascii_line(utf16_a.utf16_view());
VERIFY(segmenter_a);
auto segmenter_b = segmenter_a->clone();
segmenter_b->set_segmented_text("xy"_string);
EXPECT_EQ(segmenter_a->next_boundary(0).value_or(0u), 3u);
EXPECT_EQ(segmenter_b->next_boundary(0).value_or(0u), 2u);
}
TEST_CASE(ascii_line_segmenter_next_boundary_inclusive)
{
auto utf16 = "ab cd"_utf16;
auto segmenter = Unicode::Segmenter::try_create_for_ascii_line(utf16.utf16_view());
VERIFY(segmenter);
// Inclusive::Yes returns the index itself when it is a boundary.
EXPECT_EQ(segmenter->next_boundary(0, Unicode::Segmenter::Inclusive::Yes).value_or(0u), 0u);
EXPECT_EQ(segmenter->next_boundary(3, Unicode::Segmenter::Inclusive::Yes).value_or(0u), 3u);
EXPECT_EQ(segmenter->next_boundary(5, Unicode::Segmenter::Inclusive::Yes).value_or(0u), 5u);
// Inclusive::Yes at a non-boundary returns the next boundary.
EXPECT_EQ(segmenter->next_boundary(1, Unicode::Segmenter::Inclusive::Yes).value_or(0u), 3u);
EXPECT_EQ(segmenter->next_boundary(2, Unicode::Segmenter::Inclusive::Yes).value_or(0u), 3u);
EXPECT_EQ(segmenter->next_boundary(4, Unicode::Segmenter::Inclusive::Yes).value_or(0u), 5u);
// Past the end returns empty.
EXPECT(!segmenter->next_boundary(5).has_value());
EXPECT(!segmenter->next_boundary(10).has_value());
// Previous boundary clamps from past the end, matching the ICU-backed segmenter.
EXPECT_EQ(segmenter->previous_boundary(10).value_or(0u), 5u);
}
TEST_CASE(out_of_bounds)
{
{
auto text = "foo"_string;
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word);
segmenter->set_segmented_text(text);
auto result = segmenter->previous_boundary(text.byte_count() + 1);
EXPECT(result.has_value());
result = segmenter->next_boundary(text.byte_count() + 1);
EXPECT(!result.has_value());
result = segmenter->previous_boundary(text.byte_count());
EXPECT(result.has_value());
result = segmenter->next_boundary(text.byte_count());
EXPECT(!result.has_value());
result = segmenter->next_boundary(0);
EXPECT(result.has_value());
result = segmenter->previous_boundary(0);
EXPECT(!result.has_value());
}
{
auto text = u"foo"_utf16;
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word);
segmenter->set_segmented_text(text);
auto result = segmenter->previous_boundary(text.length_in_code_units() + 1);
EXPECT(result.has_value());
result = segmenter->next_boundary(text.length_in_code_units() + 1);
EXPECT(!result.has_value());
result = segmenter->previous_boundary(text.length_in_code_units());
EXPECT(result.has_value());
result = segmenter->next_boundary(text.length_in_code_units());
EXPECT(!result.has_value());
result = segmenter->next_boundary(0);
EXPECT(result.has_value());
result = segmenter->previous_boundary(0);
EXPECT(!result.has_value());
}
}