TextManipulationController should work with ARIA labels
https://bugs.webkit.org/show_bug.cgi?id=208759

Reviewed by Wenson Hsieh.

Source/WebCore:

This patch makes TextManipulationController work with a few ARIA content attributes.

It also makes observeParagraphs observe content across the entire document since canonicalizing
the starting position can end up skipping some content with ARIA.

Tests: TextManipulation.StartTextManipulationFindAttributeContent
       TextManipulation.CompleteTextManipulationShouldReplaceAttributeContent

* editing/TextManipulationController.cpp:
(WebCore::TextManipulationController::startObservingParagraphs): Now takes two Positions instead
of two VisiblePositions.
(WebCore::isAttributeForTextManipulation): Added.
(WebCore::TextManipulationController::observeParagraphs): Added the code to find elements with
ARIA attributes and a few other elements that don't generate RenderObject's.
(WebCore::TextManipulationController::scheduleObservartionUpdate):
(WebCore::TextManipulationController::addItem): Now takes ManipulationItemData instead of indivisual
fields in it.
(WebCore::TextManipulationController::replace): Added the logic to handle ManipulationItemData
that specify element and attributeName.
* editing/TextManipulationController.h:
(WebCore::TextManipulationController::ManipulationItemData): Added element and attributeName.

Tools:

* TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:
(TextManipulation.StartTextManipulationFindAttributeContent): Added.
(TextManipulation.CompleteTextManipulationShouldPreserveOrderOfBlockImage): Updated
the expected number of tokens for the first item as the very first image is correctly
detected per the code change. 
(TextManipulation.CompleteTextManipulationShouldReplaceAttributeContent): Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@258093 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebCore/editing/TextManipulationController.cpp b/Source/WebCore/editing/TextManipulationController.cpp
index 09d0ecd..417f2a9 100644
--- a/Source/WebCore/editing/TextManipulationController.cpp
+++ b/Source/WebCore/editing/TextManipulationController.cpp
@@ -30,6 +30,7 @@
 #include "Editing.h"
 #include "ElementAncestorIterator.h"
 #include "EventLoop.h"
+#include "HTMLNames.h"
 #include "NodeTraversal.h"
 #include "PseudoElement.h"
 #include "Range.h"
@@ -132,10 +133,7 @@
     m_callback = WTFMove(callback);
     m_exclusionRules = WTFMove(exclusionRules);
 
-    VisiblePosition start = firstPositionInNode(m_document.get());
-    VisiblePosition end = lastPositionInNode(m_document.get());
-
-    observeParagraphs(start, end);
+    observeParagraphs(firstPositionInNode(m_document.get()), lastPositionInNode(m_document.get()));
     flushPendingItemsForCallback();
 }
 
@@ -219,17 +217,38 @@
     RefPtr<Node> m_pastEndNode;
 };
 
-void TextManipulationController::observeParagraphs(VisiblePosition& start, VisiblePosition& end)
+static bool isAttributeForTextManipulation(const QualifiedName& nameToCheck)
 {
-    auto document = makeRefPtr(start.deepEquivalent().document());
+    using namespace HTMLNames;
+    static const QualifiedName* const attributeNames[] = {
+        &titleAttr.get(),
+        &altAttr.get(),
+        &placeholderAttr.get(),
+        &aria_labelAttr.get(),
+        &aria_placeholderAttr.get(),
+        &aria_roledescriptionAttr.get(),
+        &aria_valuetextAttr.get(),
+    };
+    for (auto& entry : attributeNames) {
+        if (*entry == nameToCheck)
+            return true;
+    }
+    return false;
+}
+
+void TextManipulationController::observeParagraphs(const Position& start, const Position& end)
+{
+    auto document = makeRefPtr(start.document());
     ASSERT(document);
-    ParagraphContentIterator iterator { start.deepEquivalent(), end.deepEquivalent() };
-    if (document != start.deepEquivalent().document() || document != end.deepEquivalent().document())
+    ParagraphContentIterator iterator { start, end };
+    VisiblePosition visibleStart = start;
+    VisiblePosition visibleEnd = end;
+    if (document != start.document() || document != end.document())
         return; // TextIterator's constructor may have updated the layout and executed arbitrary scripts.
 
     ExclusionRuleMatcher exclusionRuleMatcher(m_exclusionRules);
     Vector<ManipulationToken> tokensInCurrentParagraph;
-    Position startOfCurrentParagraph = start.deepEquivalent();
+    Position startOfCurrentParagraph = visibleStart.deepEquivalent();
     for (; !iterator.atEnd(); iterator.advance()) {
         auto content = iterator.currentContent();
         if (content.node) {
@@ -238,7 +257,23 @@
                     return; // We can exit early here because scheduleObservartionUpdate calls this function on each paragraph separately.
             }
 
-            if (startOfCurrentParagraph.isNull())
+            if (is<Element>(*content.node)) {
+                auto& currentElement = downcast<Element>(*content.node);
+                if (!content.isTextContent && (content.node->hasTagName(HTMLNames::titleTag) || content.node->hasTagName(HTMLNames::optionTag))) {
+                    addItem(ManipulationItemData { Position(), Position(), makeWeakPtr(currentElement), nullQName(),
+                        { ManipulationToken { m_tokenIdentifier.generate(), currentElement.textContent() } } });
+                }
+                if (currentElement.hasAttributes()) {
+                    for (auto& attribute : currentElement.attributesIterator()) {
+                        if (isAttributeForTextManipulation(attribute.name())) {
+                            addItem(ManipulationItemData { Position(), Position(), makeWeakPtr(currentElement), attribute.name(),
+                                { ManipulationToken { m_tokenIdentifier.generate(), attribute.value() } } });
+                        }
+                    }
+                }
+            }
+
+            if (startOfCurrentParagraph.isNull() && content.isTextContent)
                 startOfCurrentParagraph = iterator.startPosition();
         }
 
@@ -266,7 +301,7 @@
                     endOfCurrentParagraph = Position(&textNode, offsetOfNextNewLine);
                     startOfCurrentParagraph = Position(&textNode, offsetOfNextNewLine + 1);
                 }
-                addItem(startOfCurrentParagraph, endOfCurrentParagraph, WTFMove(tokensInCurrentParagraph));
+                addItem(ManipulationItemData { startOfCurrentParagraph, endOfCurrentParagraph, nullptr, nullQName(), WTFMove(tokensInCurrentParagraph) });
                 startOfCurrentParagraph.clear();
             }
             startOfCurrentLine = offsetOfNextNewLine + 1;
@@ -278,7 +313,7 @@
     }
 
     if (!tokensInCurrentParagraph.isEmpty())
-        addItem(startOfCurrentParagraph, end.deepEquivalent(), WTFMove(tokensInCurrentParagraph));
+        addItem(ManipulationItemData { startOfCurrentParagraph, visibleEnd.deepEquivalent(), nullptr, nullQName(), WTFMove(tokensInCurrentParagraph) });
 }
 
 void TextManipulationController::didCreateRendererForElement(Element& element)
@@ -343,13 +378,13 @@
             if (!controller)
                 return; // Finding the start/end of paragraph may have updated layout & executed arbitrary scripts.
 
-            controller->observeParagraphs(start, end);
+            controller->observeParagraphs(start.deepEquivalent(), end.deepEquivalent());
         }
         controller->flushPendingItemsForCallback();
     });
 }
 
-void TextManipulationController::addItem(const Position& startOfParagraph, const Position& endOfParagraph, Vector<ManipulationToken>&& tokens)
+void TextManipulationController::addItem(ManipulationItemData&& itemData)
 {
     const unsigned itemCallbackBatchingSize = 128;
 
@@ -357,9 +392,9 @@
     auto newID = m_itemIdentifier.generate();
     m_pendingItemsForCallback.append(ManipulationItem {
         newID,
-        tokens.map([](auto& token) { return token; })
+        itemData.tokens.map([](auto& token) { return token; })
     });
-    m_items.add(newID, ManipulationItemData { startOfParagraph, endOfParagraph, WTFMove(tokens) });
+    m_items.add(newID, WTFMove(itemData));
 
     if (m_pendingItemsForCallback.size() >= itemCallbackBatchingSize)
         flushPendingItemsForCallback();
@@ -424,6 +459,26 @@
     size_t currentTokenIndex = 0;
     HashMap<TokenIdentifier, TokenExchangeData> tokenExchangeMap;
 
+    if (item.start.isNull() && item.end.isNull()) {
+        RELEASE_ASSERT(item.tokens.size() == 1);
+        auto element = makeRefPtr(item.element.get());
+        if (!element)
+            return ManipulationFailureType::ContentChanged;
+        if (replacementTokens.size() > 1)
+            return ManipulationFailureType::InvalidToken;
+        String newValue;
+        if (!replacementTokens.isEmpty()) {
+            if (replacementTokens[0].identifier != item.tokens[0].identifier)
+                return ManipulationFailureType::InvalidToken;
+            newValue = replacementTokens[0].content;
+        }
+        if (item.attributeName == nullQName())
+            element->setTextContent(newValue);
+        else
+            element->setAttribute(item.attributeName, newValue);
+        return WTF::nullopt;
+    }
+
     RefPtr<Node> commonAncestor;
     ParagraphContentIterator iterator { item.start, item.end };
     HashSet<Ref<Node>> excludedNodes;