blob: 7e6726e3012f3ddfa53cd06042e64ff21b43c53a [file]
// Copyright 2026 The PDFium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "core/fxcrt/cfx_bidi_resolver.h"
#include <ostream>
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::ElementsAre;
bool operator==(const CFX_BidiResolver::ResolvedRun& lhs,
const CFX_BidiResolver::ResolvedRun& rhs) {
return lhs.start == rhs.start && lhs.length == rhs.length &&
lhs.is_rtl == rhs.is_rtl;
}
void PrintTo(const CFX_BidiResolver::ResolvedRun& run, std::ostream* os) {
*os << "ResolvedRun{start=" << run.start << ", length=" << run.length
<< ", is_rtl=" << (run.is_rtl ? "true" : "false") << "}";
}
TEST(fxcrt, BidiResolverPureLTR) {
auto resolver = CFX_BidiResolver::Create(
u"abc", CFX_BidiResolver::ParagraphDirection::kLeftToRight);
ASSERT_TRUE(resolver);
EXPECT_THAT(resolver->GetVisualRunsForLine(0, 3),
ElementsAre(CFX_BidiResolver::ResolvedRun{0, 3, false}));
}
TEST(fxcrt, BidiResolverPureRTL) {
// Hebrew characters Aleph, Bet, Gimel (RTL characters).
auto resolver = CFX_BidiResolver::Create(
u"\x05D0\x05D1\x05D2",
CFX_BidiResolver::ParagraphDirection::kRightToLeft);
ASSERT_TRUE(resolver);
EXPECT_THAT(resolver->GetVisualRunsForLine(0, 3),
ElementsAre(CFX_BidiResolver::ResolvedRun{0, 3, true}));
}
TEST(fxcrt, BidiResolverMixed) {
auto resolver = CFX_BidiResolver::Create(
u"ab \x05D0\x05D1 cd",
CFX_BidiResolver::ParagraphDirection::kLeftToRight);
ASSERT_TRUE(resolver);
// "ab " (LTR) -> "Aleph Bet" (RTL) -> " cd" (LTR)
EXPECT_THAT(resolver->GetVisualRunsForLine(0, 8),
ElementsAre(CFX_BidiResolver::ResolvedRun{0, 3, false},
CFX_BidiResolver::ResolvedRun{3, 2, true},
CFX_BidiResolver::ResolvedRun{5, 3, false}));
}
TEST(fxcrt, BidiResolverLineWrap) {
auto resolver = CFX_BidiResolver::Create(
u"abc def ghi", CFX_BidiResolver::ParagraphDirection::kLeftToRight);
ASSERT_TRUE(resolver);
// Get runs for the middle word "def "
EXPECT_THAT(resolver->GetVisualRunsForLine(4, 4),
ElementsAre(CFX_BidiResolver::ResolvedRun{4, 4, false}));
}
TEST(fxcrt, BidiResolverEmpty) {
auto resolver = CFX_BidiResolver::Create(
std::u16string(), CFX_BidiResolver::ParagraphDirection::kAuto);
// Just proving it doesn't crash or leak memory.
EXPECT_FALSE(resolver);
}
TEST(fxcrt, BidiResolverOutOfBounds) {
auto resolver = CFX_BidiResolver::Create(
u"abc", CFX_BidiResolver::ParagraphDirection::kLeftToRight);
ASSERT_TRUE(resolver);
// Negative start
EXPECT_THAT(resolver->GetVisualRunsForLine(-1, 3), testing::IsEmpty());
// Negative length
EXPECT_THAT(resolver->GetVisualRunsForLine(0, -1), testing::IsEmpty());
// Zero length
EXPECT_THAT(resolver->GetVisualRunsForLine(0, 0), testing::IsEmpty());
// Start + length is past the end
EXPECT_THAT(resolver->GetVisualRunsForLine(1, 3), testing::IsEmpty());
// Start is past the end
EXPECT_THAT(resolver->GetVisualRunsForLine(4, 1), testing::IsEmpty());
}
TEST(fxcrt, BidiResolverInvalidUTF16) {
// Pass an isolated trailing surrogate (invalid UTF-16 sequence)
auto resolver = CFX_BidiResolver::Create(
u"\xDC00\xDC00", CFX_BidiResolver::ParagraphDirection::kLeftToRight);
ASSERT_TRUE(resolver);
// ICU typically substitutes invalid bytes or treats them as neutral.
// Because the paragraph direction is LTR, these neutral characters
// inherit the base direction and resolve to a single LTR run.
EXPECT_THAT(resolver->GetVisualRunsForLine(0, 2),
ElementsAre(CFX_BidiResolver::ResolvedRun{0, 2, false}));
}