blob: 5416b97e1f2cc0c379ae95dcdddecc438e4e8357 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h"
#include "third_party/blink/renderer/core/editing/text_affinity.h"
#include "third_party/blink/renderer/core/layout/layout_block_flow.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
#include "third_party/blink/renderer/core/layout/ng/ng_layout_test.h"
namespace blink {
class NGCaretPositionTest : public NGLayoutTest {
public:
NGCaretPositionTest() : NGLayoutTest() {}
void SetUp() override {
NGLayoutTest::SetUp();
LoadAhem();
}
protected:
void SetInlineFormattingContext(const char* id,
const char* html,
unsigned width,
TextDirection dir = TextDirection::kLtr,
const char* style = nullptr) {
InsertStyleElement(
"body { font: 10px/10px Ahem; }"
"bdo { display:block; }");
const char* pattern =
dir == TextDirection::kLtr
? "<div id='%s' style='width: %u0px; %s'>%s</div>"
: "<bdo dir=rtl id='%s' style='width: %u0px; %s'>%s</bdo>";
SetBodyInnerHTML(String::Format(
pattern, id, width, style ? style : "word-break: break-all", html));
container_ = GetElementById(id);
DCHECK(container_);
context_ = To<LayoutBlockFlow>(container_->GetLayoutObject());
DCHECK(context_);
DCHECK(context_->IsLayoutNGMixin());
}
NGCaretPosition ComputeNGCaretPosition(unsigned offset,
TextAffinity affinity) const {
return blink::ComputeNGCaretPosition(*context_, offset, affinity);
}
NGInlineCursor FragmentOf(const Node* node) const {
NGInlineCursor cursor;
cursor.MoveTo(*node->GetLayoutObject());
return cursor;
}
Persistent<Element> container_;
const LayoutBlockFlow* context_;
};
#define TEST_CARET(caret, fragment_, type_, offset_) \
{ \
EXPECT_EQ(caret.cursor, fragment_); \
EXPECT_EQ(caret.position_type, NGCaretPositionType::type_); \
EXPECT_EQ(caret.text_offset, offset_) << caret.text_offset.value_or(-1); \
}
TEST_F(NGCaretPositionTest, CaretPositionInOneLineOfText) {
SetInlineFormattingContext("t", "foo", 3);
const Node* text = container_->firstChild();
const NGInlineCursor& text_fragment = FragmentOf(text);
// Beginning of line
TEST_CARET(ComputeNGCaretPosition(0, TextAffinity::kDownstream),
text_fragment, kAtTextOffset, base::Optional<unsigned>(0));
TEST_CARET(ComputeNGCaretPosition(0, TextAffinity::kUpstream), text_fragment,
kAtTextOffset, base::Optional<unsigned>(0));
// Middle in the line
TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kDownstream),
text_fragment, kAtTextOffset, base::Optional<unsigned>(1));
TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kUpstream), text_fragment,
kAtTextOffset, base::Optional<unsigned>(1));
// End of line
TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kDownstream),
text_fragment, kAtTextOffset, base::Optional<unsigned>(3));
TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kUpstream), text_fragment,
kAtTextOffset, base::Optional<unsigned>(3));
}
// For http://crbug.com/1021993
// We should not call |NGInlineCursor::CurrentBidiLevel()| for soft hyphen
TEST_F(NGCaretPositionTest, CaretPositionAtSoftHyphen) {
// We have three fragment "foo\u00AD", "\u2010", "bar"
SetInlineFormattingContext("t", "foo&shy;bar", 3, TextDirection::kLtr, "");
const LayoutText& text =
*To<Text>(container_->firstChild())->GetLayoutObject();
NGInlineCursor cursor;
cursor.MoveTo(text);
const NGInlineCursor foo_fragment = cursor;
TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kDownstream), foo_fragment,
kAtTextOffset, base::Optional<unsigned>(4));
TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kUpstream), foo_fragment,
kAtTextOffset, base::Optional<unsigned>(4));
}
TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrap) {
SetInlineFormattingContext("t", "foobar", 3);
const LayoutText& text =
*To<Text>(container_->firstChild())->GetLayoutObject();
NGInlineCursor cursor;
cursor.MoveTo(text);
const NGInlineCursor foo_fragment = cursor;
cursor.MoveToNextForSameLayoutObject();
const NGInlineCursor bar_fragment = cursor;
TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kDownstream), bar_fragment,
kAtTextOffset, base::Optional<unsigned>(3));
TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kUpstream), foo_fragment,
kAtTextOffset, base::Optional<unsigned>(3));
}
TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrapWithSpace) {
SetInlineFormattingContext("t", "foo bar", 3);
const LayoutText& text =
*To<Text>(container_->firstChild())->GetLayoutObject();
NGInlineCursor cursor;
cursor.MoveTo(text);
const NGInlineCursor foo_fragment = cursor;
cursor.MoveToNextForSameLayoutObject();
const NGInlineCursor bar_fragment = cursor;
// Before the space
TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kDownstream), foo_fragment,
kAtTextOffset, base::Optional<unsigned>(3));
TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kUpstream), foo_fragment,
kAtTextOffset, base::Optional<unsigned>(3));
// After the space
TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kDownstream), bar_fragment,
kAtTextOffset, base::Optional<unsigned>(4));
TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kUpstream), bar_fragment,
kAtTextOffset, base::Optional<unsigned>(4));
}
TEST_F(NGCaretPositionTest, CaretPositionAtForcedLineBreak) {
SetInlineFormattingContext("t", "foo<br>bar", 3);
const Node* foo = container_->firstChild();
const Node* br = foo->nextSibling();
const Node* bar = br->nextSibling();
const NGInlineCursor& foo_fragment = FragmentOf(foo);
const NGInlineCursor& bar_fragment = FragmentOf(bar);
// Before the BR
TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kDownstream), foo_fragment,
kAtTextOffset, base::Optional<unsigned>(3));
TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kUpstream), foo_fragment,
kAtTextOffset, base::Optional<unsigned>(3));
// After the BR
TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kDownstream), bar_fragment,
kAtTextOffset, base::Optional<unsigned>(4));
TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kUpstream), bar_fragment,
kAtTextOffset, base::Optional<unsigned>(4));
}
TEST_F(NGCaretPositionTest, CaretPositionAtEmptyLine) {
SetInlineFormattingContext("f", "foo<br><br>bar", 3);
const Node* foo = container_->firstChild();
const Node* br1 = foo->nextSibling();
const Node* br2 = br1->nextSibling();
const NGInlineCursor& br2_fragment = FragmentOf(br2);
TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kDownstream), br2_fragment,
kAtTextOffset, base::Optional<unsigned>(4));
TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kUpstream), br2_fragment,
kAtTextOffset, base::Optional<unsigned>(4));
}
TEST_F(NGCaretPositionTest, CaretPositionInOneLineOfImage) {
SetInlineFormattingContext("t", "<img>", 3);
const Node* img = container_->firstChild();
const NGInlineCursor& img_fragment = FragmentOf(img);
// Before the image
TEST_CARET(ComputeNGCaretPosition(0, TextAffinity::kDownstream), img_fragment,
kBeforeBox, base::nullopt);
TEST_CARET(ComputeNGCaretPosition(0, TextAffinity::kUpstream), img_fragment,
kBeforeBox, base::nullopt);
// After the image
TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kDownstream), img_fragment,
kAfterBox, base::nullopt);
TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kUpstream), img_fragment,
kAfterBox, base::nullopt);
}
TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrapBetweenImages) {
SetInlineFormattingContext("t",
"<img id=img1><img id=img2>"
"<style>img{width: 1em; height: 1em}</style>",
1);
const Node* img1 = container_->firstChild();
const Node* img2 = img1->nextSibling();
const NGInlineCursor& img1_fragment = FragmentOf(img1);
const NGInlineCursor& img2_fragment = FragmentOf(img2);
TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kDownstream),
img2_fragment, kBeforeBox, base::nullopt);
TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kUpstream), img1_fragment,
kAfterBox, base::nullopt);
}
TEST_F(NGCaretPositionTest,
CaretPositionAtSoftLineWrapBetweenMultipleTextNodes) {
SetInlineFormattingContext("t",
"<span>A</span>"
"<span>B</span>"
"<span id=span-c>C</span>"
"<span id=span-d>D</span>"
"<span>E</span>"
"<span>F</span>",
3);
const Node* text_c = GetElementById("span-c")->firstChild();
const Node* text_d = GetElementById("span-d")->firstChild();
const NGInlineCursor& fragment_c = FragmentOf(text_c);
const NGInlineCursor& fragment_d = FragmentOf(text_d);
const Position wrap_position(text_c, 1);
const NGOffsetMapping& mapping = *NGOffsetMapping::GetFor(wrap_position);
const unsigned wrap_offset = *mapping.GetTextContentOffset(wrap_position);
TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kUpstream),
fragment_c, kAtTextOffset, base::Optional<unsigned>(wrap_offset));
TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kDownstream),
fragment_d, kAtTextOffset, base::Optional<unsigned>(wrap_offset));
}
TEST_F(NGCaretPositionTest,
CaretPositionAtSoftLineWrapBetweenMultipleTextNodesRtl) {
SetInlineFormattingContext("t",
"<span>A</span>"
"<span>B</span>"
"<span id=span-c>C</span>"
"<span id=span-d>D</span>"
"<span>E</span>"
"<span>F</span>",
3, TextDirection::kRtl);
const Node* text_c = GetElementById("span-c")->firstChild();
const Node* text_d = GetElementById("span-d")->firstChild();
const NGInlineCursor& fragment_c = FragmentOf(text_c);
const NGInlineCursor& fragment_d = FragmentOf(text_d);
const Position wrap_position(text_c, 1);
const NGOffsetMapping& mapping = *NGOffsetMapping::GetFor(wrap_position);
const unsigned wrap_offset = *mapping.GetTextContentOffset(wrap_position);
TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kUpstream),
fragment_c, kAtTextOffset, base::Optional<unsigned>(wrap_offset));
TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kDownstream),
fragment_d, kAtTextOffset, base::Optional<unsigned>(wrap_offset));
}
TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrapBetweenDeepTextNodes) {
SetInlineFormattingContext(
"t",
"<style>span {border: 1px solid black}</style>"
"<span>A</span>"
"<span>B</span>"
"<span id=span-c>C</span>"
"<span id=span-d>D</span>"
"<span>E</span>"
"<span>F</span>",
4); // Wider space to allow border and 3 characters
const Node* text_c = GetElementById("span-c")->firstChild();
const Node* text_d = GetElementById("span-d")->firstChild();
const NGInlineCursor& fragment_c = FragmentOf(text_c);
const NGInlineCursor& fragment_d = FragmentOf(text_d);
const Position wrap_position(text_c, 1);
const NGOffsetMapping& mapping = *NGOffsetMapping::GetFor(wrap_position);
const unsigned wrap_offset = *mapping.GetTextContentOffset(wrap_position);
TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kUpstream),
fragment_c, kAtTextOffset, base::Optional<unsigned>(wrap_offset));
TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kDownstream),
fragment_d, kAtTextOffset, base::Optional<unsigned>(wrap_offset));
}
TEST_F(NGCaretPositionTest, GeneratedZeroWidthSpace) {
LoadAhem();
InsertStyleElement(
"p { font: 10px/1 Ahem; }"
"p { width: 4ch; white-space: pre-wrap;");
// We have ZWS before "abc" due by "pre-wrap".
// text content is
// [0..3] " "
// [4] ZWS
// [5..8] "abcd"
SetBodyInnerHTML("<p id=t> abcd</p>");
const Text& text = To<Text>(*GetElementById("t")->firstChild());
const Position after_zws(text, 4); // before "a".
NGInlineCursor cursor;
cursor.MoveTo(*text.GetLayoutObject());
ASSERT_EQ(NGTextOffset(0, 4), cursor.Current().TextOffset());
TEST_CARET(blink::ComputeNGCaretPosition(
PositionWithAffinity(after_zws, TextAffinity::kUpstream)),
cursor, kAtTextOffset, base::Optional<unsigned>(4));
cursor.MoveToNextForSameLayoutObject();
ASSERT_EQ(NGTextOffset(5, 9), cursor.Current().TextOffset());
TEST_CARET(blink::ComputeNGCaretPosition(
PositionWithAffinity(after_zws, TextAffinity::kDownstream)),
cursor, kAtTextOffset, base::Optional<unsigned>(5));
}
// http://crbug.com/1183269
// See also NGCaretPositionTest.CaretPositionAtSoftLineWrap
TEST_F(NGCaretPositionTest, SoftLineWrap) {
LoadAhem();
InsertStyleElement(
"p { font: 10px/1 Ahem; }"
"p { width: 4ch;");
// Note: "contenteditable" adds
// line-break: after-white-space;
// overflow-wrap: break-word;
SetBodyInnerHTML("<p id=t contenteditable>abc xyz</p>");
const Text& text = To<Text>(*GetElementById("t")->firstChild());
const Position before_xyz(text, 4); // before "w".
NGInlineCursor cursor;
cursor.MoveTo(*text.GetLayoutObject());
// Note: upstream/downstream before "xyz" are in different line.
ASSERT_EQ(NGTextOffset(0, 3), cursor.Current().TextOffset());
TEST_CARET(blink::ComputeNGCaretPosition(
PositionWithAffinity(before_xyz, TextAffinity::kUpstream)),
cursor, kAtTextOffset, base::Optional<unsigned>(3));
cursor.MoveToNextForSameLayoutObject();
ASSERT_EQ(NGTextOffset(4, 7), cursor.Current().TextOffset());
TEST_CARET(blink::ComputeNGCaretPosition(
PositionWithAffinity(before_xyz, TextAffinity::kDownstream)),
cursor, kAtTextOffset, base::Optional<unsigned>(4));
}
TEST_F(NGCaretPositionTest, ZeroWidthSpace) {
LoadAhem();
InsertStyleElement(
"p { font: 10px/1 Ahem; }"
"p { width: 4ch;");
// dom and text content is
// [0..3] "abcd"
// [4] ZWS
// [5..8] "wxyz"
SetBodyInnerHTML("<p id=t>abcd&#x200B;wxyz</p>");
const Text& text = To<Text>(*GetElementById("t")->firstChild());
const Position after_zws(text, 5); // before "w".
NGInlineCursor cursor;
cursor.MoveTo(*text.GetLayoutObject());
ASSERT_EQ(NGTextOffset(0, 5), cursor.Current().TextOffset());
TEST_CARET(blink::ComputeNGCaretPosition(
PositionWithAffinity(after_zws, TextAffinity::kUpstream)),
cursor, kAtTextOffset, base::Optional<unsigned>(4));
cursor.MoveToNextForSameLayoutObject();
ASSERT_EQ(NGTextOffset(5, 9), cursor.Current().TextOffset());
TEST_CARET(blink::ComputeNGCaretPosition(
PositionWithAffinity(after_zws, TextAffinity::kDownstream)),
cursor, kAtTextOffset, base::Optional<unsigned>(5));
}
TEST_F(NGCaretPositionTest, InlineBlockBeforeContent) {
SetInlineFormattingContext(
"t",
"<style>span::before{display:inline-block; content:'foo'}</style>"
"<span id=span>bar</span>",
100); // Line width doesn't matter here.
const Node* text = GetElementById("span")->firstChild();
const NGInlineCursor& text_fragment = FragmentOf(text);
// Test caret position of "|bar", which shouldn't be affected by ::before
const Position position(text, 0);
const NGOffsetMapping& mapping = *NGOffsetMapping::GetFor(position);
const unsigned text_offset = *mapping.GetTextContentOffset(position);
TEST_CARET(ComputeNGCaretPosition(text_offset, TextAffinity::kDownstream),
text_fragment, kAtTextOffset,
base::Optional<unsigned>(text_offset));
}
TEST_F(NGCaretPositionTest, InlineBoxesLTR) {
SetBodyInnerHTML(
"<div dir=ltr>"
"<bdo id=box1 dir=ltr>ABCD</bdo>"
"<bdo id=box2 dir=ltr style='font-size: 150%'>EFG</bdo></div>");
// text_content:
// [0] U+202D LEFT-TO_RIGHT_OVERRIDE
// [1:4] "ABCD"
// [5] U+202C POP DIRECTIONAL FORMATTING
// [6] U+202D LEFT-TO_RIGHT_OVERRIDE
// [7:8] "EF"
// [9] U+202C POP DIRECTIONAL FORMATTING
const Node& box1 = *GetElementById("box1")->firstChild();
const Node& box2 = *GetElementById("box1")->firstChild();
TEST_CARET(
blink::ComputeNGCaretPosition(PositionWithAffinity(Position(box1, 4))),
FragmentOf(&box1), kAtTextOffset, base::Optional<unsigned>(5));
TEST_CARET(
blink::ComputeNGCaretPosition(PositionWithAffinity(Position(box2, 0))),
FragmentOf(&box2), kAtTextOffset, base::Optional<unsigned>(1));
}
TEST_F(NGCaretPositionTest, InlineBoxesRTL) {
SetBodyInnerHTML(
"<div dir=rtl>"
"<bdo id=box1 dir=rtl>ABCD</bdo>"
"<bdo id=box2 dir=rtl style='font-size: 150%'>EFG</bdo></div>");
// text_content:
// [0] U+202E RIGHT-TO_LEFT _OVERRIDE
// [1:4] "ABCD"
// [5] U+202C POP DIRECTIONAL FORMATTING
// [6] U+202E RIGHT-TO_LEFT _OVERRIDE
// [7:8] "EF"
// [9] U+202C POP DIRECTIONAL FORMATTING
const Node& box1 = *GetElementById("box1")->firstChild();
const Node& box2 = *GetElementById("box1")->firstChild();
TEST_CARET(
blink::ComputeNGCaretPosition(PositionWithAffinity(Position(box1, 4))),
FragmentOf(&box1), kAtTextOffset, base::Optional<unsigned>(5));
TEST_CARET(
blink::ComputeNGCaretPosition(PositionWithAffinity(Position(box2, 0))),
FragmentOf(&box2), kAtTextOffset, base::Optional<unsigned>(1));
}
} // namespace blink