Commit f48a33c5 authored by Gayane Petrosyan's avatar Gayane Petrosyan Committed by Commit Bot

[SH-Blink] Add FindUniqueMatch and basic use case

Adding FindUniqueMatch function that checks if provided selector has a
unique match. Additionally adds a basic use case when selector is an
exact text without context.

Bug: 1102382
Change-Id: Ifd105517d4ce4af36e7cd500e17ff0ca28a8d0c8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2283244
Commit-Queue: Gayane Petrosyan <gayane@chromium.org>
Reviewed-by: default avatarDavid Bokan <bokan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#791518}
parent e130edd4
......@@ -1310,6 +1310,7 @@ jumbo_source_set("unit_tests") {
"page/scrolling/snap_coordinator_test.cc",
"page/scrolling/text_fragment_anchor_metrics_test.cc",
"page/scrolling/text_fragment_anchor_test.cc",
"page/scrolling/text_fragment_selector_generator_test.cc",
"page/scrolling/text_fragment_selector_test.cc",
"page/slot_scoped_traversal_test.cc",
"page/spatial_navigation_test.cc",
......
......@@ -65,8 +65,12 @@ Node* GetNonSearchableAncestor(const Node& node) {
return nullptr;
}
// TODO(gayane): Consider using |ComputedStyle::IsDisplayInlineType| or
// |ElementInnerTextCollector::IsDisplayBlockLevel|. See
// http://crrev.com/c/2283244/10/third_party/blink/renderer/core/editing/finder/find_buffer.cc#69
// for context.
// Returns true if the given |display| is considered a 'block'
bool IsBlock(EDisplay display) {
bool IsBlockLevel(EDisplay display) {
return display == EDisplay::kBlock || display == EDisplay::kTable ||
display == EDisplay::kFlowRoot || display == EDisplay::kGrid ||
display == EDisplay::kFlex || display == EDisplay::kListItem;
......@@ -104,20 +108,6 @@ Node* GetVisibleTextNode(Node& start_node) {
return nullptr;
}
// Returns the closest ancestor of |start_node| (including the node itself) that
// is a block.
Node& GetLowestDisplayBlockInclusiveAncestor(const Node& start_node) {
// Gets lowest inclusive ancestor that has block display value.
// <div id=outer>a<div id=inner>b</div>c</div>
// If we run this on "a" or "c" text node in we will get the outer div.
// If we run it on the "b" text node we will get the inner div.
for (Node& ancestor : FlatTreeTraversal::InclusiveAncestorsOf(start_node)) {
const ComputedStyle* style = ancestor.EnsureComputedStyle();
if (style && !ancestor.IsTextNode() && IsBlock(style->Display()))
return ancestor;
}
return *start_node.GetDocument().documentElement();
}
} // namespace
// FindBuffer implementation.
......@@ -192,6 +182,19 @@ EphemeralRangeInFlatTree FindBuffer::FindMatchInRange(
return last_match_range;
}
Node& FindBuffer::GetFirstBlockLevelAncestorInclusive(const Node& start_node) {
// Gets lowest inclusive ancestor that has block display value.
// <div id=outer>a<div id=inner>b</div>c</div>
// If we run this on "a" or "c" text node in we will get the outer div.
// If we run it on the "b" text node we will get the inner div.
for (Node& ancestor : FlatTreeTraversal::InclusiveAncestorsOf(start_node)) {
const ComputedStyle* style = ancestor.EnsureComputedStyle();
if (style && !ancestor.IsTextNode() && IsBlockLevel(style->Display()))
return ancestor;
}
return *start_node.GetDocument().documentElement();
}
FindBuffer::Results FindBuffer::FindMatches(const WebString& search_text,
const blink::FindOptions options) {
// We should return empty result if it's impossible to get a match (buffer is
......@@ -224,7 +227,7 @@ void FindBuffer::CollectTextUntilBlockBoundary(
if (!node || !node->isConnected())
return;
Node& block_ancestor = GetLowestDisplayBlockInclusiveAncestor(*node);
Node& block_ancestor = GetFirstBlockLevelAncestorInclusive(*node);
const Node* just_after_block = FlatTreeTraversal::Next(
FlatTreeTraversal::LastWithinOrSelf(block_ancestor));
const LayoutBlockFlow* last_block_flow = nullptr;
......@@ -274,7 +277,7 @@ void FindBuffer::CollectTextUntilBlockBoundary(
// This node is in its own sub-block separate from our starting position.
const auto* text_node = DynamicTo<Text>(node);
if (first_traversed_node != node && !text_node &&
IsBlock(style->Display())) {
IsBlockLevel(style->Display())) {
break;
}
......
......@@ -31,6 +31,10 @@ class CORE_EXPORT FindBuffer {
String search_text,
const FindOptions);
// Returns the closest ancestor of |start_node| (including the node itself)
// that is block level.
static Node& GetFirstBlockLevelAncestorInclusive(const Node& start_node);
// A match result, containing the starting position of the match and
// the length of the match.
struct BufferMatchResult {
......
......@@ -95,6 +95,8 @@ blink_core_sources("page") {
"scrolling/text_fragment_finder.h",
"scrolling/text_fragment_selector.cc",
"scrolling/text_fragment_selector.h",
"scrolling/text_fragment_selector_generator.cc",
"scrolling/text_fragment_selector_generator.h",
"scrolling/top_document_root_scroller_controller.cc",
"scrolling/top_document_root_scroller_controller.h",
"scrolling/viewport_scroll_callback.cc",
......
......@@ -282,10 +282,14 @@ void TextFragmentAnchor::Trace(Visitor* visitor) const {
void TextFragmentAnchor::DidFindMatch(
const EphemeralRangeInFlatTree& range,
const TextFragmentAnchorMetrics::Match match_metrics) {
const TextFragmentAnchorMetrics::Match match_metrics,
bool is_unique) {
if (search_finished_)
return;
if (!is_unique)
metrics_->DidFindAmbiguousMatch();
// TODO(nburris): Determine what we should do with overlapping text matches.
// This implementation drops a match if it overlaps a previous match, since
// overlapping ranges are likely unintentional by the URL creator and could
......@@ -395,10 +399,6 @@ void TextFragmentAnchor::DidFindMatch(
range.StartPosition().NodeAsRangeFirstNode());
}
void TextFragmentAnchor::DidFindAmbiguousMatch() {
metrics_->DidFindAmbiguousMatch();
}
void TextFragmentAnchor::DidFinishSearch() {
DCHECK(!search_finished_);
search_finished_ = true;
......
......@@ -61,10 +61,9 @@ class CORE_EXPORT TextFragmentAnchor final : public FragmentAnchor,
void Trace(Visitor*) const override;
// TextFragmentFinder::Client interface
void DidFindMatch(
const EphemeralRangeInFlatTree& range,
const TextFragmentAnchorMetrics::Match match_metrics) override;
void DidFindAmbiguousMatch() override;
void DidFindMatch(const EphemeralRangeInFlatTree& range,
const TextFragmentAnchorMetrics::Match match_metrics,
bool is_unique) override;
private:
// Called when the search is finished. Reports metrics and activates the
......
......@@ -234,15 +234,12 @@ void TextFragmentFinder::FindMatch(Document& document) {
match_metrics.text = PlainText(match);
}
client_.DidFindMatch(match, match_metrics);
// Continue searching to see if we have an ambiguous selector.
// TODO(crbug.com/919204): This is temporary and only for measuring
// ambiguous matching during prototyping.
EphemeralRangeInFlatTree ambiguous_match =
FindMatchFromPosition(document, match.EndPosition());
if (ambiguous_match.IsNotNull())
client_.DidFindAmbiguousMatch();
client_.DidFindMatch(match, match_metrics, ambiguous_match.IsNull());
}
}
......
......@@ -24,8 +24,8 @@ class CORE_EXPORT TextFragmentFinder final {
public:
virtual void DidFindMatch(
const EphemeralRangeInFlatTree& range,
const TextFragmentAnchorMetrics::Match match_metrics) = 0;
virtual void DidFindAmbiguousMatch() = 0;
const TextFragmentAnchorMetrics::Match match_metrics,
bool is_unique) = 0;
};
// Client must outlive the finder.
......
// Copyright 2020 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/page/scrolling/text_fragment_selector_generator.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/finder/find_buffer.h"
#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_finder.h"
namespace blink {
constexpr int kExactTextMaxChars = 300;
constexpr int kNoContextMinChars = 20;
TextFragmentSelectorGenerator::TextFragmentSelectorGenerator(
LocalFrame* frame,
base::OnceCallback<void(const TextFragmentSelector&)> callback)
: frame_(frame), callback_(std::move(callback)) {}
// TextFragmentSelectorGenerator is responsible for generating text fragment
// selectors that have a unique match.
void TextFragmentSelectorGenerator::GenerateSelector(
const EphemeralRangeInFlatTree& selection_range) {
const TextFragmentSelector kInvalidSelector(
TextFragmentSelector::SelectorType::kInvalid);
Node& start_first_block_ancestor =
FindBuffer::GetFirstBlockLevelAncestorInclusive(
*selection_range.StartPosition().AnchorNode());
Node& end_first_block_ancestor =
FindBuffer::GetFirstBlockLevelAncestorInclusive(
*selection_range.EndPosition().AnchorNode());
if (!start_first_block_ancestor.isSameNode(&end_first_block_ancestor))
std::move(callback_).Run(kInvalidSelector);
// TODO(gayane): If same node, need to check if start and end are interrupted
// by a block. Example: <div>start of the selection <div> sub block </div>end
// of the selection</div>.
// TODO(gayane): Move selection start and end to contain full words.
String selected_text = PlainText(selection_range);
if (selected_text.length() < kNoContextMinChars ||
selected_text.length() > kExactTextMaxChars)
std::move(callback_).Run(kInvalidSelector);
selector_ = std::make_unique<TextFragmentSelector>(
TextFragmentSelector::SelectorType::kExact, selected_text, "", "", "");
TextFragmentFinder finder(*this, *selector_);
finder.FindMatch(*frame_->GetDocument());
}
void TextFragmentSelectorGenerator::DidFindMatch(
const EphemeralRangeInFlatTree& match,
const TextFragmentAnchorMetrics::Match match_metrics,
bool is_unique) {
if (is_unique) {
std::move(callback_).Run(*selector_);
} else {
// TODO(gayane): Should add more range and/or context.
std::move(callback_).Run(
TextFragmentSelector(TextFragmentSelector::SelectorType::kInvalid));
}
}
void TextFragmentSelectorGenerator::Trace(Visitor* visitor) const {
visitor->Trace(frame_);
}
} // namespace blink
// Copyright 2020 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.
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_PAGE_SCROLLING_TEXT_FRAGMENT_SELECTOR_GENERATOR_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_PAGE_SCROLLING_TEXT_FRAGMENT_SELECTOR_GENERATOR_H_
#include "third_party/blink/renderer/core/editing/forward.h"
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_finder.h"
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_selector.h"
namespace blink {
class LocalFrame;
class CORE_EXPORT TextFragmentSelectorGenerator final
: public GarbageCollected<TextFragmentSelectorGenerator>,
public TextFragmentFinder::Client {
public:
TextFragmentSelectorGenerator(
LocalFrame* frame,
base::OnceCallback<void(const TextFragmentSelector&)> callback);
void GenerateSelector(const EphemeralRangeInFlatTree& selection_range);
// TextFragmentFinder::Client interface
void DidFindMatch(const EphemeralRangeInFlatTree& match,
const TextFragmentAnchorMetrics::Match match_metrics,
bool is_unique) override;
void Trace(Visitor*) const;
private:
Member<LocalFrame> frame_;
base::OnceCallback<void(const TextFragmentSelector&)> callback_;
std::unique_ptr<TextFragmentSelector> selector_;
DISALLOW_COPY_AND_ASSIGN(TextFragmentSelectorGenerator);
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_PAGE_SCROLLING_TEXT_FRAGMENT_SELECTOR_GENERATOR_H_
// Copyright 2020 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/page/scrolling/text_fragment_selector_generator.h"
#include <gtest/gtest.h>
#include "base/test/bind_test_util.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
namespace blink {
class TextFragmentSelectorGeneratorTest : public SimTest {
public:
void SetUp() override {
SimTest::SetUp();
WebView().MainFrameWidget()->Resize(WebSize(800, 600));
}
};
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph text that is longer than 20 chars</p>
<p id='second'>Second paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 0);
const auto& selected_end = Position(first_paragraph, 28);
bool callback_called = false;
base::OnceCallback<void(const TextFragmentSelector&)> callback =
base::BindLambdaForTesting([&](const TextFragmentSelector& selector) {
EXPECT_EQ(selector.Type(), TextFragmentSelector::SelectorType::kExact);
EXPECT_EQ(selector.Start(), "First paragraph text that is");
callback_called = true;
});
TextFragmentSelectorGenerator generator(GetDocument().GetFrame(),
std::move(callback));
generator.GenerateSelector(
ToEphemeralRangeInFlatTree(EphemeralRange(selected_start, selected_end)));
EXPECT_TRUE(callback_called);
}
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextWithNestedTextNodes) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph text that is <i>longer than 20</i> chars</p>
<p id='second'>Second paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first");
const auto& selected_start = Position(first_paragraph->firstChild(), 0);
const auto& selected_end =
Position(first_paragraph->firstChild()->nextSibling()->firstChild(), 6);
bool callback_called = false;
base::OnceCallback<void(const TextFragmentSelector&)> callback =
base::BindLambdaForTesting([&](const TextFragmentSelector& selector) {
EXPECT_EQ(selector.Type(), TextFragmentSelector::SelectorType::kExact);
EXPECT_EQ(selector.Start(), "First paragraph text that is longer");
callback_called = true;
});
TextFragmentSelectorGenerator generator(GetDocument().GetFrame(),
std::move(callback));
generator.GenerateSelector(
ToEphemeralRangeInFlatTree(EphemeralRange(selected_start, selected_end)));
EXPECT_TRUE(callback_called);
}
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextWithExtraSpace) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph text that is longer than 20 chars</p>
<p id='second'>Second paragraph text</p>
)HTML");
Node* second_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& selected_start = Position(second_paragraph, 0);
const auto& selected_end = Position(second_paragraph, 23);
bool callback_called = false;
base::OnceCallback<void(const TextFragmentSelector&)> callback =
base::BindLambdaForTesting([&](const TextFragmentSelector& selector) {
EXPECT_EQ(selector.Type(), TextFragmentSelector::SelectorType::kExact);
EXPECT_EQ(selector.Start(), "Second paragraph text");
callback_called = true;
});
TextFragmentSelectorGenerator generator(GetDocument().GetFrame(),
std::move(callback));
generator.GenerateSelector(
ToEphemeralRangeInFlatTree(EphemeralRange(selected_start, selected_end)));
EXPECT_TRUE(callback_called);
}
// Multi-block selection is currently not implemented.
TEST_F(TextFragmentSelectorGeneratorTest, MultiblockSelection) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph text that is longer than 20 chars</p>
<p id='second'>Second paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
Node* second_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& selected_start = Position(first_paragraph, 0);
const auto& selected_end = Position(second_paragraph, 6);
bool callback_called = false;
base::OnceCallback<void(const TextFragmentSelector&)> callback =
base::BindLambdaForTesting([&](const TextFragmentSelector& selector) {
EXPECT_EQ(selector.Type(),
TextFragmentSelector::SelectorType::kInvalid);
callback_called = true;
});
TextFragmentSelectorGenerator generator(GetDocument().GetFrame(),
std::move(callback));
generator.GenerateSelector(
ToEphemeralRangeInFlatTree(EphemeralRange(selected_start, selected_end)));
EXPECT_TRUE(callback_called);
}
} // 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