blob: 47429d5917a23a2edae16d0f77f9412ca8f56a0e [file] [log] [blame]
/*
* 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 WTF::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;
}
std::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 std::nullopt;
}
private:
const Vector<ExclusionRule>& m_rules;
HashMap<Ref<Element>, ExclusionRule::Type> m_cache;
};
TextManipulationController::TextManipulationController(Document& document)
: m_document(document)
{
}
void TextManipulationController::startObservingParagraphs(ManipulationItemCallback&& callback, Vector<ExclusionRule>&& exclusionRules)
{
RefPtr document { 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), TextIteratorBehavior::IgnoresStyleVisibility)
, m_node(start.firstNode())
, m_pastEndNode(end.firstNode())
{
if (shouldAdvanceIteratorPastCurrentNode())
advanceIteratorNodeAndUpdateText();
}
void advance()
{
m_text = std::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;
std::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 std::optional<TextManipulationController::ManipulationTokenInfo> tokenInfo(Node* node)
{
if (!node)
return std::nullopt;
TextManipulationController::ManipulationTokenInfo result;
result.documentURL = node->document().url();
if (RefPtr element = is<Element>(node) ? downcast<Element>(node) : node->parentElement()) {
result.tagName = element->tagName();
if (element->hasAttributeWithoutSynchronization(HTMLNames::roleAttr))
result.roleAttribute = element->attributeWithoutSynchronization(HTMLNames::roleAttr);
if (RefPtr frame = 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 (RefPtr parent = 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 = notFound;
size_t startPositionOfCurrentToken = 0;
size_t index = 0;
for (; index < text.length(); ++index) {
auto character = text[index];
if (isTokenDelimiter(character)) {
if (positionOfLastNonHTMLSpace != 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;
RefPtr document { 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(), 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(), 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 { { }, { }, 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 = WeakPtr { *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 (RefPtr host = node->shadowHost(); is<HTMLInputElement>(host) && downcast<HTMLInputElement>(*host).lastChangeWasUserEdit())
continue;
if (!commonAncestor)
commonAncestor = is<ContainerNode>(node) ? 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);
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) -> std::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);
RefPtr element = { 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 std::nullopt;
}
size_t currentTokenIndex = 0;
HashMap<TokenIdentifier, TokenExchangeData> tokenExchangeMap;
RefPtr<Node> commonAncestor;
RefPtr<Node> firstContentNode;
RefPtr<Node> lastChildOfCommonAncestorInRange;
HashSet<Ref<Node>> nodesToRemove;
for (ParagraphContentIterator iterator { item.start, item.end }; !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 = 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));
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.get());
}
return std::nullopt;
}
void TextManipulationController::removeNode(Node& node)
{
m_manipulatedNodes.remove(node);
m_textNodesWithNewRenderer.remove(node);
}
} // namespace WebCore