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