LayoutTests:
[WebKit-https.git] / WebCore / editing / IndentOutdentCommand.cpp
1 /*
2  * Copyright (C) 2006 Apple Computer, Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (IndentOutdentCommandINCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #include "config.h"
27 #include "Element.h"
28 #include "IndentOutdentCommand.h"
29 #include "InsertListCommand.h"
30 #include "Document.h"
31 #include "htmlediting.h"
32 #include "HTMLElement.h"
33 #include "HTMLNames.h"
34 #include "InsertLineBreakCommand.h"
35 #include "Range.h"
36 #include "SplitElementCommand.h"
37 #include "visible_units.h"
38
39 namespace WebCore {
40
41 using namespace HTMLNames;
42
43 static String indentBlockquoteString()
44 {
45     static String string = "webkit-indent-blockquote";
46     return string;
47 }
48
49 static PassRefPtr<Element> createIndentBlockquoteElement(Document* document)
50 {
51     RefPtr<Element> indentBlockquoteElement = createElement(document, "blockquote");
52     indentBlockquoteElement->setAttribute(classAttr, indentBlockquoteString());
53     return indentBlockquoteElement.release();
54 }
55
56 static bool isIndentBlockquote(Node* node)
57 {
58     if (!node || !node->hasTagName(blockquoteTag) || !node->isElementNode())
59         return false;
60
61     Element* elem = static_cast<Element*>(node);
62     return elem->getAttribute(classAttr) == indentBlockquoteString();
63 }
64
65 static bool isListOrIndentBlockquote(Node* node)
66 {
67     return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || isIndentBlockquote(node));
68 }
69
70 IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels)
71     : CompositeEditCommand(document), m_typeOfAction(typeOfAction), m_marginInPixels(marginInPixels)
72 {}
73
74 // This function is a workaround for moveParagraph's tendency to strip blockquotes. It updates lastBlockquote to point to the
75 // correct level for the current paragraph, and returns a pointer to a placeholder br where the insertion should be performed.
76 Node* IndentOutdentCommand::prepareBlockquoteLevelForInsertion(VisiblePosition& currentParagraph, Node** lastBlockquote)
77 {
78     int currentBlockquoteLevel = 0;
79     int lastBlockquoteLevel = 0;
80     Node* node = currentParagraph.deepEquivalent().node();
81     while ((node = enclosingNodeOfType(node, &isIndentBlockquote)))
82         currentBlockquoteLevel++;
83     node = *lastBlockquote;
84     while ((node = enclosingNodeOfType(node, &isIndentBlockquote)))
85         lastBlockquoteLevel++;
86     while (currentBlockquoteLevel > lastBlockquoteLevel) {
87         RefPtr<Node> newBlockquote = createIndentBlockquoteElement(document());
88         appendNode(newBlockquote.get(), *lastBlockquote);
89         *lastBlockquote = newBlockquote.get();
90         lastBlockquoteLevel++;
91     }
92     while (currentBlockquoteLevel < lastBlockquoteLevel) {
93         *lastBlockquote = enclosingNodeOfType(*lastBlockquote, &isIndentBlockquote);
94         lastBlockquoteLevel--;
95     }
96     RefPtr<Node> placeholder = createBreakElement(document());
97     appendNode(placeholder.get(), *lastBlockquote);
98     // Add another br before the placeholder if it collapsed.
99     VisiblePosition visiblePos(Position(placeholder.get(), 0));
100     if (!isStartOfParagraph(visiblePos))
101         insertNodeBefore(createBreakElement(document()).get(), placeholder.get());
102     return placeholder.get();
103 }
104
105 // Splits the tree parent by parent until we reach the specified ancestor. We use VisiblePositions
106 // to determine if the split is necessary. Returns the last split node.
107 Node* IndentOutdentCommand::splitTreeToNode(Node* start, Node* end, bool splitAncestor)
108 {
109     Node* node;
110     for (node = start; node && node->parent() != end; node = node->parent()) {
111         VisiblePosition positionInParent(Position(node->parent(), 0), DOWNSTREAM);
112         VisiblePosition positionInNode(Position(node, 0), DOWNSTREAM);
113         if (positionInParent != positionInNode)
114             applyCommandToComposite(new SplitElementCommand(static_cast<Element*>(node->parent()), node));
115     }
116     if (splitAncestor)
117         return splitTreeToNode(end, end->parent());
118     return node;
119 }
120
121 void IndentOutdentCommand::indentRegion()
122 {
123     VisiblePosition startOfSelection = endingSelection().visibleStart();
124     VisiblePosition endOfSelection = endingSelection().visibleEnd();
125
126     ASSERT(!startOfSelection.isNull());
127     ASSERT(!endOfSelection.isNull());
128     
129     // Special case empty root editable elements because there's nothing to split
130     // and there's nothing to move.
131     Node* startNode = startOfSelection.deepEquivalent().downstream().node();
132     if (startNode == startNode->rootEditableElement()) {
133         RefPtr<Node> blockquote = createIndentBlockquoteElement(document());
134         insertNodeAt(blockquote.get(), startNode, 0);
135         RefPtr<Node> placeholder = createBreakElement(document());
136         appendNode(placeholder.get(), blockquote.get());
137         setEndingSelection(Selection(Position(placeholder.get(), 0), DOWNSTREAM));
138         return;
139     }
140     
141     Node* previousListNode = 0;
142     Node* newListNode = 0;
143     Node* newBlockquote = 0;
144     VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
145     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
146     while (endOfCurrentParagraph != endAfterSelection) {
147         // Iterate across the selected paragraphs...
148         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
149         Node* listNode = enclosingList(endOfCurrentParagraph.deepEquivalent().node());
150         Node* insertionPoint;
151         if (listNode) {
152             RefPtr<Node> placeholder = createBreakElement(document());
153             insertionPoint = placeholder.get();
154             newBlockquote = 0;
155             RefPtr<Node> listItem = createListItemElement(document());
156             if (listNode == previousListNode) {
157                 // The previous paragraph was inside the same list, so add this list item to the list we already created
158                 appendNode(listItem.get(), newListNode);
159                 appendNode(placeholder.get(), listItem.get());
160             } else {
161                 // Clone the list element, insert it before the current paragraph, and move the paragraph into it.
162                 RefPtr<Node> clonedList = static_cast<Element*>(listNode)->cloneNode(false);
163                 insertNodeBefore(clonedList.get(), enclosingListChild(endOfCurrentParagraph.deepEquivalent().node()));
164                 appendNode(listItem.get(), clonedList.get());
165                 appendNode(placeholder.get(), listItem.get());
166                 newListNode = clonedList.get();
167                 previousListNode = listNode;
168             }
169         } else if (newBlockquote)
170             // The previous paragraph was put into a new blockquote, so move this paragraph there as well
171             insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
172         else {
173             // Create a new blockquote and insert it as a child of the root editable element. We accomplish
174             // this by splitting all parents of the current paragraph up to that point.
175             RefPtr<Node> blockquote = createIndentBlockquoteElement(document());
176             Node* startNode = startOfParagraph(endOfCurrentParagraph).deepEquivalent().node();
177             Node* startOfNewBlock = splitTreeToNode(startNode, startNode->rootEditableElement());
178             insertNodeBefore(blockquote.get(), startOfNewBlock);
179             newBlockquote = blockquote.get();
180             insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
181         }
182         moveParagraph(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, VisiblePosition(Position(insertionPoint, 0)), true);
183         endOfCurrentParagraph = endOfNextParagraph;
184     }
185 }
186
187 void IndentOutdentCommand::outdentParagraph()
188 {
189     VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
190     VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
191
192     Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent().node(), &isListOrIndentBlockquote);
193     if (!enclosingNode)
194         return;
195
196     // Use InsertListCommand to remove the selection from the list
197     if (enclosingNode->hasTagName(olTag)) {
198         applyCommandToComposite(new InsertListCommand(document(), InsertListCommand::OrderedList, ""));
199         return;        
200     } else if (enclosingNode->hasTagName(ulTag)) {
201         applyCommandToComposite(new InsertListCommand(document(), InsertListCommand::UnorderedList, ""));
202         return;
203     }
204     
205     // The selection is inside a blockquote
206     VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
207     VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
208     VisiblePosition endOfEnclosingBlock = endOfBlock(positionInEnclosingBlock);
209     if (visibleStartOfParagraph == startOfEnclosingBlock &&
210         visibleEndOfParagraph == endOfEnclosingBlock) {
211         // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
212         removeNodePreservingChildren(enclosingNode);
213         return;
214     }
215     Node* enclosingBlockFlow = enclosingBlockFlowElement(visibleStartOfParagraph);
216     Node* splitBlockquoteNode = enclosingNode;
217     if (enclosingBlockFlow != enclosingNode)
218         splitBlockquoteNode = splitTreeToNode(enclosingBlockFlowElement(visibleStartOfParagraph), enclosingNode, true);
219     RefPtr<Node> placeholder = createBreakElement(document());
220     insertNodeBefore(placeholder.get(), splitBlockquoteNode);
221     moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
222 }
223
224 void IndentOutdentCommand::outdentRegion()
225 {
226     VisiblePosition startOfSelection = endingSelection().visibleStart();
227     VisiblePosition endOfSelection = endingSelection().visibleEnd();
228     VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
229
230     ASSERT(!startOfSelection.isNull());
231     ASSERT(!endOfSelection.isNull());
232
233     if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
234         outdentParagraph();
235         return;
236     }
237
238     Position originalSelectionEnd = endingSelection().end();
239     setEndingSelection(endingSelection().visibleStart());
240     outdentParagraph();
241     Position originalSelectionStart = endingSelection().start();
242     VisiblePosition endOfCurrentParagraph = endOfParagraph(endOfParagraph(endingSelection().visibleStart()).next(true));
243     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
244     while (endOfCurrentParagraph != endAfterSelection) {
245         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
246         if (endOfCurrentParagraph == endOfLastParagraph)
247             setEndingSelection(Selection(originalSelectionEnd, DOWNSTREAM));
248         else
249             setEndingSelection(endOfCurrentParagraph);
250         outdentParagraph();
251         endOfCurrentParagraph = endOfNextParagraph;
252     }
253     setEndingSelection(Selection(originalSelectionStart, endingSelection().end(), DOWNSTREAM));
254 }
255
256 void IndentOutdentCommand::doApply()
257 {
258     if (endingSelection().isNone())
259         return;
260
261     if (!endingSelection().rootEditableElement())
262         return;
263
264     if (m_typeOfAction == Indent)
265         indentRegion();
266     else
267         outdentRegion();
268 }
269
270 }