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
38namespace WebCore {
39
40using namespace HTMLNames;
41
42static 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
50PassRefPtr<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
57HTMLElement* 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
67PassRefPtr<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
88bool 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
106InsertListCommand::InsertListCommand(Document& document, Type type)
107    : CompositeEditCommand(document)
108    , m_type(type)
109{
110}
111
112void 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
195void 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
259void 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
315static 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
334PassRefPtr<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}
405