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 "CSSComputedStyleDeclaration.h" 30#include "CSSMutableStyleDeclaration.h" 31#include "CSSPropertyNames.h" 32#include "Document.h" 33#include "HTMLElement.h" 34#include "HTMLNames.h" 35#include "InsertLineBreakCommand.h" 36#include "Logging.h" 37#include "RenderObject.h" 38#include "Text.h" 39#include "htmlediting.h" 40#include "visible_units.h" 41#include "ApplyStyleCommand.h" 42 43namespace WebCore { 44 45using namespace HTMLNames; 46 47// When inserting a new line, we want to avoid nesting empty divs if we can. Otherwise, when 48// pasting, it's easy to have each new line be a div deeper than the previous. E.g., in the case 49// below, we want to insert at ^ instead of |. 50// <div>foo<div>bar</div>|</div>^ 51static Element* highestVisuallyEquivalentDiv(Element* startBlock) 52{ 53 Element* curBlock = startBlock; 54 while (!curBlock->nextSibling() && curBlock->parentElement()->hasTagName(divTag)) { 55 NamedNodeMap* attributes = curBlock->parentElement()->attributes(true); 56 if (attributes && !attributes->isEmpty()) 57 break; 58 curBlock = curBlock->parentElement(); 59 } 60 return curBlock; 61} 62 63InsertParagraphSeparatorCommand::InsertParagraphSeparatorCommand(Document *document, bool mustUseDefaultParagraphElement) 64 : CompositeEditCommand(document) 65 , m_mustUseDefaultParagraphElement(mustUseDefaultParagraphElement) 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 m_style = editingStyleAtPosition(pos, IncludeTypingStyle); 84} 85 86void InsertParagraphSeparatorCommand::applyStyleAfterInsertion(Node* originalEnclosingBlock) 87{ 88 // Not only do we break out of header tags, but we also do not preserve the typing style, 89 // in order to match other browsers. 90 if (originalEnclosingBlock->hasTagName(h1Tag) || 91 originalEnclosingBlock->hasTagName(h2Tag) || 92 originalEnclosingBlock->hasTagName(h3Tag) || 93 originalEnclosingBlock->hasTagName(h4Tag) || 94 originalEnclosingBlock->hasTagName(h5Tag)) 95 return; 96 97 if (!m_style) 98 return; 99 100 prepareEditingStyleToApplyAt(m_style.get(), endingSelection().start()); 101 102 if (m_style->length() > 0) 103 applyStyle(m_style.get()); 104} 105 106bool 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 122void InsertParagraphSeparatorCommand::getAncestorsInsideBlock(const Node* insertionNode, Element* outerBlock, Vector<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 133PassRefPtr<Element> InsertParagraphSeparatorCommand::cloneHierarchyUnderNewBlock(const Vector<Element*>& ancestors, PassRefPtr<Element> blockToInsert) 134{ 135 // Make clones of ancestors in between the start node and the start block. 136 RefPtr<Element> parent = blockToInsert; 137 for (size_t i = ancestors.size(); i != 0; --i) { 138 RefPtr<Element> child = ancestors[i - 1]->cloneElementWithoutChildren(); 139 appendNode(child, parent); 140 parent = child.release(); 141 } 142 143 return parent.release(); 144} 145 146void InsertParagraphSeparatorCommand::doApply() 147{ 148 bool splitText = false; 149 if (endingSelection().isNone()) 150 return; 151 152 Position insertionPosition = endingSelection().start(); 153 154 EAffinity affinity = endingSelection().affinity(); 155 156 // Delete the current selection. 157 if (endingSelection().isRange()) { 158 calculateStyleBeforeInsertion(insertionPosition); 159 deleteSelection(false, true); 160 insertionPosition = endingSelection().start(); 161 affinity = endingSelection().affinity(); 162 } 163 164 // FIXME: The rangeCompliantEquivalent conversion needs to be moved into enclosingBlock. 165 Node* startBlockNode = enclosingBlock(rangeCompliantEquivalent(insertionPosition).node()); 166 Position canonicalPos = VisiblePosition(insertionPosition).deepEquivalent(); 167 Element* startBlock = static_cast<Element*>(startBlockNode); 168 if (!startBlockNode 169 || !startBlockNode->isElementNode() 170 || !startBlock->parentNode() 171 || isTableCell(startBlock) 172 || startBlock->hasTagName(formTag) 173 || (canonicalPos.node()->renderer() && canonicalPos.node()->renderer()->isTable()) 174 || canonicalPos.node()->hasTagName(hrTag)) { 175 applyCommandToComposite(InsertLineBreakCommand::create(document())); 176 return; 177 } 178 179 // Use the leftmost candidate. 180 insertionPosition = insertionPosition.upstream(); 181 if (!insertionPosition.isCandidate()) 182 insertionPosition = insertionPosition.downstream(); 183 184 // Adjust the insertion position after the delete 185 insertionPosition = positionAvoidingSpecialElementBoundary(insertionPosition); 186 VisiblePosition visiblePos(insertionPosition, affinity); 187 calculateStyleBeforeInsertion(insertionPosition); 188 189 //--------------------------------------------------------------------- 190 // Handle special case of typing return on an empty list item 191 if (breakOutOfEmptyListItem()) 192 return; 193 194 //--------------------------------------------------------------------- 195 // Prepare for more general cases. 196 197 bool isFirstInBlock = isStartOfBlock(visiblePos); 198 bool isLastInBlock = isEndOfBlock(visiblePos); 199 bool nestNewBlock = false; 200 201 // Create block to be inserted. 202 RefPtr<Element> blockToInsert; 203 if (startBlock == startBlock->rootEditableElement()) { 204 blockToInsert = createDefaultParagraphElement(document()); 205 nestNewBlock = true; 206 } else if (shouldUseDefaultParagraphElement(startBlock)) 207 blockToInsert = createDefaultParagraphElement(document()); 208 else 209 blockToInsert = startBlock->cloneElementWithoutChildren(); 210 211 //--------------------------------------------------------------------- 212 // Handle case when position is in the last visible position in its block, 213 // including when the block is empty. 214 if (isLastInBlock) { 215 bool shouldApplyStyleAfterInsertion = true; 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 (Node* highestBlockquote = highestEnclosingNodeOfType(canonicalPos, &isMailBlockquote)) { 229 startBlock = static_cast<Element*>(highestBlockquote); 230 // When inserting the newline after the blockquote, we don't want to apply the original style after the insertion 231 shouldApplyStyleAfterInsertion = false; 232 } 233 234 // Most of the time we want to stay at the nesting level of the startBlock (e.g., when nesting within lists). However, 235 // for div nodes, this can result in nested div tags that are hard to break out of. 236 Element* siblingNode = startBlock; 237 if (blockToInsert->hasTagName(divTag)) 238 siblingNode = highestVisuallyEquivalentDiv(startBlock); 239 insertNodeAfter(blockToInsert, siblingNode); 240 } 241 242 // Recreate the same structure in the new paragraph. 243 244 Vector<Element*> ancestors; 245 getAncestorsInsideBlock(insertionPosition.node(), startBlock, ancestors); 246 RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); 247 248 appendBlockPlaceholder(parent); 249 250 setEndingSelection(VisibleSelection(Position(parent.get(), 0), DOWNSTREAM)); 251 return; 252 } 253 254 255 //--------------------------------------------------------------------- 256 // Handle case when position is in the first visible position in its block, and 257 // similar case where previous position is in another, presumeably nested, block. 258 if (isFirstInBlock || !inSameBlock(visiblePos, visiblePos.previous())) { 259 Node *refNode; 260 if (isFirstInBlock && !nestNewBlock) 261 refNode = startBlock; 262 else if (insertionPosition.node() == startBlock && nestNewBlock) { 263 refNode = startBlock->childNode(insertionPosition.deprecatedEditingOffset()); 264 ASSERT(refNode); // must be true or we'd be in the end of block case 265 } else 266 refNode = insertionPosition.node(); 267 268 // find ending selection position easily before inserting the paragraph 269 insertionPosition = insertionPosition.downstream(); 270 271 insertNodeBefore(blockToInsert, refNode); 272 273 // Recreate the same structure in the new paragraph. 274 275 Vector<Element*> ancestors; 276 getAncestorsInsideBlock(positionAvoidingSpecialElementBoundary(insertionPosition).node(), startBlock, ancestors); 277 278 appendBlockPlaceholder(cloneHierarchyUnderNewBlock(ancestors, blockToInsert)); 279 280 // In this case, we need to set the new ending selection. 281 setEndingSelection(VisibleSelection(insertionPosition, DOWNSTREAM)); 282 return; 283 } 284 285 //--------------------------------------------------------------------- 286 // Handle the (more complicated) general case, 287 288 // All of the content in the current block after visiblePos is 289 // about to be wrapped in a new paragraph element. Add a br before 290 // it if visiblePos is at the start of a paragraph so that the 291 // content will move down a line. 292 if (isStartOfParagraph(visiblePos)) { 293 RefPtr<Element> br = createBreakElement(document()); 294 insertNodeAt(br.get(), insertionPosition); 295 insertionPosition = positionInParentAfterNode(br.get()); 296 } 297 298 // Move downstream. Typing style code will take care of carrying along the 299 // style of the upstream position. 300 insertionPosition = insertionPosition.downstream(); 301 302 // At this point, the insertionPosition's node could be a container, and we want to make sure we include 303 // all of the correct nodes when building the ancestor list. So this needs to be the deepest representation of the position 304 // before we walk the DOM tree. 305 insertionPosition = VisiblePosition(insertionPosition).deepEquivalent(); 306 307 // Build up list of ancestors in between the start node and the start block. 308 Vector<Element*> ancestors; 309 getAncestorsInsideBlock(insertionPosition.node(), startBlock, ancestors); 310 311 // Make sure we do not cause a rendered space to become unrendered. 312 // FIXME: We need the affinity for pos, but pos.downstream() does not give it 313 Position leadingWhitespace = insertionPosition.leadingWhitespacePosition(VP_DEFAULT_AFFINITY); 314 // FIXME: leadingWhitespacePosition is returning the position before preserved newlines for positions 315 // after the preserved newline, causing the newline to be turned into a nbsp. 316 if (leadingWhitespace.isNotNull()) { 317 Text* textNode = static_cast<Text*>(leadingWhitespace.node()); 318 ASSERT(!textNode->renderer() || textNode->renderer()->style()->collapseWhiteSpace()); 319 replaceTextInNode(textNode, leadingWhitespace.deprecatedEditingOffset(), 1, nonBreakingSpaceString()); 320 } 321 322 // Split at pos if in the middle of a text node. 323 if (insertionPosition.node()->isTextNode()) { 324 Text* textNode = static_cast<Text*>(insertionPosition.node()); 325 bool atEnd = (unsigned)insertionPosition.deprecatedEditingOffset() >= textNode->length(); 326 if (insertionPosition.deprecatedEditingOffset() > 0 && !atEnd) { 327 splitTextNode(textNode, insertionPosition.deprecatedEditingOffset()); 328 insertionPosition.moveToOffset(0); 329 visiblePos = VisiblePosition(insertionPosition); 330 splitText = true; 331 } 332 } 333 334 // Put the added block in the tree. 335 if (nestNewBlock) 336 appendNode(blockToInsert.get(), startBlock); 337 else 338 insertNodeAfter(blockToInsert.get(), startBlock); 339 340 updateLayout(); 341 342 // Make clones of ancestors in between the start node and the outer block. 343 RefPtr<Element> parent = cloneHierarchyUnderNewBlock(ancestors, blockToInsert); 344 345 // If the paragraph separator was inserted at the end of a paragraph, an empty line must be 346 // created. All of the nodes, starting at visiblePos, are about to be added to the new paragraph 347 // element. If the first node to be inserted won't be one that will hold an empty line open, add a br. 348 if (isEndOfParagraph(visiblePos) && !lineBreakExistsAtVisiblePosition(visiblePos)) 349 appendNode(createBreakElement(document()).get(), blockToInsert.get()); 350 351 // Move the start node and the siblings of the start node. 352 if (insertionPosition.node() != startBlock) { 353 Node* n = insertionPosition.node(); 354 if (insertionPosition.deprecatedEditingOffset() >= caretMaxOffset(n)) 355 n = n->nextSibling(); 356 357 while (n && n != blockToInsert) { 358 Node *next = n->nextSibling(); 359 removeNode(n); 360 appendNode(n, parent.get()); 361 n = next; 362 } 363 } 364 365 // Move everything after the start node. 366 if (!ancestors.isEmpty()) { 367 Element* leftParent = ancestors.first(); 368 while (leftParent && leftParent != startBlock) { 369 parent = parent->parentElement(); 370 if (!parent) 371 break; 372 Node* n = leftParent->nextSibling(); 373 while (n && n != blockToInsert) { 374 Node* next = n->nextSibling(); 375 removeNode(n); 376 appendNode(n, parent.get()); 377 n = next; 378 } 379 leftParent = leftParent->parentElement(); 380 } 381 } 382 383 // Handle whitespace that occurs after the split 384 if (splitText) { 385 updateLayout(); 386 insertionPosition = Position(insertionPosition.node(), 0); 387 if (!insertionPosition.isRenderedCharacter()) { 388 // Clear out all whitespace and insert one non-breaking space 389 ASSERT(insertionPosition.node()->isTextNode()); 390 ASSERT(!insertionPosition.node()->renderer() || insertionPosition.node()->renderer()->style()->collapseWhiteSpace()); 391 deleteInsignificantTextDownstream(insertionPosition); 392 insertTextIntoNode(static_cast<Text*>(insertionPosition.node()), 0, nonBreakingSpaceString()); 393 } 394 } 395 396 setEndingSelection(VisibleSelection(Position(blockToInsert.get(), 0), DOWNSTREAM)); 397 applyStyleAfterInsertion(startBlock); 398} 399 400} // namespace WebCore 401