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 "InsertParagraphSeparatorCommand.h" 28 29#include "CSSPropertyNames.h" 30#include "Document.h" 31#include "EditingStyle.h" 32#include "HTMLElement.h" 33#include "HTMLNames.h" 34#include "InsertLineBreakCommand.h" 35#include "NodeTraversal.h" 36#include "RenderObject.h" 37#include "Text.h" 38#include "VisibleUnits.h" 39#include "htmlediting.h" 40 41namespace WebCore { 42 43using 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>^ 49static 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 62InsertParagraphSeparatorCommand::InsertParagraphSeparatorCommand(Document *document, bool mustUseDefaultParagraphElement, bool pasteBlockqutoeIntoUnquotedArea) 63 : CompositeEditCommand(document) 64 , m_mustUseDefaultParagraphElement(mustUseDefaultParagraphElement) 65 , m_pasteBlockqutoeIntoUnquotedArea(pasteBlockqutoeIntoUnquotedArea) 66{ 67} 68 69bool InsertParagraphSeparatorCommand::preservesTypingStyle() const 70{ 71 return true; 72} 73 74void 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 88void 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 107bool 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 123void 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 134PassRefPtr<Element> InsertParagraphSeparatorCommand::cloneHierarchyUnderNewBlock(const Vector<RefPtr<Element> >& ancestors, PassRefPtr<Element> blockToInsert) 135{ 136 // Make clones of ancestors in between the start node and the start block. 137 RefPtr<Element> parent = blockToInsert; 138 for (size_t i = ancestors.size(); i != 0; --i) { 139 RefPtr<Element> child = ancestors[i - 1]->cloneElementWithoutChildren(); 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, parent); 143 parent = child.release(); 144 } 145 146 return parent.release(); 147} 148 149void InsertParagraphSeparatorCommand::doApply() 150{ 151 if (!endingSelection().isNonOrphanedCaretOrRange()) 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 || startBlock->hasTagName(formTag) 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 calculateStyleBeforeInsertion(insertionPosition); 189 190 //--------------------------------------------------------------------- 191 // Handle special case of typing return on an empty list item 192 if (breakOutOfEmptyListItem()) 193 return; 194 195 //--------------------------------------------------------------------- 196 // Prepare for more general cases. 197 198 bool isFirstInBlock = isStartOfBlock(visiblePos); 199 bool isLastInBlock = isEndOfBlock(visiblePos); 200 bool nestNewBlock = false; 201 202 // Create block to be inserted. 203 RefPtr<Element> blockToInsert; 204 if (startBlock->isRootEditableElement()) { 205 blockToInsert = createDefaultParagraphElement(document()); 206 nestNewBlock = true; 207 } else if (shouldUseDefaultParagraphElement(startBlock.get())) 208 blockToInsert = createDefaultParagraphElement(document()); 209 else 210 blockToInsert = startBlock->cloneElementWithoutChildren(); 211 212 //--------------------------------------------------------------------- 213 // Handle case when position is in the last visible position in its block, 214 // including when the block is empty. 215 if (isLastInBlock) { 216 if (nestNewBlock) { 217 if (isFirstInBlock && !lineBreakExistsAtVisiblePosition(visiblePos)) { 218 // The block is empty. Create an empty block to 219 // represent the paragraph that we're leaving. 220 RefPtr<Element> extraBlock = createDefaultParagraphElement(document()); 221 appendNode(extraBlock, startBlock); 222 appendBlockPlaceholder(extraBlock); 223 } 224 appendNode(blockToInsert, startBlock); 225 } else { 226 // 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 227 // into an unquoted area. We then don't want the newline within the blockquote or else it will also be quoted. 228 if (m_pasteBlockqutoeIntoUnquotedArea) { 229 if (Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote)) 230 startBlock = toElement(highestBlockquote); 231 } 232 233 // Most of the time we want to stay at the nesting level of the startBlock (e.g., when nesting within lists). However, 234 // for div nodes, this can result in nested div tags that are hard to break out of. 235 Element* siblingNode = startBlock.get(); 236 if (blockToInsert->hasTagName(divTag)) 237 siblingNode = highestVisuallyEquivalentDivBelowRoot(startBlock.get()); 238 insertNodeAfter(blockToInsert, siblingNode); 239 } 240 241 // Recreate the same structure in the new paragraph. 242 243 Vector<RefPtr<Element> > ancestors; 244 getAncestorsInsideBlock(positionOutsideTabSpan(insertionPosition).deprecatedNode(), startBlock.get(), ancestors); 245 RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); 246 247 appendBlockPlaceholder(parent); 248 249 setEndingSelection(VisibleSelection(firstPositionInNode(parent.get()), DOWNSTREAM, endingSelection().isDirectional())); 250 return; 251 } 252 253 254 //--------------------------------------------------------------------- 255 // Handle case when position is in the first visible position in its block, and 256 // similar case where previous position is in another, presumeably nested, block. 257 if (isFirstInBlock || !inSameBlock(visiblePos, visiblePos.previous())) { 258 Node *refNode; 259 260 insertionPosition = positionOutsideTabSpan(insertionPosition); 261 262 if (isFirstInBlock && !nestNewBlock) 263 refNode = startBlock.get(); 264 else if (isFirstInBlock && nestNewBlock) { 265 // startBlock should always have children, otherwise isLastInBlock would be true and it's handled above. 266 ASSERT(startBlock->firstChild()); 267 refNode = startBlock->firstChild(); 268 } 269 else if (insertionPosition.deprecatedNode() == startBlock && nestNewBlock) { 270 refNode = startBlock->childNode(insertionPosition.deprecatedEditingOffset()); 271 ASSERT(refNode); // must be true or we'd be in the end of block case 272 } else 273 refNode = insertionPosition.deprecatedNode(); 274 275 // find ending selection position easily before inserting the paragraph 276 insertionPosition = insertionPosition.downstream(); 277 278 insertNodeBefore(blockToInsert, refNode); 279 280 // Recreate the same structure in the new paragraph. 281 282 Vector<RefPtr<Element> > ancestors; 283 getAncestorsInsideBlock(positionAvoidingSpecialElementBoundary(positionOutsideTabSpan(insertionPosition)).deprecatedNode(), startBlock.get(), ancestors); 284 285 appendBlockPlaceholder(cloneHierarchyUnderNewBlock(ancestors, blockToInsert)); 286 287 // In this case, we need to set the new ending selection. 288 setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM, endingSelection().isDirectional())); 289 return; 290 } 291 292 //--------------------------------------------------------------------- 293 // Handle the (more complicated) general case, 294 295 // All of the content in the current block after visiblePos is 296 // about to be wrapped in a new paragraph element. Add a br before 297 // it if visiblePos is at the start of a paragraph so that the 298 // content will move down a line. 299 if (isStartOfParagraph(visiblePos)) { 300 RefPtr<Element> br = createBreakElement(document()); 301 insertNodeAt(br.get(), insertionPosition); 302 insertionPosition = positionInParentAfterNode(br.get()); 303 // If the insertion point is a break element, there is nothing else 304 // we need to do. 305 if (visiblePos.deepEquivalent().anchorNode()->renderer()->isBR()) { 306 setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM, endingSelection().isDirectional())); 307 return; 308 } 309 } 310 311 // Move downstream. Typing style code will take care of carrying along the 312 // style of the upstream position. 313 insertionPosition = insertionPosition.downstream(); 314 315 // At this point, the insertionPosition's node could be a container, and we want to make sure we include 316 // all of the correct nodes when building the ancestor list. So this needs to be the deepest representation of the position 317 // before we walk the DOM tree. 318 insertionPosition = positionOutsideTabSpan(VisiblePosition(insertionPosition).deepEquivalent()); 319 320 // If the returned position lies either at the end or at the start of an element that is ignored by editing 321 // we should move to its upstream or downstream position. 322 if (editingIgnoresContent(insertionPosition.deprecatedNode())) { 323 if (insertionPosition.atLastEditingPositionForNode()) 324 insertionPosition = insertionPosition.downstream(); 325 else if (insertionPosition.atFirstEditingPositionForNode()) 326 insertionPosition = insertionPosition.upstream(); 327 } 328 329 // Make sure we do not cause a rendered space to become unrendered. 330 // FIXME: We need the affinity for pos, but pos.downstream() does not give it 331 Position leadingWhitespace = insertionPosition.leadingWhitespacePosition(VP_DEFAULT_AFFINITY); 332 // FIXME: leadingWhitespacePosition is returning the position before preserved newlines for positions 333 // after the preserved newline, causing the newline to be turned into a nbsp. 334 if (leadingWhitespace.isNotNull() && leadingWhitespace.deprecatedNode()->isTextNode()) { 335 Text* textNode = toText(leadingWhitespace.deprecatedNode()); 336 ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace()); 337 replaceTextInNodePreservingMarkers(textNode, leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); 338 } 339 340 // Split at pos if in the middle of a text node. 341 Position positionAfterSplit; 342 if (insertionPosition.anchorType() == Position::PositionIsOffsetInAnchor && insertionPosition.containerNode()->isTextNode()) { 343 RefPtr<Text> textNode = toText(insertionPosition.containerNode()); 344 bool atEnd = static_cast<unsigned>(insertionPosition.offsetInContainerNode()) >= textNode->length(); 345 if (insertionPosition.deprecatedEditingOffset() > 0 && !atEnd) { 346 splitTextNode(textNode, insertionPosition.offsetInContainerNode()); 347 positionAfterSplit = firstPositionInNode(textNode.get()); 348 if (!textNode->previousSibling()) 349 return; // Bail out if mutation events detachd the split text node. 350 insertionPosition.moveToPosition(textNode->previousSibling(), insertionPosition.offsetInContainerNode()); 351 visiblePos = VisiblePosition(insertionPosition); 352 } 353 } 354 355 // If we got detached due to mutation events, just bail out. 356 if (!startBlock->parentNode()) 357 return; 358 359 // Put the added block in the tree. 360 if (nestNewBlock) 361 appendNode(blockToInsert.get(), startBlock); 362 else 363 insertNodeAfter(blockToInsert.get(), startBlock); 364 365 document()->updateLayoutIgnorePendingStylesheets(); 366 367 // If the paragraph separator was inserted at the end of a paragraph, an empty line must be 368 // created. All of the nodes, starting at visiblePos, are about to be added to the new paragraph 369 // element. If the first node to be inserted won't be one that will hold an empty line open, add a br. 370 if (isEndOfParagraph(visiblePos) && !lineBreakExistsAtVisiblePosition(visiblePos)) 371 appendNode(createBreakElement(document()).get(), blockToInsert.get()); 372 373 // Move the start node and the siblings of the start node. 374 if (VisiblePosition(insertionPosition) != VisiblePosition(positionBeforeNode(blockToInsert.get()))) { 375 Node* n; 376 if (insertionPosition.containerNode() == startBlock) 377 n = insertionPosition.computeNodeAfterPosition(); 378 else { 379 Node* splitTo = insertionPosition.containerNode(); 380 if (splitTo->isTextNode() && insertionPosition.offsetInContainerNode() >= caretMaxOffset(splitTo)) 381 splitTo = NodeTraversal::next(splitTo, startBlock.get()); 382 ASSERT(splitTo); 383 splitTreeToNode(splitTo, startBlock.get()); 384 385 for (n = startBlock->firstChild(); n; n = n->nextSibling()) { 386 VisiblePosition beforeNodePosition = positionBeforeNode(n); 387 if (!beforeNodePosition.isNull() && comparePositions(VisiblePosition(insertionPosition), beforeNodePosition) <= 0) 388 break; 389 } 390 } 391 392 moveRemainingSiblingsToNewParent(n, blockToInsert.get(), blockToInsert); 393 } 394 395 // Handle whitespace that occurs after the split 396 if (positionAfterSplit.isNotNull()) { 397 document()->updateLayoutIgnorePendingStylesheets(); 398 if (!positionAfterSplit.isRenderedCharacter()) { 399 // Clear out all whitespace and insert one non-breaking space 400 ASSERT(!positionAfterSplit.containerNode()->renderer() || positionAfterSplit.containerNode()->renderer()->style()->collapseWhiteSpace()); 401 deleteInsignificantTextDownstream(positionAfterSplit); 402 if (positionAfterSplit.deprecatedNode()->isTextNode()) 403 insertTextIntoNode(toText(positionAfterSplit.containerNode()), 0, nonBreakingSpaceString()); 404 } 405 } 406 407 setEndingSelection(VisibleSelection(firstPositionInNode(blockToInsert.get()), DOWNSTREAM, endingSelection().isDirectional())); 408 applyStyleAfterInsertion(startBlock.get()); 409} 410 411} // namespace WebCore 412