Optimize hasTagName when called on an HTMLElement
[WebKit-https.git] / Source / WebCore / editing / InsertListCommand.cpp
1 /*
2  * Copyright (C) 2006, 2010 Apple 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 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 INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 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 "InsertListCommand.h"
28
29 #include "Element.h"
30 #include "ElementTraversal.h"
31 #include "ExceptionCodePlaceholder.h"
32 #include "htmlediting.h"
33 #include "HTMLElement.h"
34 #include "HTMLNames.h"
35 #include "Range.h"
36 #include "VisibleUnits.h"
37
38 namespace WebCore {
39
40 using namespace HTMLNames;
41
42 static Node* enclosingListChild(Node* node, Node* listNode)
43 {
44     Node* listChild = enclosingListChild(node);
45     while (listChild && enclosingList(listChild) != listNode)
46         listChild = enclosingListChild(listChild->parentNode());
47     return listChild;
48 }
49
50 PassRefPtr<HTMLElement> InsertListCommand::insertList(Document& document, Type type)
51 {
52     RefPtr<InsertListCommand> insertCommand = create(document, type);
53     insertCommand->apply();
54     return insertCommand->m_listElement;
55 }
56
57 HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node)
58 {
59     RefPtr<HTMLElement> listElement = createUnorderedListElement(document());
60     insertNodeBefore(listElement, node);
61     removeNode(node);
62     appendNode(node, listElement);
63     m_listElement = listElement;
64     return listElement.get();
65 }
66
67 PassRefPtr<HTMLElement> InsertListCommand::mergeWithNeighboringLists(PassRefPtr<HTMLElement> passedList)
68 {
69     RefPtr<HTMLElement> list = passedList;
70     Element* previousList = list->previousElementSibling();
71     if (canMergeLists(previousList, list.get()))
72         mergeIdenticalElements(previousList, list);
73
74     if (!list)
75         return 0;
76     Element* sibling = ElementTraversal::nextSibling(list.get());
77     if (!sibling || !sibling->isHTMLElement())
78         return list.release();
79
80     RefPtr<HTMLElement> nextList = toHTMLElement(sibling);
81     if (canMergeLists(list.get(), nextList.get())) {
82         mergeIdenticalElements(list, nextList);
83         return nextList.release();
84     }
85     return list.release();
86 }
87
88 bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const QualifiedName& listTag)
89 {
90     VisiblePosition start = selection.visibleStart();
91
92     if (!enclosingList(start.deepEquivalent().deprecatedNode()))
93         return false;
94
95     VisiblePosition end = startOfParagraph(selection.visibleEnd());
96     while (start.isNotNull() && start != end) {
97         Element* listNode = enclosingList(start.deepEquivalent().deprecatedNode());
98         if (!listNode || !listNode->hasTagName(listTag))
99             return false;
100         start = startOfNextParagraph(start);
101     }
102
103     return true;
104 }
105
106 InsertListCommand::InsertListCommand(Document& document, Type type)
107     : CompositeEditCommand(document)
108     , m_type(type)
109 {
110 }
111
112 void InsertListCommand::doApply()
113 {
114     if (!endingSelection().isNonOrphanedCaretOrRange())
115         return;
116
117     if (!endingSelection().rootEditableElement())
118         return;
119     
120     VisiblePosition visibleEnd = endingSelection().visibleEnd();
121     VisiblePosition visibleStart = endingSelection().visibleStart();
122     // When a selection ends at the start of a paragraph, we rarely paint 
123     // the selection gap before that paragraph, because there often is no gap.  
124     // In a case like this, it's not obvious to the user that the selection 
125     // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordered}List 
126     // operated on that paragraph.
127     // FIXME: We paint the gap before some paragraphs that are indented with left 
128     // margin/padding, but not others.  We should make the gap painting more consistent and 
129     // then use a left margin/padding rule here.
130     if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd, CanSkipOverEditingBoundary))
131         setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(CannotCrossEditingBoundary), endingSelection().isDirectional()));
132
133     auto& listTag = (m_type == OrderedList) ? olTag : ulTag;
134     if (endingSelection().isRange()) {
135         VisibleSelection selection = selectionForParagraphIteration(endingSelection());
136         ASSERT(selection.isRange());
137         VisiblePosition startOfSelection = selection.visibleStart();
138         VisiblePosition endOfSelection = selection.visibleEnd();
139         VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary);
140
141         if (startOfParagraph(startOfSelection, CanSkipOverEditingBoundary) != startOfLastParagraph) {
142             bool forceCreateList = !selectionHasListOfType(selection, listTag);
143
144             RefPtr<Range> currentSelection = endingSelection().firstRange();
145             VisiblePosition startOfCurrentParagraph = startOfSelection;
146             while (!inSameParagraph(startOfCurrentParagraph, startOfLastParagraph, CanCrossEditingBoundary)) {
147                 // doApply() may operate on and remove the last paragraph of the selection from the document 
148                 // if it's in the same list item as startOfCurrentParagraph.  Return early to avoid an 
149                 // infinite loop and because there is no more work to be done.
150                 // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here.  Compute 
151                 // the new location of endOfSelection and use it as the end of the new selection.
152                 if (!startOfLastParagraph.deepEquivalent().anchorNode()->inDocument())
153                     return;
154                 setEndingSelection(startOfCurrentParagraph);
155
156                 // Save and restore endOfSelection and startOfLastParagraph when necessary
157                 // since moveParagraph and movePragraphWithClones can remove nodes.
158                 // FIXME: This is an inefficient way to keep selection alive because indexForVisiblePosition walks from
159                 // the beginning of the document to the endOfSelection everytime this code is executed.
160                 // But not using index is hard because there are so many ways we can lose selection inside doApplyForSingleParagraph.
161                 RefPtr<ContainerNode> scope;
162                 int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, scope);
163                 doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get());
164                 if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) {
165                     endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scope.get());
166                     // If endOfSelection is null, then some contents have been deleted from the document.
167                     // This should never happen and if it did, exit early immediately because we've lost the loop invariant.
168                     ASSERT(endOfSelection.isNotNull());
169                     if (endOfSelection.isNull())
170                         return;
171                     startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary);
172                 }
173
174                 // Fetch the start of the selection after moving the first paragraph,
175                 // because moving the paragraph will invalidate the original start.  
176                 // We'll use the new start to restore the original selection after 
177                 // we modified all selected paragraphs.
178                 if (startOfCurrentParagraph == startOfSelection)
179                     startOfSelection = endingSelection().visibleStart();
180
181                 startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart());
182             }
183             setEndingSelection(endOfSelection);
184             doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get());
185             // Fetch the end of the selection, for the reason mentioned above.
186             endOfSelection = endingSelection().visibleEnd();
187             setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, endingSelection().isDirectional()));
188             return;
189         }
190     }
191
192     doApplyForSingleParagraph(false, listTag, endingSelection().firstRange().get());
193 }
194
195 void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const HTMLQualifiedName& listTag, Range* currentSelection)
196 {
197     // FIXME: This will produce unexpected results for a selection that starts just before a
198     // table and ends inside the first cell, selectionForParagraphIteration should probably
199     // be renamed and deployed inside setEndingSelection().
200     Node* selectionNode = endingSelection().start().deprecatedNode();
201     Node* listChildNode = enclosingListChild(selectionNode);
202     bool switchListType = false;
203     if (listChildNode) {
204         // Remove the list chlild.
205         RefPtr<HTMLElement> listNode = enclosingList(listChildNode);
206         if (!listNode) {
207             listNode = fixOrphanedListChild(listChildNode);
208             listNode = mergeWithNeighboringLists(listNode);
209         }
210         if (!listNode->hasTagName(listTag)) {
211             // listChildNode will be removed from the list and a list of type m_type will be created.
212             switchListType = true;
213         }
214
215         // If the list is of the desired type, and we are not removing the list, then exit early.
216         if (!switchListType && forceCreateList)
217             return;
218
219         // If the entire list is selected, then convert the whole list.
220         if (switchListType && isNodeVisiblyContainedWithin(listNode.get(), currentSelection)) {
221             bool rangeStartIsInList = visiblePositionBeforeNode(listNode.get()) == currentSelection->startPosition();
222             bool rangeEndIsInList = visiblePositionAfterNode(listNode.get()) == currentSelection->endPosition();
223
224             RefPtr<HTMLElement> newList = createHTMLElement(document(), listTag);
225             insertNodeBefore(newList, listNode);
226
227             Node* firstChildInList = enclosingListChild(VisiblePosition(firstPositionInNode(listNode.get())).deepEquivalent().deprecatedNode(), listNode.get());
228             Node* outerBlock = isBlockFlowElement(firstChildInList) ? firstChildInList : listNode.get();
229             
230             moveParagraphWithClones(firstPositionInNode(listNode.get()), lastPositionInNode(listNode.get()), newList.get(), outerBlock);
231
232             // Manually remove listNode because moveParagraphWithClones sometimes leaves it behind in the document.
233             // See the bug 33668 and editing/execCommand/insert-list-orphaned-item-with-nested-lists.html.
234             // FIXME: This might be a bug in moveParagraphWithClones or deleteSelection.
235             if (listNode && listNode->inDocument())
236                 removeNode(listNode);
237
238             newList = mergeWithNeighboringLists(newList);
239
240             // Restore the start and the end of current selection if they started inside listNode
241             // because moveParagraphWithClones could have removed them.
242             if (rangeStartIsInList && newList)
243                 currentSelection->setStart(newList, 0, IGNORE_EXCEPTION);
244             if (rangeEndIsInList && newList)
245                 currentSelection->setEnd(newList, lastOffsetInNode(newList.get()), IGNORE_EXCEPTION);
246
247             setEndingSelection(VisiblePosition(firstPositionInNode(newList.get())));
248
249             return;
250         }
251         
252         unlistifyParagraph(endingSelection().visibleStart(), listNode.get(), listChildNode);
253     }
254
255     if (!listChildNode || switchListType || forceCreateList)
256         m_listElement = listifyParagraph(endingSelection().visibleStart(), listTag);
257 }
258
259 void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listNode, Node* listChildNode)
260 {
261     Node* nextListChild;
262     Node* previousListChild;
263     VisiblePosition start;
264     VisiblePosition end;
265     if (listChildNode->hasTagName(liTag)) {
266         start = firstPositionInNode(listChildNode);
267         end = lastPositionInNode(listChildNode);
268         nextListChild = listChildNode->nextSibling();
269         previousListChild = listChildNode->previousSibling();
270     } else {
271         // A paragraph is visually a list item minus a list marker.  The paragraph will be moved.
272         start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
273         end = endOfParagraph(start, CanSkipOverEditingBoundary);
274         nextListChild = enclosingListChild(end.next().deepEquivalent().deprecatedNode(), listNode);
275         ASSERT(nextListChild != listChildNode);
276         previousListChild = enclosingListChild(start.previous().deepEquivalent().deprecatedNode(), listNode);
277         ASSERT(previousListChild != listChildNode);
278     }
279     // When removing a list, we must always create a placeholder to act as a point of insertion
280     // for the list content being removed.
281     RefPtr<Element> placeholder = createBreakElement(document());
282     RefPtr<Element> nodeToInsert = placeholder;
283     // If the content of the list item will be moved into another list, put it in a list item
284     // so that we don't create an orphaned list child.
285     if (enclosingList(listNode)) {
286         nodeToInsert = createListItemElement(document());
287         appendNode(placeholder, nodeToInsert);
288     }
289
290     if (nextListChild && previousListChild) {
291         // We want to pull listChildNode out of listNode, and place it before nextListChild 
292         // and after previousListChild, so we split listNode and insert it between the two lists.  
293         // But to split listNode, we must first split ancestors of listChildNode between it and listNode,
294         // if any exist.
295         // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove
296         // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is 
297         // unrendered. But we ought to remove nextListChild too, if it is unrendered.
298         splitElement(listNode, splitTreeToNode(nextListChild, listNode));
299         insertNodeBefore(nodeToInsert, listNode);
300     } else if (nextListChild || listChildNode->parentNode() != listNode) {
301         // Just because listChildNode has no previousListChild doesn't mean there isn't any content
302         // in listNode that comes before listChildNode, as listChildNode could have ancestors
303         // between it and listNode. So, we split up to listNode before inserting the placeholder
304         // where we're about to move listChildNode to.
305         if (listChildNode->parentNode() != listNode)
306             splitElement(listNode, splitTreeToNode(listChildNode, listNode).get());
307         insertNodeBefore(nodeToInsert, listNode);
308     } else
309         insertNodeAfter(nodeToInsert, listNode);
310
311     VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placeholder.get()));
312     moveParagraphs(start, end, insertionPoint, true);
313 }
314
315 static Element* adjacentEnclosingList(const VisiblePosition& pos, const VisiblePosition& adjacentPos, const QualifiedName& listTag)
316 {
317     Element* listNode = outermostEnclosingList(adjacentPos.deepEquivalent().deprecatedNode());
318
319     if (!listNode)
320         return 0;
321
322     Node* previousCell = enclosingTableCell(pos.deepEquivalent());
323     Node* currentCell = enclosingTableCell(adjacentPos.deepEquivalent());
324
325     if (!listNode->hasTagName(listTag)
326         || listNode->contains(pos.deepEquivalent().deprecatedNode())
327         || previousCell != currentCell
328         || enclosingList(listNode) != enclosingList(pos.deepEquivalent().deprecatedNode()))
329         return 0;
330
331     return listNode;
332 }
333
334 PassRefPtr<HTMLElement> InsertListCommand::listifyParagraph(const VisiblePosition& originalStart, const QualifiedName& listTag)
335 {
336     VisiblePosition start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
337     VisiblePosition end = endOfParagraph(start, CanSkipOverEditingBoundary);
338     
339     if (start.isNull() || end.isNull())
340         return 0;
341
342     // Check for adjoining lists.
343     RefPtr<HTMLElement> listItemElement = createListItemElement(document());
344     RefPtr<HTMLElement> placeholder = createBreakElement(document());
345     appendNode(placeholder, listItemElement);
346
347     // Place list item into adjoining lists.
348     Element* previousList = adjacentEnclosingList(start.deepEquivalent(), start.previous(CannotCrossEditingBoundary), listTag);
349     Element* nextList = adjacentEnclosingList(start.deepEquivalent(), end.next(CannotCrossEditingBoundary), listTag);
350     RefPtr<HTMLElement> listElement;
351     if (previousList)
352         appendNode(listItemElement, previousList);
353     else if (nextList)
354         insertNodeAt(listItemElement, positionBeforeNode(nextList));
355     else {
356         // Create the list.
357         listElement = createHTMLElement(document(), listTag);
358         appendNode(listItemElement, listElement);
359
360         if (start == end && isBlock(start.deepEquivalent().deprecatedNode())) {
361             // Inserting the list into an empty paragraph that isn't held open 
362             // by a br or a '\n', will invalidate start and end.  Insert 
363             // a placeholder and then recompute start and end.
364             RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent());
365             start = positionBeforeNode(placeholder.get());
366             end = start;
367         }
368
369         // Insert the list at a position visually equivalent to start of the
370         // paragraph that is being moved into the list. 
371         // Try to avoid inserting it somewhere where it will be surrounded by 
372         // inline ancestors of start, since it is easier for editing to produce 
373         // clean markup when inline elements are pushed down as far as possible.
374         Position insertionPos(start.deepEquivalent().upstream());
375         // Also avoid the containing list item.
376         Node* listChild = enclosingListChild(insertionPos.deprecatedNode());
377         if (listChild && listChild->hasTagName(liTag))
378             insertionPos = positionInParentBeforeNode(listChild);
379
380         insertNodeAt(listElement, insertionPos);
381
382         // We inserted the list at the start of the content we're about to move
383         // Update the start of content, so we don't try to move the list into itself.  bug 19066
384         // Layout is necessary since start's node's inline renderers may have been destroyed by the insertion
385         // The end of the content may have changed after the insertion and layout so update it as well.
386         if (insertionPos == start.deepEquivalent()) {
387             listElement->document().updateLayoutIgnorePendingStylesheets();
388             start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
389             end = endOfParagraph(start, CanSkipOverEditingBoundary);
390         }
391     }
392
393     moveParagraph(start, end, positionBeforeNode(placeholder.get()), true);
394
395     if (listElement)
396         return mergeWithNeighboringLists(listElement);
397
398     if (canMergeLists(previousList, nextList))
399         mergeIdenticalElements(previousList, nextList);
400
401     return listElement;
402 }
403
404 }