TextManipulationController should put one Node in only one paragraph
https://bugs.webkit.org/show_bug.cgi?id=212548
Reviewed by Wenson Hsieh.
Source/WebCore:
TextManipulationController mainly uses line break as delimiter to split paragraphs. In our current
implementation, if text of a Node has line break, the part before the line break is in one paragraph and the
part after the line break is in another paragraph, which means the Node is in the ranges of two paragraphs.
In this case, when TextManipulationController manipulates the first paragraph, it replaces all the Nodes in the
range of first paragraph with new Nodes. Then when it manipulates the second paragraph, if will find Node in the
range of second paragraph does not exist and fail (because the Node is removed when handling the first
paragraph.). Also, TextManipulationController currently does not preserve line breaks in text, which can be an
issue if these line breaks are visible.
This patch makes the ParagraphContentIterator iterate over Nodes instead of text, so a Node can only be in the
range of one paragraph. To do this, it makes line break and spaces around it as a special excluded token.
Here are the rules for splitting paragraphs by line break now:
1. If the special token is the first token in a Node, text in Nodes before the Node will make a paragraph.
2. If the special token is the last token in a Node, text in Nodes before the Node and in the Node will make a
paragraph.
3. If the special token in the middle of tokens in a Node, then we don't make a new paragraph until next special
token meets condition 1 or 2.
This patch also fixes the issue that Nodes out of the paragraph range can be removed due to the preorder Node
traversal, by finding and adding those Nodes back.
* editing/TextManipulationController.cpp:
(WebCore::ParagraphContentIterator::m_pastEndNode):
(WebCore::ParagraphContentIterator::advance):
(WebCore::ParagraphContentIterator::currentContent):
(WebCore::ParagraphContentIterator::atEnd const):
(WebCore::ParagraphContentIterator::advanceNode):
(WebCore::ParagraphContentIterator::advanceIteratorNodeAndUpdateText):
(WebCore::isEnclosingItemBoundaryElement):
(WebCore::TextManipulationController::parse):
(WebCore::TextManipulationController::observeParagraphs):
(WebCore::TextManipulationController::addItem):
(WebCore::TextManipulationController::getPath):
(WebCore::TextManipulationController::updateInsertions):
(WebCore::TextManipulationController::replace):
(WebCore::ParagraphContentIterator::startPosition): Deleted.
(WebCore::ParagraphContentIterator::endPosition): Deleted.
(WebCore::ParagraphContentIterator::moveCurrentNodeForward): Deleted.
(WebCore::containsOnlyHTMLSpaces): Deleted.
* editing/TextManipulationController.h:
Tools:
* TestWebKitAPI/Tests/WebKitCocoa/TextManipulation.mm:
(TestWebKitAPI::TEST):
git-svn-id: http://svn.webkit.org/repository/webkit/trunk@262398 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebCore/editing/TextManipulationController.cpp b/Source/WebCore/editing/TextManipulationController.cpp
index e0ee1d2..8e7463f 100644
--- a/Source/WebCore/editing/TextManipulationController.cpp
+++ b/Source/WebCore/editing/TextManipulationController.cpp
@@ -147,28 +147,20 @@
ParagraphContentIterator(const Position& start, const Position& end)
: m_iterator({ *makeBoundaryPoint(start), *makeBoundaryPoint(end) }, TextIteratorIgnoresStyleVisibility)
, m_iteratorNode(m_iterator.atEnd() ? nullptr : createLiveRange(m_iterator.range())->firstNode())
- , m_currentNodeForFindingInvisibleContent(start.firstNode())
+ , m_node(start.firstNode())
, m_pastEndNode(end.firstNode())
{
+ if (shouldAdvanceIteratorPastCurrentNode())
+ advanceIteratorNodeAndUpdateText();
}
void advance()
{
- // FIXME: Add a node callback to TextIterator instead of traversing the node tree twice like this.
- if (m_currentNodeForFindingInvisibleContent != m_iteratorNode && m_currentNodeForFindingInvisibleContent != m_pastEndNode) {
- moveCurrentNodeForward();
- return;
- }
+ m_text = WTF::nullopt;
+ advanceNode();
- if (m_iterator.atEnd())
- return;
-
- auto previousIteratorNode = m_iteratorNode;
-
- m_iterator.advance();
- m_iteratorNode = m_iterator.atEnd() ? nullptr : createLiveRange(m_iterator.range())->firstNode();
- if (previousIteratorNode != m_iteratorNode)
- moveCurrentNodeForward();
+ if (shouldAdvanceIteratorPastCurrentNode())
+ advanceIteratorNodeAndUpdateText();
}
struct CurrentContent {
@@ -180,11 +172,7 @@
CurrentContent currentContent()
{
- CurrentContent content;
- if (m_currentNodeForFindingInvisibleContent && m_currentNodeForFindingInvisibleContent != m_iteratorNode)
- content = { m_currentNodeForFindingInvisibleContent.copyRef(), StringView { } };
- else
- content = { m_iterator.node(), m_iterator.text(), true };
+ CurrentContent content = { m_node.copyRef(), m_text ? m_text.value() : StringView { }, !!m_text };
if (content.node) {
if (auto* renderer = content.node->renderer()) {
if (renderer->isRenderReplaced()) {
@@ -196,33 +184,39 @@
return content;
}
- Position startPosition()
- {
- return createLegacyEditingPosition(m_iterator.range().start);
- }
-
- Position endPosition()
- {
- return createLegacyEditingPosition(m_iterator.range().end);
- }
-
- bool atEnd() const { return m_iterator.atEnd() && m_currentNodeForFindingInvisibleContent == m_pastEndNode; }
+ bool atEnd() const { return !m_text && m_iterator.atEnd() && m_node == m_pastEndNode; }
private:
- void moveCurrentNodeForward()
+ bool shouldAdvanceIteratorPastCurrentNode() const { return !m_iterator.atEnd() && m_iteratorNode == m_node; }
+
+ void advanceNode()
{
- if (m_currentNodeForFindingInvisibleContent == m_pastEndNode)
+ if (m_node == m_pastEndNode)
return;
- m_currentNodeForFindingInvisibleContent = NodeTraversal::next(*m_currentNodeForFindingInvisibleContent);
- if (!m_currentNodeForFindingInvisibleContent)
- m_currentNodeForFindingInvisibleContent = m_pastEndNode;
+ m_node = NodeTraversal::next(*m_node);
+ if (!m_node)
+ m_node = m_pastEndNode;
+ }
+
+ void advanceIteratorNodeAndUpdateText()
+ {
+ ASSERT(shouldAdvanceIteratorPastCurrentNode());
+
+ StringBuilder stringBuilder;
+ while (shouldAdvanceIteratorPastCurrentNode()) {
+ stringBuilder.append(m_iterator.text());
+ m_iterator.advance();
+ m_iteratorNode = m_iterator.atEnd() ? nullptr : createLiveRange(m_iterator.range())->firstNode();
+ }
+ m_text = { stringBuilder.toString() };
}
TextIterator m_iterator;
RefPtr<Node> m_iteratorNode;
- RefPtr<Node> m_currentNodeForFindingInvisibleContent;
+ RefPtr<Node> m_node;
RefPtr<Node> m_pastEndNode;
+ Optional<String> m_text;
};
static bool isAttributeForTextManipulation(const Element& element, const QualifiedName& nameToCheck)
@@ -253,16 +247,6 @@
return element.hasTagName(HTMLNames::titleTag) || element.hasTagName(HTMLNames::optionTag);
}
-static bool containsOnlyHTMLSpaces(StringView text)
-{
- for (unsigned index = 0; index < text.length(); ++index) {
- if (isNotHTMLSpace(text[index]))
- return false;
- }
-
- return true;
-}
-
static Optional<TextManipulationController::ManipulationTokenInfo> tokenInfo(Node* node)
{
if (!node)
@@ -278,6 +262,83 @@
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;
+
+ if (element.hasTagName(HTMLNames::liTag) || element.hasTagName(HTMLNames::aTag)) {
+ auto displayType = renderer->style().display();
+ 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;
+ }
+ }
+
+ return false;
+}
+
+TextManipulationController::ManipulationTokens TextManipulationController::parse(StringView text, Node* textNode)
+{
+ Vector<ManipulationToken> tokens;
+ ExclusionRuleMatcher exclusionRuleMatcher(m_exclusionRules);
+ size_t positionOfLastNonHTMLSpace = WTF::notFound;
+ size_t startPositionOfCurrentToken = 0;
+ bool containsOnlyHTMLSpace = true;
+ bool containsLineBreak = false;
+ bool firstTokenContainsLineBreak = false;
+ bool lastTokenContainsLineBreak = false;
+
+ size_t index = 0;
+ for (; index < text.length(); ++index) {
+ auto character = text[index];
+ if (isNotHTMLSpace(character)) {
+ containsOnlyHTMLSpace = false;
+ positionOfLastNonHTMLSpace = index;
+ } else if (isHTMLLineBreak(character)) {
+ containsLineBreak = true;
+ if (positionOfLastNonHTMLSpace != WTF::notFound && startPositionOfCurrentToken <= positionOfLastNonHTMLSpace) {
+ auto tokenString = text.substring(startPositionOfCurrentToken, positionOfLastNonHTMLSpace + 1 - startPositionOfCurrentToken).toString();
+ tokens.append(ManipulationToken { m_tokenIdentifier.generate(), tokenString, tokenInfo(textNode), exclusionRuleMatcher.isExcluded(textNode) });
+ startPositionOfCurrentToken = positionOfLastNonHTMLSpace + 1;
+ }
+
+ while (index < text.length() && !isNotHTMLSpace(text[index]))
+ ++index;
+
+ --index;
+
+ auto lineBreakTokenString = text.substring(startPositionOfCurrentToken, index + 1 - startPositionOfCurrentToken).toString();
+ if (tokens.isEmpty())
+ firstTokenContainsLineBreak = true;
+ tokens.append(ManipulationToken { m_tokenIdentifier.generate(), lineBreakTokenString, tokenInfo(textNode), true });
+ startPositionOfCurrentToken = index + 1;
+ lastTokenContainsLineBreak = true;
+
+ continue;
+ }
+ }
+
+ if (startPositionOfCurrentToken < text.length()) {
+ auto tokenString = text.substring(startPositionOfCurrentToken, index + 1 - startPositionOfCurrentToken).toString();
+ tokens.append(ManipulationToken { m_tokenIdentifier.generate(), tokenString, tokenInfo(textNode), exclusionRuleMatcher.isExcluded(textNode) });
+ lastTokenContainsLineBreak = false;
+ }
+
+ return { WTFMove(tokens), containsOnlyHTMLSpace, containsLineBreak, firstTokenContainsLineBreak, lastTokenContainsLineBreak };
+}
+
void TextManipulationController::observeParagraphs(const Position& start, const Position& end)
{
if (start.isNull() || end.isNull())
@@ -286,126 +347,74 @@
auto document = makeRefPtr(start.document());
ASSERT(document);
ParagraphContentIterator iterator { start, end };
- VisiblePosition visibleStart = start;
- VisiblePosition visibleEnd = end;
+ // TextIterator's constructor may have updated the layout and executed arbitrary scripts.
if (document != start.document() || document != end.document())
- return; // TextIterator's constructor may have updated the layout and executed arbitrary scripts.
+ return;
- ExclusionRuleMatcher exclusionRuleMatcher(m_exclusionRules);
Vector<ManipulationToken> tokensInCurrentParagraph;
Position startOfCurrentParagraph;
Position endOfCurrentParagraph;
RefPtr<Element> enclosingItemBoundaryElement;
- auto 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;
-
- if (element.hasTagName(HTMLNames::liTag) || element.hasTagName(HTMLNames::aTag)) {
- auto displayType = renderer->style().display();
- 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;
- }
- }
-
- return false;
- };
-
for (; !iterator.atEnd(); iterator.advance()) {
auto content = iterator.currentContent();
- if (content.node) {
- if (enclosingItemBoundaryElement && !enclosingItemBoundaryElement->contains(content.node.get())) {
- if (!tokensInCurrentParagraph.isEmpty())
- addItem(ManipulationItemData { startOfCurrentParagraph, endOfCurrentParagraph, nullptr, nullQName(), std::exchange(tokensInCurrentParagraph, { }) });
- enclosingItemBoundaryElement = nullptr;
- }
+ auto* contentNode = content.node.get();
+ ASSERT(contentNode);
- if (RefPtr<Element> currentElementAncestor = is<Element>(*content.node) ? downcast<Element>(content.node.get()) : content.node->parentOrShadowHostElement()) {
- if (isInManipulatedElement(*currentElementAncestor))
- return; // We can exit early here because scheduleObservartionUpdate calls this function on each paragraph separately.
- }
+ if (enclosingItemBoundaryElement && !enclosingItemBoundaryElement->contains(contentNode)) {
+ if (!tokensInCurrentParagraph.isEmpty())
+ addItem(ManipulationItemData { startOfCurrentParagraph, endOfCurrentParagraph, nullptr, nullQName(), std::exchange(tokensInCurrentParagraph, { }) });
+ enclosingItemBoundaryElement = nullptr;
+ }
- if (is<Element>(*content.node)) {
- auto& currentElement = downcast<Element>(*content.node);
- if (!content.isTextContent && canPerformTextManipulationByReplacingEntireTextContent(currentElement)) {
- addItem(ManipulationItemData { Position(), Position(), makeWeakPtr(currentElement), nullQName(),
- { ManipulationToken { m_tokenIdentifier.generate(), currentElement.textContent(), tokenInfo(¤tElement) } } });
+ if (RefPtr<Element> currentElementAncestor = is<Element>(*contentNode) ? downcast<Element>(contentNode) : contentNode->parentOrShadowHostElement()) {
+ // We can exit early here because scheduleObservationUpdate calls this function on each paragraph separately.
+ if (isInManipulatedElement(*currentElementAncestor))
+ return;
+ }
+
+ 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(¤tElement) } } });
+
+ if (currentElement.hasAttributes()) {
+ for (auto& attribute : currentElement.attributesIterator()) {
+ if (isAttributeForTextManipulation(currentElement, attribute.name()))
+ addItem(ManipulationItemData { Position(), Position(), makeWeakPtr(currentElement), attribute.name(), { ManipulationToken { m_tokenIdentifier.generate(), attribute.value(), tokenInfo(¤tElement) } } });
}
- if (currentElement.hasAttributes()) {
- for (auto& attribute : currentElement.attributesIterator()) {
- if (isAttributeForTextManipulation(currentElement, attribute.name())) {
- addItem(ManipulationItemData { Position(), Position(), makeWeakPtr(currentElement), attribute.name(),
- { ManipulationToken { m_tokenIdentifier.generate(), attribute.value(), tokenInfo(¤tElement) } } });
- }
- }
- }
- if (!enclosingItemBoundaryElement && isEnclosingItemBoundaryElement(currentElement))
- enclosingItemBoundaryElement = ¤tElement;
}
+ if (!enclosingItemBoundaryElement && isEnclosingItemBoundaryElement(currentElement))
+ enclosingItemBoundaryElement = ¤tElement;
}
if (content.isReplacedContent) {
- if (tokensInCurrentParagraph.isEmpty())
- continue;
-
- auto currentEndOfCurrentParagraph = positionAfterNode(content.node.get());
- // This is at the same Node as last token, so it is already included in current range.
- if (!is<Text>(content.node) && currentEndOfCurrentParagraph.equals(endOfCurrentParagraph))
- continue;
-
- endOfCurrentParagraph = currentEndOfCurrentParagraph;
- tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), "[]", tokenInfo(content.node.get()), true });
-
+ if (!tokensInCurrentParagraph.isEmpty()) {
+ tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), "[]", tokenInfo(content.node.get()), true });
+ endOfCurrentParagraph = positionAfterNode(contentNode);
+ }
continue;
}
if (!content.isTextContent)
continue;
- size_t startOfCurrentLine = 0;
- size_t offsetOfNextNewLine = 0;
- StringView currentText = content.text;
- while ((offsetOfNextNewLine = currentText.find('\n', startOfCurrentLine)) != notFound) {
- if (is<Text>(content.node) && startOfCurrentLine < offsetOfNextNewLine) {
- auto stringUntilEndOfLine = currentText.substring(startOfCurrentLine, offsetOfNextNewLine - startOfCurrentLine).toString();
- auto& textNode = downcast<Text>(*content.node);
- endOfCurrentParagraph = Position(&textNode, offsetOfNextNewLine);
- if (tokensInCurrentParagraph.isEmpty())
- startOfCurrentParagraph = Position(&textNode, startOfCurrentLine);
+ auto tokensInCurrentNode = parse(content.text, contentNode);
+ if (!tokensInCurrentParagraph.isEmpty() && tokensInCurrentNode.firstTokenContainsLineBreak)
+ addItem(ManipulationItemData { startOfCurrentParagraph, endOfCurrentParagraph, nullptr, nullQName(), std::exchange(tokensInCurrentParagraph, { }) });
- tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), stringUntilEndOfLine, tokenInfo(&textNode), exclusionRuleMatcher.isExcluded(content.node.get()) });
- }
-
- if (!tokensInCurrentParagraph.isEmpty()) {
- if (is<HTMLBRElement>(content.node))
- endOfCurrentParagraph = positionAfterNode(content.node.get());
- addItem(ManipulationItemData { startOfCurrentParagraph, endOfCurrentParagraph, nullptr, nullQName(), std::exchange(tokensInCurrentParagraph, { }) });
- }
- startOfCurrentLine = offsetOfNextNewLine + 1;
+ if (tokensInCurrentParagraph.isEmpty()) {
+ if (tokensInCurrentNode.containsOnlyHTMLSpace)
+ continue;
+ startOfCurrentParagraph = firstPositionInOrBeforeNode(contentNode);
}
- auto remainingText = currentText.substring(startOfCurrentLine);
- if (!containsOnlyHTMLSpaces(remainingText)) {
- if (tokensInCurrentParagraph.isEmpty()) {
- if (startOfCurrentLine && is<Text>(content.node))
- startOfCurrentParagraph = Position(&downcast<Text>(*content.node), startOfCurrentLine);
- else
- startOfCurrentParagraph = iterator.startPosition();
- }
- endOfCurrentParagraph = iterator.endPosition();
- tokensInCurrentParagraph.append(ManipulationToken { m_tokenIdentifier.generate(), remainingText.toString(), tokenInfo(content.node.get()), exclusionRuleMatcher.isExcluded(content.node.get()) });
+ tokensInCurrentParagraph.appendVector(tokensInCurrentNode.tokens);
+ endOfCurrentParagraph = positionAfterNode(contentNode);
+
+ if (!tokensInCurrentParagraph.isEmpty() && tokensInCurrentNode.lastTokenContainsLineBreak) {
+ ASSERT(!tokensInCurrentParagraph.isEmpty());
+ addItem(ManipulationItemData { startOfCurrentParagraph, endOfCurrentParagraph, nullptr, nullQName(), std::exchange(tokensInCurrentParagraph, { }) });
}
}
@@ -489,6 +498,7 @@
const unsigned itemCallbackBatchingSize = 128;
ASSERT(m_document);
+ ASSERT(!itemData.tokens.isEmpty());
auto newID = m_itemIdentifier.generate();
m_pendingItemsForCallback.append(ManipulationItem {
newID,
@@ -546,19 +556,48 @@
String newData;
};
-struct NodeInsertion {
- RefPtr<Node> parentIfDifferentFromCommonAncestor;
- Ref<Node> child;
-};
+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) -> Optional<ManipulationFailureType>
{
if (item.start.isOrphan() || item.end.isOrphan())
return ManipulationFailureType::ContentChanged;
- 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());
@@ -582,123 +621,114 @@
return WTF::nullopt;
}
+ size_t currentTokenIndex = 0;
+ HashMap<TokenIdentifier, TokenExchangeData> tokenExchangeMap;
RefPtr<Node> commonAncestor;
RefPtr<Node> firstContentNode;
- ParagraphContentIterator iterator { item.start, item.end };
- HashSet<Ref<Node>> excludedNodes;
+ RefPtr<Node> lastChildOfCommonAncestorInRange;
HashSet<Ref<Node>> nodesToRemove;
- RefPtr<Node> nodeToInsertBackAtEnd;
+ ParagraphContentIterator iterator { item.start, item.end };
for (; !iterator.atEnd(); iterator.advance()) {
auto content = iterator.currentContent();
-
- if (content.node)
- nodesToRemove.add(*content.node);
+ ASSERT(content.node);
+
+ lastChildOfCommonAncestorInRange = content.node;
+ nodesToRemove.add(*content.node);
if (!content.isReplacedContent && !content.isTextContent)
continue;
- if (currentTokenIndex >= item.tokens.size()) {
- if (content.node && !nodeToInsertBackAtEnd && content.isTextContent && content.text == "\n") { // br
- nodeToInsertBackAtEnd = content.node;
- continue;
- }
- return ManipulationFailureType::ContentChanged;
- }
+ Vector<ManipulationToken> tokensInCurrentNode;
+ if (content.isReplacedContent)
+ tokensInCurrentNode.append(item.tokens[currentTokenIndex]);
+ else
+ tokensInCurrentNode = parse(content.text, content.node.get()).tokens;
- if (content.isTextContent && containsOnlyHTMLSpaces(content.text)) {
- // <br> should not exist in the middle of a paragraph.
- if (is<HTMLBRElement>(content.node))
+ bool isNodeIncluded = WTF::anyOf(tokensInCurrentNode, [] (auto& token) {
+ return !token.isExcluded;
+ });
+ for (auto& token : tokensInCurrentNode) {
+ if (currentTokenIndex > item.tokens.size())
return ManipulationFailureType::ContentChanged;
- continue;
+
+ auto& currentToken = item.tokens[currentTokenIndex++];
+ if (!content.isReplacedContent && currentToken.content != token.content)
+ return ManipulationFailureType::ContentChanged;
+
+ tokenExchangeMap.set(currentToken.identifier, TokenExchangeData { content.node.copyRef(), currentToken.content, !isNodeIncluded });
}
- auto& currentToken = item.tokens[currentTokenIndex];
- if (!content.isReplacedContent && content.text != currentToken.content)
- return ManipulationFailureType::ContentChanged;
-
- tokenExchangeMap.set(currentToken.identifier, TokenExchangeData { content.node.copyRef(), currentToken.content, currentToken.isExcluded });
- ++currentTokenIndex;
-
if (!firstContentNode)
firstContentNode = content.node;
- // FIXME: Take care of when currentNode is nullptr.
- if (RefPtr<Node> parent = content.node ? content.node->parentNode() : nullptr) {
- if (!commonAncestor)
- commonAncestor = parent;
- else if (!parent->isDescendantOf(commonAncestor.get())) {
- commonAncestor = commonInclusiveAncestor(*commonAncestor, *parent);
- ASSERT(commonAncestor);
- }
+ auto parentNode = content.node->parentNode();
+ if (!commonAncestor)
+ commonAncestor = parentNode;
+ else if (!parentNode->isDescendantOf(commonAncestor.get())) {
+ commonAncestor = commonInclusiveAncestor(*commonAncestor, *parentNode);
+ ASSERT(commonAncestor);
}
}
+ while (lastChildOfCommonAncestorInRange && lastChildOfCommonAncestorInRange->parentNode() != commonAncestor)
+ lastChildOfCommonAncestorInRange = lastChildOfCommonAncestorInRange->parentNode();
+
for (auto node = commonAncestor; node; node = node->parentNode())
nodesToRemove.remove(*node);
- Vector<Ref<Node>> currentElementStack;
HashSet<Ref<Node>> reusedOriginalNodes;
+ Vector<NodeEntry> lastTopDownPath;
Vector<NodeInsertion> insertions;
- for (auto& newToken : replacementTokens) {
- auto it = tokenExchangeMap.find(newToken.identifier);
+ 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> contentNode;
+ RefPtr<Node> replacementNode;
if (exchangeData.isExcluded) {
if (exchangeData.isConsumed)
return ManipulationFailureType::ExclusionViolation;
exchangeData.isConsumed = true;
- if (!newToken.content.isNull() && newToken.content != exchangeData.originalContent)
+
+ if (!replacementToken.content.isNull() && replacementToken.content != exchangeData.originalContent)
return ManipulationFailureType::ExclusionViolation;
- contentNode = exchangeData.node;
- for (RefPtr<Node> currentDescendentNode = NodeTraversal::next(*contentNode, contentNode.get()); currentDescendentNode; currentDescendentNode = NodeTraversal::next(*currentDescendentNode, contentNode.get()))
- nodesToRemove.remove(*currentDescendentNode);
+
+ replacementNode = originalNode;
+ for (RefPtr<Node> descendentNode = NodeTraversal::next(*originalNode, originalNode); descendentNode; descendentNode = NodeTraversal::next(*descendentNode, originalNode))
+ nodesToRemove.remove(*descendentNode);
} else
- contentNode = Text::create(commonAncestor->document(), newToken.content);
+ replacementNode = Text::create(commonAncestor->document(), replacementText);
- 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() });
- }
+ auto topDownPath = getPath(commonAncestor.get(), originalNode);
+ updateInsertions(lastTopDownPath, topDownPath, replacementNode.get(), reusedOriginalNodes, insertions);
}
- if (nodeToInsertBackAtEnd)
- insertions.append(NodeInsertion { nullptr, nodeToInsertBackAtEnd.releaseNonNull() });
+
+ auto end = lastChildOfCommonAncestorInRange ? positionInParentAfterNode(lastChildOfCommonAncestorInRange.get()) : positionAfterNode(commonAncestor.get());
+ RefPtr<Node> node = item.end.firstNode();
+ RefPtr<Node> endNode = end.firstNode();
+ if (node && node != endNode) {
+ auto topDownPath = getPath(commonAncestor.get(), node->parentNode());
+ updateInsertions(lastTopDownPath, topDownPath, nullptr, reusedOriginalNodes, insertions);
+ }
+ while (node != endNode) {
+ 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 });
+ lastTopDownPath.append({ *node, *node });
+ node = NodeTraversal::next(*node);
+ }
Position insertionPoint = positionBeforeNode(firstContentNode.get()).parentAnchoredEquivalent();
while (insertionPoint.containerNode() != commonAncestor)
insertionPoint = positionInParentBeforeNode(insertionPoint.containerNode());
- ASSERT(!insertionPoint.isNull());
- ASSERT(!insertionPoint.isOrphan());
for (auto& node : nodesToRemove)
node->remove();