diff --git a/third_party/blink/public/web/web_node.h b/third_party/blink/public/web/web_node.h index 62688012b5f1c5..64dba34d7a0794 100644 --- a/third_party/blink/public/web/web_node.h +++ b/third_party/blink/public/web/web_node.h @@ -104,6 +104,10 @@ class BLINK_EXPORT WebNode { WebVector QuerySelectorAll(const WebString& selector) const; + // Returns the contents of the first descendant element, if any, that contains + // only text, a part of which is the given substring. + WebString FindTextInElementWith(const WebString& substring) const; + bool Focused() const; WebPluginContainer* PluginContainer() const; diff --git a/third_party/blink/renderer/core/dom/build.gni b/third_party/blink/renderer/core/dom/build.gni index 6434527a99a16c..fb914afef53772 100644 --- a/third_party/blink/renderer/core/dom/build.gni +++ b/third_party/blink/renderer/core/dom/build.gni @@ -296,6 +296,7 @@ blink_core_sources_dom = [ blink_core_tests_dom = [ "abort_signal_test.cc", "attr_test.cc", + "container_node_test.cc", "document_statistics_collector_test.cc", "document_test.cc", "dom_node_ids_test.cc", diff --git a/third_party/blink/renderer/core/dom/container_node.cc b/third_party/blink/renderer/core/dom/container_node.cc index 76c96806834f9c..3675f393711986 100644 --- a/third_party/blink/renderer/core/dom/container_node.cc +++ b/third_party/blink/renderer/core/dom/container_node.cc @@ -73,6 +73,7 @@ #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" #include "third_party/blink/renderer/platform/runtime_enabled_features.h" #include "third_party/blink/renderer/platform/wtf/casting.h" +#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h" namespace blink { @@ -1253,6 +1254,24 @@ unsigned ContainerNode::CountChildren() const { return count; } +bool ContainerNode::HasOnlyText() const { + bool has_text = false; + for (Node* child = firstChild(); child; child = child->nextSibling()) { + switch (child->getNodeType()) { + case kTextNode: + case kCdataSectionNode: + has_text = has_text || !To(child)->data().empty(); + break; + case kCommentNode: + // Ignore comments. + break; + default: + return false; + } + } + return has_text; +} + Element* ContainerNode::QuerySelector(const AtomicString& selectors, ExceptionState& exception_state) { SelectorQuery* selector_query = GetDocument().GetSelectorQueryCache().Add( @@ -1571,6 +1590,19 @@ RadioNodeList* ContainerNode::GetRadioNodeList(const AtomicString& name, return EnsureCachedCollection(type, name); } +String ContainerNode::FindTextInElementWith( + const AtomicString& substring) const { + for (Element& element : ElementTraversal::DescendantsOf(*this)) { + if (element.HasOnlyText()) { + const String& text = element.TextFromChildren(); + if (text.Find(substring) != WTF::kNotFound) { + return text; + } + } + } + return String(); +} + Element* ContainerNode::getElementById(const AtomicString& id) const { // According to https://dom.spec.whatwg.org/#concept-id, empty IDs are // treated as equivalent to the lack of an id attribute. diff --git a/third_party/blink/renderer/core/dom/container_node.h b/third_party/blink/renderer/core/dom/container_node.h index 5e7a8ef0b28d17..49e9502f0866f5 100644 --- a/third_party/blink/renderer/core/dom/container_node.h +++ b/third_party/blink/renderer/core/dom/container_node.h @@ -111,12 +111,17 @@ class CORE_EXPORT ContainerNode : public Node { bool HasOneChild() const { return first_child_ && !first_child_->HasNextSibling(); } + + bool HasChildCount(unsigned) const; + unsigned CountChildren() const; + bool HasOneTextChild() const { return HasOneChild() && first_child_->IsTextNode(); } - bool HasChildCount(unsigned) const; - unsigned CountChildren() const; + // Returns true if all children are text nodes and at least one of them is not + // empty. Ignores comments. + bool HasOnlyText() const; Element* QuerySelector(const AtomicString& selectors, ExceptionState&); Element* QuerySelector(const AtomicString& selectors); @@ -146,6 +151,10 @@ class CORE_EXPORT ContainerNode : public Node { RadioNodeList* GetRadioNodeList(const AtomicString&, bool only_match_img_elements = false); + // Returns the contents of the first descendant element, if any, that contains + // only text, a part of which is the given substring. + String FindTextInElementWith(const AtomicString& substring) const; + // These methods are only used during parsing. // They don't send DOM mutation events or accept DocumentFragments. void ParserAppendChild(Node*); diff --git a/third_party/blink/renderer/core/dom/container_node_test.cc b/third_party/blink/renderer/core/dom/container_node_test.cc new file mode 100644 index 00000000000000..08cdef666530ca --- /dev/null +++ b/third_party/blink/renderer/core/dom/container_node_test.cc @@ -0,0 +1,131 @@ +// Copyright 2023 The Chromium Authors +// 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/dom/container_node.h" + +#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h" +#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" + +namespace blink { + +using ContainerNodeTest = EditingTestBase; + +TEST_F(ContainerNodeTest, HasOnlyTextReturnsFalseForEmptySpan) { + SetBodyContent(R"HTML()HTML"); + + EXPECT_FALSE(GetDocument().getElementById(AtomicString("id"))->HasOnlyText()); +} + +TEST_F(ContainerNodeTest, HasOnlyTextReturnsFalseForNonTextChild) { + SetBodyContent(R"HTML(
Nested
)HTML"); + + EXPECT_FALSE(GetDocument().getElementById(AtomicString("id"))->HasOnlyText()); +} + +TEST_F(ContainerNodeTest, HasOnlyTextReturnsTrueForSomeText) { + SetBodyContent(R"HTML(

Here is some text

)HTML"); + + EXPECT_TRUE(GetDocument().getElementById(AtomicString("id"))->HasOnlyText()); +} + +TEST_F(ContainerNodeTest, HasOnlyTextIgnoresComments) { + SetBodyContent(R"HTML( + +

Here is some text + +

+ + )HTML"); + + EXPECT_TRUE(GetDocument().getElementById(AtomicString("id"))->HasOnlyText()); +} + +TEST_F(ContainerNodeTest, CannotFindTextInElementWithoutDescendants) { + SetBodyContent(R"HTML()HTML"); + + String text = GetDocument().FindTextInElementWith(AtomicString("anything")); + + EXPECT_TRUE(text.empty()); +} + +TEST_F(ContainerNodeTest, CannotFindTextInElementWithNonTextDescendants) { + SetBodyContent(R"HTML( Hello + world! )HTML"); + + String text = GetDocument().FindTextInElementWith(AtomicString("anything")); + + EXPECT_TRUE(text.empty()); +} + +TEST_F(ContainerNodeTest, CannotFindTextInElementWithoutMatchingSubtring) { + SetBodyContent(R"HTML( Hello )HTML"); + + String text = GetDocument().FindTextInElementWith(AtomicString("Goodbye")); + + EXPECT_TRUE(text.empty()); +} + +TEST_F(ContainerNodeTest, CanFindTextInElementWithOnlyTextDescendants) { + SetBodyContent( + R"HTML( Find me please )HTML"); + + String text = GetDocument().FindTextInElementWith(AtomicString("me")); + + EXPECT_EQ(String(" Find me please "), text); +} + +TEST_F(ContainerNodeTest, CanFindTextInElementWithManyDescendants) { + SetBodyContent(R"HTML( + +

+
+ No need to find this +
+
+ Something something here +
Find me please
+ also over here +
+
+ And more information here +
+
+
+ Hi +
+ + )HTML"); + + String text = GetDocument().FindTextInElementWith(AtomicString(" me ")); + + EXPECT_EQ(String(" Find me please "), text); +} + +TEST_F(ContainerNodeTest, FindTextInElementWithFirstMatch) { + SetBodyContent(R"HTML( +
+
Text match #1
+
Text match #2
+
+ )HTML"); + + String text = GetDocument().FindTextInElementWith(AtomicString(" match ")); + + EXPECT_EQ(String(" Text match #1 "), text); +} + +TEST_F(ContainerNodeTest, FindTextInElementWithSubstringIgnoresComments) { + SetBodyContent(R"HTML( + +

Before comment, after comment.

+ + )HTML"); + + String text = GetDocument().FindTextInElementWith(AtomicString("comment")); + + EXPECT_EQ(String(" Before comment, after comment. "), text); +} + +} // namespace blink diff --git a/third_party/blink/renderer/core/exported/web_node.cc b/third_party/blink/renderer/core/exported/web_node.cc index cffa5e9a05df6a..0afaba812e4655 100644 --- a/third_party/blink/renderer/core/exported/web_node.cc +++ b/third_party/blink/renderer/core/exported/web_node.cc @@ -219,6 +219,15 @@ WebVector WebNode::QuerySelectorAll( return WebVector(); } +WebString WebNode::FindTextInElementWith(const WebString& substring) const { + ContainerNode* container_node = + blink::DynamicTo(private_.Get()); + if (!container_node) { + return WebString(); + } + return WebString(container_node->FindTextInElementWith(substring)); +} + bool WebNode::Focused() const { return private_->IsFocused(); } diff --git a/third_party/blink/renderer/core/exported/web_node_test.cc b/third_party/blink/renderer/core/exported/web_node_test.cc index 1442465f8ed98d..e594771adfde93 100644 --- a/third_party/blink/renderer/core/exported/web_node_test.cc +++ b/third_party/blink/renderer/core/exported/web_node_test.cc @@ -87,4 +87,25 @@ TEST_F(WebNodeSimTest, IsFocused) { EXPECT_TRUE(input_node.IsFocusable()); } +TEST_F(WebNodeTest, CannotFindTextInElementThatIsNotAContainer) { + SetInnerHTML(R"HTML( +


Hello world!
+ )HTML"); + WebElement element = Root().QuerySelector(AtomicString(".not-a-container")); + + EXPECT_FALSE(element.IsNull()); + EXPECT_TRUE(element.FindTextInElementWith("Hello world").IsEmpty()); +} + +TEST_F(WebNodeTest, CanFindTextInElementThatIsAContainer) { + SetInnerHTML(R"HTML( +
Hello world!
+ )HTML"); + WebElement element = Root().QuerySelector(AtomicString(".container")); + + EXPECT_FALSE(element.IsNull()); + EXPECT_EQ(WebString(" Hello world! "), + element.FindTextInElementWith("Hello world")); +} + } // namespace blink