/* * 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 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 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 "config.h" #include "ReplaceSelectionCommand.h" #include "ApplyStyleCommand.h" #include "BeforeTextInsertedEvent.h" #include "BreakBlockquoteCommand.h" #include "CSSStyleDeclaration.h" #include "Document.h" #include "DocumentFragment.h" #include "Element.h" #include "ElementIterator.h" #include "EventNames.h" #include "ExceptionCodePlaceholder.h" #include "Frame.h" #include "FrameSelection.h" #include "HTMLInputElement.h" #include "HTMLNames.h" #include "HTMLTitleElement.h" #include "NodeList.h" #include "NodeRenderStyle.h" #include "RenderInline.h" #include "RenderObject.h" #include "RenderText.h" #include "ReplaceDeleteFromTextNodeCommand.h" #include "ReplaceInsertIntoTextNodeCommand.h" #include "SimplifyMarkupCommand.h" #include "SmartReplace.h" #include "StyleProperties.h" #include "Text.h" #include "TextIterator.h" #include "VisibleUnits.h" #include "htmlediting.h" #include "markup.h" #include #include namespace WebCore { using namespace HTMLNames; enum EFragmentType { EmptyFragment, SingleTextNodeFragment, TreeFragment }; // --- ReplacementFragment helper class class ReplacementFragment { WTF_MAKE_NONCOPYABLE(ReplacementFragment); public: ReplacementFragment(Document&, DocumentFragment*, const VisibleSelection&); DocumentFragment* fragment() { return m_fragment.get(); } 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(Node* rootEditableNode); void removeUnrenderedNodes(Node*); void restoreAndRemoveTestRenderingNodesToFragment(StyledElement*); void removeInterchangeNodes(Node*); void insertNodeBefore(PassRefPtr node, Node* refNode); Document& document() { return *m_document; } RefPtr m_document; RefPtr m_fragment; bool m_hasInterchangeNewlineAtStart; bool m_hasInterchangeNewlineAtEnd; }; static bool isInterchangeNewlineNode(const Node *node) { static NeverDestroyed interchangeNewlineClassString(AppleInterchangeNewline); return node && node->hasTagName(brTag) && static_cast(node)->getAttribute(classAttr) == interchangeNewlineClassString; } static bool isInterchangeConvertedSpaceSpan(const Node *node) { static NeverDestroyed convertedSpaceSpanClassString(AppleConvertedSpace); return node->isHTMLElement() && static_cast(node)->getAttribute(classAttr) == convertedSpaceSpanClassString; } 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. Node* enclosingBlockNode = enclosingBlock(pos.containerNode()); for (Position nextPosition = pos; nextPosition.containerNode() != enclosingBlockNode; pos = nextPosition) { if (lineBreakExistsAtPosition(pos)) break; if (pos.containerNode()->nonShadowBoundaryParentNode()) nextPosition = positionInParentAfterNode(pos.containerNode()); if (nextPosition == pos || enclosingBlock(nextPosition.containerNode()) != enclosingBlockNode || 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_fragment) return; if (!m_fragment->firstChild()) return; RefPtr editableRoot = selection.rootEditableElement(); ASSERT(editableRoot); if (!editableRoot) return; Node* shadowAncestorNode = editableRoot->deprecatedShadowAncestorNode(); if (!editableRoot->getAttributeEventListener(eventNames().webkitBeforeTextInsertedEvent) && // FIXME: Remove these checks once textareas and textfields actually register an event handler. !(shadowAncestorNode && shadowAncestorNode->renderer() && shadowAncestorNode->renderer()->isTextControl()) && editableRoot->hasRichlyEditableStyle()) { 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. Ref event = BeforeTextInsertedEvent::create(text); editableRoot->dispatchEvent(event); if (text != event->text() || !editableRoot->hasRichlyEditableStyle()) { restoreAndRemoveTestRenderingNodesToFragment(holder.get()); RefPtr range = selection.toNormalizedRange(); if (!range) return; m_fragment = createFragmentFromText(*range, event->text()); if (!m_fragment->firstChild()) return; holder = insertFragmentForTestRendering(editableRoot.get()); removeInterchangeNodes(holder.get()); removeUnrenderedNodes(holder.get()); restoreAndRemoveTestRenderingNodesToFragment(holder.get()); } } bool ReplacementFragment::isEmpty() const { return (!m_fragment || !m_fragment->firstChild()) && !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, ASSERT_NO_EXCEPTION); } void ReplacementFragment::insertNodeBefore(PassRefPtr node, Node* refNode) { if (!node || !refNode) return; ContainerNode* parent = refNode->nonShadowBoundaryParentNode(); if (!parent) return; parent->insertBefore(*node, refNode, ASSERT_NO_EXCEPTION); } PassRefPtr ReplacementFragment::insertFragmentForTestRendering(Node* rootEditableElement) { RefPtr holder = createDefaultParagraphElement(document()); holder->appendChild(*m_fragment, ASSERT_NO_EXCEPTION); rootEditableElement->appendChild(holder.get(), ASSERT_NO_EXCEPTION); document().updateLayoutIgnorePendingStylesheets(); return holder.release(); } void ReplacementFragment::restoreAndRemoveTestRenderingNodesToFragment(StyledElement* holder) { if (!holder) return; while (RefPtr node = holder->firstChild()) { holder->removeChild(*node, ASSERT_NO_EXCEPTION); m_fragment->appendChild(*node, ASSERT_NO_EXCEPTION); } removeNode(holder); } void ReplacementFragment::removeUnrenderedNodes(Node* holder) { Vector> unrendered; for (Node* node = holder->firstChild(); node; node = NodeTraversal::next(*node, holder)) { if (!isNodeRendered(node) && !isTableStructureNode(node)) unrendered.append(node); } for (auto& node : unrendered) removeNode(node); } void ReplacementFragment::removeInterchangeNodes(Node* container) { m_hasInterchangeNewlineAtStart = false; m_hasInterchangeNewlineAtEnd = false; // Interchange newlines at the "start" of the incoming fragment must be // either the first node in the fragment or the first leaf in the fragment. Node* node = container->firstChild(); while (node) { if (isInterchangeNewlineNode(node)) { m_hasInterchangeNewlineAtStart = true; removeNode(node); break; } node = node->firstChild(); } if (!container->hasChildNodes()) return; // Interchange newlines at the "end" of the incoming fragment must be // either the last node in the fragment or the last leaf in the fragment. node = container->lastChild(); while (node) { if (isInterchangeNewlineNode(node)) { m_hasInterchangeNewlineAtEnd = true; removeNode(node); break; } node = node->lastChild(); } node = container->firstChild(); while (node) { RefPtr next = NodeTraversal::next(*node); if (isInterchangeConvertedSpaceSpan(node)) { next = NodeTraversal::nextSkippingChildren(*node); removeNodePreservingChildren(node); } node = next.get(); } } inline void ReplaceSelectionCommand::InsertedNodes::respondToNodeInsertion(Node* node) { if (!node) return; if (!m_firstNodeInserted) m_firstNodeInserted = node; m_lastNodeInserted = node; } inline void ReplaceSelectionCommand::InsertedNodes::willRemoveNodePreservingChildren(Node* node) { if (m_firstNodeInserted == node) m_firstNodeInserted = NodeTraversal::next(*node); if (m_lastNodeInserted == node) m_lastNodeInserted = node->lastChild() ? node->lastChild() : NodeTraversal::nextSkippingChildren(*node); } inline void ReplaceSelectionCommand::InsertedNodes::willRemoveNode(Node* node) { if (m_firstNodeInserted == node && m_lastNodeInserted == node) { m_firstNodeInserted = nullptr; m_lastNodeInserted = nullptr; } else if (m_firstNodeInserted == node) m_firstNodeInserted = NodeTraversal::nextSkippingChildren(*m_firstNodeInserted); else if (m_lastNodeInserted == node) m_lastNodeInserted = NodeTraversal::previousSkippingChildren(*m_lastNodeInserted); } inline void ReplaceSelectionCommand::InsertedNodes::didReplaceNode(Node* node, Node* newNode) { if (m_firstNodeInserted == node) m_firstNodeInserted = newNode; if (m_lastNodeInserted == node) m_lastNodeInserted = newNode; } ReplaceSelectionCommand::ReplaceSelectionCommand(Document& document, RefPtr&& fragment, CommandOptions options, EditAction editAction) : CompositeEditCommand(document, editAction) , m_selectReplacement(options & SelectReplacement) , m_smartReplace(options & SmartReplace) , m_matchStyle(options & MatchStyle) , m_documentFragment(fragment) , m_preventNesting(options & PreventNesting) , m_movingParagraph(options & MovingParagraph) , m_sanitizeFragment(options & SanitizeFragment) , m_shouldMergeEnd(false) , m_ignoreMailBlockquote(options & IgnoreMailBlockquote) { } static bool hasMatchingQuoteLevel(VisiblePosition endOfExistingContent, VisiblePosition endOfInsertedContent) { Position existing = endOfExistingContent.deepEquivalent(); Position inserted = endOfInsertedContent.deepEquivalent(); bool isInsideMailBlockquote = enclosingNodeOfType(inserted, isMailBlockquote, 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) && !startOfInsertedContent.deepEquivalent().deprecatedNode()->hasTagName(brTag) && 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) && !endOfInsertedContent.deepEquivalent().deprecatedNode()->hasTagName(brTag) && shouldMerge(endOfInsertedContent, next); } static bool isMailPasteAsQuotationNode(const Node* node) { return node && node->hasTagName(blockquoteTag) && downcast(node)->getAttribute(classAttr) == ApplePasteAsQuotation; } static bool isHeaderElement(const Node* a) { if (!a) return false; return a->hasTagName(h1Tag) || a->hasTagName(h2Tag) || a->hasTagName(h3Tag) || a->hasTagName(h4Tag) || a->hasTagName(h5Tag) || a->hasTagName(h6Tag); } static bool haveSameTagName(Node* a, Node* b) { return is(a) && is(b) && downcast(*a).tagName() == downcast(*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(); Node* sourceBlock = enclosingBlock(sourceNode); Node* destinationBlock = enclosingBlock(destinationNode); return !enclosingNodeOfType(source.deepEquivalent(), &isMailPasteAsQuotationNode) && sourceBlock && (!sourceBlock->hasTagName(blockquoteTag) || isMailBlockquote(sourceBlock)) && enclosingListChild(sourceBlock) == enclosingListChild(destinationNode) && enclosingTableCell(source.deepEquivalent()) == enclosingTableCell(destination.deepEquivalent()) && (!isHeaderElement(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) { RefPtr pastEndNode = insertedNodes.pastLastLeaf(); RefPtr next; for (RefPtr node = insertedNodes.firstNodeInserted(); node && node != pastEndNode; node = next) { // FIXME: Style rules that match pasted content can change it's appearance next = NodeTraversal::next(*node); if (!is(*node)) continue; StyledElement* element = downcast(node.get()); const StyleProperties* inlineStyle = element->inlineStyle(); RefPtr newInlineStyle = EditingStyle::create(inlineStyle); if (inlineStyle) { if (is(*element)) { Vector attributes; HTMLElement& htmlElement = downcast(*element); if (newInlineStyle->conflictsWithImplicitStyleOfElement(&htmlElement)) { // e.g. is converted to node = replaceElementWithSpanPreservingChildrenAndAttributes(&htmlElement); element = downcast(node.get()); insertedNodes.didReplaceNode(&htmlElement, node.get()); } else if (newInlineStyle->extractConflictingImplicitStyleOfAttributes(&htmlElement, EditingStyle::PreserveWritingDirection, 0, attributes, EditingStyle::DoNotExtractMatchingStyle)) { // e.g. is converted to for (auto& attribute : attributes) removeNodeAttribute(element, attribute); } } ContainerNode* context = element->parentNode(); // If Mail wraps the fragment with a Paste as Quotation blockquote, or if you're pasting into a quoted region, // styles from blockquoteNode are allowed to override those from the source document, see and . Node* blockquoteNode = isMailPasteAsQuotationNode(context) ? context : enclosingNodeOfType(firstPositionInNode(context), isMailBlockquote, CanCrossEditingBoundary); if (blockquoteNode) newInlineStyle->removeStyleFromRulesAndContext(element, document().documentElement()); newInlineStyle->removeStyleFromRulesAndContext(element, context); } if (!inlineStyle || newInlineStyle->isEmpty()) { if (isStyleSpanOrSpanWithOnlyStyleAttribute(element) || isEmptyFontTag(element, AllowNonEmptyStyleAttribute)) { insertedNodes.willRemoveNodePreservingChildren(element); removeNodePreservingChildren(element); continue; } removeNodeAttribute(element, styleAttr); } else if (newInlineStyle->style()->propertyCount() != inlineStyle->propertyCount()) setNodeAttribute(element, styleAttr, newInlineStyle->style()->asText()); // FIXME: Tolerate differences in id, class, and style attributes. if (element->parentNode() && isNonTableCellHTMLBlockElement(element) && areIdenticalElements(element, element->parentNode()) && VisiblePosition(firstPositionInNode(element->parentNode())) == VisiblePosition(firstPositionInNode(element)) && VisiblePosition(lastPositionInNode(element->parentNode())) == VisiblePosition(lastPositionInNode(element))) { insertedNodes.willRemoveNodePreservingChildren(element); removeNodePreservingChildren(element); continue; } if (element->parentNode() && element->parentNode()->hasRichlyEditableStyle()) removeNodeAttribute(element, contenteditableAttr); // WebKit used to not add display: inline and float: none on copy. // Keep this code around for backward compatibility if (isLegacyAppleStyleSpan(element)) { if (!element->firstChild()) { insertedNodes.willRemoveNodePreservingChildren(element); removeNodePreservingChildren(element); continue; } // There are other styles that style rules can give to style spans, // but these are the two important ones because they'll prevent // inserted content from appearing in the right paragraph. // FIXME: Hyatt is concerned that selectively using display:inline will give inconsistent // results. We already know one issue because td elements ignore their display property // in quirks mode (which Mail.app is always in). We should look for an alternative. // Mutate using the CSSOM wrapper so we get the same event behavior as a script. if (isBlock(element)) element->cssomStyle()->setPropertyInternal(CSSPropertyDisplay, "inline", false, IGNORE_EXCEPTION); if (element->renderer() && element->renderer()->style().isFloating()) element->cssomStyle()->setPropertyInternal(CSSPropertyFloat, "none", false, IGNORE_EXCEPTION); } } } static bool isProhibitedParagraphChild(const AtomicString& name) { // https://dvcs.w3.org/hg/editing/raw-file/57abe6d3cb60/editing.html#prohibited-paragraph-child static NeverDestroyed> elements; if (elements.get().isEmpty()) { elements.get().add(addressTag.localName()); elements.get().add(articleTag.localName()); elements.get().add(asideTag.localName()); elements.get().add(blockquoteTag.localName()); elements.get().add(captionTag.localName()); elements.get().add(centerTag.localName()); elements.get().add(colTag.localName()); elements.get().add(colgroupTag.localName()); elements.get().add(ddTag.localName()); elements.get().add(detailsTag.localName()); elements.get().add(dirTag.localName()); elements.get().add(divTag.localName()); elements.get().add(dlTag.localName()); elements.get().add(dtTag.localName()); elements.get().add(fieldsetTag.localName()); elements.get().add(figcaptionTag.localName()); elements.get().add(figureTag.localName()); elements.get().add(footerTag.localName()); elements.get().add(formTag.localName()); elements.get().add(h1Tag.localName()); elements.get().add(h2Tag.localName()); elements.get().add(h3Tag.localName()); elements.get().add(h4Tag.localName()); elements.get().add(h5Tag.localName()); elements.get().add(h6Tag.localName()); elements.get().add(headerTag.localName()); elements.get().add(hgroupTag.localName()); elements.get().add(hrTag.localName()); elements.get().add(liTag.localName()); elements.get().add(listingTag.localName()); elements.get().add(mainTag.localName()); // Missing in the specification. elements.get().add(menuTag.localName()); elements.get().add(navTag.localName()); elements.get().add(olTag.localName()); elements.get().add(pTag.localName()); elements.get().add(plaintextTag.localName()); elements.get().add(preTag.localName()); elements.get().add(sectionTag.localName()); elements.get().add(summaryTag.localName()); elements.get().add(tableTag.localName()); elements.get().add(tbodyTag.localName()); elements.get().add(tdTag.localName()); elements.get().add(tfootTag.localName()); elements.get().add(thTag.localName()); elements.get().add(theadTag.localName()); elements.get().add(trTag.localName()); elements.get().add(ulTag.localName()); elements.get().add(xmpTag.localName()); } return elements.get().contains(name); } void ReplaceSelectionCommand::makeInsertedContentRoundTrippableWithHTMLTreeBuilder(InsertedNodes& insertedNodes) { RefPtr pastEndNode = insertedNodes.pastLastLeaf(); RefPtr next; for (RefPtr node = insertedNodes.firstNodeInserted(); node && node != pastEndNode; node = next) { next = NodeTraversal::next(*node); if (!is(*node)) continue; if (isProhibitedParagraphChild(downcast(*node).localName())) { if (auto* paragraphElement = enclosingElementWithTag(positionInParentBeforeNode(node.get()), pTag)) { auto* parent = paragraphElement->parentNode(); if (parent && parent->hasEditableStyle()) moveNodeOutOfAncestor(node, paragraphElement, insertedNodes); } } if (isHeaderElement(node.get())) { auto* headerElement = highestEnclosingNodeOfType(positionInParentBeforeNode(node.get()), isHeaderElement); if (headerElement) { if (headerElement->parentNode() && headerElement->parentNode()->isContentRichlyEditable()) moveNodeOutOfAncestor(node, headerElement, insertedNodes); else { HTMLElement* newSpanElement = replaceElementWithSpanPreservingChildrenAndAttributes(downcast(node.get())); insertedNodes.didReplaceNode(node.get(), newSpanElement); } } } } } void ReplaceSelectionCommand::moveNodeOutOfAncestor(PassRefPtr prpNode, PassRefPtr prpAncestor, InsertedNodes& insertedNodes) { RefPtr node = prpNode; RefPtr ancestor = prpAncestor; VisiblePosition positionAtEndOfNode = lastPositionInOrAfterNode(node.get()); VisiblePosition lastPositionInParagraph = lastPositionInNode(ancestor.get()); if (positionAtEndOfNode == lastPositionInParagraph) { removeNode(node); if (ancestor->nextSibling()) insertNodeBefore(node, ancestor->nextSibling()); else appendNode(node, ancestor->parentNode()); } else { RefPtr nodeToSplitTo = splitTreeToNode(node.get(), ancestor.get(), true); removeNode(node); insertNodeBefore(node, nodeToSplitTo); } if (!ancestor->firstChild()) { insertedNodes.willRemoveNode(ancestor.get()); removeNode(ancestor.release()); } } static inline bool hasRenderedText(const Text& text) { return text.renderer() && text.renderer()->hasRenderedText(); } void ReplaceSelectionCommand::removeUnrenderedTextNodesAtEnds(InsertedNodes& insertedNodes) { document().updateLayoutIgnorePendingStylesheets(); Node* lastLeafInserted = insertedNodes.lastLeafInserted(); if (is(lastLeafInserted) && !hasRenderedText(downcast(*lastLeafInserted)) && !enclosingElementWithTag(firstPositionInOrBeforeNode(lastLeafInserted), selectTag) && !enclosingElementWithTag(firstPositionInOrBeforeNode(lastLeafInserted), scriptTag)) { insertedNodes.willRemoveNode(lastLeafInserted); removeNode(lastLeafInserted); } // We don't have to make sure that firstNodeInserted isn't inside a select or script element // because it is a top level node in the fragment and the user can't insert into those elements. Node* firstNodeInserted = insertedNodes.firstNodeInserted(); if (is(firstNodeInserted) && !hasRenderedText(downcast(*firstNodeInserted))) { insertedNodes.willRemoveNode(firstNodeInserted); removeNode(firstNodeInserted); } } VisiblePosition ReplaceSelectionCommand::positionAtEndOfInsertedContent() const { // FIXME: Why is this hack here? What's special about