blob: 912b017a6317bb7089ae689ab9368c6d934fe611 [file] [log] [blame]
// Copyright 2016 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_inline_items_builder.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/layout/layout_inline.h"
#include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h"
#include "third_party/blink/renderer/core/layout/ng/layout_ng_ruby_run.h"
#include "third_party/blink/renderer/core/layout/ng/ng_layout_test.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
namespace blink {
// The spec turned into a discussion that may change. Put this logic on hold
// until CSSWG resolves the issue.
// https://github.com/w3c/csswg-drafts/issues/337
#define SEGMENT_BREAK_TRANSFORMATION_FOR_EAST_ASIAN_WIDTH 0
#define EXPECT_ITEM_OFFSET(item, type, start, end) \
EXPECT_EQ(type, (item).Type()); \
EXPECT_EQ(start, (item).StartOffset()); \
EXPECT_EQ(end, (item).EndOffset());
class NGInlineItemsBuilderTest : public NGLayoutTest {
protected:
void SetUp() override {
NGLayoutTest::SetUp();
style_ = ComputedStyle::Create();
block_flow_ = LayoutBlockFlow::CreateAnonymous(&GetDocument(), style_,
LegacyLayout::kAuto);
anonymous_objects_.push_back(block_flow_);
}
void TearDown() override {
for (LayoutObject* anonymous_object : anonymous_objects_)
anonymous_object->Destroy();
NGLayoutTest::TearDown();
}
LayoutBlockFlow* GetLayoutBlockFlow() const { return block_flow_; }
void SetWhiteSpace(EWhiteSpace whitespace) {
style_->SetWhiteSpace(whitespace);
}
scoped_refptr<ComputedStyle> GetStyle(EWhiteSpace whitespace) {
if (whitespace == EWhiteSpace::kNormal)
return style_;
scoped_refptr<ComputedStyle> style(ComputedStyle::Create());
style->SetWhiteSpace(whitespace);
return style;
}
bool HasRuby(const NGInlineItemsBuilder& builder) const {
return builder.has_ruby_;
}
void AppendText(const String& text, NGInlineItemsBuilder* builder) {
LayoutText* layout_text = LayoutText::CreateEmptyAnonymous(
GetDocument(), style_.get(), LegacyLayout::kAuto);
anonymous_objects_.push_back(layout_text);
builder->AppendText(text, layout_text);
}
void AppendAtomicInline(NGInlineItemsBuilder* builder) {
LayoutBlockFlow* layout_block_flow = LayoutBlockFlow::CreateAnonymous(
&GetDocument(), style_, LegacyLayout::kAuto);
anonymous_objects_.push_back(layout_block_flow);
builder->AppendAtomicInline(layout_block_flow);
}
void AppendRubyRun(NGInlineItemsBuilder* builder) {
LayoutNGRubyRun* ruby_run = new LayoutNGRubyRun();
ruby_run->SetDocumentForAnonymous(&GetDocument());
ruby_run->SetStyle(style_);
anonymous_objects_.push_back(ruby_run);
builder->AppendAtomicInline(ruby_run);
}
struct Input {
const String text;
EWhiteSpace whitespace = EWhiteSpace::kNormal;
LayoutText* layout_text = nullptr;
};
const String& TestAppend(Vector<Input> inputs) {
items_.clear();
Vector<LayoutText*> anonymous_objects;
NGInlineItemsBuilder builder(GetLayoutBlockFlow(), &items_);
for (Input& input : inputs) {
if (!input.layout_text) {
input.layout_text = LayoutText::CreateEmptyAnonymous(
GetDocument(), GetStyle(input.whitespace), LegacyLayout::kAuto);
anonymous_objects.push_back(input.layout_text);
}
builder.AppendText(input.text, input.layout_text);
}
builder.ExitBlock();
text_ = builder.ToString();
ValidateItems();
CheckReuseItemsProducesSameResult(inputs, builder.HasBidiControls());
for (LayoutObject* anonymous_object : anonymous_objects)
anonymous_object->Destroy();
return text_;
}
const String& TestAppend(const String& input) {
return TestAppend({Input{input}});
}
const String& TestAppend(const Input& input1, const Input& input2) {
return TestAppend({input1, input2});
}
const String& TestAppend(const String& input1, const String& input2) {
return TestAppend(Input{input1}, Input{input2});
}
const String& TestAppend(const String& input1,
const String& input2,
const String& input3) {
return TestAppend({{input1}, {input2}, {input3}});
}
void ValidateItems() {
unsigned current_offset = 0;
for (unsigned i = 0; i < items_.size(); i++) {
const NGInlineItem& item = items_[i];
EXPECT_EQ(current_offset, item.StartOffset());
EXPECT_LE(item.StartOffset(), item.EndOffset());
current_offset = item.EndOffset();
}
EXPECT_EQ(current_offset, text_.length());
}
void CheckReuseItemsProducesSameResult(Vector<Input> inputs,
bool has_bidi_controls) {
NGInlineNodeData fake_data;
fake_data.text_content = text_;
fake_data.is_bidi_enabled_ = has_bidi_controls;
Vector<NGInlineItem> reuse_items;
NGInlineItemsBuilder reuse_builder(GetLayoutBlockFlow(), &reuse_items);
for (Input& input : inputs) {
// Collect items for this LayoutObject.
DCHECK(input.layout_text);
for (NGInlineItem* item = items_.begin(); item != items_.end();) {
if (item->GetLayoutObject() == input.layout_text) {
NGInlineItem* begin = item;
for (++item; item != items_.end(); ++item) {
if (item->GetLayoutObject() != input.layout_text)
break;
}
input.layout_text->SetInlineItems(begin, item);
} else {
++item;
}
}
// Try to re-use previous items, or Append if it was not re-usable.
bool reused =
input.layout_text->HasValidInlineItems() &&
reuse_builder.AppendTextReusing(fake_data, input.layout_text);
if (!reused) {
reuse_builder.AppendText(input.text, input.layout_text);
}
}
reuse_builder.ExitBlock();
String reuse_text = reuse_builder.ToString();
EXPECT_EQ(text_, reuse_text);
}
LayoutBlockFlow* block_flow_ = nullptr;
Vector<NGInlineItem> items_;
String text_;
scoped_refptr<ComputedStyle> style_;
Vector<LayoutObject*> anonymous_objects_;
};
#define TestWhitespaceValue(expected_text, input, whitespace) \
SetWhiteSpace(whitespace); \
EXPECT_EQ(expected_text, TestAppend(input)) << "white-space: " #whitespace;
TEST_F(NGInlineItemsBuilderTest, CollapseSpaces) {
String input("text text text text");
String collapsed("text text text text");
TestWhitespaceValue(collapsed, input, EWhiteSpace::kNormal);
TestWhitespaceValue(collapsed, input, EWhiteSpace::kNowrap);
TestWhitespaceValue(collapsed, input, EWhiteSpace::kWebkitNowrap);
TestWhitespaceValue(collapsed, input, EWhiteSpace::kPreLine);
TestWhitespaceValue(input, input, EWhiteSpace::kPre);
TestWhitespaceValue(input, input, EWhiteSpace::kPreWrap);
}
TEST_F(NGInlineItemsBuilderTest, CollapseTabs) {
String input("text text text text");
String collapsed("text text text text");
TestWhitespaceValue(collapsed, input, EWhiteSpace::kNormal);
TestWhitespaceValue(collapsed, input, EWhiteSpace::kNowrap);
TestWhitespaceValue(collapsed, input, EWhiteSpace::kWebkitNowrap);
TestWhitespaceValue(collapsed, input, EWhiteSpace::kPreLine);
TestWhitespaceValue(input, input, EWhiteSpace::kPre);
TestWhitespaceValue(input, input, EWhiteSpace::kPreWrap);
}
TEST_F(NGInlineItemsBuilderTest, CollapseNewLines) {
String input("text\ntext \ntext\n\ntext");
String collapsed("text text text text");
TestWhitespaceValue(collapsed, input, EWhiteSpace::kNormal);
TestWhitespaceValue(collapsed, input, EWhiteSpace::kNowrap);
TestWhitespaceValue("text\ntext\ntext\n\ntext", input, EWhiteSpace::kPreLine);
TestWhitespaceValue(input, input, EWhiteSpace::kPre);
TestWhitespaceValue(input, input, EWhiteSpace::kPreWrap);
}
TEST_F(NGInlineItemsBuilderTest, CollapseNewlinesAsSpaces) {
EXPECT_EQ("text text", TestAppend("text\ntext"));
EXPECT_EQ("text text", TestAppend("text\n\ntext"));
EXPECT_EQ("text text", TestAppend("text \n\n text"));
EXPECT_EQ("text text", TestAppend("text \n \n text"));
}
TEST_F(NGInlineItemsBuilderTest, CollapseAcrossElements) {
EXPECT_EQ("text text", TestAppend("text ", " text"))
<< "Spaces are collapsed even when across elements.";
}
TEST_F(NGInlineItemsBuilderTest, CollapseLeadingSpaces) {
EXPECT_EQ("text", TestAppend(" text"));
EXPECT_EQ("text", TestAppend(" ", "text"));
EXPECT_EQ("text", TestAppend(" ", " text"));
}
TEST_F(NGInlineItemsBuilderTest, CollapseTrailingSpaces) {
EXPECT_EQ("text", TestAppend("text "));
EXPECT_EQ("text", TestAppend("text", " "));
EXPECT_EQ("text", TestAppend("text ", " "));
}
TEST_F(NGInlineItemsBuilderTest, CollapseAllSpaces) {
EXPECT_EQ("", TestAppend(" "));
EXPECT_EQ("", TestAppend(" ", " "));
EXPECT_EQ("", TestAppend(" ", "\n"));
EXPECT_EQ("", TestAppend("\n", " "));
}
TEST_F(NGInlineItemsBuilderTest, CollapseLeadingNewlines) {
EXPECT_EQ("text", TestAppend("\ntext"));
EXPECT_EQ("text", TestAppend("\n\ntext"));
EXPECT_EQ("text", TestAppend("\n", "text"));
EXPECT_EQ("text", TestAppend("\n\n", "text"));
EXPECT_EQ("text", TestAppend(" \n", "text"));
EXPECT_EQ("text", TestAppend("\n", " text"));
EXPECT_EQ("text", TestAppend("\n\n", " text"));
EXPECT_EQ("text", TestAppend(" \n", " text"));
EXPECT_EQ("text", TestAppend("\n", "\ntext"));
EXPECT_EQ("text", TestAppend("\n\n", "\ntext"));
EXPECT_EQ("text", TestAppend(" \n", "\ntext"));
}
TEST_F(NGInlineItemsBuilderTest, CollapseTrailingNewlines) {
EXPECT_EQ("text", TestAppend("text\n"));
EXPECT_EQ("text", TestAppend("text", "\n"));
EXPECT_EQ("text", TestAppend("text\n", "\n"));
EXPECT_EQ("text", TestAppend("text\n", " "));
EXPECT_EQ("text", TestAppend("text ", "\n"));
}
TEST_F(NGInlineItemsBuilderTest, CollapseNewlineAcrossElements) {
EXPECT_EQ("text text", TestAppend("text ", "\ntext"));
EXPECT_EQ("text text", TestAppend("text ", "\n text"));
EXPECT_EQ("text text", TestAppend("text", " ", "\ntext"));
}
TEST_F(NGInlineItemsBuilderTest, CollapseBeforeAndAfterNewline) {
SetWhiteSpace(EWhiteSpace::kPreLine);
EXPECT_EQ("text\ntext", TestAppend("text \n text"))
<< "Spaces before and after newline are removed.";
}
TEST_F(NGInlineItemsBuilderTest,
CollapsibleSpaceAfterNonCollapsibleSpaceAcrossElements) {
EXPECT_EQ("text text",
TestAppend({"text ", EWhiteSpace::kPreWrap}, {" text"}))
<< "The whitespace in constructions like '<span style=\"white-space: "
"pre-wrap\">text <span><span> text</span>' does not collapse.";
}
TEST_F(NGInlineItemsBuilderTest, CollapseZeroWidthSpaces) {
EXPECT_EQ(String(u"text\u200Btext"), TestAppend(u"text\u200B\ntext"))
<< "Newline is removed if the character before is ZWS.";
EXPECT_EQ(String(u"text\u200Btext"), TestAppend(u"text\n\u200Btext"))
<< "Newline is removed if the character after is ZWS.";
EXPECT_EQ(String(u"text\u200B\u200Btext"),
TestAppend(u"text\u200B\n\u200Btext"))
<< "Newline is removed if the character before/after is ZWS.";
EXPECT_EQ(String(u"text\u200Btext"), TestAppend(u"text\n", u"\u200Btext"))
<< "Newline is removed if the character after across elements is ZWS.";
EXPECT_EQ(String(u"text\u200Btext"), TestAppend(u"text\u200B", u"\ntext"))
<< "Newline is removed if the character before is ZWS even across "
"elements.";
EXPECT_EQ(String(u"text\u200Btext"), TestAppend(u"text \n", u"\u200Btext"))
<< "Collapsible space before newline does not affect the result.";
EXPECT_EQ(String(u"text\u200B text"), TestAppend(u"text\u200B\n", u" text"))
<< "Collapsible space after newline is removed even when the "
"newline was removed.";
EXPECT_EQ(String(u"text\u200Btext"), TestAppend(u"text\u200B ", u"\ntext"))
<< "A white space sequence containing a segment break before or after "
"a zero width space is collapsed to a zero width space.";
}
TEST_F(NGInlineItemsBuilderTest, CollapseZeroWidthSpaceAndNewLineAtEnd) {
EXPECT_EQ(String(u"\u200B"), TestAppend(u"\u200B\n"));
EXPECT_EQ(NGInlineItem::kNotCollapsible, items_[0].EndCollapseType());
}
#if SEGMENT_BREAK_TRANSFORMATION_FOR_EAST_ASIAN_WIDTH
TEST_F(NGInlineItemsBuilderTest, CollapseEastAsianWidth) {
EXPECT_EQ(String(u"\u4E00\u4E00"), TestAppend(u"\u4E00\n\u4E00"))
<< "Newline is removed when both sides are Wide.";
EXPECT_EQ(String(u"\u4E00 A"), TestAppend(u"\u4E00\nA"))
<< "Newline is not removed when after is Narrow.";
EXPECT_EQ(String(u"A \u4E00"), TestAppend(u"A\n\u4E00"))
<< "Newline is not removed when before is Narrow.";
EXPECT_EQ(String(u"\u4E00\u4E00"), TestAppend(u"\u4E00\n", u"\u4E00"))
<< "Newline at the end of elements is removed when both sides are Wide.";
EXPECT_EQ(String(u"\u4E00\u4E00"), TestAppend(u"\u4E00", u"\n\u4E00"))
<< "Newline at the beginning of elements is removed "
"when both sides are Wide.";
}
#endif
TEST_F(NGInlineItemsBuilderTest, OpaqueToSpaceCollapsing) {
NGInlineItemsBuilder builder(GetLayoutBlockFlow(), &items_);
AppendText("Hello ", &builder);
builder.AppendOpaque(NGInlineItem::kBidiControl,
kFirstStrongIsolateCharacter);
AppendText(" ", &builder);
builder.AppendOpaque(NGInlineItem::kBidiControl,
kFirstStrongIsolateCharacter);
AppendText(" World", &builder);
EXPECT_EQ(String(u"Hello \u2068\u2068World"), builder.ToString());
}
TEST_F(NGInlineItemsBuilderTest, CollapseAroundReplacedElement) {
NGInlineItemsBuilder builder(GetLayoutBlockFlow(), &items_);
AppendText("Hello ", &builder);
AppendAtomicInline(&builder);
AppendText(" World", &builder);
EXPECT_EQ(String(u"Hello \uFFFC World"), builder.ToString());
}
TEST_F(NGInlineItemsBuilderTest, CollapseNewlineAfterObject) {
NGInlineItemsBuilder builder(GetLayoutBlockFlow(), &items_);
AppendAtomicInline(&builder);
AppendText("\n", &builder);
AppendAtomicInline(&builder);
EXPECT_EQ(String(u"\uFFFC \uFFFC"), builder.ToString());
EXPECT_EQ(3u, items_.size());
EXPECT_ITEM_OFFSET(items_[0], NGInlineItem::kAtomicInline, 0u, 1u);
EXPECT_ITEM_OFFSET(items_[1], NGInlineItem::kText, 1u, 2u);
EXPECT_ITEM_OFFSET(items_[2], NGInlineItem::kAtomicInline, 2u, 3u);
}
TEST_F(NGInlineItemsBuilderTest, AppendEmptyString) {
EXPECT_EQ("", TestAppend(""));
EXPECT_EQ(1u, items_.size());
EXPECT_ITEM_OFFSET(items_[0], NGInlineItem::kText, 0u, 0u);
}
TEST_F(NGInlineItemsBuilderTest, NewLines) {
SetWhiteSpace(EWhiteSpace::kPre);
EXPECT_EQ("apple\norange\ngrape\n", TestAppend("apple\norange\ngrape\n"));
EXPECT_EQ(6u, items_.size());
EXPECT_EQ(NGInlineItem::kText, items_[0].Type());
EXPECT_EQ(NGInlineItem::kControl, items_[1].Type());
EXPECT_EQ(NGInlineItem::kText, items_[2].Type());
EXPECT_EQ(NGInlineItem::kControl, items_[3].Type());
EXPECT_EQ(NGInlineItem::kText, items_[4].Type());
EXPECT_EQ(NGInlineItem::kControl, items_[5].Type());
}
TEST_F(NGInlineItemsBuilderTest, IgnorablePre) {
SetWhiteSpace(EWhiteSpace::kPre);
EXPECT_EQ(
"apple"
"\x0c"
"orange"
"\n"
"grape",
TestAppend("apple"
"\x0c"
"orange"
"\n"
"grape"));
EXPECT_EQ(5u, items_.size());
EXPECT_ITEM_OFFSET(items_[0], NGInlineItem::kText, 0u, 5u);
EXPECT_ITEM_OFFSET(items_[1], NGInlineItem::kControl, 5u, 6u);
EXPECT_ITEM_OFFSET(items_[2], NGInlineItem::kText, 6u, 12u);
EXPECT_ITEM_OFFSET(items_[3], NGInlineItem::kControl, 12u, 13u);
EXPECT_ITEM_OFFSET(items_[4], NGInlineItem::kText, 13u, 18u);
}
TEST_F(NGInlineItemsBuilderTest, Empty) {
Vector<NGInlineItem> items;
NGInlineItemsBuilder builder(GetLayoutBlockFlow(), &items);
scoped_refptr<ComputedStyle> block_style(ComputedStyle::Create());
builder.EnterBlock(block_style.get());
builder.ExitBlock();
EXPECT_EQ("", builder.ToString());
}
class CollapsibleSpaceTest : public NGInlineItemsBuilderTest,
public testing::WithParamInterface<UChar> {};
INSTANTIATE_TEST_SUITE_P(NGInlineItemsBuilderTest,
CollapsibleSpaceTest,
testing::Values(kSpaceCharacter,
kTabulationCharacter,
kNewlineCharacter));
TEST_P(CollapsibleSpaceTest, CollapsedSpaceAfterNoWrap) {
UChar space = GetParam();
EXPECT_EQ(
String("nowrap "
u"\u200B"
"wrap"),
TestAppend({String("nowrap") + space, EWhiteSpace::kNowrap}, {" wrap"}));
}
TEST_F(NGInlineItemsBuilderTest, GenerateBreakOpportunityAfterLeadingSpaces) {
EXPECT_EQ(String(" "
u"\u200B"
"a"),
TestAppend({{" a", EWhiteSpace::kPreWrap}}));
EXPECT_EQ(String(" "
u"\u200B"
"a"),
TestAppend({{" a", EWhiteSpace::kPreWrap}}));
EXPECT_EQ(String("a\n"
u" \u200B"),
TestAppend({{"a\n ", EWhiteSpace::kPreWrap}}));
}
TEST_F(NGInlineItemsBuilderTest, BidiBlockOverride) {
Vector<NGInlineItem> items;
NGInlineItemsBuilder builder(GetLayoutBlockFlow(), &items);
scoped_refptr<ComputedStyle> block_style(ComputedStyle::Create());
block_style->SetUnicodeBidi(UnicodeBidi::kBidiOverride);
block_style->SetDirection(TextDirection::kRtl);
builder.EnterBlock(block_style.get());
AppendText("Hello", &builder);
builder.ExitBlock();
// Expected control characters as defined in:
// https://drafts.csswg.org/css-writing-modes-3/#bidi-control-codes-injection-table
EXPECT_EQ(String(u"\u202E"
u"Hello"
u"\u202C"),
builder.ToString());
}
static LayoutInline* CreateLayoutInline(
Document* document,
void (*initialize_style)(ComputedStyle*)) {
scoped_refptr<ComputedStyle> style(ComputedStyle::Create());
initialize_style(style.get());
LayoutInline* const node = LayoutInline::CreateAnonymous(document);
node->SetModifiedStyleOutsideStyleRecalc(
std::move(style), LayoutObject::ApplyStyleChanges::kNo);
node->SetIsInLayoutNGInlineFormattingContext(true);
return node;
}
TEST_F(NGInlineItemsBuilderTest, BidiIsolate) {
Vector<NGInlineItem> items;
NGInlineItemsBuilder builder(GetLayoutBlockFlow(), &items);
AppendText("Hello ", &builder);
LayoutInline* const isolate_rtl =
CreateLayoutInline(&GetDocument(), [](ComputedStyle* style) {
style->SetUnicodeBidi(UnicodeBidi::kIsolate);
style->SetDirection(TextDirection::kRtl);
});
builder.EnterInline(isolate_rtl);
AppendText(u"\u05E2\u05D1\u05E8\u05D9\u05EA", &builder);
builder.ExitInline(isolate_rtl);
AppendText(" World", &builder);
// Expected control characters as defined in:
// https://drafts.csswg.org/css-writing-modes-3/#bidi-control-codes-injection-table
EXPECT_EQ(String(u"Hello "
u"\u2067"
u"\u05E2\u05D1\u05E8\u05D9\u05EA"
u"\u2069"
u" World"),
builder.ToString());
isolate_rtl->Destroy();
}
TEST_F(NGInlineItemsBuilderTest, BidiIsolateOverride) {
Vector<NGInlineItem> items;
NGInlineItemsBuilder builder(GetLayoutBlockFlow(), &items);
AppendText("Hello ", &builder);
LayoutInline* const isolate_override_rtl =
CreateLayoutInline(&GetDocument(), [](ComputedStyle* style) {
style->SetUnicodeBidi(UnicodeBidi::kIsolateOverride);
style->SetDirection(TextDirection::kRtl);
});
builder.EnterInline(isolate_override_rtl);
AppendText(u"\u05E2\u05D1\u05E8\u05D9\u05EA", &builder);
builder.ExitInline(isolate_override_rtl);
AppendText(" World", &builder);
// Expected control characters as defined in:
// https://drafts.csswg.org/css-writing-modes-3/#bidi-control-codes-injection-table
EXPECT_EQ(String(u"Hello "
u"\u2068\u202E"
u"\u05E2\u05D1\u05E8\u05D9\u05EA"
u"\u202C\u2069"
u" World"),
builder.ToString());
isolate_override_rtl->Destroy();
}
TEST_F(NGInlineItemsBuilderTest, HasRuby) {
Vector<NGInlineItem> items;
NGInlineItemsBuilder builder(GetLayoutBlockFlow(), &items);
EXPECT_FALSE(HasRuby(builder)) << "has_ruby_ should be false initially.";
AppendText("Hello ", &builder);
EXPECT_FALSE(HasRuby(builder))
<< "Adding non-AtomicInline should not affect it.";
AppendAtomicInline(&builder);
EXPECT_FALSE(HasRuby(builder))
<< "Adding non-ruby AtomicInline should not affect it.";
AppendRubyRun(&builder);
EXPECT_TRUE(HasRuby(builder))
<< "Adding a ruby AtomicInline should set it to true.";
AppendAtomicInline(&builder);
EXPECT_TRUE(HasRuby(builder))
<< "Adding non-ruby AtomicInline should not clear it.";
}
} // namespace blink