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