TextManipulationController.cpp   [plain text]


/*
 * Copyright (C) 2019 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "TextManipulationController.h"

#include "AccessibilityObject.h"
#include "CharacterData.h"
#include "Editing.h"
#include "ElementAncestorIterator.h"
#include "EventLoop.h"
#include "FrameView.h"
#include "HTMLBRElement.h"
#include "HTMLElement.h"
#include "HTMLInputElement.h"
#include "HTMLNames.h"
#include "HTMLParserIdioms.h"
#include "InputTypeNames.h"
#include "NodeRenderStyle.h"
#include "NodeTraversal.h"
#include "PseudoElement.h"
#include "RenderBox.h"
#include "ScriptDisallowedScope.h"
#include "Text.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);
    }, [&element] (ClassRule rule) {
        return element.hasClass() && element.classNames().contains(rule.className);
    });
}

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 : lineageOfType<Element>(*startingElement)) {
            if (auto typeOrNullopt = typeForElement(element)) {
                type = *typeOrNullopt;
                matchingElement = &element;
                break;
            }
        }

        for (auto& element : lineageOfType<Element>(*startingElement)) {
            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, Vector<ExclusionRule>&& exclusionRules)
{
    auto document = makeRefPtr(m_document.get());
    if (!document)
        return;

    m_callback = WTFMove(callback);
    m_exclusionRules = WTFMove(exclusionRules);

    observeParagraphs(firstPositionInNode(m_document.get()), lastPositionInNode(m_document.get()));
    flushPendingItemsForCallback();
}

static bool isInPrivateUseArea(UChar character)
{
    return 0xE000 <= character && character <= 0xF8FF;
}

static bool isTokenDelimiter(UChar character)
{
    return isHTMLLineBreak(character) || isInPrivateUseArea(character);
}

static bool isNotSpace(UChar character)
{
    if (character == noBreakSpace)
        return false;

    return isNotHTMLSpace(character);
}

class ParagraphContentIterator {
public:
    ParagraphContentIterator(const Position& start, const Position& end)
        : m_iterator(*makeSimpleRange(start, end), TextIteratorIgnoresStyleVisibility)
        , m_node(start.firstNode())
        , m_pastEndNode(end.firstNode())
    {
        if (shouldAdvanceIteratorPastCurrentNode())
            advanceIteratorNodeAndUpdateText();
    }

    void advance()
    {
        m_text = WTF::nullopt;
        advanceNode();

        if (shouldAdvanceIteratorPastCurrentNode())
            advanceIteratorNodeAndUpdateText();
    }

    struct CurrentContent {
        RefPtr<Node> node;
        Vector<String> text;
        bool isTextContent { false };
        bool isReplacedContent { false };
    };

    CurrentContent currentContent()
    {
        CurrentContent content = { m_node.copyRef(), m_text ? m_text.value() : Vector<String> { }, !!m_text };
        if (content.node) {
            if (auto* renderer = content.node->renderer()) {
                if (renderer->isRenderReplaced()) {
                    content.isTextContent = false;
                    content.isReplacedContent = true;
                }
            }
        }
        return content;
    }

    bool atEnd() const { return !m_text && m_iterator.atEnd() && m_node == m_pastEndNode; }

private:
    bool shouldAdvanceIteratorPastCurrentNode() const
    {
        if (m_iterator.atEnd())
            return false;

        auto* iteratorNode = m_iterator.node();
        return !iteratorNode || iteratorNode == m_node;
    }

    void advanceNode()
    {
        if (m_node == m_pastEndNode)
            return;

        m_node = NodeTraversal::next(*m_node);
        if (!m_node)
            m_node = m_pastEndNode;
    }

    void appendToText(Vector<String>& text, StringBuilder& stringBuilder)
    {
        if (!stringBuilder.isEmpty()) {
            text.append(stringBuilder.toString());
            stringBuilder.clear();
        }
    }

    void advanceIteratorNodeAndUpdateText()
    {
        ASSERT(shouldAdvanceIteratorPastCurrentNode());

        StringBuilder stringBuilder;
        Vector<String> text;
        while (shouldAdvanceIteratorPastCurrentNode()) {
            auto iteratorText = m_iterator.text();
            if (m_iterator.range().collapsed()) {
                if (iteratorText == "\n") {
                    appendToText(text, stringBuilder);
                    text.append({ });
                }
            } else
                stringBuilder.append(iteratorText);

            m_iterator.advance();
        }
        appendToText(text, stringBuilder);
        m_text = text;
    }

    TextIterator m_iterator;
    RefPtr<Node> m_node;
    RefPtr<Node> m_pastEndNode;
    Optional<Vector<String>> m_text;
};

static bool shouldExtractValueForTextManipulation(const HTMLInputElement& input)
{
    if (input.isSearchField() || equalIgnoringASCIICase(input.attributeWithoutSynchronization(HTMLNames::typeAttr), InputTypeNames::text()))
        return !input.lastChangeWasUserEdit();

    return input.isTextButton();
}

static bool isAttributeForTextManipulation(const QualifiedName& nameToCheck)
{
    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;
}

static bool canPerformTextManipulationByReplacingEntireTextContent(const Element& element)
{
    return element.hasTagName(HTMLNames::titleTag) || element.hasTagName(HTMLNames::optionTag);
}

static bool areEqualIgnoringLeadingAndTrailingWhitespaces(const String& content, const String& originalContent)
{
    return content.stripWhiteSpace() == originalContent.stripWhiteSpace();
}

static Optional<TextManipulationController::ManipulationTokenInfo> tokenInfo(Node* node)
{
    if (!node)
        return WTF::nullopt;

    TextManipulationController::ManipulationTokenInfo result;
    result.documentURL = node->document().url();
    if (auto element = is<Element>(node) ? makeRefPtr(downcast<Element>(*node)) : makeRefPtr(node->parentElement())) {
        result.tagName = element->tagName();
        if (element->hasAttributeWithoutSynchronization(HTMLNames::roleAttr))
            result.roleAttribute = element->attributeWithoutSynchronization(HTMLNames::roleAttr);
        if (auto frame = makeRefPtr(node->document().frame()); frame && frame->view() && element->renderer()) {
            // FIXME: This doesn't account for overflow clip.
            auto elementRect = element->renderer()->absoluteAnchorRect();
            auto visibleContentRect = frame->view()->visibleContentRect();
            result.isVisible = visibleContentRect.intersects(enclosingIntRect(elementRect));
        }
    }
    return result;
}

static bool isEnclosingItemBoundaryElement(const Element& element)
{
    auto* renderer = element.renderer();
    if (!renderer)
        return false;

    auto role = [](const Element& element) -> AccessibilityRole {
        return AccessibilityObject::ariaRoleToWebCoreRole(element.attributeWithoutSynchronization(HTMLNames::roleAttr));
    };

    if (element.hasTagName(HTMLNames::buttonTag) || role(element) == AccessibilityRole::Button)
        return true;

    auto displayType = renderer->style().display();
    if (element.hasTagName(HTMLNames::liTag) || element.hasTagName(HTMLNames::aTag)) {
        if (displayType == DisplayType::Block || displayType == DisplayType::InlineBlock)
            return true;

        for (auto parent = makeRefPtr(element.parentElement()); parent; parent = parent->parentElement()) {
            if (parent->hasTagName(HTMLNames::navTag) || role(*parent) == AccessibilityRole::LandmarkNavigation)
                return true;
        }
    }

    if (displayType == DisplayType::TableCell)
        return true;

    if (element.hasTagName(HTMLNames::spanTag) && displayType == DisplayType::InlineBlock)
        return true;

    if (displayType == DisplayType::Block && (element.hasTagName(HTMLNames::h1Tag) || element.hasTagName(HTMLNames::h2Tag) || element.hasTagName(HTMLNames::h3Tag)
        || element.hasTagName(HTMLNames::h4Tag) || element.hasTagName(HTMLNames::h5Tag) || element.hasTagName(HTMLNames::h6Tag)))
        return true;

    return false;
}

TextManipulationController::ManipulationUnit TextManipulationController::createUnit(const Vector<String>& text, Node& textNode)
{
    ManipulationUnit unit = { textNode, { } };
    for (auto& textEntry : text) {
        if (!textEntry.isNull())
            parse(unit, textEntry, textNode);
        else {
            if (unit.tokens.isEmpty())
                unit.firstTokenContainsDelimiter = true;
            unit.lastTokenContainsDelimiter = true;
        }
    }
    return unit;
}

bool TextManipulationController::shouldExcludeNodeBasedOnStyle(const Node& node)
{
    auto* style = node.renderStyle();
    if (!style)
        return false;

    auto& font = style->fontCascade().primaryFont();
    auto familyName = font.platformData().familyName();
    if (familyName.isEmpty())
        return false;

    auto iter = m_cachedFontFamilyExclusionResults.find(familyName);
    if (iter != m_cachedFontFamilyExclusionResults.end())
        return iter->value;

    // FIXME: We should reconsider whether a node should be excluded if the primary font
    // used to render the node changes, since this "icon font" heuristic may return a
    // different result.
    bool result = font.isProbablyOnlyUsedToRenderIcons();
    m_cachedFontFamilyExclusionResults.set(familyName, result);
    return result;
}

void TextManipulationController::parse(ManipulationUnit& unit, const String& text, Node& textNode)
{
    ExclusionRuleMatcher exclusionRuleMatcher(m_exclusionRules);
    bool isNodeExcluded = exclusionRuleMatcher.isExcluded(&textNode) || shouldExcludeNodeBasedOnStyle(textNode);
    size_t positionOfLastNonHTMLSpace = WTF::notFound;
    size_t startPositionOfCurrentToken = 0;
    size_t index = 0;
    for (; index < text.length(); ++index) {
        auto character = text[index];
        if (isTokenDelimiter(character)) {
            if (positionOfLastNonHTMLSpace != WTF::notFound && startPositionOfCurrentToken <= positionOfLastNonHTMLSpace) {
                auto stringForToken = text.substring(startPositionOfCurrentToken, positionOfLastNonHTMLSpace + 1 - startPositionOfCurrentToken);
                unit.tokens.append(ManipulationToken { m_tokenIdentifier.generate(), stringForToken, tokenInfo(&textNode), isNodeExcluded });
                startPositionOfCurrentToken = positionOfLastNonHTMLSpace + 1;
            }

            while (index < text.length() && (isHTMLSpace(text[index]) || isInPrivateUseArea(text[index])))
                ++index;

            --index;

            auto stringForToken = text.substring(startPositionOfCurrentToken, index + 1 - startPositionOfCurrentToken);
            if (unit.tokens.isEmpty() && !unit.firstTokenContainsDelimiter)
                unit.firstTokenContainsDelimiter = true;
            unit.tokens.append(ManipulationToken { m_tokenIdentifier.generate(), stringForToken, tokenInfo(&textNode), true });
            startPositionOfCurrentToken = index + 1;
            unit.lastTokenContainsDelimiter = true;
        } else if (isNotSpace(character)) {
            if (!isNodeExcluded)
                unit.areAllTokensExcluded = false;
            positionOfLastNonHTMLSpace = index;
        }
    }

    if (startPositionOfCurrentToken < text.length()) {
        auto stringForToken = text.substring(startPositionOfCurrentToken, index + 1 - startPositionOfCurrentToken);
        unit.tokens.append(ManipulationToken { m_tokenIdentifier.generate(), stringForToken, tokenInfo(&textNode), isNodeExcluded });
        unit.lastTokenContainsDelimiter = false;
    }
}

void TextManipulationController::addItemIfPossible(Vector<ManipulationUnit>&& units)
{
    if (units.isEmpty())
        return;

    size_t index = 0;
    size_t end = units.size();
    while (index < units.size() && units[index].areAllTokensExcluded)
        ++index;

    while (end > 0 && units[end - 1].areAllTokensExcluded)
        --end;

    if (index == end)
        return;

    ASSERT(end);
    auto startPosition = firstPositionInOrBeforeNode(units[index].node.ptr());
    auto endPosition = positionAfterNode(units[end - 1].node.ptr());
    Vector<ManipulationToken> tokens;
    for (; index < end; ++index)
        tokens.appendVector(WTFMove(units[index].tokens));

    addItem(ManipulationItemData { startPosition, endPosition, nullptr, nullQName(), WTFMove(tokens) });
}

void TextManipulationController::observeParagraphs(const Position& start, const Position& end)
{
    if (start.isNull() || end.isNull() || start.isOrphan() || end.isOrphan())
        return;

    auto document = makeRefPtr(start.document());
    ASSERT(document);
    // TextIterator's constructor may have updated the layout and executed arbitrary scripts.
    if (document != start.document() || document != end.document())
        return;

    Vector<ManipulationUnit> unitsInCurrentParagraph;
    Vector<Ref<Element>> enclosingItemBoundaryElements;
    ParagraphContentIterator iterator { start, end };
    for (; !iterator.atEnd(); iterator.advance()) {
        auto content = iterator.currentContent();
        auto* contentNode = content.node.get();
        ASSERT(contentNode);

        while (!enclosingItemBoundaryElements.isEmpty() && !enclosingItemBoundaryElements.last()->contains(contentNode)) {
            addItemIfPossible(std::exchange(unitsInCurrentParagraph, { }));
            enclosingItemBoundaryElements.removeLast();
        }

        if (m_manipulatedNodes.contains(contentNode)) {
            addItemIfPossible(std::exchange(unitsInCurrentParagraph, { }));
            continue;
        }

        if (is<Element>(*contentNode)) {
            auto& currentElement = downcast<Element>(*contentNode);
            if (!content.isTextContent && canPerformTextManipulationByReplacingEntireTextContent(currentElement))
                addItem(ManipulationItemData { Position(), Position(), makeWeakPtr(currentElement), nullQName(), { ManipulationToken { m_tokenIdentifier.generate(), currentElement.textContent(), tokenInfo(&currentElement) } } });

            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(), tokenInfo(&currentElement) } } });
                }
            }

            if (is<HTMLInputElement>(currentElement)) {
                auto& input = downcast<HTMLInputElement>(currentElement);
                if (shouldExtractValueForTextManipulation(input))
                    addItem(ManipulationItemData { { }, { }, makeWeakPtr(currentElement), HTMLNames::valueAttr, { ManipulationToken { m_tokenIdentifier.generate(), input.value(), tokenInfo(&currentElement) } } });
            }

            if (isEnclosingItemBoundaryElement(currentElement)) {
                addItemIfPossible(std::exchange(unitsInCurrentParagraph, { }));
                enclosingItemBoundaryElements.append(currentElement);
            }
        }

        if (content.isReplacedContent) {
            if (!unitsInCurrentParagraph.isEmpty())
                unitsInCurrentParagraph.append(ManipulationUnit { *contentNode, { ManipulationToken { m_tokenIdentifier.generate(), "[]", tokenInfo(content.node.get()), true } } });
            continue;
        }

        if (!content.isTextContent)
            continue;

        auto currentUnit = createUnit(content.text, *contentNode);
        if (currentUnit.firstTokenContainsDelimiter)
            addItemIfPossible(std::exchange(unitsInCurrentParagraph, { }));

        if (unitsInCurrentParagraph.isEmpty() && currentUnit.areAllTokensExcluded)
            continue;

        bool currentUnitEndsWithDelimiter = currentUnit.lastTokenContainsDelimiter;
        unitsInCurrentParagraph.append(WTFMove(currentUnit));

        if (currentUnitEndsWithDelimiter)
            addItemIfPossible(std::exchange(unitsInCurrentParagraph, { }));
    }

    addItemIfPossible(std::exchange(unitsInCurrentParagraph, { }));
}

void TextManipulationController::didCreateRendererForElement(Element& element)
{
    if (m_manipulatedNodes.contains(&element))
        return;

    scheduleObservationUpdate();

    if (is<PseudoElement>(element)) {
        if (auto* host = downcast<PseudoElement>(element).hostElement())
            m_elementsWithNewRenderer.add(*host);
    } else
        m_elementsWithNewRenderer.add(element);
}

void TextManipulationController::didUpdateContentForText(Text& text)
{
    if (!m_manipulatedNodes.contains(&text))
        return;

    scheduleObservationUpdate();

    m_manipulatedTextsWithNewContent.add(&text);
}

void TextManipulationController::didCreateRendererForTextNode(Text& text)
{
    if (m_manipulatedNodes.contains(&text))
        return;

    scheduleObservationUpdate();

    m_textNodesWithNewRenderer.add(&text);
}

void TextManipulationController::scheduleObservationUpdate()
{
    if (m_didScheduleObservationUpdate)
        return;

    if (!m_document)
        return;

    m_didScheduleObservationUpdate = true;

    m_document->eventLoop().queueTask(TaskSource::InternalAsyncTask, [weakThis = makeWeakPtr(*this)] {
        auto* controller = weakThis.get();
        if (!controller)
            return;

        controller->m_didScheduleObservationUpdate = false;

        HashSet<Ref<Node>> nodesToObserve;
        for (auto& weakElement : controller->m_elementsWithNewRenderer)
            nodesToObserve.add(weakElement);
        controller->m_elementsWithNewRenderer.clear();

        for (auto* text : controller->m_manipulatedTextsWithNewContent) {
            if (!controller->m_manipulatedNodes.contains(text))
                continue;
            controller->m_manipulatedNodes.remove(text);
            nodesToObserve.add(*text);
        }
        controller->m_manipulatedTextsWithNewContent.clear();

        for (auto* text : controller->m_textNodesWithNewRenderer)
            nodesToObserve.add(*text);
        controller->m_textNodesWithNewRenderer.clear();

        if (nodesToObserve.isEmpty())
            return;

        RefPtr<Node> commonAncestor;
        for (auto& node : nodesToObserve) {
            if (!node->isConnected())
                continue;

            if (auto host = makeRefPtr(node->shadowHost()); is<HTMLInputElement>(host.get()) && downcast<HTMLInputElement>(*host).lastChangeWasUserEdit())
                continue;

            if (!commonAncestor)
                commonAncestor = is<ContainerNode>(node.get()) ? node.ptr() : node->parentNode();
            else if (!node->isDescendantOf(commonAncestor.get()))
                commonAncestor = commonInclusiveAncestor<ComposedTree>(*commonAncestor, node.get());
        }

        auto start = firstPositionInOrBeforeNode(commonAncestor.get());
        auto end = lastPositionInOrAfterNode(commonAncestor.get());
        controller->observeParagraphs(start, end);

        if (controller->m_items.isEmpty() && commonAncestor) {
            controller->m_manipulatedNodes.add(commonAncestor.get());
            return;
        }

        controller->flushPendingItemsForCallback();
    });
}

void TextManipulationController::addItem(ManipulationItemData&& itemData)
{
    const unsigned itemCallbackBatchingSize = 128;

    ASSERT(m_document);
    ASSERT(!itemData.tokens.isEmpty());
    auto newID = m_itemIdentifier.generate();
    m_pendingItemsForCallback.append(ManipulationItem {
        newID,
        itemData.tokens.map([](auto& token) { return token; })
    });
    m_items.add(newID, WTFMove(itemData));

    if (m_pendingItemsForCallback.size() >= itemCallbackBatchingSize)
        flushPendingItemsForCallback();
}

void TextManipulationController::flushPendingItemsForCallback()
{
    if (m_pendingItemsForCallback.isEmpty())
        return;

    m_callback(*m_document, m_pendingItemsForCallback);
    m_pendingItemsForCallback.clear();
}

auto TextManipulationController::completeManipulation(const Vector<WebCore::TextManipulationController::ManipulationItem>& completionItems) -> Vector<ManipulationFailure>
{
    Vector<ManipulationFailure> failures;
    HashSet<Ref<Node>> containersWithoutVisualOverflowBeforeReplacement;
    for (unsigned i = 0; i < completionItems.size(); ++i) {
        auto& itemToComplete = completionItems[i];
        auto identifier = itemToComplete.identifier;
        if (!identifier) {
            failures.append(ManipulationFailure { identifier, i, ManipulationFailureType::InvalidItem });
            continue;
        }

        auto itemDataIterator = m_items.find(identifier);
        if (itemDataIterator == m_items.end()) {
            failures.append(ManipulationFailure { identifier, i, ManipulationFailureType::InvalidItem });
            continue;
        }

        ManipulationItemData itemData;
        std::exchange(itemData, itemDataIterator->value);
        m_items.remove(itemDataIterator);

        auto failureOrNullopt = replace(itemData, itemToComplete.tokens, containersWithoutVisualOverflowBeforeReplacement);
        if (failureOrNullopt)
            failures.append(ManipulationFailure { identifier, i, *failureOrNullopt });
    }

    if (!containersWithoutVisualOverflowBeforeReplacement.isEmpty()) {
        if (m_document)
            m_document->updateLayoutIgnorePendingStylesheets();

        for (auto& container : containersWithoutVisualOverflowBeforeReplacement) {
            if (!is<StyledElement>(container))
                continue;

            auto& element = downcast<StyledElement>(container.get());
            auto* box = element.renderBox();
            if (!box || !box->hasVisualOverflow())
                continue;

            auto& style = box->style();
            if (style.width().isFixed() && style.height().isFixed() && !style.hasOutOfFlowPosition() && !style.hasClip()) {
                element.setInlineStyleProperty(CSSPropertyOverflowX, CSSValueHidden);
                element.setInlineStyleProperty(CSSPropertyOverflowY, CSSValueAuto);
            }
        }
    }

    return failures;
}

struct TokenExchangeData {
    RefPtr<Node> node;
    String originalContent;
    bool isExcluded { false };
    bool isConsumed { false };
};

struct ReplacementData {
    Ref<Node> originalNode;
    String newData;
};

Vector<Ref<Node>> TextManipulationController::getPath(Node* ancestor, Node* node)
{
    Vector<Ref<Node>> path;
    RefPtr<ContainerNode> containerNode = is<ContainerNode>(*node) ? &downcast<ContainerNode>(*node) : node->parentNode();
    for (; containerNode && containerNode != ancestor; containerNode = containerNode->parentNode())
        path.append(*containerNode);
    path.reverse();
    return path;
}

void TextManipulationController::updateInsertions(Vector<NodeEntry>& lastTopDownPath, const Vector<Ref<Node>>& currentTopDownPath, Node* currentNode, HashSet<Ref<Node>>& insertedNodes, Vector<NodeInsertion>& insertions)
{
    size_t i = 0;
    while (i < lastTopDownPath.size() && i < currentTopDownPath.size() && lastTopDownPath[i].first.ptr() == currentTopDownPath[i].ptr())
        ++i;

    if (i != lastTopDownPath.size() || i != currentTopDownPath.size()) {
        if (i < lastTopDownPath.size())
            lastTopDownPath.shrink(i);

        for (;i < currentTopDownPath.size(); ++i) {
            Ref<Node> node = currentTopDownPath[i];
            if (!insertedNodes.add(node.copyRef()).isNewEntry) {
                auto clonedNode = node->cloneNodeInternal(node->document(), Node::CloningOperation::OnlySelf);
                if (auto* data = node->eventTargetData())
                    data->eventListenerMap.copyEventListenersNotCreatedFromMarkupToTarget(clonedNode.ptr());
                node = WTFMove(clonedNode);
            }
            insertions.append(NodeInsertion { lastTopDownPath.size() ? lastTopDownPath.last().second.ptr() : nullptr, node.copyRef() });
            lastTopDownPath.append({ currentTopDownPath[i].copyRef(), WTFMove(node) });
        }
    }

    if (currentNode)
        insertions.append(NodeInsertion { lastTopDownPath.size() ? lastTopDownPath.last().second.ptr() : nullptr, *currentNode });
}

auto TextManipulationController::replace(const ManipulationItemData& item, const Vector<ManipulationToken>& replacementTokens, HashSet<Ref<Node>>& containersWithoutVisualOverflowBeforeReplacement) -> Optional<ManipulationFailureType>
{
    if (item.start.isOrphan() || item.end.isOrphan())
        return ManipulationFailureType::ContentChanged;

    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 && !canPerformTextManipulationByReplacingEntireTextContent(*element) && item.attributeName == nullQName())
            return ManipulationFailureType::InvalidToken;
        auto expectedTokenIdentifier = item.tokens[0].identifier;
        StringBuilder newValue;
        for (size_t i = 0; i < replacementTokens.size(); ++i) {
            if (replacementTokens[i].identifier != expectedTokenIdentifier)
                return ManipulationFailureType::InvalidToken;
            if (i)
                newValue.append(' ');
            newValue.append(replacementTokens[i].content);
        }
        if (item.attributeName == nullQName())
            element->setTextContent(newValue.toString());
        else if (item.attributeName == HTMLNames::valueAttr && is<HTMLInputElement>(*element))
            downcast<HTMLInputElement>(*element).setValue(newValue.toString());
        else
            element->setAttribute(item.attributeName, newValue.toString());
        return WTF::nullopt;
    }

    size_t currentTokenIndex = 0;
    HashMap<TokenIdentifier, TokenExchangeData> tokenExchangeMap;
    RefPtr<Node> commonAncestor;
    RefPtr<Node> firstContentNode;
    RefPtr<Node> lastChildOfCommonAncestorInRange;
    HashSet<Ref<Node>> nodesToRemove;
    ParagraphContentIterator iterator { item.start, item.end };
    for (; !iterator.atEnd(); iterator.advance()) {
        auto content = iterator.currentContent();
        ASSERT(content.node);

        lastChildOfCommonAncestorInRange = content.node;
        nodesToRemove.add(*content.node);

        if (!content.isReplacedContent && !content.isTextContent)
            continue;

        Vector<ManipulationToken> tokensInCurrentNode;
        if (content.isReplacedContent) {
            if (currentTokenIndex >= item.tokens.size())
                return ManipulationFailureType::ContentChanged;

            tokensInCurrentNode.append(item.tokens[currentTokenIndex]);
        } else
            tokensInCurrentNode = createUnit(content.text, *content.node).tokens;

        bool isNodeIncluded = WTF::anyOf(tokensInCurrentNode, [] (auto& token) {
            return !token.isExcluded;
        });
        for (auto& token : tokensInCurrentNode) {
            if (currentTokenIndex >= item.tokens.size())
                return ManipulationFailureType::ContentChanged;

            auto& currentToken = item.tokens[currentTokenIndex++];
            bool isContentUnchanged = currentToken.content == token.content;
            if (!UNLIKELY(isContentUnchanged)) {
                bool isFirstOrLastToken = currentTokenIndex == 1 || currentTokenIndex == item.tokens.size();
                isContentUnchanged = isFirstOrLastToken && areEqualIgnoringLeadingAndTrailingWhitespaces(currentToken.content, token.content);
            }

            if (!content.isReplacedContent && !isContentUnchanged)
                return ManipulationFailureType::ContentChanged;

            tokenExchangeMap.set(currentToken.identifier, TokenExchangeData { content.node.copyRef(), currentToken.content, !isNodeIncluded });
        }

        if (!firstContentNode)
            firstContentNode = content.node;

        auto parentNode = content.node->parentNode();
        if (!commonAncestor)
            commonAncestor = parentNode;
        else if (!parentNode->isDescendantOf(commonAncestor.get())) {
            commonAncestor = commonInclusiveAncestor<ComposedTree>(*commonAncestor, *parentNode);
            ASSERT(commonAncestor);
        }
    }

    if (!firstContentNode)
        return ManipulationFailureType::ContentChanged;

    while (lastChildOfCommonAncestorInRange && lastChildOfCommonAncestorInRange->parentNode() != commonAncestor)
        lastChildOfCommonAncestorInRange = lastChildOfCommonAncestorInRange->parentNode();

    for (auto node = commonAncestor; node; node = node->parentNode())
        nodesToRemove.remove(*node);

    HashSet<Ref<Node>> reusedOriginalNodes;
    Vector<NodeInsertion> insertions;
    auto startTopDownPath = getPath(commonAncestor.get(), firstContentNode.get());
    while (!startTopDownPath.isEmpty()) {
        auto lastNode = startTopDownPath.last();
        ASSERT(is<ContainerNode>(lastNode.get()));
        if (!downcast<ContainerNode>(lastNode.get()).hasOneChild())
            break;
        nodesToRemove.add(startTopDownPath.takeLast());
    }
    auto lastTopDownPath = startTopDownPath.map([&](auto node) -> NodeEntry {
        reusedOriginalNodes.add(node.copyRef());
        return { node, node };
    });

    for (size_t index = 0; index < replacementTokens.size(); ++index) {
        auto& replacementToken = replacementTokens[index];
        auto it = tokenExchangeMap.find(replacementToken.identifier);
        if (it == tokenExchangeMap.end())
            return ManipulationFailureType::InvalidToken;

        auto& exchangeData = it->value;
        auto* originalNode = exchangeData.node.get();
        ASSERT(originalNode);
        auto replacementText = replacementToken.content;

        RefPtr<Node> replacementNode;
        if (exchangeData.isExcluded) {
            if (exchangeData.isConsumed)
                return ManipulationFailureType::ExclusionViolation;
            exchangeData.isConsumed = true;

            if (!replacementToken.content.isNull() && replacementToken.content != exchangeData.originalContent)
                return ManipulationFailureType::ExclusionViolation;

            replacementNode = originalNode;
            for (RefPtr<Node> descendentNode = NodeTraversal::next(*originalNode, originalNode); descendentNode; descendentNode = NodeTraversal::next(*descendentNode, originalNode))
                nodesToRemove.remove(*descendentNode);
        } else
            replacementNode = Text::create(commonAncestor->document(), replacementText);

        auto topDownPath = getPath(commonAncestor.get(), originalNode);
        updateInsertions(lastTopDownPath, topDownPath, replacementNode.get(), reusedOriginalNodes, insertions);
    }

    RefPtr<Node> node = item.end.firstNode();
    if (node && lastChildOfCommonAncestorInRange->contains(node.get())) {
        auto topDownPath = getPath(commonAncestor.get(), node->parentNode());
        updateInsertions(lastTopDownPath, topDownPath, nullptr, reusedOriginalNodes, insertions);
    }
    while (lastChildOfCommonAncestorInRange->contains(node.get())) {
        Ref<Node> parentNode = *node->parentNode();
        while (!lastTopDownPath.isEmpty() && lastTopDownPath.last().first.ptr() != parentNode.ptr())
            lastTopDownPath.removeLast();

        insertions.append(NodeInsertion { lastTopDownPath.size() ? lastTopDownPath.last().second.ptr() : nullptr, *node, IsNodeManipulated::No });
        lastTopDownPath.append({ *node, *node });
        node = NodeTraversal::next(*node);
    }

    RefPtr<Node> insertionPointNode = lastChildOfCommonAncestorInRange->nextSibling();

    for (auto& node : nodesToRemove)
        node->remove();

    for (auto& insertion : insertions) {
        auto parentContainer = insertion.parentIfDifferentFromCommonAncestor;
        if (!parentContainer) {
            parentContainer = commonAncestor;
            parentContainer->insertBefore(insertion.child, insertionPointNode.get());
        } else
            parentContainer->appendChild(insertion.child);

        if (auto* box = parentContainer->renderBox()) {
            if (!box->hasVisualOverflow())
                containersWithoutVisualOverflowBeforeReplacement.add(*parentContainer);
        }

        if (insertion.isChildManipulated == IsNodeManipulated::Yes)
            m_manipulatedNodes.add(insertion.child.ptr());
    }

    return WTF::nullopt;
}

void TextManipulationController::removeNode(Node* node)
{
    m_manipulatedNodes.remove(node);
    m_textNodesWithNewRenderer.remove(node);
}

} // namespace WebCore