blob: cde9a78c8fe7da131e11f44f4bb091c5fe6f02f3 [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 "CharacterData.h"
#include "Editing.h"
#include "ElementAncestorIterator.h"
#include "EventLoop.h"
#include "NodeTraversal.h"
#include "PseudoElement.h"
#include "Range.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 : 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, 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());
observeParagraphs(start, end);
}
void TextManipulationController::observeParagraphs(VisiblePosition& start, VisiblePosition& end)
{
auto document = makeRefPtr(start.deepEquivalent().document());
ASSERT(document);
TextIterator iterator { start.deepEquivalent(), end.deepEquivalent() };
if (document != start.deepEquivalent().document() || document != end.deepEquivalent().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();
while (!iterator.atEnd()) {
StringView currentText = iterator.text();
if (startOfCurrentParagraph.isNull())
startOfCurrentParagraph = iterator.range()->startPosition();
size_t endOfLastNewLine = 0;
size_t offsetOfNextNewLine = 0;
while ((offsetOfNextNewLine = currentText.find('\n', endOfLastNewLine)) != notFound) {
if (endOfLastNewLine < offsetOfNextNewLine) {
auto stringUntilEndOfLine = currentText.substring(endOfLastNewLine, offsetOfNextNewLine - endOfLastNewLine).toString();
tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), stringUntilEndOfLine, exclusionRuleMatcher.isExcluded(iterator.node()) });
}
auto lastRange = iterator.range();
if (offsetOfNextNewLine < currentText.length()) {
lastRange->setStart(firstPositionInOrBeforeNode(iterator.node())); // Move the start to the beginning of the current node.
TextIterator::subrange(lastRange, 0, offsetOfNextNewLine);
}
Position endOfCurrentParagraph = lastRange->endPosition();
if (!tokensInCurrentParagraph.isEmpty())
addItem(startOfCurrentParagraph, endOfCurrentParagraph, WTFMove(tokensInCurrentParagraph));
startOfCurrentParagraph.clear();
endOfLastNewLine = offsetOfNextNewLine + 1;
}
auto remainingText = currentText.substring(endOfLastNewLine);
if (remainingText.length())
tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), remainingText.toString(), exclusionRuleMatcher.isExcluded(iterator.node()) });
iterator.advance();
}
if (!tokensInCurrentParagraph.isEmpty())
addItem(startOfCurrentParagraph, end.deepEquivalent(), WTFMove(tokensInCurrentParagraph));
}
void TextManipulationController::didCreateRendererForElement(Element& element)
{
if (m_recentlyInsertedElements.contains(element))
return;
if (m_mutatedElements.computesEmpty())
scheduleObservartionUpdate();
if (is<PseudoElement>(element)) {
if (auto* host = downcast<PseudoElement>(element).hostElement())
m_mutatedElements.add(*host);
} else
m_mutatedElements.add(element);
}
using PositionTuple = std::tuple<RefPtr<Node>, unsigned, unsigned>;
static const PositionTuple makePositionTuple(const Position& position)
{
return { position.anchorNode(), static_cast<unsigned>(position.anchorType()), position.anchorType() == Position::PositionIsOffsetInAnchor ? position.offsetInContainerNode() : 0 };
}
static const std::pair<PositionTuple, PositionTuple> makeHashablePositionRange(const VisiblePosition& start, const VisiblePosition& end)
{
return { makePositionTuple(start.deepEquivalent()), makePositionTuple(end.deepEquivalent()) };
}
void TextManipulationController::scheduleObservartionUpdate()
{
if (!m_document)
return;
m_document->eventLoop().queueTask(TaskSource::InternalAsyncTask, [weakThis = makeWeakPtr(*this)] {
auto* controller = weakThis.get();
if (!controller)
return;
HashSet<Ref<Element>> mutatedElements;
for (auto& weakElement : controller->m_mutatedElements)
mutatedElements.add(weakElement);
controller->m_mutatedElements.clear();
HashSet<Ref<Element>> filteredElements;
for (auto& element : mutatedElements) {
auto* parentElement = element->parentElement();
if (!parentElement || !mutatedElements.contains(parentElement))
filteredElements.add(element.copyRef());
}
mutatedElements.clear();
HashSet<std::pair<PositionTuple, PositionTuple>> paragraphSets;
for (auto& element : filteredElements) {
auto start = startOfParagraph(firstPositionInOrBeforeNode(element.ptr()));
auto end = endOfParagraph(lastPositionInOrAfterNode(element.ptr()));
auto key = makeHashablePositionRange(start, end);
if (!paragraphSets.add(key).isNewEntry)
continue;
auto* controller = weakThis.get();
if (!controller)
return; // Finding the start/end of paragraph may have updated layout & executed arbitrary scripts.
controller->observeParagraphs(start, end);
}
});
}
void TextManipulationController::addItem(const Position& startOfParagraph, const Position& endOfParagraph, Vector<ManipulationToken>&& tokens)
{
ASSERT(m_document);
auto result = m_items.add(m_itemIdentifier.generate(), ManipulationItem { startOfParagraph, endOfParagraph, WTFMove(tokens) });
m_callback(*m_document, result.iterator->key, result.iterator->value.tokens);
}
auto TextManipulationController::completeManipulation(ItemIdentifier itemIdentifier, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
{
if (!itemIdentifier)
return ManipulationResult::InvalidItem;
auto itemIterator = m_items.find(itemIdentifier);
if (itemIterator == m_items.end())
return ManipulationResult::InvalidItem;
ManipulationItem item;
std::exchange(item, itemIterator->value);
m_items.remove(itemIterator);
return replace(item, replacementTokens);
}
struct TokenExchangeData {
RefPtr<Node> node;
String originalContent;
bool isExcluded { false };
bool isConsumed { false };
};
struct ReplacementData {
Ref<Node> originalNode;
String newData;
};
struct NodeInsertion {
RefPtr<Node> parentIfDifferentFromCommonAncestor;
Ref<Node> child;
};
auto TextManipulationController::replace(const ManipulationItem& item, const Vector<ManipulationToken>& replacementTokens) -> ManipulationResult
{
if (item.start.isOrphan() || item.end.isOrphan())
return ManipulationResult::ContentChanged;
TextIterator iterator { item.start, item.end };
size_t currentTokenIndex = 0;
HashMap<TokenIdentifier, TokenExchangeData> tokenExchangeMap;
RefPtr<Node> commonAncestor;
while (!iterator.atEnd()) {
auto string = iterator.text().toString();
if (currentTokenIndex >= item.tokens.size())
return ManipulationResult::ContentChanged;
auto& currentToken = item.tokens[currentTokenIndex];
if (iterator.text() != currentToken.content)
return ManipulationResult::ContentChanged;
auto currentNode = makeRefPtr(iterator.node());
tokenExchangeMap.set(currentToken.identifier, TokenExchangeData { currentNode.copyRef(), currentToken.content, currentToken.isExcluded });
if (currentNode) {
// FIXME: Take care of when currentNode is nullptr.
if (!commonAncestor)
commonAncestor = currentNode;
else if (!currentNode->isDescendantOf(commonAncestor.get())) {
commonAncestor = Range::commonAncestorContainer(commonAncestor.get(), currentNode.get());
ASSERT(commonAncestor);
}
}
iterator.advance();
++currentTokenIndex;
}
ASSERT(commonAncestor);
RefPtr<Node> nodeAfterStart = item.start.computeNodeAfterPosition();
if (!nodeAfterStart)
nodeAfterStart = item.start.containerNode();
RefPtr<Node> nodeAfterEnd = item.end.computeNodeAfterPosition();
if (!nodeAfterEnd)
nodeAfterEnd = NodeTraversal::nextSkippingChildren(*item.end.containerNode());
HashSet<Ref<Node>> nodesToRemove;
for (RefPtr<Node> currentNode = nodeAfterStart; currentNode && currentNode != nodeAfterEnd; currentNode = NodeTraversal::next(*currentNode)) {
if (commonAncestor == currentNode)
commonAncestor = currentNode->parentNode();
nodesToRemove.add(*currentNode);
}
Vector<Ref<Node>> currentElementStack;
HashSet<Ref<Node>> reusedOriginalNodes;
Vector<NodeInsertion> insertions;
for (auto& newToken : replacementTokens) {
auto it = tokenExchangeMap.find(newToken.identifier);
if (it == tokenExchangeMap.end())
return ManipulationResult::InvalidToken;
auto& exchangeData = it->value;
RefPtr<Node> contentNode;
if (exchangeData.isExcluded) {
if (exchangeData.isConsumed)
return ManipulationResult::ExclusionViolation;
exchangeData.isConsumed = true;
if (!newToken.content.isNull() && newToken.content != exchangeData.originalContent)
return ManipulationResult::ExclusionViolation;
contentNode = Text::create(commonAncestor->document(), exchangeData.originalContent);
} else
contentNode = Text::create(commonAncestor->document(), newToken.content);
auto& originalNode = exchangeData.node ? *exchangeData.node : *commonAncestor;
RefPtr<ContainerNode> currentNode = is<ContainerNode>(originalNode) ? &downcast<ContainerNode>(originalNode) : originalNode.parentNode();
Vector<Ref<Node>> currentAncestors;
for (; currentNode && currentNode != commonAncestor; currentNode = currentNode->parentNode())
currentAncestors.append(*currentNode);
currentAncestors.reverse();
size_t i =0;
while (i < currentElementStack.size() && i < currentAncestors.size() && currentElementStack[i].ptr() == currentAncestors[i].ptr())
++i;
if (i == currentElementStack.size() && i == currentAncestors.size())
insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, contentNode.releaseNonNull() });
else {
if (i < currentElementStack.size())
currentElementStack.shrink(i);
for (;i < currentAncestors.size(); ++i) {
Ref<Node> currentNode = currentAncestors[i].copyRef();
if (!reusedOriginalNodes.add(currentNode.copyRef()).isNewEntry) {
auto clonedNode = currentNode->cloneNodeInternal(currentNode->document(), Node::CloningOperation::OnlySelf);
if (auto* data = currentNode->eventTargetData())
data->eventListenerMap.copyEventListenersNotCreatedFromMarkupToTarget(clonedNode.ptr());
currentNode = WTFMove(clonedNode);
}
insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, currentNode.copyRef() });
currentElementStack.append(WTFMove(currentNode));
}
insertions.append(NodeInsertion { currentElementStack.size() ? currentElementStack.last().ptr() : nullptr, contentNode.releaseNonNull() });
}
}
Position insertionPoint = item.start;
while (insertionPoint.containerNode() != commonAncestor)
insertionPoint = positionInParentBeforeNode(insertionPoint.containerNode());
ASSERT(!insertionPoint.isNull());
for (auto& node : nodesToRemove)
node->remove();
for (auto& insertion : insertions) {
if (!insertion.parentIfDifferentFromCommonAncestor)
insertionPoint.containerNode()->insertBefore(insertion.child, insertionPoint.computeNodeBeforePosition());
else
insertion.parentIfDifferentFromCommonAncestor->appendChild(insertion.child);
if (is<Element>(insertion.child.get()))
m_recentlyInsertedElements.add(downcast<Element>(insertion.child.get()));
}
m_document->eventLoop().queueTask(TaskSource::InternalAsyncTask, [weakThis = makeWeakPtr(*this)] {
if (auto strongThis = weakThis.get())
strongThis->m_recentlyInsertedElements.clear();
});
return ManipulationResult::Success;
}
} // namespace WebCore