Commit e349a45e authored by Xiaocheng Hu's avatar Xiaocheng Hu Committed by Commit Bot

Make TextOffsetMapping not cross text control boundaries

This patch solves the issue of crossing text control boundaries when
using TextOffsetMapping. The changes include:

1. Change TextOffsetMapping::FindForward/BackwardInlineContents() not to
   cross text control boundaries:
   - If the initial position is in text control, keep traversal inside
     the text control
   - If the initial position is outside text control, all descendants of
     text control elements are skipped

2. Change flat tree TextIterator to respect the EntersTextControls flag,
   so that it doesn't dump the internal text of text controls elements
   to TextOffsetMapping, so that TextOffsetMapping won't give a result
   boundary inside text control.

Many test cases are also added to TextIterator, TextOffsetMapping and
word/sentence boundary algorithms.

Bug: 901492
Change-Id: I61aa3d3679eb77b975b50fd52dc8f79e049d8e47
Reviewed-on: https://chromium-review.googlesource.com/c/1327962
Commit-Queue: Xiaocheng Hu <xiaochengh@chromium.org>
Reviewed-by: default avatarKent Tamura <tkent@chromium.org>
Reviewed-by: default avatarYoichi Osato <yoichio@chromium.org>
Reviewed-by: default avatarYoshifumi Inoue <yosin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#607787}
parent 89209304
......@@ -60,6 +60,9 @@ class CORE_EXPORT FlatTreeTraversal {
static Node* Next(const Node&);
static Node* Next(const Node&, const Node* stay_within);
static Node* Previous(const Node&);
// Returns the previous of |node| in preorder. When |stay_within| is given,
// returns nullptr if the previous is not a descendant of |stay_within|.
static Node* Previous(const Node& node, const Node* stay_within);
static Node* FirstChild(const Node&);
static Node* LastChild(const Node&);
......@@ -282,6 +285,17 @@ inline Node* FlatTreeTraversal::TraversePrevious(const Node& node) {
return TraverseParent(node);
}
inline Node* FlatTreeTraversal::Previous(const Node& node,
const Node* stay_within) {
if (!stay_within)
return Previous(node);
DCHECK(IsDescendantOf(node, *stay_within));
Node* previous = Previous(node);
if (previous == stay_within)
return nullptr;
return previous;
}
inline Node* FlatTreeTraversal::FirstChild(const Node& node) {
AssertPrecondition(node);
Node* result = TraverseChild(node, kTraversalDirectionForward);
......
......@@ -74,7 +74,6 @@ TextIteratorBehavior AdjustBehaviorFlags<EditingInFlatTreeStrategy>(
.SetExcludeAutofilledValue(behavior.ForSelectionToString() ||
behavior.ExcludeAutofilledValue())
.SetEntersOpenShadowRoots(false)
.SetEntersTextControls(false)
.Build();
}
......@@ -165,6 +164,19 @@ bool IsRenderedAsTable(const Node* node) {
return layout_object && layout_object->IsTable();
}
bool ShouldHandleChildren(const Node& node,
const TextIteratorBehavior& behavior) {
// To support |TextIteratorEmitsImageAltText|, we don't traversal child
// nodes, in flat tree.
if (IsHTMLImageElement(node))
return false;
// Traverse internals of text control elements in flat tree only when
// |EntersTextControls| flag is set.
if (!behavior.EntersTextControls() && IsTextControl(node))
return false;
return true;
}
} // namespace
template <typename Strategy>
......@@ -307,7 +319,8 @@ void TextIteratorAlgorithm<Strategy>::Advance() {
} else {
// Enter author shadow roots, from youngest, if any and if necessary.
if (iteration_progress_ < kHandledOpenShadowRoots) {
if (EntersOpenShadowRoots() && node_->IsElementNode() &&
if (std::is_same<Strategy, EditingStrategy>::value &&
EntersOpenShadowRoots() && node_->IsElementNode() &&
ToElement(node_)->OpenShadowRoot()) {
ShadowRoot* youngest_shadow_root = ToElement(node_)->OpenShadowRoot();
DCHECK(youngest_shadow_root->GetType() == ShadowRootType::V0 ||
......@@ -324,7 +337,8 @@ void TextIteratorAlgorithm<Strategy>::Advance() {
// Enter user-agent shadow root, if necessary.
if (iteration_progress_ < kHandledUserAgentShadowRoot) {
if (EntersTextControls() && layout_object->IsTextControl()) {
if (std::is_same<Strategy, EditingStrategy>::value &&
EntersTextControls() && layout_object->IsTextControl()) {
ShadowRoot* user_agent_shadow_root =
ToElement(node_)->UserAgentShadowRoot();
DCHECK(user_agent_shadow_root->IsUserAgent());
......@@ -369,12 +383,10 @@ void TextIteratorAlgorithm<Strategy>::Advance() {
// calling exitNode() as we come back thru a parent node.
//
// 1. Iterate over child nodes, if we haven't done yet.
// To support |TextIteratorEmitsImageAltText|, we don't traversal child
// nodes, in flat tree.
Node* next =
iteration_progress_ < kHandledChildren && !IsHTMLImageElement(*node_)
? Strategy::FirstChild(*node_)
: nullptr;
Node* next = iteration_progress_ < kHandledChildren &&
ShouldHandleChildren(*node_, behavior_)
? Strategy::FirstChild(*node_)
: nullptr;
if (!next) {
// 2. If we've already iterated children or they are not available, go to
// the next sibling node.
......
......@@ -69,6 +69,12 @@ TextIteratorBehavior EmitsSmallXForTextSecurityBehavior() {
.Build();
}
TextIteratorBehavior EmitsCharactersBetweenAllVisiblePositionsBehavior() {
return TextIteratorBehavior::Builder()
.SetEmitsCharactersBetweenAllVisiblePositions(true)
.Build();
}
struct DOMTree : NodeTraversal {
using PositionType = Position;
using TextIteratorType = TextIterator;
......@@ -189,8 +195,7 @@ TEST_F(TextIteratorTest, IgnoreAltTextInTextControls) {
static const char* input = "<p>Hello <input type='text' value='value'>!</p>";
SetBodyContent(input);
EXPECT_EQ("[Hello ][][!]", Iterate<DOMTree>(EmitsImageAltTextBehavior()));
EXPECT_EQ("[Hello ][][\n][value][\n][!]",
Iterate<FlatTree>(EmitsImageAltTextBehavior()));
EXPECT_EQ("[Hello ][][!]", Iterate<FlatTree>(EmitsImageAltTextBehavior()));
}
TEST_F(TextIteratorTest, DisplayAltTextInImageControls) {
......@@ -204,7 +209,7 @@ TEST_F(TextIteratorTest, NotEnteringTextControls) {
static const char* input = "<p>Hello <input type='text' value='input'>!</p>";
SetBodyContent(input);
EXPECT_EQ("[Hello ][][!]", Iterate<DOMTree>());
EXPECT_EQ("[Hello ][][\n][input][\n][!]", Iterate<FlatTree>());
EXPECT_EQ("[Hello ][][!]", Iterate<FlatTree>());
}
TEST_F(TextIteratorTest, EnteringTextControlsWithOption) {
......@@ -212,7 +217,7 @@ TEST_F(TextIteratorTest, EnteringTextControlsWithOption) {
SetBodyContent(input);
EXPECT_EQ("[Hello ][\n][input][!]",
Iterate<DOMTree>(EntersTextControlsBehavior()));
EXPECT_EQ("[Hello ][][\n][input][\n][!]",
EXPECT_EQ("[Hello ][\n][input][\n][!]",
Iterate<FlatTree>(EntersTextControlsBehavior()));
}
......@@ -224,10 +229,8 @@ TEST_F(TextIteratorTest, EnteringTextControlsWithOptionComplex) {
SetBodyContent(input);
EXPECT_EQ("[\n][Beginning of range][\n][Under DOM nodes][\n][End of range]",
Iterate<DOMTree>(EntersTextControlsBehavior()));
EXPECT_EQ(
"[][\n][Beginning of range][\n][][\n][Under DOM nodes][\n][][\n][End of "
"range]",
Iterate<FlatTree>(EntersTextControlsBehavior()));
EXPECT_EQ("[Beginning of range][\n][Under DOM nodes][\n][End of range]",
Iterate<FlatTree>(EntersTextControlsBehavior()));
}
TEST_F(TextIteratorTest, NotEnteringShadowTree) {
......@@ -545,6 +548,7 @@ TEST_F(TextIteratorTest, WhitespaceCollapseForReplacedElements) {
SetBodyContent(body_content);
EXPECT_EQ("[Some text ][][Some more text]",
Iterate<DOMTree>(CollapseTrailingSpaceBehavior()));
// <input type=button> is not text control element
EXPECT_EQ("[Some text ][][Button text][Some more text]",
Iterate<FlatTree>(CollapseTrailingSpaceBehavior()));
}
......@@ -1143,5 +1147,14 @@ TEST_P(ParameterizedTextIteratorTest, HiddenFirstLetterInPre) {
EXPECT_EQ("[oo]", Iterate<DOMTree>());
}
TEST_P(ParameterizedTextIteratorTest, TextOffsetMappingAndFlatTree) {
// Tests that TextOffsetMapping should skip text control even though it runs
// on flat tree.
SetBodyContent("foo <input value='bla bla. bla bla.'> bar");
EXPECT_EQ(
"[foo ][,][ bar]",
Iterate<FlatTree>(EmitsCharactersBetweenAllVisiblePositionsBehavior()));
}
} // namespace text_iterator_test
} // namespace blink
......@@ -10,6 +10,7 @@
#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
#include "third_party/blink/renderer/core/layout/layout_block_flow.h"
namespace blink {
......@@ -233,11 +234,11 @@ TextOffsetMapping::ForwardRange TextOffsetMapping::ForwardRangeOf(
}
// static
TextOffsetMapping::InlineContents TextOffsetMapping::FindBackwardInlineContents(
const PositionInFlatTree& position) {
// TODO(xiaochengh): Make this function not cross text control boundaries.
for (const Node* node = position.NodeAsRangeLastNode(); node;
node = FlatTreeTraversal::Previous(*node)) {
template <typename Traverser>
TextOffsetMapping::InlineContents TextOffsetMapping::FindInlineContentsInternal(
const Node* start_node,
Traverser traverser) {
for (const Node* node = start_node; node; node = traverser(*node)) {
const InlineContents inline_contents = ComputeInlineContentsFromNode(*node);
if (inline_contents.IsNotNull())
return inline_contents;
......@@ -245,19 +246,79 @@ TextOffsetMapping::InlineContents TextOffsetMapping::FindBackwardInlineContents(
return InlineContents();
}
// static
TextOffsetMapping::InlineContents TextOffsetMapping::FindBackwardInlineContents(
const PositionInFlatTree& position) {
const Node* previous_node = position.NodeAsRangeLastNode();
if (!previous_node)
return InlineContents();
if (const TextControlElement* enclosing_text_control =
EnclosingTextControl(position)) {
if (!FlatTreeTraversal::IsDescendantOf(*previous_node,
*enclosing_text_control)) {
// The first position in a text control reaches here.
return InlineContents();
}
return TextOffsetMapping::FindInlineContentsInternal(
previous_node, [enclosing_text_control](const Node& node) {
return FlatTreeTraversal::Previous(node, enclosing_text_control);
});
}
auto previous_skipping_text_control = [](const Node& node) -> const Node* {
DCHECK(!EnclosingTextControl(&node));
const Node* previous = FlatTreeTraversal::Previous(node);
const TextControlElement* previous_text_control =
EnclosingTextControl(previous);
if (!previous_text_control)
return previous;
return previous_text_control;
};
if (const TextControlElement* last_enclosing_text_control =
EnclosingTextControl(previous_node)) {
// Example, <input value=foo><span>bar</span>, span@beforeAnchor
return TextOffsetMapping::FindInlineContentsInternal(
last_enclosing_text_control, previous_skipping_text_control);
}
return TextOffsetMapping::FindInlineContentsInternal(
previous_node, previous_skipping_text_control);
}
// static
// Note: "doubleclick-whitespace-img-crash.html" call |NextWordPosition())
// with AfterNode(IMG) for <body><img></body>
TextOffsetMapping::InlineContents TextOffsetMapping::FindForwardInlineContents(
const PositionInFlatTree& position) {
// TODO(xiaochengh): Make this function not cross text control boundaries.
for (const Node* node = position.NodeAsRangeFirstNode(); node;
node = FlatTreeTraversal::Next(*node)) {
const InlineContents inline_contents = ComputeInlineContentsFromNode(*node);
if (inline_contents.IsNotNull())
return inline_contents;
const Node* next_node = position.NodeAsRangeFirstNode();
if (!next_node)
return InlineContents();
if (const TextControlElement* enclosing_text_control =
EnclosingTextControl(position)) {
if (!FlatTreeTraversal::IsDescendantOf(*next_node,
*enclosing_text_control)) {
// The last position in a text control reaches here.
return InlineContents();
}
return TextOffsetMapping::FindInlineContentsInternal(
next_node, [enclosing_text_control](const Node& node) {
return FlatTreeTraversal::Next(node, enclosing_text_control);
});
}
return InlineContents();
auto next_skipping_text_control = [](const Node& node) {
DCHECK(!EnclosingTextControl(&node));
if (IsTextControl(node))
return FlatTreeTraversal::NextSkippingChildren(node);
return FlatTreeTraversal::Next(node);
};
DCHECK(!EnclosingTextControl(next_node));
return TextOffsetMapping::FindInlineContentsInternal(
next_node, next_skipping_text_control);
}
// ----
......
......@@ -204,6 +204,9 @@ class CORE_EXPORT TextOffsetMapping final {
private:
TextOffsetMapping(const InlineContents&, const TextIteratorBehavior&);
template <typename Traverser>
static InlineContents FindInlineContentsInternal(const Node*, Traverser);
const TextIteratorBehavior behavior_;
const EphemeralRangeInFlatTree range_;
const String text16_;
......
......@@ -40,7 +40,11 @@ class ParameterizedTextOffsetMappingTest
std::string GetRange(const std::string& selection_text) {
const PositionInFlatTree position =
ToPositionInFlatTree(SetSelectionTextToBody(selection_text).Base());
TextOffsetMapping mapping(GetInlineContents(position));
return GetRange(GetInlineContents(position));
}
std::string GetRange(const TextOffsetMapping::InlineContents& contents) {
TextOffsetMapping mapping(contents);
return GetSelectionTextInFlatTreeFromBody(
SelectionInFlatTree::Builder()
.SetBaseAndExtent(mapping.GetRange())
......@@ -244,6 +248,62 @@ TEST_P(ParameterizedTextOffsetMappingTest,
EXPECT_TRUE(previous_contents.IsNull());
}
TEST_P(ParameterizedTextOffsetMappingTest, ForwardRangesWithTextControl) {
// InlineContents for positions outside text control should cover the entire
// containing block.
const PositionInFlatTree outside_position = ToPositionInFlatTree(
SetCaretTextToBody("foo<!--|--><input value=\"bla\">bar"));
const TextOffsetMapping::InlineContents outside_contents =
TextOffsetMapping::FindForwardInlineContents(outside_position);
EXPECT_EQ("^foo<input value=\"bla\"><div>bla</div></input>bar|",
GetRange(outside_contents));
// InlineContents for positions inside text control should not escape the text
// control in forward iteration.
const Element* input = GetDocument().QuerySelector("input");
const PositionInFlatTree inside_first =
PositionInFlatTree::FirstPositionInNode(*input);
const TextOffsetMapping::InlineContents inside_contents =
TextOffsetMapping::FindForwardInlineContents(inside_first);
EXPECT_EQ("foo<input value=\"bla\"><div>^bla|</div></input>bar",
GetRange(inside_contents));
EXPECT_TRUE(
TextOffsetMapping::InlineContents::NextOf(inside_contents).IsNull());
const PositionInFlatTree inside_last =
PositionInFlatTree::LastPositionInNode(*input);
EXPECT_TRUE(
TextOffsetMapping::FindForwardInlineContents(inside_last).IsNull());
}
TEST_P(ParameterizedTextOffsetMappingTest, BackwardRangesWithTextControl) {
// InlineContents for positions outside text control should cover the entire
// containing block.
const PositionInFlatTree outside_position = ToPositionInFlatTree(
SetCaretTextToBody("foo<input value=\"bla\"><!--|-->bar"));
const TextOffsetMapping::InlineContents outside_contents =
TextOffsetMapping::FindBackwardInlineContents(outside_position);
EXPECT_EQ("^foo<input value=\"bla\"><div>bla</div></input>bar|",
GetRange(outside_contents));
// InlineContents for positions inside text control should not escape the text
// control in backward iteration.
const Element* input = GetDocument().QuerySelector("input");
const PositionInFlatTree inside_last =
PositionInFlatTree::LastPositionInNode(*input);
const TextOffsetMapping::InlineContents inside_contents =
TextOffsetMapping::FindBackwardInlineContents(inside_last);
EXPECT_EQ("foo<input value=\"bla\"><div>^bla|</div></input>bar",
GetRange(inside_contents));
EXPECT_TRUE(
TextOffsetMapping::InlineContents::PreviousOf(inside_contents).IsNull());
const PositionInFlatTree inside_first =
PositionInFlatTree::FirstPositionInNode(*input);
EXPECT_TRUE(
TextOffsetMapping::FindBackwardInlineContents(inside_first).IsNull());
}
// http://crbug.com/832497
TEST_P(ParameterizedTextOffsetMappingTest, RangeWithCollapsedWhitespace) {
// Whitespaces after <div> is collapsed.
......
......@@ -9,6 +9,7 @@
#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
#include "third_party/blink/renderer/core/editing/visible_position.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
#include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
......@@ -174,4 +175,26 @@ TEST_F(VisibleUnitsSentenceTest, startOfSentence) {
.DeepEquivalent());
}
TEST_F(VisibleUnitsSentenceTest, SentenceBoundarySkipTextControl) {
SetBodyContent("foo <input value=\"xx. xx.\"> bar.");
const Node* foo = GetDocument().QuerySelector("input")->previousSibling();
const Node* bar = GetDocument().QuerySelector("input")->nextSibling();
EXPECT_EQ(Position(bar, 5), EndOfSentence(Position(foo, 1)).GetPosition());
EXPECT_EQ(PositionInFlatTree(bar, 5),
EndOfSentence(PositionInFlatTree(foo, 1)).GetPosition());
EXPECT_EQ(Position(foo, 0),
StartOfSentence(CreateVisiblePosition(Position(bar, 3)))
.DeepEquivalent());
// TODO(xiaochengh): Should be "foo"@0
const Node* xx = ToHTMLInputElement(GetDocument().QuerySelector("input"))
->InnerEditorElement()
->firstChild();
EXPECT_EQ(PositionInFlatTree(xx, 0),
StartOfSentence(CreateVisiblePosition(PositionInFlatTree(bar, 3)))
.DeepEquivalent());
}
} // namespace blink
......@@ -241,6 +241,54 @@ TEST_F(VisibleUnitsWordTest, StartOfWordTextSecurity) {
EXPECT_EQ("|abc<s>foo bar</s>baz", DoStartOfWord("abc<s>foo bar</s>b|az"));
}
TEST_F(VisibleUnitsWordTest, StartOfWordTextControl) {
// TODO(xiaochengh): <input> should be treated as word boundary
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("|foo<input value=\"bla\">bar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("f|oo<input value=\"bla\">bar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("fo|o<input value=\"bla\">bar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("foo|<input value=\"bla\">bar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("foo<input value=\"bla\">|bar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("foo<input value=\"bla\">b|ar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("foo<input value=\"bla\">ba|r"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoStartOfWord("foo<input value=\"bla\">bar|"));
}
TEST_F(VisibleUnitsWordTest, StartOfWordPreviousWordIfOnBoundaryTextControl) {
// TODO(xiaochengh): <input> should be treated as word boundary
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("|foo<input value=\"bla\">bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("f|oo<input value=\"bla\">bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("fo|o<input value=\"bla\">bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("foo|<input value=\"bla\">bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("foo<input value=\"bla\">|bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("foo<input value=\"bla\">b|ar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("foo<input value=\"bla\">ba|r",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoStartOfWord("foo<input value=\"bla\">bar|",
EWordSide::kPreviousWordIfOnBoundary));
}
TEST_P(ParameterizedVisibleUnitsWordTest, EndOfWordBasic) {
EXPECT_EQ("<p> (|1) abc def</p>", DoEndOfWord("<p>| (1) abc def</p>"));
EXPECT_EQ("<p> (|1) abc def</p>", DoEndOfWord("<p> |(1) abc def</p>"));
......@@ -383,6 +431,53 @@ TEST_P(ParameterizedVisibleUnitsWordTest, EndOfWordTextSecurity) {
EXPECT_EQ("abc<s>foo bar</s>baz|", DoEndOfWord("abc<s>foo bar</s>b|az"));
}
TEST_P(ParameterizedVisibleUnitsWordTest, EndOfWordTextControl) {
EXPECT_EQ("foo|<input value=\"bla\">bar",
DoEndOfWord("|foo<input value=\"bla\">bar"));
EXPECT_EQ("foo|<input value=\"bla\">bar",
DoEndOfWord("f|oo<input value=\"bla\">bar"));
EXPECT_EQ("foo|<input value=\"bla\">bar",
DoEndOfWord("fo|o<input value=\"bla\">bar"));
EXPECT_EQ("foo<input value=\"bla\">|bar",
DoEndOfWord("foo|<input value=\"bla\">bar"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoEndOfWord("foo<input value=\"bla\">|bar"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoEndOfWord("foo<input value=\"bla\">b|ar"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoEndOfWord("foo<input value=\"bla\">ba|r"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoEndOfWord("foo<input value=\"bla\">bar|"));
}
TEST_P(ParameterizedVisibleUnitsWordTest,
EndOfWordPreviousWordIfOnBoundaryTextControl) {
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoEndOfWord("|foo<input value=\"bla\">bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("foo|<input value=\"bla\">bar",
DoEndOfWord("f|oo<input value=\"bla\">bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("foo|<input value=\"bla\">bar",
DoEndOfWord("fo|o<input value=\"bla\">bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("foo|<input value=\"bla\">bar",
DoEndOfWord("foo|<input value=\"bla\">bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("foo<input value=\"bla\">|bar",
DoEndOfWord("foo<input value=\"bla\">|bar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoEndOfWord("foo<input value=\"bla\">b|ar",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoEndOfWord("foo<input value=\"bla\">ba|r",
EWordSide::kPreviousWordIfOnBoundary));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoEndOfWord("foo<input value=\"bla\">bar|",
EWordSide::kPreviousWordIfOnBoundary));
}
TEST_P(ParameterizedVisibleUnitsWordTest, NextWordBasic) {
EXPECT_EQ("<p> (1|) abc def</p>", DoNextWord("<p>| (1) abc def</p>"));
EXPECT_EQ("<p> (1|) abc def</p>", DoNextWord("<p> |(1) abc def</p>"));
......@@ -449,6 +544,25 @@ TEST_P(ParameterizedVisibleUnitsWordTest, NextWordSkipTab) {
EXPECT_EQ("<p><s>\t</s>foo|</p>", DoNextWord("<p><s>\t|</s>foo</p>"));
}
TEST_P(ParameterizedVisibleUnitsWordTest, NextWordSkipTextControl) {
EXPECT_EQ("foo|<input value=\"bla\">bar",
DoNextWord("|foo<input value=\"bla\">bar"));
EXPECT_EQ("foo|<input value=\"bla\">bar",
DoNextWord("f|oo<input value=\"bla\">bar"));
EXPECT_EQ("foo|<input value=\"bla\">bar",
DoNextWord("fo|o<input value=\"bla\">bar"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoNextWord("foo|<input value=\"bla\">bar"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoNextWord("foo<input value=\"bla\">|bar"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoNextWord("foo<input value=\"bla\">b|ar"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoNextWord("foo<input value=\"bla\">ba|r"));
EXPECT_EQ("foo<input value=\"bla\">bar|",
DoNextWord("foo<input value=\"bla\">bar|"));
}
//----
TEST_P(ParameterizedVisibleUnitsWordTest, PreviousWordBasic) {
......@@ -468,4 +582,23 @@ TEST_P(ParameterizedVisibleUnitsWordTest, PreviousWordBasic) {
EXPECT_EQ("<p> (1) abc |def</p>", DoPreviousWord("<p> (1) abc def</p>|"));
}
TEST_P(ParameterizedVisibleUnitsWordTest, PreviousWordSkipTextControl) {
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoPreviousWord("|foo<input value=\"bla\">bar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoPreviousWord("f|oo<input value=\"bla\">bar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoPreviousWord("fo|o<input value=\"bla\">bar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoPreviousWord("foo|<input value=\"bla\">bar"));
EXPECT_EQ("|foo<input value=\"bla\">bar",
DoPreviousWord("foo<input value=\"bla\">|bar"));
EXPECT_EQ("foo<input value=\"bla\">|bar",
DoPreviousWord("foo<input value=\"bla\">b|ar"));
EXPECT_EQ("foo<input value=\"bla\">|bar",
DoPreviousWord("foo<input value=\"bla\">ba|r"));
EXPECT_EQ("foo<input value=\"bla\">|bar",
DoPreviousWord("foo<input value=\"bla\">bar|"));
}
} // namespace blink
......@@ -916,6 +916,15 @@ TextControlElement* EnclosingTextControl(const Position& position) {
return EnclosingTextControl(position.ComputeContainerNode());
}
TextControlElement* EnclosingTextControl(const PositionInFlatTree& position) {
Node* container = position.ComputeContainerNode();
if (IsTextControl(container)) {
// For example, #inner-editor@beforeAnchor reaches here.
return ToTextControl(container);
}
return EnclosingTextControl(container);
}
TextControlElement* EnclosingTextControl(const Node* container) {
if (!container)
return nullptr;
......
......@@ -264,6 +264,7 @@ DEFINE_TEXT_CONTROL_CASTS(const TextControlElement, const Node);
#undef DEFINE_TEXT_CONTROL_CASTS
TextControlElement* EnclosingTextControl(const Position&);
TextControlElement* EnclosingTextControl(const PositionInFlatTree&);
TextControlElement* EnclosingTextControl(const Node*);
} // namespace blink
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment