1 | /* |
2 | * Copyright (C) 2005, 2006 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 "InsertParagraphSeparatorCommand.h" |
28 | |
29 | #include "Document.h" |
30 | #include "Editing.h" |
31 | #include "EditingStyle.h" |
32 | #include "HTMLBRElement.h" |
33 | #include "HTMLFormElement.h" |
34 | #include "HTMLNames.h" |
35 | #include "InsertLineBreakCommand.h" |
36 | #include "NodeTraversal.h" |
37 | #include "RenderText.h" |
38 | #include "Text.h" |
39 | #include "VisibleUnits.h" |
40 | |
41 | namespace WebCore { |
42 | |
43 | using namespace HTMLNames; |
44 | |
45 | // When inserting a new line, we want to avoid nesting empty divs if we can. Otherwise, when |
46 | // pasting, it's easy to have each new line be a div deeper than the previous. E.g., in the case |
47 | // below, we want to insert at ^ instead of |. |
48 | // <div>foo<div>bar</div>|</div>^ |
49 | static Element* highestVisuallyEquivalentDivBelowRoot(Element* startBlock) |
50 | { |
51 | Element* curBlock = startBlock; |
52 | // We don't want to return a root node (if it happens to be a div, e.g., in a document fragment) because there are no |
53 | // siblings for us to append to. |
54 | while (!curBlock->nextSibling() && curBlock->parentElement()->hasTagName(divTag) && curBlock->parentElement()->parentElement()) { |
55 | if (curBlock->parentElement()->hasAttributes()) |
56 | break; |
57 | curBlock = curBlock->parentElement(); |
58 | } |
59 | return curBlock; |
60 | } |
61 | |
62 | InsertParagraphSeparatorCommand::InsertParagraphSeparatorCommand(Document& document, bool mustUseDefaultParagraphElement, bool pasteBlockqutoeIntoUnquotedArea, EditAction editingAction) |
63 | : CompositeEditCommand(document, editingAction) |
64 | , m_mustUseDefaultParagraphElement(mustUseDefaultParagraphElement) |
65 | , m_pasteBlockqutoeIntoUnquotedArea(pasteBlockqutoeIntoUnquotedArea) |
66 | { |
67 | } |
68 | |
69 | bool InsertParagraphSeparatorCommand::preservesTypingStyle() const |
70 | { |
71 | return true; |
72 | } |
73 | |
74 | void InsertParagraphSeparatorCommand::calculateStyleBeforeInsertion(const Position &pos) |
75 | { |
76 | // It is only important to set a style to apply later if we're at the boundaries of |
77 | // a paragraph. Otherwise, content that is moved as part of the work of the command |
78 | // will lend their styles to the new paragraph without any extra work needed. |
79 | VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY); |
80 | if (!isStartOfParagraph(visiblePos) && !isEndOfParagraph(visiblePos)) |
81 | return; |
82 | |
83 | ASSERT(pos.isNotNull()); |
84 | m_style = EditingStyle::create(pos, EditingStyle::EditingPropertiesInEffect); |
85 | m_style->mergeTypingStyle(pos.anchorNode()->document()); |
86 | } |
87 | |
88 | void InsertParagraphSeparatorCommand::applyStyleAfterInsertion(Node* originalEnclosingBlock) |
89 | { |
90 | // Not only do we break out of header tags, but we also do not preserve the typing style, |
91 | // in order to match other browsers. |
92 | if (originalEnclosingBlock->hasTagName(h1Tag) || |
93 | originalEnclosingBlock->hasTagName(h2Tag) || |
94 | originalEnclosingBlock->hasTagName(h3Tag) || |
95 | originalEnclosingBlock->hasTagName(h4Tag) || |
96 | originalEnclosingBlock->hasTagName(h5Tag)) |
97 | return; |
98 | |
99 | if (!m_style) |
100 | return; |
101 | |
102 | m_style->prepareToApplyAt(endingSelection().start()); |
103 | if (!m_style->isEmpty()) |
104 | applyStyle(m_style.get()); |
105 | } |
106 | |
107 | bool InsertParagraphSeparatorCommand::shouldUseDefaultParagraphElement(Node* enclosingBlock) const |
108 | { |
109 | if (m_mustUseDefaultParagraphElement) |
110 | return true; |
111 | |
112 | // Assumes that if there was a range selection, it was already deleted. |
113 | if (!isEndOfBlock(endingSelection().visibleStart())) |
114 | return false; |
115 | |
116 | return enclosingBlock->hasTagName(h1Tag) || |
117 | enclosingBlock->hasTagName(h2Tag) || |
118 | enclosingBlock->hasTagName(h3Tag) || |
119 | enclosingBlock->hasTagName(h4Tag) || |
120 | enclosingBlock->hasTagName(h5Tag); |
121 | } |
122 | |
123 | void InsertParagraphSeparatorCommand::getAncestorsInsideBlock(const Node* insertionNode, Element* outerBlock, Vector<RefPtr<Element>>& ancestors) |
124 | { |
125 | ancestors.clear(); |
126 | |
127 | // Build up list of ancestors elements between the insertion node and the outer block. |
128 | if (insertionNode != outerBlock) { |
129 | for (Element* n = insertionNode->parentElement(); n && n != outerBlock; n = n->parentElement()) |
130 | ancestors.append(n); |
131 | } |
132 | } |
133 | |
134 | Ref<Element> InsertParagraphSeparatorCommand::cloneHierarchyUnderNewBlock(const Vector<RefPtr<Element>>& ancestors, Ref<Element>&& blockToInsert) |
135 | { |
136 | // Make clones of ancestors in between the start node and the start block. |
137 | RefPtr<Element> parent = WTFMove(blockToInsert); |
138 | for (size_t i = ancestors.size(); i != 0; --i) { |
139 | auto child = ancestors[i - 1]->cloneElementWithoutChildren(document()); |
140 | // It should always be okay to remove id from the cloned elements, since the originals are not deleted. |
141 | child->removeAttribute(idAttr); |
142 | appendNode(child.copyRef(), parent.releaseNonNull()); |
143 | parent = WTFMove(child); |
144 | } |
145 | |
146 | return parent.releaseNonNull(); |
147 | } |
148 | |
149 | void InsertParagraphSeparatorCommand::doApply() |
150 | { |
151 | if (endingSelection().isNoneOrOrphaned()) |
152 | return; |
153 | |
154 | Position insertionPosition = endingSelection().start(); |
155 | |
156 | EAffinity affinity = endingSelection().affinity(); |
157 | |
158 | // Delete the current selection. |
159 | if (endingSelection().isRange()) { |
160 | calculateStyleBeforeInsertion(insertionPosition); |
161 | deleteSelection(false, true); |
162 | insertionPosition = endingSelection().start(); |
163 | affinity = endingSelection().affinity(); |
164 | } |
165 | |
166 | // FIXME: The parentAnchoredEquivalent conversion needs to be moved into enclosingBlock. |
167 | RefPtr<Element> startBlock = enclosingBlock(insertionPosition.parentAnchoredEquivalent().containerNode()); |
168 | Position canonicalPos = VisiblePosition(insertionPosition).deepEquivalent(); |
169 | if (!startBlock |
170 | || !startBlock->nonShadowBoundaryParentNode() |
171 | || isTableCell(startBlock.get()) |
172 | || is<HTMLFormElement>(*startBlock) |
173 | // FIXME: If the node is hidden, we don't have a canonical position so we will do the wrong thing for tables and <hr>. https://bugs.webkit.org/show_bug.cgi?id=40342 |
174 | || (!canonicalPos.isNull() && canonicalPos.deprecatedNode()->renderer() && canonicalPos.deprecatedNode()->renderer()->isTable()) |
175 | || (!canonicalPos.isNull() && canonicalPos.deprecatedNode()->hasTagName(hrTag))) { |
176 | applyCommandToComposite(InsertLineBreakCommand::create(document())); |
177 | return; |
178 | } |
179 | |
180 | // Use the leftmost candidate. |
181 | insertionPosition = insertionPosition.upstream(); |
182 | if (!insertionPosition.isCandidate()) |
183 | insertionPosition = insertionPosition.downstream(); |
184 | |
185 | // Adjust the insertion position after the delete |
186 | insertionPosition = positionAvoidingSpecialElementBoundary(insertionPosition); |
187 | VisiblePosition visiblePos(insertionPosition, affinity); |
188 | if (visiblePos.isNull()) |
189 | return; |
190 | |
191 | calculateStyleBeforeInsertion(insertionPosition); |
192 | |
193 | //--------------------------------------------------------------------- |
194 | // Handle special case of typing return on an empty list item |
195 | if (breakOutOfEmptyListItem()) |
196 | return; |
197 | |
198 | //--------------------------------------------------------------------- |
199 | // Prepare for more general cases. |
200 | |
201 | bool isFirstInBlock = isStartOfBlock(visiblePos); |
202 | bool isLastInBlock = isEndOfBlock(visiblePos); |
203 | bool nestNewBlock = false; |
204 | |
205 | // Create block to be inserted. |
206 | RefPtr<Element> blockToInsert; |
207 | if (startBlock->isRootEditableElement()) { |
208 | blockToInsert = createDefaultParagraphElement(document()); |
209 | nestNewBlock = true; |
210 | } else if (shouldUseDefaultParagraphElement(startBlock.get())) |
211 | blockToInsert = createDefaultParagraphElement(document()); |
212 | else |
213 | blockToInsert = startBlock->cloneElementWithoutChildren(document()); |
214 | |
215 | //--------------------------------------------------------------------- |
216 | // Handle case when position is in the last visible position in its block, |
217 | // including when the block is empty. |
218 | if (isLastInBlock) { |
219 | if (nestNewBlock) { |
220 | if (isFirstInBlock && !lineBreakExistsAtVisiblePosition(visiblePos)) { |
221 | // The block is empty. Create an empty block to |
222 | // represent the paragraph that we're leaving. |
223 | auto = createDefaultParagraphElement(document()); |
224 | appendNode(extraBlock.copyRef(), *startBlock); |
225 | appendBlockPlaceholder(WTFMove(extraBlock)); |
226 | } |
227 | appendNode(*blockToInsert, *startBlock); |
228 | } else { |
229 | // We can get here if we pasted a copied portion of a blockquote with a newline at the end and are trying to paste it |
230 | // into an unquoted area. We then don't want the newline within the blockquote or else it will also be quoted. |
231 | if (m_pasteBlockqutoeIntoUnquotedArea) { |
232 | if (Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote)) |
233 | startBlock = downcast<Element>(highestBlockquote); |
234 | } |
235 | |
236 | // Most of the time we want to stay at the nesting level of the startBlock (e.g., when nesting within lists). However, |
237 | // for div nodes, this can result in nested div tags that are hard to break out of. |
238 | Element* siblingNode = startBlock.get(); |
239 | if (blockToInsert->hasTagName(divTag)) |
240 | siblingNode = highestVisuallyEquivalentDivBelowRoot(startBlock.get()); |
241 | insertNodeAfter(*blockToInsert, *siblingNode); |
242 | } |
243 | |
244 | // Recreate the same structure in the new paragraph. |
245 | |
246 | Vector<RefPtr<Element>> ancestors; |
247 | getAncestorsInsideBlock(positionOutsideTabSpan(insertionPosition).deprecatedNode(), startBlock.get(), ancestors); |
248 | auto parent = cloneHierarchyUnderNewBlock(ancestors, *blockToInsert); |
249 | auto* parentPtr = parent.ptr(); |
250 | |
251 | appendBlockPlaceholder(WTFMove(parent)); |
252 | |
253 | setEndingSelection(VisibleSelection(firstPositionInNode(parentPtr), DOWNSTREAM, endingSelection().isDirectional())); |
254 | return; |
255 | } |
256 | |
257 | |
258 | //--------------------------------------------------------------------- |
259 | // Handle case when position is in the first visible position in its block, and |
260 | // similar case where previous position is in another, presumeably nested, block. |
261 | if (isFirstInBlock || !inSameBlock(visiblePos, visiblePos.previous())) { |
262 | Node *refNode; |
263 | |
264 | insertionPosition = positionOutsideTabSpan(insertionPosition); |
265 | |
266 | if (isFirstInBlock && !nestNewBlock) |
267 | refNode = startBlock.get(); |
268 | else if (isFirstInBlock && nestNewBlock) { |
269 | // startBlock should always have children, otherwise isLastInBlock would be true and it's handled above. |
270 | ASSERT(startBlock->firstChild()); |
271 | refNode = startBlock->firstChild(); |
272 | } else if (insertionPosition.containerNode() == startBlock && nestNewBlock) { |
273 | refNode = startBlock->traverseToChildAt(insertionPosition.computeOffsetInContainerNode()); |
274 | ASSERT(refNode); // must be true or we'd be in the end of block case |
275 | } else |
276 | refNode = insertionPosition.deprecatedNode(); |
277 | |
278 | // find ending selection position easily before inserting the paragraph |
279 | insertionPosition = insertionPosition.downstream(); |
280 | |
281 | insertNodeBefore(*blockToInsert, *refNode); |
282 | |
283 | // Recreate the same structure in the new paragraph. |
284 | |
285 | Vector<RefPtr<Element>> ancestors; |
286 | getAncestorsInsideBlock(positionAvoidingSpecialElementBoundary(positionOutsideTabSpan(insertionPosition)).deprecatedNode(), startBlock.get(), ancestors); |
287 | |
288 | appendBlockPlaceholder(cloneHierarchyUnderNewBlock(ancestors, *blockToInsert)); |
289 | |
290 | // In this case, we need to set the new ending selection. |
291 | setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM, endingSelection().isDirectional())); |
292 | return; |
293 | } |
294 | |
295 | //--------------------------------------------------------------------- |
296 | // Handle the (more complicated) general case, |
297 | |
298 | // All of the content in the current block after visiblePos is |
299 | // about to be wrapped in a new paragraph element. Add a br before |
300 | // it if visiblePos is at the start of a paragraph so that the |
301 | // content will move down a line. |
302 | if (isStartOfParagraph(visiblePos)) { |
303 | auto br = HTMLBRElement::create(document()); |
304 | auto* brPtr = br.ptr(); |
305 | insertNodeAt(WTFMove(br), insertionPosition); |
306 | insertionPosition = positionInParentAfterNode(brPtr); |
307 | // If the insertion point is a break element, there is nothing else |
308 | // we need to do. |
309 | if (visiblePos.deepEquivalent().anchorNode()->renderer()->isBR()) { |
310 | setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM, endingSelection().isDirectional())); |
311 | return; |
312 | } |
313 | } |
314 | |
315 | // Move downstream. Typing style code will take care of carrying along the |
316 | // style of the upstream position. |
317 | insertionPosition = insertionPosition.downstream(); |
318 | |
319 | // At this point, the insertionPosition's node could be a container, and we want to make sure we include |
320 | // all of the correct nodes when building the ancestor list. So this needs to be the deepest representation of the position |
321 | // before we walk the DOM tree. |
322 | insertionPosition = positionOutsideTabSpan(VisiblePosition(insertionPosition).deepEquivalent()); |
323 | |
324 | // If the returned position lies either at the end or at the start of an element that is ignored by editing |
325 | // we should move to its upstream or downstream position. |
326 | if (editingIgnoresContent(*insertionPosition.deprecatedNode())) { |
327 | if (insertionPosition.atLastEditingPositionForNode()) |
328 | insertionPosition = insertionPosition.downstream(); |
329 | else if (insertionPosition.atFirstEditingPositionForNode()) |
330 | insertionPosition = insertionPosition.upstream(); |
331 | } |
332 | |
333 | // Make sure we do not cause a rendered space to become unrendered. |
334 | // FIXME: We need the affinity for pos, but pos.downstream() does not give it |
335 | Position leadingWhitespace = insertionPosition.leadingWhitespacePosition(VP_DEFAULT_AFFINITY); |
336 | // FIXME: leadingWhitespacePosition is returning the position before preserved newlines for positions |
337 | // after the preserved newline, causing the newline to be turned into a nbsp. |
338 | if (is<Text>(leadingWhitespace.deprecatedNode())) { |
339 | Text& textNode = downcast<Text>(*leadingWhitespace.deprecatedNode()); |
340 | ASSERT(!textNode.renderer() || textNode.renderer()->style().collapseWhiteSpace()); |
341 | replaceTextInNodePreservingMarkers(textNode, leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); |
342 | } |
343 | |
344 | // Split at pos if in the middle of a text node. |
345 | Position positionAfterSplit; |
346 | if (insertionPosition.anchorType() == Position::PositionIsOffsetInAnchor && is<Text>(*insertionPosition.containerNode())) { |
347 | Ref<Text> textNode = downcast<Text>(*insertionPosition.containerNode()); |
348 | bool atEnd = static_cast<unsigned>(insertionPosition.offsetInContainerNode()) >= textNode->length(); |
349 | if (insertionPosition.deprecatedEditingOffset() > 0 && !atEnd) { |
350 | splitTextNode(textNode, insertionPosition.offsetInContainerNode()); |
351 | positionAfterSplit = firstPositionInNode(textNode.ptr()); |
352 | if (!textNode->previousSibling()) |
353 | return; // Bail out if mutation events detachd the split text node. |
354 | insertionPosition.moveToPosition(textNode->previousSibling(), insertionPosition.offsetInContainerNode()); |
355 | visiblePos = VisiblePosition(insertionPosition); |
356 | } |
357 | } |
358 | |
359 | // If we got detached due to mutation events, just bail out. |
360 | if (!startBlock->parentNode()) |
361 | return; |
362 | |
363 | // Put the added block in the tree. |
364 | if (nestNewBlock) |
365 | appendNode(*blockToInsert, *startBlock); |
366 | else |
367 | insertNodeAfter(*blockToInsert, *startBlock); |
368 | |
369 | document().updateLayoutIgnorePendingStylesheets(); |
370 | |
371 | // If the paragraph separator was inserted at the end of a paragraph, an empty line must be |
372 | // created. All of the nodes, starting at visiblePos, are about to be added to the new paragraph |
373 | // element. If the first node to be inserted won't be one that will hold an empty line open, add a br. |
374 | if (isEndOfParagraph(visiblePos) && !lineBreakExistsAtVisiblePosition(visiblePos)) |
375 | appendNode(HTMLBRElement::create(document()), *blockToInsert); |
376 | |
377 | // Move the start node and the siblings of the start node. |
378 | if (VisiblePosition(insertionPosition) != VisiblePosition(positionBeforeNode(blockToInsert.get()))) { |
379 | Node* n; |
380 | if (insertionPosition.containerNode() == startBlock) |
381 | n = insertionPosition.computeNodeAfterPosition(); |
382 | else { |
383 | Node* splitTo = insertionPosition.containerNode(); |
384 | if (is<Text>(*splitTo) && insertionPosition.offsetInContainerNode() >= caretMaxOffset(*splitTo)) |
385 | splitTo = NodeTraversal::next(*splitTo, startBlock.get()); |
386 | ASSERT(splitTo); |
387 | splitTreeToNode(*splitTo, *startBlock); |
388 | |
389 | for (n = startBlock->firstChild(); n; n = n->nextSibling()) { |
390 | VisiblePosition beforeNodePosition = positionBeforeNode(n); |
391 | if (!beforeNodePosition.isNull() && comparePositions(VisiblePosition(insertionPosition), beforeNodePosition) <= 0) |
392 | break; |
393 | } |
394 | } |
395 | |
396 | moveRemainingSiblingsToNewParent(n, blockToInsert.get(), *blockToInsert); |
397 | } |
398 | |
399 | // Handle whitespace that occurs after the split |
400 | if (positionAfterSplit.isNotNull()) { |
401 | document().updateLayoutIgnorePendingStylesheets(); |
402 | if (!positionAfterSplit.isRenderedCharacter()) { |
403 | // Clear out all whitespace and insert one non-breaking space |
404 | ASSERT(!positionAfterSplit.containerNode()->renderer() || positionAfterSplit.containerNode()->renderer()->style().collapseWhiteSpace()); |
405 | deleteInsignificantTextDownstream(positionAfterSplit); |
406 | if (is<Text>(*positionAfterSplit.deprecatedNode())) |
407 | insertTextIntoNode(downcast<Text>(*positionAfterSplit.containerNode()), 0, nonBreakingSpaceString()); |
408 | } |
409 | } |
410 | |
411 | setEndingSelection(VisibleSelection(firstPositionInNode(blockToInsert.get()), DOWNSTREAM, endingSelection().isDirectional())); |
412 | applyStyleAfterInsertion(startBlock.get()); |
413 | } |
414 | |
415 | } // namespace WebCore |
416 | |