/* * Copyright (C) 2005, 2006, 2008 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 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 "config.h" #include "ReplaceSelectionCommand.h" #include "ApplyStyleCommand.h" #include "BeforeTextInsertedEvent.h" #include "BreakBlockquoteCommand.h" #include "CSSComputedStyleDeclaration.h" #include "CSSMutableStyleDeclaration.h" #include "CSSPropertyNames.h" #include "CSSValueKeywords.h" #include "Document.h" #include "DocumentFragment.h" #include "EditingText.h" #include "Element.h" #include "EventNames.h" #include "Frame.h" #include "HTMLElement.h" #include "HTMLInputElement.h" #include "HTMLInterchange.h" #include "HTMLNames.h" #include "NodeList.h" #include "SelectionController.h" #include "SmartReplace.h" #include "TextIterator.h" #include "htmlediting.h" #include "markup.h" #include "visible_units.h" #include #include namespace WebCore { typedef Vector > NodeVector; using namespace HTMLNames; enum EFragmentType { EmptyFragment, SingleTextNodeFragment, TreeFragment }; // --- ReplacementFragment helper class class ReplacementFragment { WTF_MAKE_NONCOPYABLE(ReplacementFragment); public: ReplacementFragment(Document*, DocumentFragment*, bool matchStyle, 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(Node*); private: PassRefPtr insertFragmentForTestRendering(Node* context); void removeUnrenderedNodes(Node*); void restoreTestRenderingNodesToFragment(StyledElement*); void removeInterchangeNodes(Node*); void insertNodeBefore(PassRefPtr node, Node* refNode); RefPtr m_document; RefPtr m_fragment; bool m_matchStyle; bool m_hasInterchangeNewlineAtStart; bool m_hasInterchangeNewlineAtEnd; }; static bool isInterchangeNewlineNode(const Node *node) { DEFINE_STATIC_LOCAL(String, interchangeNewlineClassString, (AppleInterchangeNewline)); return node && node->hasTagName(brTag) && static_cast(node)->getAttribute(classAttr) == interchangeNewlineClassString; } static bool isInterchangeConvertedSpaceSpan(const Node *node) { DEFINE_STATIC_LOCAL(String, 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* stopNode = pos.deprecatedNode()->enclosingBlockFlowElement(); while (stopNode != pos.deprecatedNode() && VisiblePosition(pos) == VisiblePosition(pos.next())) pos = pos.next(); return pos; } ReplacementFragment::ReplacementFragment(Document* document, DocumentFragment* fragment, bool matchStyle, const VisibleSelection& selection) : m_document(document), m_fragment(fragment), m_matchStyle(matchStyle), m_hasInterchangeNewlineAtStart(false), m_hasInterchangeNewlineAtEnd(false) { if (!m_document) return; if (!m_fragment) return; if (!m_fragment->firstChild()) return; Element* editableRoot = selection.rootEditableElement(); ASSERT(editableRoot); if (!editableRoot) return; Node* shadowAncestorNode = editableRoot->shadowAncestorNode(); 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->rendererIsRichlyEditable()) { removeInterchangeNodes(m_fragment.get()); return; } Node* styleNode = selection.base().deprecatedNode(); RefPtr holder = insertFragmentForTestRendering(styleNode); RefPtr range = VisibleSelection::selectionFromContentsOfNode(holder.get()).toNormalizedRange(); String text = plainText(range.get()); // Give the root a chance to change the text. RefPtr evt = BeforeTextInsertedEvent::create(text); ExceptionCode ec = 0; editableRoot->dispatchEvent(evt, ec); ASSERT(ec == 0); if (text != evt->text() || !editableRoot->rendererIsRichlyEditable()) { restoreTestRenderingNodesToFragment(holder.get()); removeNode(holder); m_fragment = createFragmentFromText(selection.toNormalizedRange().get(), evt->text()); if (!m_fragment->firstChild()) return; holder = insertFragmentForTestRendering(styleNode); } removeInterchangeNodes(holder.get()); removeUnrenderedNodes(holder.get()); restoreTestRenderingNodesToFragment(holder.get()); removeNode(holder); } 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(Node *node) { if (!node) return; while (RefPtr n = node->firstChild()) { removeNode(n); insertNodeBefore(n.release(), node); } removeNode(node); } void ReplacementFragment::removeNode(PassRefPtr node) { if (!node) return; ContainerNode* parent = node->parentNode(); if (!parent) return; ExceptionCode ec = 0; parent->removeChild(node.get(), ec); ASSERT(ec == 0); } void ReplacementFragment::insertNodeBefore(PassRefPtr node, Node* refNode) { if (!node || !refNode) return; ContainerNode* parent = refNode->parentNode(); if (!parent) return; ExceptionCode ec = 0; parent->insertBefore(node, refNode, ec); ASSERT(ec == 0); } PassRefPtr ReplacementFragment::insertFragmentForTestRendering(Node* context) { HTMLElement* body = m_document->body(); if (!body) return 0; RefPtr holder = createDefaultParagraphElement(m_document.get()); ExceptionCode ec = 0; // Copy the whitespace and user-select style from the context onto this element. // FIXME: We should examine other style properties to see if they would be appropriate to consider during the test rendering. Node* n = context; while (n && !n->isElementNode()) n = n->parentNode(); if (n) { RefPtr conFontStyle = computedStyle(n); CSSStyleDeclaration* style = holder->style(); style->setProperty(CSSPropertyWhiteSpace, conFontStyle->getPropertyValue(CSSPropertyWhiteSpace), false, ec); ASSERT(ec == 0); style->setProperty(CSSPropertyWebkitUserSelect, conFontStyle->getPropertyValue(CSSPropertyWebkitUserSelect), false, ec); ASSERT(ec == 0); } holder->appendChild(m_fragment, ec); ASSERT(ec == 0); body->appendChild(holder.get(), ec); ASSERT(ec == 0); m_document->updateLayoutIgnorePendingStylesheets(); return holder.release(); } void ReplacementFragment::restoreTestRenderingNodesToFragment(StyledElement* holder) { if (!holder) return; ExceptionCode ec = 0; while (RefPtr node = holder->firstChild()) { holder->removeChild(node.get(), ec); ASSERT(ec == 0); m_fragment->appendChild(node.get(), ec); ASSERT(ec == 0); } } void ReplacementFragment::removeUnrenderedNodes(Node* holder) { Vector unrendered; for (Node* node = holder->firstChild(); node; node = node->traverseNextNode(holder)) if (!isNodeRendered(node) && !isTableStructureNode(node)) unrendered.append(node); size_t n = unrendered.size(); for (size_t i = 0; i < n; ++i) removeNode(unrendered[i]); } void ReplacementFragment::removeInterchangeNodes(Node* container) { // 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) { Node *next = node->traverseNextNode(); if (isInterchangeConvertedSpaceSpan(node)) { RefPtr n = 0; while ((n = node->firstChild())) { removeNode(n); insertNodeBefore(n, node); } removeNode(node); if (n) next = n->traverseNextNode(); } node = next; } } 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, 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) && node->isElementNode() && static_cast(node)->getAttribute(classAttr) == ApplePasteAsQuotation; } // Wrap CompositeEditCommand::removeNodePreservingChildren() so we can update the nodes we track void ReplaceSelectionCommand::removeNodePreservingChildren(Node* node) { if (m_firstNodeInserted == node) m_firstNodeInserted = node->traverseNextNode(); if (m_lastLeafInserted == node) m_lastLeafInserted = node->lastChild() ? node->lastChild() : node->traverseNextSibling(); CompositeEditCommand::removeNodePreservingChildren(node); } // Wrap CompositeEditCommand::removeNodeAndPruneAncestors() so we can update the nodes we track void ReplaceSelectionCommand::removeNodeAndPruneAncestors(Node* node) { // prepare in case m_firstNodeInserted and/or m_lastLeafInserted get removed // FIXME: shouldn't m_lastLeafInserted be adjusted using traversePreviousNode()? Node* afterFirst = m_firstNodeInserted ? m_firstNodeInserted->traverseNextSibling() : 0; Node* afterLast = m_lastLeafInserted ? m_lastLeafInserted->traverseNextSibling() : 0; CompositeEditCommand::removeNodeAndPruneAncestors(node); // adjust m_firstNodeInserted and m_lastLeafInserted since either or both may have been removed if (m_lastLeafInserted && !m_lastLeafInserted->inDocument()) m_lastLeafInserted = afterLast; if (m_firstNodeInserted && !m_firstNodeInserted->inDocument()) m_firstNodeInserted = m_lastLeafInserted && m_lastLeafInserted->inDocument() ? afterFirst : 0; } static bool isHeaderElement(Node* a) { if (!a) return false; return a->hasTagName(h1Tag) || a->hasTagName(h2Tag) || a->hasTagName(h3Tag) || a->hasTagName(h4Tag) || a->hasTagName(h5Tag); } static bool haveSameTagName(Node* a, Node* b) { return a && b && a->isElementNode() && b->isElementNode() && static_cast(a)->tagName() == static_cast(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::negateStyleRulesThatAffectAppearance() { for (RefPtr node = m_firstNodeInserted.get(); node; node = node->traverseNextNode()) { // FIXME: Style rules that match pasted content can change it's appearance if (isStyleSpan(node.get())) { HTMLElement* e = toHTMLElement(node.get()); // 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. if (isBlock(e)) e->getInlineStyleDecl()->setProperty(CSSPropertyDisplay, CSSValueInline); if (e->renderer() && e->renderer()->style()->floating() != FNONE) e->getInlineStyleDecl()->setProperty(CSSPropertyFloat, CSSValueNone); } if (node == m_lastLeafInserted) break; } } void ReplaceSelectionCommand::removeUnrenderedTextNodesAtEnds() { document()->updateLayoutIgnorePendingStylesheets(); if (!m_lastLeafInserted->renderer() && m_lastLeafInserted->isTextNode() && !enclosingNodeWithTag(firstPositionInOrBeforeNode(m_lastLeafInserted.get()), selectTag) && !enclosingNodeWithTag(firstPositionInOrBeforeNode(m_lastLeafInserted.get()), scriptTag)) { if (m_firstNodeInserted == m_lastLeafInserted) { removeNode(m_lastLeafInserted.get()); m_lastLeafInserted = 0; m_firstNodeInserted = 0; return; } RefPtr previous = m_lastLeafInserted->traversePreviousNode(); removeNode(m_lastLeafInserted.get()); m_lastLeafInserted = previous; } // We don't have to make sure that m_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. if (!m_firstNodeInserted->renderer() && m_firstNodeInserted->isTextNode()) { if (m_firstNodeInserted == m_lastLeafInserted) { removeNode(m_firstNodeInserted.get()); m_firstNodeInserted = 0; m_lastLeafInserted = 0; return; } RefPtr next = m_firstNodeInserted->traverseNextSibling(); removeNode(m_firstNodeInserted.get()); m_firstNodeInserted = next; } } void ReplaceSelectionCommand::handlePasteAsQuotationNode() { Node* node = m_firstNodeInserted.get(); if (isMailPasteAsQuotationNode(node)) removeNodeAttribute(static_cast(node), classAttr); } VisiblePosition ReplaceSelectionCommand::positionAtEndOfInsertedContent() { Node* lastNode = m_lastLeafInserted.get(); // FIXME: Why is this hack here? What's special about