WebCore:
[WebKit-https.git] / WebCore / editing / CompositeEditCommand.cpp
index e4dfc4b..976d0f8 100644 (file)
 #include "ApplyStyleCommand.h"
 #include "CSSComputedStyleDeclaration.h"
 #include "CSSMutableStyleDeclaration.h"
+#include "CharacterNames.h"
 #include "DeleteFromTextNodeCommand.h"
 #include "DeleteSelectionCommand.h"
 #include "Document.h"
 #include "DocumentFragment.h"
+#include "EditorInsertAction.h"
 #include "Element.h"
 #include "HTMLNames.h"
 #include "InlineTextBox.h"
 #include "InsertParagraphSeparatorCommand.h"
 #include "InsertTextCommand.h"
 #include "JoinTextNodesCommand.h"
-#include "markup.h"
 #include "MergeIdenticalElementsCommand.h"
 #include "Range.h"
-#include "RebalanceWhitespaceCommand.h"
 #include "RemoveCSSPropertyCommand.h"
 #include "RemoveNodeAttributeCommand.h"
 #include "RemoveNodeCommand.h"
 #include "SplitElementCommand.h"
 #include "SplitTextNodeCommand.h"
 #include "SplitTextNodeContainingElementCommand.h"
+#include "Text.h"
 #include "TextIterator.h"
 #include "WrapContentsInDummySpanCommand.h"
 #include "htmlediting.h"
+#include "markup.h"
 #include "visible_units.h"
 
 using namespace std;
@@ -115,9 +117,9 @@ void CompositeEditCommand::removeStyledElement(Element* element)
     applyCommandToComposite(new ApplyStyleCommand(element, true));
 }
 
-void CompositeEditCommand::insertParagraphSeparator()
+void CompositeEditCommand::insertParagraphSeparator(bool useDefaultParagraphElement)
 {
-    applyCommandToComposite(new InsertParagraphSeparatorCommand(document()));
+    applyCommandToComposite(new InsertParagraphSeparatorCommand(document(), useDefaultParagraphElement));
 }
 
 void CompositeEditCommand::insertNodeBefore(Node* insertChild, Node* refChild)
@@ -137,8 +139,15 @@ void CompositeEditCommand::insertNodeAfter(Node* insertChild, Node* refChild)
     }
 }
 
-void CompositeEditCommand::insertNodeAt(Node* insertChild, Node* refChild, int offset)
+void CompositeEditCommand::insertNodeAt(Node* insertChild, const Position& editingPosition)
 {
+    ASSERT(isEditablePosition(editingPosition));
+    // For editing positions like [table, 0], insert before the table,
+    // likewise for replaced elements, brs, etc.
+    Position p = rangeCompliantEquivalent(editingPosition);
+    Node* refChild = p.node();
+    int offset = p.offset();
+    
     if (canHaveChildrenForEditing(refChild)) {
         Node* child = refChild->firstChild();
         for (int i = 0; child && i < offset; i++)
@@ -295,19 +304,19 @@ void CompositeEditCommand::insertNodeAtTabSpanPosition(Node* node, const Positio
 {
     // insert node before, after, or at split of tab span
     Position insertPos = positionOutsideTabSpan(pos);
-    insertNodeAt(node, insertPos.node(), insertPos.offset());
+    insertNodeAt(node, insertPos);
 }
 
-void CompositeEditCommand::deleteSelection(bool smartDelete, bool mergeBlocksAfterDelete, bool replace)
+void CompositeEditCommand::deleteSelection(bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements)
 {
     if (endingSelection().isRange())
-        applyCommandToComposite(new DeleteSelectionCommand(document(), smartDelete, mergeBlocksAfterDelete, replace));
+        applyCommandToComposite(new DeleteSelectionCommand(document(), smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements));
 }
 
-void CompositeEditCommand::deleteSelection(const Selection &selection, bool smartDelete, bool mergeBlocksAfterDelete, bool replace)
+void CompositeEditCommand::deleteSelection(const Selection &selection, bool smartDelete, bool mergeBlocksAfterDelete, bool replace, bool expandForSpecialElements)
 {
     if (selection.isRange())
-        applyCommandToComposite(new DeleteSelectionCommand(selection, smartDelete, mergeBlocksAfterDelete, replace));
+        applyCommandToComposite(new DeleteSelectionCommand(selection, smartDelete, mergeBlocksAfterDelete, replace, expandForSpecialElements));
 }
 
 void CompositeEditCommand::removeCSSProperty(CSSStyleDeclaration *decl, int property)
@@ -327,27 +336,99 @@ void CompositeEditCommand::setNodeAttribute(Element* element, const QualifiedNam
     applyCommandToComposite(new SetNodeAttributeCommand(element, attribute, value));
 }
 
+static inline bool isWhitespace(UChar c)
+{
+    return c == noBreakSpace || c == ' ' || c == '\n' || c == '\t';
+}
+
+// FIXME: Doesn't go into text nodes that contribute adjacent text (siblings, cousins, etc).
 void CompositeEditCommand::rebalanceWhitespaceAt(const Position& position)
 {
-    Node* textNode = position.node();
-    if (!textNode || !textNode->isTextNode())
+    Node* node = position.node();
+    if (!node || !node->isTextNode())
+        return;
+    Text* textNode = static_cast<Text*>(node);    
+    
+    if (textNode->length() == 0)
+        return;
+    RenderObject* renderer = textNode->renderer();
+    if (renderer && !renderer->style()->collapseWhiteSpace())
+        return;
+        
+    String text = textNode->data();
+    ASSERT(!text.isEmpty());
+
+    int offset = position.offset();
+    // If neither text[offset] nor text[offset - 1] are some form of whitespace, do nothing.
+    if (!isWhitespace(text[offset])) {
+        offset--;
+        if (offset < 0 || !isWhitespace(text[offset]))
+            return;
+    }
+    
+    // Set upstream and downstream to define the extent of the whitespace surrounding text[offset].
+    int upstream = offset;
+    while (upstream > 0 && isWhitespace(text[upstream - 1]))
+        upstream--;
+    
+    int downstream = offset;
+    while ((unsigned)downstream + 1 < text.length() && isWhitespace(text[downstream + 1]))
+        downstream++;
+    
+    int length = downstream - upstream + 1;
+    ASSERT(length > 0);
+    
+    VisiblePosition visibleUpstreamPos(Position(position.node(), upstream));
+    VisiblePosition visibleDownstreamPos(Position(position.node(), downstream + 1));
+    
+    String string = text.substring(upstream, length);
+    String rebalancedString = stringWithRebalancedWhitespace(string,
+    // FIXME: Because of the problem mentioned at the top of this function, we must also use nbsps at the start/end of the string because
+    // this function doesn't get all surrounding whitespace, just the whitespace in the current text node.
+                                                             isStartOfParagraph(visibleUpstreamPos) || upstream == 0, 
+                                                             isEndOfParagraph(visibleDownstreamPos) || (unsigned)downstream == text.length() - 1);
+    
+    if (string != rebalancedString)
+        replaceTextInNode(textNode, upstream, length, rebalancedString);
+}
+
+void CompositeEditCommand::prepareWhitespaceAtPositionForSplit(Position& position)
+{
+    Node* node = position.node();
+    if (!node || !node->isTextNode())
         return;
-    if (static_cast<Text*>(textNode)->length() == 0)
+    Text* textNode = static_cast<Text*>(node);    
+    
+    if (textNode->length() == 0)
         return;
     RenderObject* renderer = textNode->renderer();
     if (renderer && !renderer->style()->collapseWhiteSpace())
         return;
-    applyCommandToComposite(new RebalanceWhitespaceCommand(position));    
+
+    // Delete collapsed whitespace so that inserting nbsps doesn't uncollapse it.
+    Position upstreamPos = position.upstream();
+    deleteInsignificantText(position.upstream(), position.downstream());
+    position = upstreamPos.downstream();
+
+    VisiblePosition visiblePos(position);
+    VisiblePosition previousVisiblePos(visiblePos.next());
+    Position previous(previousVisiblePos.deepEquivalent());
+    
+    if (isCollapsibleWhitespace(previousVisiblePos.characterAfter()) && previous.node()->isTextNode() && !previous.node()->hasTagName(brTag))
+        replaceTextInNode(static_cast<Text*>(previous.node()), previous.offset(), 1, nonBreakingSpaceString());
+    if (isCollapsibleWhitespace(visiblePos.characterAfter()) && position.node()->isTextNode() && !position.node()->hasTagName(brTag))
+        replaceTextInNode(static_cast<Text*>(position.node()), position.offset(), 1, nonBreakingSpaceString());
 }
 
 void CompositeEditCommand::rebalanceWhitespace()
 {
     Selection selection = endingSelection();
-    if (selection.isCaretOrRange()) {
-        rebalanceWhitespaceAt(endingSelection().start());
-        if (selection.isRange())
-            rebalanceWhitespaceAt(endingSelection().end());
-    }
+    if (selection.isNone())
+        return;
+        
+    rebalanceWhitespaceAt(selection.start());
+    if (selection.isRange())
+        rebalanceWhitespaceAt(selection.end());
 }
 
 void CompositeEditCommand::deleteInsignificantText(Text* textNode, int start, int end)
@@ -461,7 +542,7 @@ Node* CompositeEditCommand::insertBlockPlaceholder(const Position& pos)
     ASSERT(pos.node()->renderer());
 
     RefPtr<Node> placeholder = createBlockPlaceholderElement(document());
-    insertNodeAt(placeholder.get(), pos.node(), pos.offset());
+    insertNodeAt(placeholder.get(), pos);
     return placeholder.get();
 }
 
@@ -499,15 +580,15 @@ void CompositeEditCommand::removePlaceholderAt(const VisiblePosition& visiblePos
     if (isEndOfBlock(visiblePosition) && isStartOfParagraph(visiblePosition)) {
         if (p.node()->hasTagName(brTag) && p.offset() == 0)
             removeNode(p.node());
-        else if (p.node()->renderer()->style()->preserveNewline() && visiblePosition.characterAfter() == '\n')
+        else if (lineBreakExistsAtPosition(visiblePosition))
             deleteTextFromNode(static_cast<Text*>(p.node()), p.offset(), 1);
     }
 }
 
-void CompositeEditCommand::moveParagraphContentsToNewBlockIfNecessary(const Position& pos)
+Node* CompositeEditCommand::moveParagraphContentsToNewBlockIfNecessary(const Position& pos)
 {
     if (pos.isNull())
-        return;
+        return 0;
     
     updateLayout();
     
@@ -523,7 +604,7 @@ void CompositeEditCommand::moveParagraphContentsToNewBlockIfNecessary(const Posi
     // If there are no VisiblePositions in the same block as pos then 
     // paragraphStart will be outside the paragraph
     if (Range::compareBoundaryPoints(pos, paragraphStart) < 0)
-        return;
+        return 0;
 
     // Perform some checks to see if we need to perform work in this function.
     if (isBlock(paragraphStart.node())) {
@@ -531,51 +612,28 @@ void CompositeEditCommand::moveParagraphContentsToNewBlockIfNecessary(const Posi
             if (!end.node()->isDescendantOf(paragraphStart.node())) {
                 // If the paragraph end is a descendant of paragraph start, then we need to run
                 // the rest of this function. If not, we can bail here.
-                return;
+                return 0;
             }
         }
         else if (enclosingBlock(end.node()) != paragraphStart.node()) {
             // The visibleEnd.  It must be an ancestor of the paragraph start.
             // We can bail as we have a full block to work with.
             ASSERT(paragraphStart.node()->isDescendantOf(enclosingBlock(end.node())));
-            return;
+            return 0;
         }
         else if (isEndOfDocument(visibleEnd)) {
             // At the end of the document. We can bail here as well.
-            return;
+            return 0;
         }
     }
 
     RefPtr<Node> newBlock = createDefaultParagraphElement(document());
-
-    Node* moveNode = paragraphStart.node();
-    if (paragraphStart.offset() >= paragraphStart.node()->caretMaxOffset())
-        moveNode = moveNode->traverseNextNode();
-    Node* endNode = end.node();
+    appendNode(createBreakElement(document()).get(), newBlock.get());
+    insertNodeAt(newBlock.get(), paragraphStart);
     
-    if (!isAtomicNode(paragraphStart.node()))
-        insertNodeAt(newBlock.get(), paragraphStart.node(), paragraphStart.offset());
-    else {
-        ASSERT(paragraphStart.offset() <= 1);
-        ASSERT(paragraphStart.node()->parentNode());
-        insertNodeAt(newBlock.get(), paragraphStart.node()->parentNode(), paragraphStart.node()->nodeIndex() + paragraphStart.offset());
-    }
-
-    while (moveNode && !isBlock(moveNode)) {
-        Node* next = moveNode->traverseNextSibling();
-        removeNode(moveNode);
-        appendNode(moveNode, newBlock.get());
-        if (moveNode == endNode)
-            break;
-        moveNode = next;
-    }
-}
-
-Node* enclosingAnchorElement(Node* node)
-{
-    while (node && !(node->isElementNode() && node->isLink()))
-        node = node->parentNode();
-    return node;
+    moveParagraphs(visibleParagraphStart, visibleParagraphEnd, VisiblePosition(Position(newBlock.get(), 0)));
+    
+    return newBlock.get();
 }
 
 void CompositeEditCommand::pushAnchorElementDown(Node* anchorNode)
@@ -587,6 +645,9 @@ void CompositeEditCommand::pushAnchorElementDown(Node* anchorNode)
     
     setEndingSelection(Selection::selectionFromContentsOfNode(anchorNode));
     applyStyledElement(static_cast<Element*>(anchorNode));
+    // Clones of anchorNode have been pushed down, now remove it.
+    if (anchorNode->inDocument())
+        removeNodePreservingChildren(anchorNode);
 }
 
 // We must push partially selected anchors down before creating or removing
@@ -599,12 +660,12 @@ void CompositeEditCommand::pushPartiallySelectedAnchorElementsDown()
     VisiblePosition visibleStart(originalSelection.start());
     VisiblePosition visibleEnd(originalSelection.end());
     
-    Node* startAnchor = enclosingAnchorElement(originalSelection.start().node());
+    Node* startAnchor = enclosingAnchorElement(originalSelection.start());
     VisiblePosition startOfStartAnchor(Position(startAnchor, 0));
     if (startAnchor && startOfStartAnchor != visibleStart)
         pushAnchorElementDown(startAnchor);
 
-    Node* endAnchor = enclosingAnchorElement(originalSelection.end().node());
+    Node* endAnchor = enclosingAnchorElement(originalSelection.end());
     VisiblePosition endOfEndAnchor(Position(endAnchor, 0));
     if (endAnchor && endOfEndAnchor != visibleEnd)
         pushAnchorElementDown(endAnchor);
@@ -643,31 +704,38 @@ void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagrap
             startIndex = 0;
             if (startInParagraph) {
                 RefPtr<Range> startRange = new Range(document(), startOfParagraphToMove.deepEquivalent(), visibleStart.deepEquivalent());
-                startIndex = TextIterator::rangeLength(startRange.get());
+                startIndex = TextIterator::rangeLength(startRange.get(), true);
             }
 
             endIndex = 0;
             if (endInParagraph) {
                 RefPtr<Range> endRange = new Range(document(), startOfParagraphToMove.deepEquivalent(), visibleEnd.deepEquivalent());
-                endIndex = TextIterator::rangeLength(endRange.get());
+                endIndex = TextIterator::rangeLength(endRange.get(), true);
             }
         }
     }
     
     VisiblePosition beforeParagraph = startOfParagraphToMove.previous();
+    VisiblePosition afterParagraph(endOfParagraphToMove.next());
 
     // We upstream() the end and downstream() the start so that we don't include collapsed whitespace in the move.
     // When we paste a fragment, spaces after the end and before the start are treated as though they were rendered.    
     Position start = startOfParagraphToMove.deepEquivalent().downstream();
     Position end = endOfParagraphToMove.deepEquivalent().upstream();
-    RefPtr<Range> range = new Range(document(), start.node(), start.offset(), end.node(), end.offset());
+    
+    // start and end can't be used directly to create a Range; they are "editing positions"
+    Position startRangeCompliant = rangeCompliantEquivalent(start);
+    Position endRangeCompliant = rangeCompliantEquivalent(end);
+    RefPtr<Range> range = new Range(document(), startRangeCompliant.node(), startRangeCompliant.offset(), endRangeCompliant.node(), endRangeCompliant.offset());
 
     // FIXME: This is an inefficient way to preserve style on nodes in the paragraph to move.  It 
     // shouldn't matter though, since moved paragraphs will usually be quite small.
-    RefPtr<DocumentFragment> fragment = startOfParagraphToMove != endOfParagraphToMove ? createFragmentFromMarkup(document(), range->toHTML(), "") : 0;
+    RefPtr<DocumentFragment> fragment = startOfParagraphToMove != endOfParagraphToMove ? createFragmentFromMarkup(document(), createMarkup(range.get(), 0, DoNotAnnotateForInterchange, true), "") : 0;
+    
+    // FIXME (5098931): We should add a new insert action "WebViewInsertActionMoved" and call shouldInsertFragment here.
     
     setEndingSelection(Selection(start, end, DOWNSTREAM));
-    deleteSelection(false, false);
+    deleteSelection(false, false, false, false);
 
     ASSERT(destination.deepEquivalent().node()->inDocument());
     
@@ -691,7 +759,7 @@ void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagrap
         // expects this behavior).
         else if (isBlock(node))
             removeNodeAndPruneAncestors(node);
-        else if (node->renderer() && node->renderer()->style()->preserveNewline() && caretAfterDelete.characterAfter() == '\n')
+        else if (lineBreakExistsAtPosition(caretAfterDelete))
             deleteTextFromNode(static_cast<Text*>(node), position.offset(), 1);
     }
 
@@ -701,19 +769,32 @@ void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagrap
     // baz
     // Imagine moving 'bar' to ^.  'bar' will be deleted and its div pruned.  That would
     // cause 'baz' to collapse onto the line with 'foobar' unless we insert a br.
-    if (beforeParagraph.isNotNull() && !isEndOfParagraph(beforeParagraph))
-        insertNodeAt(createBreakElement(document()).get(), beforeParagraph.deepEquivalent().node(), beforeParagraph.deepEquivalent().offset());
+    // Must recononicalize these two VisiblePositions after the pruning above.
+    beforeParagraph = VisiblePosition(beforeParagraph.deepEquivalent());
+    afterParagraph = VisiblePosition(afterParagraph.deepEquivalent());
+    if (beforeParagraph.isNotNull() && (!isEndOfParagraph(beforeParagraph) || beforeParagraph == afterParagraph)) {
+        // FIXME: Trim text between beforeParagraph and afterParagraph if they aren't equal.
+        insertNodeAt(createBreakElement(document()).get(), beforeParagraph.deepEquivalent());
+        // Need an updateLayout here in case inserting the br has split a text node.
+        updateLayout();
+    }
         
-    RefPtr<Range> startToDestinationRange(new Range(document(), Position(document(), 0), destination.deepEquivalent()));
-    destinationIndex = TextIterator::rangeLength(startToDestinationRange.get());
+    RefPtr<Range> startToDestinationRange(new Range(document(), Position(document(), 0), rangeCompliantEquivalent(destination.deepEquivalent())));
+    destinationIndex = TextIterator::rangeLength(startToDestinationRange.get(), true);
     
     setEndingSelection(destination);
-    applyCommandToComposite(new ReplaceSelectionCommand(document(), fragment.get(), true, false, !preserveStyle, false));
+    applyCommandToComposite(new ReplaceSelectionCommand(document(), fragment.get(), true, false, !preserveStyle, false, true));
     
     if (preserveSelection && startIndex != -1) {
-        setEndingSelection(Selection(TextIterator::rangeFromLocationAndLength(document()->documentElement(), destinationIndex + startIndex, 0)->startPosition(), 
-                                     TextIterator::rangeFromLocationAndLength(document()->documentElement(), destinationIndex + endIndex, 0)->startPosition(), 
-                                     DOWNSTREAM));
+        // Fragment creation (using createMarkup) incorrectly uses regular
+        // spaces instead of nbsps for some spaces that were rendered (11475), which
+        // causes spaces to be collapsed during the move operation.  This results
+        // in a call to rangeFromLocationAndLength with a location past the end
+        // of the document (which will return null).
+        RefPtr<Range> start = TextIterator::rangeFromLocationAndLength(document()->documentElement(), destinationIndex + startIndex, 0, true);
+        RefPtr<Range> end = TextIterator::rangeFromLocationAndLength(document()->documentElement(), destinationIndex + endIndex, 0, true);
+        if (start && end)
+            setEndingSelection(Selection(start->startPosition(), end->startPosition(), DOWNSTREAM));
     }
 }
 
@@ -750,6 +831,60 @@ bool CompositeEditCommand::breakOutOfEmptyListItem()
     return true;
 }
 
+// Operations use this function to avoid inserting content into an anchor when at the start or the end of 
+// that anchor, as in NSTextView.
+// FIXME: This is only an approximation of NSTextViews insertion behavior, which varies depending on how
+// the caret was made. 
+Position CompositeEditCommand::positionAvoidingSpecialElementBoundary(const Position& original, bool alwaysAvoidAnchors)
+{
+    if (original.isNull())
+        return original;
+        
+    VisiblePosition visiblePos(original);
+    Node* enclosingAnchor = enclosingAnchorElement(original);
+    Position result = original;
+    // Don't avoid block level anchors, because that would insert content into the wrong paragraph.
+    if (enclosingAnchor && !isBlock(enclosingAnchor)) {
+        VisiblePosition firstInAnchor(Position(enclosingAnchor, 0));
+        VisiblePosition lastInAnchor(Position(enclosingAnchor, maxDeepOffset(enclosingAnchor)));
+        // If visually just after the anchor, insert *inside* the anchor unless it's the last 
+        // VisiblePosition in the document, to match NSTextView.
+        if (visiblePos == lastInAnchor && (isEndOfDocument(visiblePos) || alwaysAvoidAnchors)) {
+            // Make sure anchors are pushed down before avoiding them so that we don't
+            // also avoid structural elements like lists and blocks (5142012).
+            if (original.node() != enclosingAnchor && original.node()->parentNode() != enclosingAnchor) {
+                pushAnchorElementDown(enclosingAnchor);
+                enclosingAnchor = enclosingAnchorElement(original);
+                if (!enclosingAnchor)
+                    return original;
+            }
+            // Don't insert outside an anchor if doing so would skip over a line break.  It would
+            // probably be safe to move the line break so that we could still avoid the anchor here.
+            Position downstream(visiblePos.deepEquivalent().downstream());
+            if (lineBreakExistsAtPosition(visiblePos) && downstream.node()->isDescendantOf(enclosingAnchor))
+                return original;
+            
+            result = positionAfterNode(enclosingAnchor);
+        }
+        // If visually just before an anchor, insert *outside* the anchor unless it's the first
+        // VisiblePosition in a paragraph, to match NSTextView.
+        if (visiblePos == firstInAnchor && (!isStartOfParagraph(visiblePos) || alwaysAvoidAnchors)) {
+            // Make sure anchors are pushed down before avoiding them so that we don't
+            // also avoid structural elements like lists and blocks (5142012).
+            if (original.node() != enclosingAnchor && original.node()->parentNode() != enclosingAnchor) {
+                pushAnchorElementDown(enclosingAnchor);
+                enclosingAnchor = enclosingAnchorElement(original);
+            }
+            result = positionBeforeNode(enclosingAnchor);
+        }
+    }
+        
+    if (result.isNull() || !editableRootForPosition(result))
+        result = original;
+    
+    return result;
+}
+
 PassRefPtr<Element> createBlockPlaceholderElement(Document* document)
 {
     ExceptionCode ec = 0;