/* * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved. * Copyright (C) 2009, 2010, 2011 Google 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 COMPUTER, INC. ``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 COMPUTER, INC. OR * 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 "sky/engine/core/editing/ReplaceSelectionCommand.h" #include "gen/sky/core/CSSPropertyNames.h" #include "gen/sky/core/HTMLNames.h" #include "sky/engine/bindings/exception_state_placeholder.h" #include "sky/engine/core/css/CSSStyleDeclaration.h" #include "sky/engine/core/css/StylePropertySet.h" #include "sky/engine/core/dom/Document.h" #include "sky/engine/core/dom/DocumentFragment.h" #include "sky/engine/core/dom/Element.h" #include "sky/engine/core/dom/Text.h" #include "sky/engine/core/editing/FrameSelection.h" #include "sky/engine/core/editing/HTMLInterchange.h" #include "sky/engine/core/editing/SmartReplace.h" #include "sky/engine/core/editing/TextIterator.h" #include "sky/engine/core/editing/VisibleUnits.h" #include "sky/engine/core/editing/htmlediting.h" #include "sky/engine/core/events/BeforeTextInsertedEvent.h" #include "sky/engine/core/frame/LocalFrame.h" #include "sky/engine/core/html/HTMLElement.h" #include "sky/engine/core/rendering/RenderObject.h" #include "sky/engine/core/rendering/RenderText.h" #include "sky/engine/wtf/StdLibExtras.h" #include "sky/engine/wtf/Vector.h" namespace blink { enum EFragmentType { EmptyFragment, SingleTextNodeFragment, TreeFragment }; // --- ReplacementFragment helper class class ReplacementFragment final { WTF_MAKE_NONCOPYABLE(ReplacementFragment); STACK_ALLOCATED(); public: ReplacementFragment(Document*, DocumentFragment*, const VisibleSelection&); Node* firstChild() const; Node* lastChild() const; bool isEmpty() const; bool hasInterchangeNewlineAtStart() const { return m_hasInterchangeNewlineAtStart; } bool hasInterchangeNewlineAtEnd() const { return m_hasInterchangeNewlineAtEnd; } void removeNode(PassRefPtr); void removeNodePreservingChildren(PassRefPtr); private: PassRefPtr insertFragmentForTestRendering(Element* rootEditableElement); void removeUnrenderedNodes(ContainerNode*); void restoreAndRemoveTestRenderingNodesToFragment(Element*); void removeInterchangeNodes(ContainerNode*); void insertNodeBefore(PassRefPtr, Node* refNode); RefPtr m_document; RefPtr m_fragment; bool m_hasInterchangeNewlineAtStart; bool m_hasInterchangeNewlineAtEnd; }; static Position positionAvoidingPrecedingNodes(Position pos) { // If we're already on a break, it's probably a placeholder and we shouldn't change our position. if (editingIgnoresContent(pos.deprecatedNode())) return pos; // We also stop when changing block flow elements because even though the visual position is the // same. E.g., //
foo^
^ // The two positions above are the same visual position, but we want to stay in the same block. Element* enclosingBlockElement = enclosingBlock(pos.containerNode()); for (Position nextPosition = pos; nextPosition.containerNode() != enclosingBlockElement; pos = nextPosition) { if (lineBreakExistsAtPosition(pos)) break; if (pos.containerNode()->nonShadowBoundaryParentNode()) nextPosition = positionInParentAfterNode(*pos.containerNode()); if (nextPosition == pos || enclosingBlock(nextPosition.containerNode()) != enclosingBlockElement || VisiblePosition(pos) != VisiblePosition(nextPosition)) break; } return pos; } ReplacementFragment::ReplacementFragment(Document* document, DocumentFragment* fragment, const VisibleSelection& selection) : m_document(document), m_fragment(fragment), m_hasInterchangeNewlineAtStart(false), m_hasInterchangeNewlineAtEnd(false) { if (!m_document) return; if (!m_fragment || !m_fragment->hasChildren()) return; RefPtr editableRoot = selection.rootEditableElement(); ASSERT(editableRoot); if (!editableRoot) return; Element* shadowAncestorElement; if (editableRoot->isInShadowTree()) shadowAncestorElement = editableRoot->shadowHost(); else shadowAncestorElement = editableRoot.get(); if (editableRoot->rendererIsRichlyEditable()) { removeInterchangeNodes(m_fragment.get()); return; } RefPtr holder = insertFragmentForTestRendering(editableRoot.get()); if (!holder) { removeInterchangeNodes(m_fragment.get()); return; } RefPtr range = VisibleSelection::selectionFromContentsOfNode(holder.get()).toNormalizedRange(); String text = plainText(range.get(), static_cast(TextIteratorEmitsOriginalText | TextIteratorIgnoresStyleVisibility)); removeInterchangeNodes(holder.get()); removeUnrenderedNodes(holder.get()); restoreAndRemoveTestRenderingNodesToFragment(holder.get()); // Give the root a chance to change the text. RefPtr evt = BeforeTextInsertedEvent::create(text); if (text != evt->text() || !editableRoot->rendererIsRichlyEditable()) { restoreAndRemoveTestRenderingNodesToFragment(holder.get()); m_fragment = nullptr; return; } } bool ReplacementFragment::isEmpty() const { return (!m_fragment || !m_fragment->hasChildren()) && !m_hasInterchangeNewlineAtStart && !m_hasInterchangeNewlineAtEnd; } Node* ReplacementFragment::firstChild() const { return m_fragment ? m_fragment->firstChild() : 0; } Node* ReplacementFragment::lastChild() const { return m_fragment ? m_fragment->lastChild() : 0; } void ReplacementFragment::removeNodePreservingChildren(PassRefPtr node) { if (!node) return; while (RefPtr n = node->firstChild()) { removeNode(n); insertNodeBefore(n.release(), node.get()); } removeNode(node); } void ReplacementFragment::removeNode(PassRefPtr node) { if (!node) return; ContainerNode* parent = node->nonShadowBoundaryParentNode(); if (!parent) return; parent->removeChild(node.get()); } void ReplacementFragment::insertNodeBefore(PassRefPtr node, Node* refNode) { if (!node || !refNode) return; ContainerNode* parent = refNode->nonShadowBoundaryParentNode(); if (!parent) return; parent->insertBefore(node, refNode); } PassRefPtr ReplacementFragment::insertFragmentForTestRendering(Element* rootEditableElement) { ASSERT(m_document); RefPtr holder = createDefaultParagraphElement(*m_document.get()); holder->appendChild(m_fragment); rootEditableElement->appendChild(holder.get()); m_document->updateLayout(); return holder.release(); } void ReplacementFragment::restoreAndRemoveTestRenderingNodesToFragment(Element* holder) { if (!holder) return; while (RefPtr node = holder->firstChild()) { holder->removeChild(node.get()); m_fragment->appendChild(node.get()); } removeNode(holder); } void ReplacementFragment::removeUnrenderedNodes(ContainerNode* holder) { Vector > unrendered; for (Node* node = holder->firstChild(); node; node = NodeTraversal::next(*node, holder)) { if (!isNodeRendered(node)) unrendered.append(node); } size_t n = unrendered.size(); for (size_t i = 0; i < n; ++i) removeNode(unrendered[i]); } void ReplacementFragment::removeInterchangeNodes(ContainerNode* container) { } inline void ReplaceSelectionCommand::InsertedNodes::respondToNodeInsertion(Node& node) { if (!m_firstNodeInserted) m_firstNodeInserted = &node; m_lastNodeInserted = &node; } inline void ReplaceSelectionCommand::InsertedNodes::willRemoveNodePreservingChildren(Node& node) { if (m_firstNodeInserted.get() == node) m_firstNodeInserted = NodeTraversal::next(node); if (m_lastNodeInserted.get() == node) m_lastNodeInserted = node.lastChild() ? node.lastChild() : NodeTraversal::nextSkippingChildren(node); } inline void ReplaceSelectionCommand::InsertedNodes::willRemoveNode(Node& node) { if (m_firstNodeInserted.get() == node && m_lastNodeInserted.get() == node) { m_firstNodeInserted = nullptr; m_lastNodeInserted = nullptr; } else if (m_firstNodeInserted.get() == node) { m_firstNodeInserted = NodeTraversal::nextSkippingChildren(*m_firstNodeInserted); } else if (m_lastNodeInserted.get() == node) { m_lastNodeInserted = NodeTraversal::previousSkippingChildren(*m_lastNodeInserted); } } inline void ReplaceSelectionCommand::InsertedNodes::didReplaceNode(Node& node, Node& newNode) { if (m_firstNodeInserted.get() == node) m_firstNodeInserted = &newNode; if (m_lastNodeInserted.get() == node) m_lastNodeInserted = &newNode; } ReplaceSelectionCommand::ReplaceSelectionCommand(Document& document, PassRefPtr fragment, CommandOptions options, EditAction editAction) : CompositeEditCommand(document) , m_selectReplacement(options & SelectReplacement) , m_smartReplace(options & SmartReplace) , m_matchStyle(options & MatchStyle) , m_documentFragment(fragment) , m_preventNesting(options & PreventNesting) , m_movingParagraph(options & MovingParagraph) , m_editAction(editAction) , m_shouldMergeEnd(false) { } static bool hasMatchingQuoteLevel(VisiblePosition endOfExistingContent, VisiblePosition endOfInsertedContent) { Position existing = endOfExistingContent.deepEquivalent(); Position inserted = endOfInsertedContent.deepEquivalent(); bool isInsideMailBlockquote = enclosingNodeOfType(inserted, isMailHTMLBlockquoteElement, CanCrossEditingBoundary); return isInsideMailBlockquote && (numEnclosingMailBlockquotes(existing) == numEnclosingMailBlockquotes(inserted)); } bool ReplaceSelectionCommand::shouldMergeStart(bool selectionStartWasStartOfParagraph, bool fragmentHasInterchangeNewlineAtStart, bool selectionStartWasInsideMailBlockquote) { if (m_movingParagraph) return false; VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent()); VisiblePosition prev = startOfInsertedContent.previous(CannotCrossEditingBoundary); if (prev.isNull()) return false; // When we have matching quote levels, its ok to merge more frequently. // For a successful merge, we still need to make sure that the inserted content starts with the beginning of a paragraph. // And we should only merge here if the selection start was inside a mail blockquote. This prevents against removing a // blockquote from newly pasted quoted content that was pasted into an unquoted position. If that unquoted position happens // to be right after another blockquote, we don't want to merge and risk stripping a valid block (and newline) from the pasted content. if (isStartOfParagraph(startOfInsertedContent) && selectionStartWasInsideMailBlockquote && hasMatchingQuoteLevel(prev, positionAtEndOfInsertedContent())) return true; return !selectionStartWasStartOfParagraph && !fragmentHasInterchangeNewlineAtStart && isStartOfParagraph(startOfInsertedContent) && shouldMerge(startOfInsertedContent, prev); } bool ReplaceSelectionCommand::shouldMergeEnd(bool selectionEndWasEndOfParagraph) { VisiblePosition endOfInsertedContent(positionAtEndOfInsertedContent()); VisiblePosition next = endOfInsertedContent.next(CannotCrossEditingBoundary); if (next.isNull()) return false; return !selectionEndWasEndOfParagraph && isEndOfParagraph(endOfInsertedContent) && shouldMerge(endOfInsertedContent, next); } static bool isMailPasteAsQuotationHTMLBlockQuoteElement(const Node* node) { return false; } static bool haveSameTagName(Element* a, Element* b) { return a && b && a->tagName() == b->tagName(); } bool ReplaceSelectionCommand::shouldMerge(const VisiblePosition& source, const VisiblePosition& destination) { if (source.isNull() || destination.isNull()) return false; Node* sourceNode = source.deepEquivalent().deprecatedNode(); Node* destinationNode = destination.deepEquivalent().deprecatedNode(); Element* sourceBlock = enclosingBlock(sourceNode); Element* destinationBlock = enclosingBlock(destinationNode); return !enclosingNodeOfType(source.deepEquivalent(), &isMailPasteAsQuotationHTMLBlockQuoteElement) && sourceBlock && (isMailHTMLBlockquoteElement(sourceBlock)) && haveSameTagName(sourceBlock, destinationBlock) // Don't merge to or from a position before or after a block because it would // be a no-op and cause infinite recursion. && !isBlock(sourceNode) && !isBlock(destinationNode); } // Style rules that match just inserted elements could change their appearance, like // a div inserted into a document with div { display:inline; }. void ReplaceSelectionCommand::removeRedundantStylesAndKeepStyleSpanInline(InsertedNodes& insertedNodes) { } void ReplaceSelectionCommand::makeInsertedContentRoundTrippableWithHTMLTreeBuilder(const InsertedNodes& insertedNodes) { } void ReplaceSelectionCommand::moveElementOutOfAncestor(PassRefPtr prpElement, PassRefPtr prpAncestor) { RefPtr element = prpElement; RefPtr ancestor = prpAncestor; if (!ancestor->parentNode()->hasEditableStyle()) return; VisiblePosition positionAtEndOfNode(lastPositionInOrAfterNode(element.get())); VisiblePosition lastPositionInParagraph(lastPositionInNode(ancestor.get())); if (positionAtEndOfNode == lastPositionInParagraph) { removeNode(element); if (ancestor->nextSibling()) insertNodeBefore(element, ancestor->nextSibling()); else appendNode(element, ancestor->parentNode()); } else { RefPtr nodeToSplitTo = splitTreeToNode(element.get(), ancestor.get(), true); removeNode(element); insertNodeBefore(element, nodeToSplitTo); } if (!ancestor->hasChildren()) removeNode(ancestor.release()); } void ReplaceSelectionCommand::removeUnrenderedTextNodesAtEnds(InsertedNodes& insertedNodes) { } VisiblePosition ReplaceSelectionCommand::positionAtEndOfInsertedContent() const { return VisiblePosition(m_endOfInsertedContent); } VisiblePosition ReplaceSelectionCommand::positionAtStartOfInsertedContent() const { return VisiblePosition(m_startOfInsertedContent); } void ReplaceSelectionCommand::mergeEndIfNeeded() { if (!m_shouldMergeEnd) return; VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent()); VisiblePosition endOfInsertedContent(positionAtEndOfInsertedContent()); // Bail to avoid infinite recursion. if (m_movingParagraph) { ASSERT_NOT_REACHED(); return; } // Merging two paragraphs will destroy the moved one's block styles. Always move the end of inserted forward // to preserve the block style of the paragraph already in the document, unless the paragraph to move would // include the what was the start of the selection that was pasted into, so that we preserve that paragraph's // block styles. bool mergeForward = !(inSameParagraph(startOfInsertedContent, endOfInsertedContent) && !isStartOfParagraph(startOfInsertedContent)); VisiblePosition destination = mergeForward ? endOfInsertedContent.next() : endOfInsertedContent; VisiblePosition startOfParagraphToMove = mergeForward ? startOfParagraph(endOfInsertedContent) : endOfInsertedContent.next(); moveParagraph(startOfParagraphToMove, endOfParagraph(startOfParagraphToMove), destination); // Merging forward will remove m_endOfInsertedContent from the document. if (mergeForward) { if (m_startOfInsertedContent.isOrphan()) m_startOfInsertedContent = endingSelection().visibleStart().deepEquivalent(); m_endOfInsertedContent = endingSelection().visibleEnd().deepEquivalent(); // If we merged text nodes, m_endOfInsertedContent could be null. If this is the case, we use m_startOfInsertedContent. if (m_endOfInsertedContent.isNull()) m_endOfInsertedContent = m_startOfInsertedContent; } } static bool isInlineHTMLElementWithStyle(const Node* node) { // We don't want to skip over any block elements. if (isBlock(node)) return false; if (!node->isElementNode()) return false; // We can skip over elements whose class attribute is // one of our internal classes. const Element* element = toElement(node); const AtomicString& classAttributeValue = element->getAttribute(HTMLNames::classAttr); if (classAttributeValue == AppleTabSpanClass) return true; if (classAttributeValue == AppleConvertedSpace) return true; if (classAttributeValue == ApplePasteAsQuotation) return true; return EditingStyle::elementIsStyledSpanOrHTMLEquivalent(element); } static inline HTMLElement* elementToSplitToAvoidPastingIntoInlineElementsWithStyle(const Position& insertionPos) { Element* containingBlock = enclosingBlock(insertionPos.containerNode()); return toHTMLElement(highestEnclosingNodeOfType(insertionPos, isInlineHTMLElementWithStyle, CannotCrossEditingBoundary, containingBlock)); } void ReplaceSelectionCommand::doApply() { VisibleSelection selection = endingSelection(); ASSERT(selection.isCaretOrRange()); ASSERT(selection.start().deprecatedNode()); if (!selection.isNonOrphanedCaretOrRange() || !selection.start().deprecatedNode()) return; if (!selection.rootEditableElement()) return; ReplacementFragment fragment(&document(), m_documentFragment.get(), selection); if (performTrivialReplace(fragment)) return; // We can skip matching the style if the selection is plain text. if ((selection.start().deprecatedNode()->renderer() && selection.start().deprecatedNode()->renderer()->style()->userModify() == READ_WRITE_PLAINTEXT_ONLY) && (selection.end().deprecatedNode()->renderer() && selection.end().deprecatedNode()->renderer()->style()->userModify() == READ_WRITE_PLAINTEXT_ONLY)) m_matchStyle = false; if (m_matchStyle) { m_insertionStyle = EditingStyle::create(selection.start()); m_insertionStyle->mergeTypingStyle(&document()); } VisiblePosition visibleStart = selection.visibleStart(); VisiblePosition visibleEnd = selection.visibleEnd(); bool selectionEndWasEndOfParagraph = isEndOfParagraph(visibleEnd); bool selectionStartWasStartOfParagraph = isStartOfParagraph(visibleStart); Element* enclosingBlockOfVisibleStart = enclosingBlock(visibleStart.deepEquivalent().deprecatedNode()); Position insertionPos = selection.start(); bool startIsInsideMailBlockquote = enclosingNodeOfType(insertionPos, isMailHTMLBlockquoteElement, CanCrossEditingBoundary); bool selectionIsPlainText = !selection.isContentRichlyEditable(); Element* currentRoot = selection.rootEditableElement(); if ((selectionStartWasStartOfParagraph && selectionEndWasEndOfParagraph && !startIsInsideMailBlockquote) || enclosingBlockOfVisibleStart == currentRoot || selectionIsPlainText) m_preventNesting = false; if (selection.isRange()) { // When the end of the selection being pasted into is at the end of a paragraph, and that selection // spans multiple blocks, not merging may leave an empty line. // When the start of the selection being pasted into is at the start of a block, not merging // will leave hanging block(s). // Merge blocks if the start of the selection was in a Mail blockquote, since we handle // that case specially to prevent nesting. bool mergeBlocksAfterDelete = startIsInsideMailBlockquote || isEndOfParagraph(visibleEnd) || isStartOfBlock(visibleStart); // FIXME: We should only expand to include fully selected special elements if we are copying a // selection and pasting it on top of itself. deleteSelection(false, mergeBlocksAfterDelete, false); visibleStart = endingSelection().visibleStart(); if (fragment.hasInterchangeNewlineAtStart()) { if (isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleStart)) { if (!isEndOfEditableOrNonEditableContent(visibleStart)) setEndingSelection(visibleStart.next()); } else insertParagraphSeparator(); } insertionPos = endingSelection().start(); } else { ASSERT(selection.isCaret()); if (fragment.hasInterchangeNewlineAtStart()) { VisiblePosition next = visibleStart.next(CannotCrossEditingBoundary); if (isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleStart) && next.isNotNull()) setEndingSelection(next); else { insertParagraphSeparator(); visibleStart = endingSelection().visibleStart(); } } // We split the current paragraph in two to avoid nesting the blocks from the fragment inside the current block. // For example paste
foo
bar
baz
into
x^x
, where ^ is the caret. // As long as the div styles are the same, visually you'd expect:
xbar
bar
bazx
, // not
xbar
bar
bazx
. // Don't do this if the selection started in a Mail blockquote. if (m_preventNesting && !startIsInsideMailBlockquote && !isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleStart)) { insertParagraphSeparator(); setEndingSelection(endingSelection().visibleStart().previous()); } insertionPos = endingSelection().start(); } // Inserting content could cause whitespace to collapse, e.g. inserting
foo
into hello^ world. prepareWhitespaceAtPositionForSplit(insertionPos); // If the downstream node has been removed there's no point in continuing. if (!insertionPos.downstream().deprecatedNode()) return; RefPtr enclosingBlockOfInsertionPos = enclosingBlock(insertionPos.deprecatedNode()); // Adjust insertionPos to prevent nesting. // If the start was in a Mail blockquote, we will have already handled adjusting insertionPos above. if (m_preventNesting && enclosingBlockOfInsertionPos && !startIsInsideMailBlockquote) { ASSERT(enclosingBlockOfInsertionPos != currentRoot); VisiblePosition visibleInsertionPos(insertionPos); if (isEndOfBlock(visibleInsertionPos) && !(isStartOfBlock(visibleInsertionPos) && fragment.hasInterchangeNewlineAtEnd())) insertionPos = positionInParentAfterNode(*enclosingBlockOfInsertionPos); else if (isStartOfBlock(visibleInsertionPos)) insertionPos = positionInParentBeforeNode(*enclosingBlockOfInsertionPos); } // Paste at start or end of link goes outside of link. insertionPos = positionAvoidingSpecialElementBoundary(insertionPos); // FIXME: Can this wait until after the operation has been performed? There doesn't seem to be // any work performed after this that queries or uses the typing style. if (LocalFrame* frame = document().frame()) frame->selection().clearTypingStyle(); // We don't want the destination to end up inside nodes that weren't selected. To avoid that, we move the // position forward without changing the visible position so we're still at the same visible location, but // outside of preceding tags. insertionPos = positionAvoidingPrecedingNodes(insertionPos); // Paste into run of tabs splits the tab span. insertionPos = positionOutsideTabSpan(insertionPos); // We're finished if there is nothing to add. if (fragment.isEmpty() || !fragment.firstChild()) return; // If we are not trying to match the destination style we prefer a position // that is outside inline elements that provide style. // This way we can produce a less verbose markup. // We can skip this optimization for fragments not wrapped in one of // our style spans and for positions inside list items // since insertAsListItems already does the right thing. if (!m_matchStyle) { if (insertionPos.containerNode()->isTextNode() && insertionPos.offsetInContainerNode() && !insertionPos.atLastEditingPositionForNode()) { splitTextNode(insertionPos.containerText(), insertionPos.offsetInContainerNode()); insertionPos = firstPositionInNode(insertionPos.containerNode()); } if (RefPtr elementToSplitTo = elementToSplitToAvoidPastingIntoInlineElementsWithStyle(insertionPos)) { if (insertionPos.containerNode() != elementToSplitTo->parentNode()) { Node* splitStart = insertionPos.computeNodeAfterPosition(); if (!splitStart) splitStart = insertionPos.containerNode(); RefPtr nodeToSplitTo = splitTreeToNode(splitStart, elementToSplitTo->parentNode()).get(); insertionPos = positionInParentBeforeNode(*nodeToSplitTo); } } } // FIXME: When pasting rich content we're often prevented from heading down the fast path by style spans. Try // again here if they've been removed. // 1) Insert the content. // 2) Remove redundant styles and style tags, this inner for example: foo bar baz. // 3) Merge the start of the added content with the content before the position being pasted into. // 4) Do one of the following: a) expand the last br if the fragment ends with one and it collapsed, // b) merge the last paragraph of the incoming fragment with the paragraph that contained the // end of the selection that was pasted into, or c) handle an interchange newline at the end of the // incoming fragment. // 5) Add spaces for smart replace. // 6) Select the replacement if requested, and match style if requested. InsertedNodes insertedNodes; RefPtr refNode = fragment.firstChild(); ASSERT(refNode); RefPtr node = refNode->nextSibling(); fragment.removeNode(refNode); insertNodeAt(refNode, insertionPos); insertedNodes.respondToNodeInsertion(*refNode); // Mutation events (bug 22634) may have already removed the inserted content if (!refNode->inDocument()) return; while (node) { RefPtr next = node->nextSibling(); fragment.removeNode(node.get()); insertNodeAfter(node, refNode); insertedNodes.respondToNodeInsertion(*node); // Mutation events (bug 22634) may have already removed the inserted content if (!node->inDocument()) return; refNode = node; node = next; } removeUnrenderedTextNodesAtEnds(insertedNodes); // Mutation events (bug 20161) may have already removed the inserted content if (!insertedNodes.firstNodeInserted() || !insertedNodes.firstNodeInserted()->inDocument()) return; // Scripts specified in javascript protocol may remove |enclosingBlockOfInsertionPos| // during insertion, e.g.