Add exclusion rules to text manipulation SPI
https://bugs.webkit.org/show_bug.cgi?id=203398
<rdar://problem/56567256>

Reviewed by Wenson Hsieh.

Source/WebCore:

This patch adds the ability to define a ordered set of rules to exclude or include content an element of
a certain name or one with a certain attribute value.

Also made completeManipulation return more detailed errors for debugging purposes.

Tests: TextManipulation.StartTextManipulationApplySingleExcluionRuleForElement
       TextManipulation.StartTextManipulationApplyInclusionExclusionRulesForAttributes
       TextManipulation.CompleteTextManipulationFailWhenExclusionIsViolated

* editing/TextManipulationController.cpp:
(WebCore::TextManipulationController::ExclusionRule::match const): Added.
(WebCore::ExclusionRuleMatcher): Added. This class is responsible for figuring out whether a given node
is excluded or included in the text manipulation.
(WebCore::ExclusionRuleMatcher::ExclusionRuleMatcher): Added.
(WebCore::ExclusionRuleMatcher::isExcluded): Added.
(WebCore::ExclusionRuleMatcher::typeForElement): Added.
(WebCore::TextManipulationController::startObservingParagraphs): Added a Vector of ExclusionRule as
an argument.
(WebCore::TextManipulationController::completeManipulation):
(WebCore::TextManipulationController::replace):
* editing/TextManipulationController.h:
(WebCore::TextManipulationController::ExclusionRule): Added.
(WebCore::TextManipulationController::ManipulationResult): Added.
(WebCore::TextManipulationController::ManipulationToken::encode const): Include isExcluded boolean.
(WebCore::TextManipulationController::ManipulationToken::decode): Ditto.
(WebCore::TextManipulationController::ExclusionRule::encode const): Added.
(WebCore::TextManipulationController::ExclusionRule::decode): Added.
(WebCore::TextManipulationController::ExclusionRule::ElementRule::encode const): Added.
(WebCore::TextManipulationController::ExclusionRule::ElementRule::decode): Added.
(WebCore::TextManipulationController::ExclusionRule::AttributeRule::encode const): Added.
(WebCore::TextManipulationController::ExclusionRule::AttributeRule::decode): Added.

Source/WebKit:

Added SPI to specify the configuration for the text manipulation (see r251574), in particular, the set of rules
governing which content should be excluded or included in text manipulations.

Test: TextManipulation.StartTextManipulationExitEarlyWithoutDelegate

* SourcesCocoa.txt:
* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _startTextManipulationsWithConfiguration:completion:]): Takes _WKTextManipulationConfiguration
as an argument. Also fixed a bug that we weren't calling the completion handler when the delegate was not set.
(-[WKWebView _completeTextManipulation:completion:]):
(-[WKWebView _startTextManipulationsWithCompletionHandler:]): Deleted.
* UIProcess/API/Cocoa/WKWebViewPrivate.h:
* UIProcess/API/Cocoa/_WKTextManipulationConfiguration.h: Added.
* UIProcess/API/Cocoa/_WKTextManipulationConfiguration.mm: Added.
* UIProcess/API/Cocoa/_WKTextManipulationExclusionRule.h: Added.
* UIProcess/API/Cocoa/_WKTextManipulationExclusionRule.mm: Added.
(-[_WKTextManipulationExclusionRule initExclusion:forElement:]): Added.
(-[_WKTextManipulationExclusionRule initExclusion:forAttribute:value:]): Added.
(-[_WKTextManipulationExclusionRule elementName]): Added.
(-[_WKTextManipulationExclusionRule attributeName]): Added.
(-[_WKTextManipulationExclusionRule attributeValue]): Added.
* UIProcess/API/Cocoa/_WKTextManipulationToken.h: Added excluded boolean property.
* UIProcess/API/Cocoa/_WKTextManipulationToken.mm: Removed the superflous import of RetainPtr.h
* UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::startTextManipulations):
(WebKit::WebPageProxy::completeTextManipulation):
* UIProcess/WebPageProxy.h:
* WebKit.xcodeproj/project.pbxproj:
* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::startTextManipulations):
(WebKit::WebPage::completeTextManipulation):
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/WebPage.messages.in:

Tools:

Added tests for including & excluding content based on element names and attribute values.

Also added a test to make sure _startTextManipulationsWithConfiguration calls the completion handler
even when the _WKTextManipulationDelegate isn't set.

* TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251600 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebCore/editing/TextManipulationController.cpp b/Source/WebCore/editing/TextManipulationController.cpp
index 2e9d813..5741edd 100644
--- a/Source/WebCore/editing/TextManipulationController.cpp
+++ b/Source/WebCore/editing/TextManipulationController.cpp
@@ -26,25 +26,93 @@
 #include "config.h"
 #include "TextManipulationController.h"
 
+#include "CharacterData.h"
 #include "Editing.h"
+#include "ElementAncestorIterator.h"
 #include "ScriptDisallowedScope.h"
 #include "TextIterator.h"
 #include "VisibleUnits.h"
 
 namespace WebCore {
 
+inline bool TextManipulationController::ExclusionRule::match(const Element& element) const
+{
+    return switchOn(rule, [&element] (ElementRule rule) {
+        return rule.localName == element.localName();
+    }, [&element] (AttributeRule rule) {
+        return equalIgnoringASCIICase(element.getAttribute(rule.name), rule.value);
+    });
+}
+
+class ExclusionRuleMatcher {
+public:
+    using ExclusionRule = TextManipulationController::ExclusionRule;
+    using Type = TextManipulationController::ExclusionRule::Type;
+
+    ExclusionRuleMatcher(const Vector<ExclusionRule>& rules)
+        : m_rules(rules)
+    { }
+
+    bool isExcluded(Node* node)
+    {
+        if (!node)
+            return false;
+
+        RefPtr<Element> startingElement = is<Element>(*node) ? downcast<Element>(node) : node->parentElement();
+        if (!startingElement)
+            return false;
+
+        Type type = Type::Include;
+        RefPtr<Element> matchingElement;
+        for (auto& element : elementLineage(startingElement.get())) {
+            if (auto typeOrNullopt = typeForElement(element)) {
+                type = *typeOrNullopt;
+                matchingElement = &element;
+                break;
+            }
+        }
+
+        for (auto& element : elementLineage(startingElement.get())) {
+            m_cache.set(element, type);
+            if (&element == matchingElement)
+                break;
+        }
+
+        return type == Type::Exclude;
+    }
+
+    Optional<Type> typeForElement(Element& element)
+    {
+        auto it = m_cache.find(element);
+        if (it != m_cache.end())
+            return it->value;
+
+        for (auto& rule : m_rules) {
+            if (rule.match(element))
+                return rule.type;
+        }
+
+        return WTF::nullopt;
+    }
+
+private:
+    const Vector<ExclusionRule>& m_rules;
+    HashMap<Ref<Element>, ExclusionRule::Type> m_cache;
+};
+
 TextManipulationController::TextManipulationController(Document& document)
     : m_document(makeWeakPtr(document))
 {
 }
 
-void TextManipulationController::startObservingParagraphs(ManipulationItemCallback&& callback)
+void TextManipulationController::startObservingParagraphs(ManipulationItemCallback&& callback, Vector<ExclusionRule>&& exclusionRules)
 {
     auto document = makeRefPtr(m_document.get());
     if (!document)
         return;
 
     m_callback = WTFMove(callback);
+    m_exclusionRules = WTFMove(exclusionRules);
 
     VisiblePosition start = firstPositionInNode(m_document.get());
     VisiblePosition end = lastPositionInNode(m_document.get());
@@ -52,6 +120,7 @@
     if (!document)
         return; // VisiblePosition or TextIterator's constructor may have updated the layout and executed arbitrary scripts.
 
+    ExclusionRuleMatcher exclusionRuleMatcher(m_exclusionRules);
     Vector<ManipulationToken> tokensInCurrentParagraph;
     Position startOfCurrentParagraph = start.deepEquivalent();
     while (!iterator.atEnd()) {
@@ -64,8 +133,8 @@
         size_t offsetOfNextNewLine = 0;
         while ((offsetOfNextNewLine = currentText.find('\n', endOfLastNewLine)) != notFound) {
             if (endOfLastNewLine < offsetOfNextNewLine) {
-                tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(),
-                    currentText.substring(endOfLastNewLine, offsetOfNextNewLine - endOfLastNewLine).toString() });
+                auto stringUntilEndOfLine = currentText.substring(endOfLastNewLine, offsetOfNextNewLine - endOfLastNewLine).toString();
+                tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), stringUntilEndOfLine, exclusionRuleMatcher.isExcluded(iterator.node()) });
             }
 
             auto lastRange = iterator.range();
@@ -83,7 +152,7 @@
 
         auto remainingText = currentText.substring(endOfLastNewLine);
         if (remainingText.length())
-            tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), remainingText.toString() });
+            tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), remainingText.toString(), exclusionRuleMatcher.isExcluded(iterator.node()) });
 
         iterator.advance();
     }
@@ -99,14 +168,14 @@
     m_callback(*m_document, result.iterator->key, result.iterator->value.tokens);
 }
 
-bool TextManipulationController::completeManipulation(ItemIdentifier itemIdentifier, const Vector<ManipulationToken>& replacementTokens)
+auto TextManipulationController::completeManipulation(ItemIdentifier itemIdentifier, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
 {
     if (!itemIdentifier)
-        return false;
+        return ManipulationResult::InvalidItem;
 
     auto itemIterator = m_items.find(itemIdentifier);
     if (itemIterator == m_items.end())
-        return false;
+        return ManipulationResult::InvalidItem;
 
     auto didReplace = replace(itemIterator->value, replacementTokens);
 
@@ -115,35 +184,48 @@
     return didReplace;
 }
 
-bool TextManipulationController::replace(const ManipulationItem& item, const Vector<ManipulationToken>& replacementTokens)
+struct DOMChange {
+    Ref<CharacterData> node;
+    String newData;
+};
+
+auto TextManipulationController::replace(const ManipulationItem& item, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
 {
     TextIterator iterator { item.start, item.end };
     size_t currentTokenIndex = 0;
-    HashMap<TokenIdentifier, Ref<Node>> tokenToNode;
+    HashMap<TokenIdentifier, std::pair<RefPtr<Node>, const ManipulationToken*>> tokenToNodeTokenPair;
+
     while (!iterator.atEnd()) {
         auto string = iterator.text().toString();
         if (currentTokenIndex >= item.tokens.size())
-            return false;
+            return ManipulationResult::ContentChanged;
         auto& currentToken = item.tokens[currentTokenIndex];
         if (iterator.text() != currentToken.content)
-            return false;
-        tokenToNode.add(currentToken.identifier, *iterator.node());
+            return ManipulationResult::ContentChanged;
+        tokenToNodeTokenPair.set(currentToken.identifier, std::pair<RefPtr<Node>, const ManipulationToken*> { iterator.node(), &currentToken });
         iterator.advance();
         ++currentTokenIndex;
     }
 
     // FIXME: This doesn't preseve the order of the replacement at all.
-    for (auto& token : replacementTokens) {
-        auto* node = tokenToNode.get(token.identifier);
-        if (!node)
-            return false;
-        if (!is<CharacterData>(node))
+    Vector<DOMChange> changes;
+    for (auto& newToken : replacementTokens) {
+        auto it = tokenToNodeTokenPair.find(newToken.identifier);
+        if (it == tokenToNodeTokenPair.end())
+            return ManipulationResult::InvalidToken;
+        auto& oldToken = *it->value.second;
+        if (oldToken.isExcluded)
+            return ManipulationResult::ExclusionViolation;
+        auto* node = it->value.first.get();
+        if (!node || !is<CharacterData>(*node))
             continue;
-        // FIXME: It's not safe to update DOM while iterating over the tokens.
-        downcast<CharacterData>(node)->setData(token.content);
+        changes.append({ downcast<CharacterData>(*node), newToken.content });
     }
 
-    return true;
+    for (auto& change : changes)
+        change.node->setData(change.newData);
+
+    return ManipulationResult::Success;
 }
 
 } // namespace WebCore