| OLD | NEW |
| (Empty) |
| 1 /* | |
| 2 * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved. | |
| 3 * Copyright (C) 2009, 2010, 2011 Google Inc. All rights reserved. | |
| 4 * | |
| 5 * Redistribution and use in source and binary forms, with or without | |
| 6 * modification, are permitted provided that the following conditions | |
| 7 * are met: | |
| 8 * 1. Redistributions of source code must retain the above copyright | |
| 9 * notice, this list of conditions and the following disclaimer. | |
| 10 * 2. Redistributions in binary form must reproduce the above copyright | |
| 11 * notice, this list of conditions and the following disclaimer in the | |
| 12 * documentation and/or other materials provided with the distribution. | |
| 13 * | |
| 14 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY | |
| 15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| 16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| 17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR | |
| 18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
| 19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
| 20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
| 21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
| 22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 25 */ | |
| 26 | |
| 27 #include "config.h" | |
| 28 #include "core/editing/ReplaceSelectionCommand.h" | |
| 29 | |
| 30 #include "bindings/core/v8/ExceptionStatePlaceholder.h" | |
| 31 #include "core/CSSPropertyNames.h" | |
| 32 #include "core/HTMLNames.h" | |
| 33 #include "core/InputTypeNames.h" | |
| 34 #include "core/css/CSSStyleDeclaration.h" | |
| 35 #include "core/css/StylePropertySet.h" | |
| 36 #include "core/dom/Document.h" | |
| 37 #include "core/dom/DocumentFragment.h" | |
| 38 #include "core/dom/Element.h" | |
| 39 #include "core/dom/Text.h" | |
| 40 #include "core/editing/ApplyStyleCommand.h" | |
| 41 #include "core/editing/BreakBlockquoteCommand.h" | |
| 42 #include "core/editing/EditingUtilities.h" | |
| 43 #include "core/editing/FrameSelection.h" | |
| 44 #include "core/editing/SimplifyMarkupCommand.h" | |
| 45 #include "core/editing/SmartReplace.h" | |
| 46 #include "core/editing/VisibleUnits.h" | |
| 47 #include "core/editing/iterators/TextIterator.h" | |
| 48 #include "core/editing/serializers/HTMLInterchange.h" | |
| 49 #include "core/editing/serializers/Serialization.h" | |
| 50 #include "core/events/BeforeTextInsertedEvent.h" | |
| 51 #include "core/frame/LocalFrame.h" | |
| 52 #include "core/frame/UseCounter.h" | |
| 53 #include "core/html/HTMLBRElement.h" | |
| 54 #include "core/html/HTMLElement.h" | |
| 55 #include "core/html/HTMLInputElement.h" | |
| 56 #include "core/html/HTMLLIElement.h" | |
| 57 #include "core/html/HTMLQuoteElement.h" | |
| 58 #include "core/html/HTMLSelectElement.h" | |
| 59 #include "core/html/HTMLSpanElement.h" | |
| 60 #include "core/layout/LayoutObject.h" | |
| 61 #include "core/layout/LayoutText.h" | |
| 62 #include "wtf/StdLibExtras.h" | |
| 63 #include "wtf/Vector.h" | |
| 64 | |
| 65 namespace blink { | |
| 66 | |
| 67 using namespace HTMLNames; | |
| 68 | |
| 69 enum EFragmentType { EmptyFragment, SingleTextNodeFragment, TreeFragment }; | |
| 70 | |
| 71 // --- ReplacementFragment helper class | |
| 72 | |
| 73 class ReplacementFragment final { | |
| 74 WTF_MAKE_NONCOPYABLE(ReplacementFragment); | |
| 75 STACK_ALLOCATED(); | |
| 76 public: | |
| 77 ReplacementFragment(Document*, DocumentFragment*, const VisibleSelection&); | |
| 78 | |
| 79 Node* firstChild() const; | |
| 80 Node* lastChild() const; | |
| 81 | |
| 82 bool isEmpty() const; | |
| 83 | |
| 84 bool hasInterchangeNewlineAtStart() const { return m_hasInterchangeNewlineAt
Start; } | |
| 85 bool hasInterchangeNewlineAtEnd() const { return m_hasInterchangeNewlineAtEn
d; } | |
| 86 | |
| 87 void removeNode(PassRefPtrWillBeRawPtr<Node>); | |
| 88 void removeNodePreservingChildren(PassRefPtrWillBeRawPtr<ContainerNode>); | |
| 89 | |
| 90 private: | |
| 91 PassRefPtrWillBeRawPtr<HTMLElement> insertFragmentForTestRendering(Element*
rootEditableElement); | |
| 92 void removeUnrenderedNodes(ContainerNode*); | |
| 93 void restoreAndRemoveTestRenderingNodesToFragment(Element*); | |
| 94 void removeInterchangeNodes(ContainerNode*); | |
| 95 | |
| 96 void insertNodeBefore(PassRefPtrWillBeRawPtr<Node>, Node* refNode); | |
| 97 | |
| 98 RefPtrWillBeMember<Document> m_document; | |
| 99 RefPtrWillBeMember<DocumentFragment> m_fragment; | |
| 100 bool m_hasInterchangeNewlineAtStart; | |
| 101 bool m_hasInterchangeNewlineAtEnd; | |
| 102 }; | |
| 103 | |
| 104 static bool isInterchangeHTMLBRElement(const Node* node) | |
| 105 { | |
| 106 DEFINE_STATIC_LOCAL(String, interchangeNewlineClassString, (AppleInterchange
Newline)); | |
| 107 if (!isHTMLBRElement(node) || toHTMLBRElement(node)->getAttribute(classAttr)
!= interchangeNewlineClassString) | |
| 108 return false; | |
| 109 UseCounter::count(node->document(), UseCounter::EditingAppleInterchangeNewli
ne); | |
| 110 return true; | |
| 111 } | |
| 112 | |
| 113 static bool isHTMLInterchangeConvertedSpaceSpan(const Node* node) | |
| 114 { | |
| 115 DEFINE_STATIC_LOCAL(String, convertedSpaceSpanClassString, (AppleConvertedSp
ace)); | |
| 116 if (!node->isHTMLElement() || toHTMLElement(node)->getAttribute(classAttr) !
= convertedSpaceSpanClassString) | |
| 117 return false; | |
| 118 UseCounter::count(node->document(), UseCounter::EditingAppleConvertedSpace); | |
| 119 return true; | |
| 120 } | |
| 121 | |
| 122 static Position positionAvoidingPrecedingNodes(Position pos) | |
| 123 { | |
| 124 // If we're already on a break, it's probably a placeholder and we shouldn't
change our position. | |
| 125 if (editingIgnoresContent(pos.anchorNode())) | |
| 126 return pos; | |
| 127 | |
| 128 // We also stop when changing block flow elements because even though the vi
sual position is the | |
| 129 // same. E.g., | |
| 130 // <div>foo^</div>^ | |
| 131 // The two positions above are the same visual position, but we want to stay
in the same block. | |
| 132 Element* enclosingBlockElement = enclosingBlock(pos.computeContainerNode()); | |
| 133 for (Position nextPosition = pos; nextPosition.computeContainerNode() != enc
losingBlockElement; pos = nextPosition) { | |
| 134 if (lineBreakExistsAtPosition(pos)) | |
| 135 break; | |
| 136 | |
| 137 if (pos.computeContainerNode()->nonShadowBoundaryParentNode()) | |
| 138 nextPosition = positionInParentAfterNode(*pos.computeContainerNode()
); | |
| 139 | |
| 140 if (nextPosition == pos | |
| 141 || enclosingBlock(nextPosition.computeContainerNode()) != enclosingB
lockElement | |
| 142 || VisiblePosition(pos).deepEquivalent() != VisiblePosition(nextPosi
tion).deepEquivalent()) | |
| 143 break; | |
| 144 } | |
| 145 return pos; | |
| 146 } | |
| 147 | |
| 148 ReplacementFragment::ReplacementFragment(Document* document, DocumentFragment* f
ragment, const VisibleSelection& selection) | |
| 149 : m_document(document) | |
| 150 , m_fragment(fragment) | |
| 151 , m_hasInterchangeNewlineAtStart(false) | |
| 152 , m_hasInterchangeNewlineAtEnd(false) | |
| 153 { | |
| 154 if (!m_document) | |
| 155 return; | |
| 156 if (!m_fragment || !m_fragment->hasChildren()) | |
| 157 return; | |
| 158 | |
| 159 RefPtrWillBeRawPtr<Element> editableRoot = selection.rootEditableElement(); | |
| 160 ASSERT(editableRoot); | |
| 161 if (!editableRoot) | |
| 162 return; | |
| 163 | |
| 164 Element* shadowAncestorElement; | |
| 165 if (editableRoot->isInShadowTree()) | |
| 166 shadowAncestorElement = editableRoot->shadowHost(); | |
| 167 else | |
| 168 shadowAncestorElement = editableRoot.get(); | |
| 169 | |
| 170 if (!editableRoot->getAttributeEventListener(EventTypeNames::webkitBeforeTex
tInserted) | |
| 171 // FIXME: Remove these checks once textareas and textfields actually reg
ister an event handler. | |
| 172 && !(shadowAncestorElement && shadowAncestorElement->layoutObject() && s
hadowAncestorElement->layoutObject()->isTextControl()) | |
| 173 && editableRoot->layoutObjectIsRichlyEditable()) { | |
| 174 removeInterchangeNodes(m_fragment.get()); | |
| 175 return; | |
| 176 } | |
| 177 | |
| 178 RefPtrWillBeRawPtr<HTMLElement> holder = insertFragmentForTestRendering(edit
ableRoot.get()); | |
| 179 if (!holder) { | |
| 180 removeInterchangeNodes(m_fragment.get()); | |
| 181 return; | |
| 182 } | |
| 183 | |
| 184 const EphemeralRange range = VisibleSelection::selectionFromContentsOfNode(h
older.get()).toNormalizedEphemeralRange(); | |
| 185 String text = plainText(range, static_cast<TextIteratorBehavior>(TextIterato
rEmitsOriginalText | TextIteratorIgnoresStyleVisibility)); | |
| 186 | |
| 187 removeInterchangeNodes(holder.get()); | |
| 188 removeUnrenderedNodes(holder.get()); | |
| 189 restoreAndRemoveTestRenderingNodesToFragment(holder.get()); | |
| 190 | |
| 191 // Give the root a chance to change the text. | |
| 192 RefPtrWillBeRawPtr<BeforeTextInsertedEvent> evt = BeforeTextInsertedEvent::c
reate(text); | |
| 193 editableRoot->dispatchEvent(evt); | |
| 194 if (text != evt->text() || !editableRoot->layoutObjectIsRichlyEditable()) { | |
| 195 restoreAndRemoveTestRenderingNodesToFragment(holder.get()); | |
| 196 | |
| 197 m_fragment = createFragmentFromText(selection.toNormalizedEphemeralRange
(), evt->text()); | |
| 198 if (!m_fragment->hasChildren()) | |
| 199 return; | |
| 200 | |
| 201 holder = insertFragmentForTestRendering(editableRoot.get()); | |
| 202 removeInterchangeNodes(holder.get()); | |
| 203 removeUnrenderedNodes(holder.get()); | |
| 204 restoreAndRemoveTestRenderingNodesToFragment(holder.get()); | |
| 205 } | |
| 206 } | |
| 207 | |
| 208 bool ReplacementFragment::isEmpty() const | |
| 209 { | |
| 210 return (!m_fragment || !m_fragment->hasChildren()) && !m_hasInterchangeNewli
neAtStart && !m_hasInterchangeNewlineAtEnd; | |
| 211 } | |
| 212 | |
| 213 Node* ReplacementFragment::firstChild() const | |
| 214 { | |
| 215 return m_fragment ? m_fragment->firstChild() : 0; | |
| 216 } | |
| 217 | |
| 218 Node* ReplacementFragment::lastChild() const | |
| 219 { | |
| 220 return m_fragment ? m_fragment->lastChild() : 0; | |
| 221 } | |
| 222 | |
| 223 void ReplacementFragment::removeNodePreservingChildren(PassRefPtrWillBeRawPtr<Co
ntainerNode> node) | |
| 224 { | |
| 225 if (!node) | |
| 226 return; | |
| 227 | |
| 228 while (RefPtrWillBeRawPtr<Node> n = node->firstChild()) { | |
| 229 removeNode(n); | |
| 230 insertNodeBefore(n.release(), node.get()); | |
| 231 } | |
| 232 removeNode(node); | |
| 233 } | |
| 234 | |
| 235 void ReplacementFragment::removeNode(PassRefPtrWillBeRawPtr<Node> node) | |
| 236 { | |
| 237 if (!node) | |
| 238 return; | |
| 239 | |
| 240 ContainerNode* parent = node->nonShadowBoundaryParentNode(); | |
| 241 if (!parent) | |
| 242 return; | |
| 243 | |
| 244 parent->removeChild(node.get()); | |
| 245 } | |
| 246 | |
| 247 void ReplacementFragment::insertNodeBefore(PassRefPtrWillBeRawPtr<Node> node, No
de* refNode) | |
| 248 { | |
| 249 if (!node || !refNode) | |
| 250 return; | |
| 251 | |
| 252 ContainerNode* parent = refNode->nonShadowBoundaryParentNode(); | |
| 253 if (!parent) | |
| 254 return; | |
| 255 | |
| 256 parent->insertBefore(node, refNode); | |
| 257 } | |
| 258 | |
| 259 PassRefPtrWillBeRawPtr<HTMLElement> ReplacementFragment::insertFragmentForTestRe
ndering(Element* rootEditableElement) | |
| 260 { | |
| 261 ASSERT(m_document); | |
| 262 RefPtrWillBeRawPtr<HTMLElement> holder = createDefaultParagraphElement(*m_do
cument.get()); | |
| 263 | |
| 264 holder->appendChild(m_fragment); | |
| 265 rootEditableElement->appendChild(holder.get()); | |
| 266 m_document->updateLayoutIgnorePendingStylesheets(); | |
| 267 | |
| 268 return holder.release(); | |
| 269 } | |
| 270 | |
| 271 void ReplacementFragment::restoreAndRemoveTestRenderingNodesToFragment(Element*
holder) | |
| 272 { | |
| 273 if (!holder) | |
| 274 return; | |
| 275 | |
| 276 while (RefPtrWillBeRawPtr<Node> node = holder->firstChild()) { | |
| 277 holder->removeChild(node.get()); | |
| 278 m_fragment->appendChild(node.get()); | |
| 279 } | |
| 280 | |
| 281 removeNode(holder); | |
| 282 } | |
| 283 | |
| 284 void ReplacementFragment::removeUnrenderedNodes(ContainerNode* holder) | |
| 285 { | |
| 286 WillBeHeapVector<RefPtrWillBeMember<Node>> unrendered; | |
| 287 | |
| 288 for (Node& node : NodeTraversal::descendantsOf(*holder)) { | |
| 289 if (!isNodeRendered(node) && !isTableStructureNode(&node)) | |
| 290 unrendered.append(&node); | |
| 291 } | |
| 292 | |
| 293 for (auto& node : unrendered) | |
| 294 removeNode(node); | |
| 295 } | |
| 296 | |
| 297 void ReplacementFragment::removeInterchangeNodes(ContainerNode* container) | |
| 298 { | |
| 299 m_hasInterchangeNewlineAtStart = false; | |
| 300 m_hasInterchangeNewlineAtEnd = false; | |
| 301 | |
| 302 // Interchange newlines at the "start" of the incoming fragment must be | |
| 303 // either the first node in the fragment or the first leaf in the fragment. | |
| 304 Node* node = container->firstChild(); | |
| 305 while (node) { | |
| 306 if (isInterchangeHTMLBRElement(node)) { | |
| 307 m_hasInterchangeNewlineAtStart = true; | |
| 308 removeNode(node); | |
| 309 break; | |
| 310 } | |
| 311 node = node->firstChild(); | |
| 312 } | |
| 313 if (!container->hasChildren()) | |
| 314 return; | |
| 315 // Interchange newlines at the "end" of the incoming fragment must be | |
| 316 // either the last node in the fragment or the last leaf in the fragment. | |
| 317 node = container->lastChild(); | |
| 318 while (node) { | |
| 319 if (isInterchangeHTMLBRElement(node)) { | |
| 320 m_hasInterchangeNewlineAtEnd = true; | |
| 321 removeNode(node); | |
| 322 break; | |
| 323 } | |
| 324 node = node->lastChild(); | |
| 325 } | |
| 326 | |
| 327 node = container->firstChild(); | |
| 328 while (node) { | |
| 329 RefPtrWillBeRawPtr<Node> next = NodeTraversal::next(*node); | |
| 330 if (isHTMLInterchangeConvertedSpaceSpan(node)) { | |
| 331 HTMLElement& element = toHTMLElement(*node); | |
| 332 next = NodeTraversal::nextSkippingChildren(element); | |
| 333 removeNodePreservingChildren(&element); | |
| 334 } | |
| 335 node = next.get(); | |
| 336 } | |
| 337 } | |
| 338 | |
| 339 inline void ReplaceSelectionCommand::InsertedNodes::respondToNodeInsertion(Node&
node) | |
| 340 { | |
| 341 if (!m_firstNodeInserted) | |
| 342 m_firstNodeInserted = &node; | |
| 343 | |
| 344 m_lastNodeInserted = &node; | |
| 345 } | |
| 346 | |
| 347 inline void ReplaceSelectionCommand::InsertedNodes::willRemoveNodePreservingChil
dren(Node& node) | |
| 348 { | |
| 349 if (m_firstNodeInserted.get() == node) | |
| 350 m_firstNodeInserted = NodeTraversal::next(node); | |
| 351 if (m_lastNodeInserted.get() == node) | |
| 352 m_lastNodeInserted = node.lastChild() ? node.lastChild() : NodeTraversal
::nextSkippingChildren(node); | |
| 353 } | |
| 354 | |
| 355 inline void ReplaceSelectionCommand::InsertedNodes::willRemoveNode(Node& node) | |
| 356 { | |
| 357 if (m_firstNodeInserted.get() == node && m_lastNodeInserted.get() == node) { | |
| 358 m_firstNodeInserted = nullptr; | |
| 359 m_lastNodeInserted = nullptr; | |
| 360 } else if (m_firstNodeInserted.get() == node) { | |
| 361 m_firstNodeInserted = NodeTraversal::nextSkippingChildren(*m_firstNodeIn
serted); | |
| 362 } else if (m_lastNodeInserted.get() == node) { | |
| 363 m_lastNodeInserted = NodeTraversal::previousSkippingChildren(*m_lastNode
Inserted); | |
| 364 } | |
| 365 } | |
| 366 | |
| 367 inline void ReplaceSelectionCommand::InsertedNodes::didReplaceNode(Node& node, N
ode& newNode) | |
| 368 { | |
| 369 if (m_firstNodeInserted.get() == node) | |
| 370 m_firstNodeInserted = &newNode; | |
| 371 if (m_lastNodeInserted.get() == node) | |
| 372 m_lastNodeInserted = &newNode; | |
| 373 } | |
| 374 | |
| 375 ReplaceSelectionCommand::ReplaceSelectionCommand(Document& document, PassRefPtrW
illBeRawPtr<DocumentFragment> fragment, CommandOptions options, EditAction editA
ction) | |
| 376 : CompositeEditCommand(document) | |
| 377 , m_selectReplacement(options & SelectReplacement) | |
| 378 , m_smartReplace(options & SmartReplace) | |
| 379 , m_matchStyle(options & MatchStyle) | |
| 380 , m_documentFragment(fragment) | |
| 381 , m_preventNesting(options & PreventNesting) | |
| 382 , m_movingParagraph(options & MovingParagraph) | |
| 383 , m_editAction(editAction) | |
| 384 , m_sanitizeFragment(options & SanitizeFragment) | |
| 385 , m_shouldMergeEnd(false) | |
| 386 { | |
| 387 } | |
| 388 | |
| 389 static bool hasMatchingQuoteLevel(VisiblePosition endOfExistingContent, VisibleP
osition endOfInsertedContent) | |
| 390 { | |
| 391 Position existing = endOfExistingContent.deepEquivalent(); | |
| 392 Position inserted = endOfInsertedContent.deepEquivalent(); | |
| 393 bool isInsideMailBlockquote = enclosingNodeOfType(inserted, isMailHTMLBlockq
uoteElement, CanCrossEditingBoundary); | |
| 394 return isInsideMailBlockquote && (numEnclosingMailBlockquotes(existing) == n
umEnclosingMailBlockquotes(inserted)); | |
| 395 } | |
| 396 | |
| 397 bool ReplaceSelectionCommand::shouldMergeStart(bool selectionStartWasStartOfPara
graph, bool fragmentHasInterchangeNewlineAtStart, bool selectionStartWasInsideMa
ilBlockquote) | |
| 398 { | |
| 399 if (m_movingParagraph) | |
| 400 return false; | |
| 401 | |
| 402 VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent()); | |
| 403 VisiblePosition prev = startOfInsertedContent.previous(CannotCrossEditingBou
ndary); | |
| 404 if (prev.isNull()) | |
| 405 return false; | |
| 406 | |
| 407 // When we have matching quote levels, its ok to merge more frequently. | |
| 408 // For a successful merge, we still need to make sure that the inserted cont
ent starts with the beginning of a paragraph. | |
| 409 // And we should only merge here if the selection start was inside a mail bl
ockquote. This prevents against removing a | |
| 410 // blockquote from newly pasted quoted content that was pasted into an unquo
ted position. If that unquoted position happens | |
| 411 // to be right after another blockquote, we don't want to merge and risk str
ipping a valid block (and newline) from the pasted content. | |
| 412 if (isStartOfParagraph(startOfInsertedContent) && selectionStartWasInsideMai
lBlockquote && hasMatchingQuoteLevel(prev, positionAtEndOfInsertedContent())) | |
| 413 return true; | |
| 414 | |
| 415 return !selectionStartWasStartOfParagraph | |
| 416 && !fragmentHasInterchangeNewlineAtStart | |
| 417 && isStartOfParagraph(startOfInsertedContent) | |
| 418 && !isHTMLBRElement(*startOfInsertedContent.deepEquivalent().anchorNode(
)) | |
| 419 && shouldMerge(startOfInsertedContent, prev); | |
| 420 } | |
| 421 | |
| 422 bool ReplaceSelectionCommand::shouldMergeEnd(bool selectionEndWasEndOfParagraph) | |
| 423 { | |
| 424 VisiblePosition endOfInsertedContent(positionAtEndOfInsertedContent()); | |
| 425 VisiblePosition next = endOfInsertedContent.next(CannotCrossEditingBoundary)
; | |
| 426 if (next.isNull()) | |
| 427 return false; | |
| 428 | |
| 429 return !selectionEndWasEndOfParagraph | |
| 430 && isEndOfParagraph(endOfInsertedContent) | |
| 431 && !isHTMLBRElement(*endOfInsertedContent.deepEquivalent().anchorNode()) | |
| 432 && shouldMerge(endOfInsertedContent, next); | |
| 433 } | |
| 434 | |
| 435 static bool isMailPasteAsQuotationHTMLBlockQuoteElement(const Node* node) | |
| 436 { | |
| 437 if (!node || !node->isHTMLElement()) | |
| 438 return false; | |
| 439 const HTMLElement& element = toHTMLElement(*node); | |
| 440 if (!element.hasTagName(blockquoteTag) || element.getAttribute(classAttr) !=
ApplePasteAsQuotation) | |
| 441 return false; | |
| 442 UseCounter::count(node->document(), UseCounter::EditingApplePasteAsQuotation
); | |
| 443 return true; | |
| 444 } | |
| 445 | |
| 446 static bool isHTMLHeaderElement(const Node* a) | |
| 447 { | |
| 448 if (!a || !a->isHTMLElement()) | |
| 449 return false; | |
| 450 | |
| 451 const HTMLElement& element = toHTMLElement(*a); | |
| 452 return element.hasTagName(h1Tag) | |
| 453 || element.hasTagName(h2Tag) | |
| 454 || element.hasTagName(h3Tag) | |
| 455 || element.hasTagName(h4Tag) | |
| 456 || element.hasTagName(h5Tag) | |
| 457 || element.hasTagName(h6Tag); | |
| 458 } | |
| 459 | |
| 460 static bool haveSameTagName(Element* a, Element* b) | |
| 461 { | |
| 462 return a && b && a->tagName() == b->tagName(); | |
| 463 } | |
| 464 | |
| 465 bool ReplaceSelectionCommand::shouldMerge(const VisiblePosition& source, const V
isiblePosition& destination) | |
| 466 { | |
| 467 if (source.isNull() || destination.isNull()) | |
| 468 return false; | |
| 469 | |
| 470 Node* sourceNode = source.deepEquivalent().anchorNode(); | |
| 471 Node* destinationNode = destination.deepEquivalent().anchorNode(); | |
| 472 Element* sourceBlock = enclosingBlock(sourceNode); | |
| 473 Element* destinationBlock = enclosingBlock(destinationNode); | |
| 474 return !enclosingNodeOfType(source.deepEquivalent(), &isMailPasteAsQuotation
HTMLBlockQuoteElement) | |
| 475 && sourceBlock && (!sourceBlock->hasTagName(blockquoteTag) || isMailHTML
BlockquoteElement(sourceBlock)) | |
| 476 && enclosingListChild(sourceBlock) == enclosingListChild(destinationNode
) | |
| 477 && enclosingTableCell(source.deepEquivalent()) == enclosingTableCell(des
tination.deepEquivalent()) | |
| 478 && (!isHTMLHeaderElement(sourceBlock) || haveSameTagName(sourceBlock, de
stinationBlock)) | |
| 479 // Don't merge to or from a position before or after a block because it
would | |
| 480 // be a no-op and cause infinite recursion. | |
| 481 && !isBlock(sourceNode) && !isBlock(destinationNode); | |
| 482 } | |
| 483 | |
| 484 // Style rules that match just inserted elements could change their appearance,
like | |
| 485 // a div inserted into a document with div { display:inline; }. | |
| 486 void ReplaceSelectionCommand::removeRedundantStylesAndKeepStyleSpanInline(Insert
edNodes& insertedNodes) | |
| 487 { | |
| 488 RefPtrWillBeRawPtr<Node> pastEndNode = insertedNodes.pastLastLeaf(); | |
| 489 RefPtrWillBeRawPtr<Node> next = nullptr; | |
| 490 for (RefPtrWillBeRawPtr<Node> node = insertedNodes.firstNodeInserted(); node
&& node != pastEndNode; node = next) { | |
| 491 // FIXME: <rdar://problem/5371536> Style rules that match pasted content
can change it's appearance | |
| 492 | |
| 493 next = NodeTraversal::next(*node); | |
| 494 if (!node->isStyledElement()) | |
| 495 continue; | |
| 496 | |
| 497 Element* element = toElement(node); | |
| 498 | |
| 499 const StylePropertySet* inlineStyle = element->inlineStyle(); | |
| 500 RefPtrWillBeRawPtr<EditingStyle> newInlineStyle = EditingStyle::create(i
nlineStyle); | |
| 501 if (inlineStyle) { | |
| 502 if (element->isHTMLElement()) { | |
| 503 Vector<QualifiedName> attributes; | |
| 504 HTMLElement* htmlElement = toHTMLElement(element); | |
| 505 ASSERT(htmlElement); | |
| 506 | |
| 507 if (newInlineStyle->conflictsWithImplicitStyleOfElement(htmlElem
ent)) { | |
| 508 // e.g. <b style="font-weight: normal;"> is converted to <sp
an style="font-weight: normal;"> | |
| 509 element = replaceElementWithSpanPreservingChildrenAndAttribu
tes(htmlElement); | |
| 510 inlineStyle = element->inlineStyle(); | |
| 511 insertedNodes.didReplaceNode(*htmlElement, *element); | |
| 512 } else if (newInlineStyle->extractConflictingImplicitStyleOfAttr
ibutes(htmlElement, EditingStyle::PreserveWritingDirection, 0, attributes, | |
| 513 EditingStyle::DoNotExtractMatchingStyle)) { | |
| 514 // e.g. <font size="3" style="font-size: 20px;"> is converte
d to <font style="font-size: 20px;"> | |
| 515 for (size_t i = 0; i < attributes.size(); i++) | |
| 516 removeElementAttribute(htmlElement, attributes[i]); | |
| 517 } | |
| 518 } | |
| 519 | |
| 520 ContainerNode* context = element->parentNode(); | |
| 521 | |
| 522 // If Mail wraps the fragment with a Paste as Quotation blockquote,
or if you're pasting into a quoted region, | |
| 523 // styles from blockquoteNode are allowed to override those from the
source document, see <rdar://problem/4930986> and <rdar://problem/5089327>. | |
| 524 HTMLQuoteElement* blockquoteElement = !context || isMailPasteAsQuota
tionHTMLBlockQuoteElement(context) ? | |
| 525 toHTMLQuoteElement(context) : | |
| 526 toHTMLQuoteElement(enclosingNodeOfType(firstPositionInNode(conte
xt), isMailHTMLBlockquoteElement, CanCrossEditingBoundary)); | |
| 527 if (blockquoteElement) | |
| 528 newInlineStyle->removeStyleFromRulesAndContext(element, document
().documentElement()); | |
| 529 | |
| 530 newInlineStyle->removeStyleFromRulesAndContext(element, context); | |
| 531 } | |
| 532 | |
| 533 if (!inlineStyle || newInlineStyle->isEmpty()) { | |
| 534 if (isStyleSpanOrSpanWithOnlyStyleAttribute(element) || isEmptyFontT
ag(element, AllowNonEmptyStyleAttribute)) { | |
| 535 insertedNodes.willRemoveNodePreservingChildren(*element); | |
| 536 removeNodePreservingChildren(element); | |
| 537 continue; | |
| 538 } | |
| 539 removeElementAttribute(element, styleAttr); | |
| 540 } else if (newInlineStyle->style()->propertyCount() != inlineStyle->prop
ertyCount()) { | |
| 541 setNodeAttribute(element, styleAttr, AtomicString(newInlineStyle->st
yle()->asText())); | |
| 542 } | |
| 543 | |
| 544 // FIXME: Tolerate differences in id, class, and style attributes. | |
| 545 if (element->parentNode() && isNonTableCellHTMLBlockElement(element) &&
areIdenticalElements(element, element->parentNode()) | |
| 546 && VisiblePosition(firstPositionInNode(element->parentNode())).deepE
quivalent() == VisiblePosition(firstPositionInNode(element)).deepEquivalent() | |
| 547 && VisiblePosition(lastPositionInNode(element->parentNode())).deepEq
uivalent() == VisiblePosition(lastPositionInNode(element)).deepEquivalent()) { | |
| 548 insertedNodes.willRemoveNodePreservingChildren(*element); | |
| 549 removeNodePreservingChildren(element); | |
| 550 continue; | |
| 551 } | |
| 552 | |
| 553 if (element->parentNode() && element->parentNode()->layoutObjectIsRichly
Editable()) | |
| 554 removeElementAttribute(element, contenteditableAttr); | |
| 555 | |
| 556 // WebKit used to not add display: inline and float: none on copy. | |
| 557 // Keep this code around for backward compatibility | |
| 558 if (isLegacyAppleHTMLSpanElement(element)) { | |
| 559 if (!element->hasChildren()) { | |
| 560 insertedNodes.willRemoveNodePreservingChildren(*element); | |
| 561 removeNodePreservingChildren(element); | |
| 562 continue; | |
| 563 } | |
| 564 // There are other styles that style rules can give to style spans, | |
| 565 // but these are the two important ones because they'll prevent | |
| 566 // inserted content from appearing in the right paragraph. | |
| 567 // FIXME: Hyatt is concerned that selectively using display:inline w
ill give inconsistent | |
| 568 // results. We already know one issue because td elements ignore the
ir display property | |
| 569 // in quirks mode (which Mail.app is always in). We should look for
an alternative. | |
| 570 | |
| 571 // Mutate using the CSSOM wrapper so we get the same event behavior
as a script. | |
| 572 if (isBlock(element)) | |
| 573 element->style()->setPropertyInternal(CSSPropertyDisplay, "inlin
e", false, IGNORE_EXCEPTION); | |
| 574 if (element->layoutObject() && element->layoutObject()->style()->isF
loating()) | |
| 575 element->style()->setPropertyInternal(CSSPropertyFloat, "none",
false, IGNORE_EXCEPTION); | |
| 576 } | |
| 577 } | |
| 578 } | |
| 579 | |
| 580 static bool isProhibitedParagraphChild(const AtomicString& name) | |
| 581 { | |
| 582 // https://dvcs.w3.org/hg/editing/raw-file/57abe6d3cb60/editing.html#prohibi
ted-paragraph-child | |
| 583 DEFINE_STATIC_LOCAL(HashSet<AtomicString>, elements, ()); | |
| 584 if (elements.isEmpty()) { | |
| 585 elements.add(addressTag.localName()); | |
| 586 elements.add(articleTag.localName()); | |
| 587 elements.add(asideTag.localName()); | |
| 588 elements.add(blockquoteTag.localName()); | |
| 589 elements.add(captionTag.localName()); | |
| 590 elements.add(centerTag.localName()); | |
| 591 elements.add(colTag.localName()); | |
| 592 elements.add(colgroupTag.localName()); | |
| 593 elements.add(ddTag.localName()); | |
| 594 elements.add(detailsTag.localName()); | |
| 595 elements.add(dirTag.localName()); | |
| 596 elements.add(divTag.localName()); | |
| 597 elements.add(dlTag.localName()); | |
| 598 elements.add(dtTag.localName()); | |
| 599 elements.add(fieldsetTag.localName()); | |
| 600 elements.add(figcaptionTag.localName()); | |
| 601 elements.add(figureTag.localName()); | |
| 602 elements.add(footerTag.localName()); | |
| 603 elements.add(formTag.localName()); | |
| 604 elements.add(h1Tag.localName()); | |
| 605 elements.add(h2Tag.localName()); | |
| 606 elements.add(h3Tag.localName()); | |
| 607 elements.add(h4Tag.localName()); | |
| 608 elements.add(h5Tag.localName()); | |
| 609 elements.add(h6Tag.localName()); | |
| 610 elements.add(headerTag.localName()); | |
| 611 elements.add(hgroupTag.localName()); | |
| 612 elements.add(hrTag.localName()); | |
| 613 elements.add(liTag.localName()); | |
| 614 elements.add(listingTag.localName()); | |
| 615 elements.add(mainTag.localName()); // Missing in the specification. | |
| 616 elements.add(menuTag.localName()); | |
| 617 elements.add(navTag.localName()); | |
| 618 elements.add(olTag.localName()); | |
| 619 elements.add(pTag.localName()); | |
| 620 elements.add(plaintextTag.localName()); | |
| 621 elements.add(preTag.localName()); | |
| 622 elements.add(sectionTag.localName()); | |
| 623 elements.add(summaryTag.localName()); | |
| 624 elements.add(tableTag.localName()); | |
| 625 elements.add(tbodyTag.localName()); | |
| 626 elements.add(tdTag.localName()); | |
| 627 elements.add(tfootTag.localName()); | |
| 628 elements.add(thTag.localName()); | |
| 629 elements.add(theadTag.localName()); | |
| 630 elements.add(trTag.localName()); | |
| 631 elements.add(ulTag.localName()); | |
| 632 elements.add(xmpTag.localName()); | |
| 633 } | |
| 634 return elements.contains(name); | |
| 635 } | |
| 636 | |
| 637 void ReplaceSelectionCommand::makeInsertedContentRoundTrippableWithHTMLTreeBuild
er(const InsertedNodes& insertedNodes) | |
| 638 { | |
| 639 RefPtrWillBeRawPtr<Node> pastEndNode = insertedNodes.pastLastLeaf(); | |
| 640 RefPtrWillBeRawPtr<Node> next = nullptr; | |
| 641 for (RefPtrWillBeRawPtr<Node> node = insertedNodes.firstNodeInserted(); node
&& node != pastEndNode; node = next) { | |
| 642 next = NodeTraversal::next(*node); | |
| 643 | |
| 644 if (!node->isHTMLElement()) | |
| 645 continue; | |
| 646 | |
| 647 HTMLElement& element = toHTMLElement(*node); | |
| 648 if (isProhibitedParagraphChild(element.localName())) { | |
| 649 if (HTMLElement* paragraphElement = toHTMLElement(enclosingElementWi
thTag(positionInParentBeforeNode(element), pTag))) | |
| 650 moveElementOutOfAncestor(&element, paragraphElement); | |
| 651 } | |
| 652 | |
| 653 if (isHTMLHeaderElement(&element)) { | |
| 654 if (HTMLElement* headerElement = toHTMLElement(highestEnclosingNodeO
fType(positionInParentBeforeNode(element), isHTMLHeaderElement))) | |
| 655 moveElementOutOfAncestor(&element, headerElement); | |
| 656 } | |
| 657 } | |
| 658 } | |
| 659 | |
| 660 void ReplaceSelectionCommand::moveElementOutOfAncestor(PassRefPtrWillBeRawPtr<El
ement> prpElement, PassRefPtrWillBeRawPtr<ContainerNode> prpAncestor) | |
| 661 { | |
| 662 RefPtrWillBeRawPtr<Element> element = prpElement; | |
| 663 RefPtrWillBeRawPtr<ContainerNode> ancestor = prpAncestor; | |
| 664 | |
| 665 if (!ancestor->parentNode()->hasEditableStyle()) | |
| 666 return; | |
| 667 | |
| 668 VisiblePosition positionAtEndOfNode(lastPositionInOrAfterNode(element.get())
); | |
| 669 VisiblePosition lastPositionInParagraph(lastPositionInNode(ancestor.get())); | |
| 670 if (positionAtEndOfNode.deepEquivalent() == lastPositionInParagraph.deepEqui
valent()) { | |
| 671 removeNode(element); | |
| 672 if (ancestor->nextSibling()) | |
| 673 insertNodeBefore(element, ancestor->nextSibling()); | |
| 674 else | |
| 675 appendNode(element, ancestor->parentNode()); | |
| 676 } else { | |
| 677 RefPtrWillBeRawPtr<Node> nodeToSplitTo = splitTreeToNode(element.get(),
ancestor.get(), true); | |
| 678 removeNode(element); | |
| 679 insertNodeBefore(element, nodeToSplitTo); | |
| 680 } | |
| 681 if (!ancestor->hasChildren()) | |
| 682 removeNode(ancestor.release()); | |
| 683 } | |
| 684 | |
| 685 static inline bool nodeHasVisibleLayoutText(Text& text) | |
| 686 { | |
| 687 return text.layoutObject() && text.layoutObject()->resolvedTextLength() > 0; | |
| 688 } | |
| 689 | |
| 690 void ReplaceSelectionCommand::removeUnrenderedTextNodesAtEnds(InsertedNodes& ins
ertedNodes) | |
| 691 { | |
| 692 document().updateLayoutIgnorePendingStylesheets(); | |
| 693 | |
| 694 Node* lastLeafInserted = insertedNodes.lastLeafInserted(); | |
| 695 if (lastLeafInserted && lastLeafInserted->isTextNode() && !nodeHasVisibleLay
outText(toText(*lastLeafInserted)) | |
| 696 && !enclosingElementWithTag(firstPositionInOrBeforeNode(lastLeafInserted
), selectTag) | |
| 697 && !enclosingElementWithTag(firstPositionInOrBeforeNode(lastLeafInserted
), scriptTag)) { | |
| 698 insertedNodes.willRemoveNode(*lastLeafInserted); | |
| 699 removeNode(lastLeafInserted); | |
| 700 } | |
| 701 | |
| 702 // We don't have to make sure that firstNodeInserted isn't inside a select o
r script element, because | |
| 703 // it is a top level node in the fragment and the user can't insert into tho
se elements. | |
| 704 Node* firstNodeInserted = insertedNodes.firstNodeInserted(); | |
| 705 if (firstNodeInserted && firstNodeInserted->isTextNode() && !nodeHasVisibleL
ayoutText(toText(*firstNodeInserted))) { | |
| 706 insertedNodes.willRemoveNode(*firstNodeInserted); | |
| 707 removeNode(firstNodeInserted); | |
| 708 } | |
| 709 } | |
| 710 | |
| 711 VisiblePosition ReplaceSelectionCommand::positionAtEndOfInsertedContent() const | |
| 712 { | |
| 713 // FIXME: Why is this hack here? What's special about <select> tags? | |
| 714 HTMLSelectElement* enclosingSelect = toHTMLSelectElement(enclosingElementWit
hTag(m_endOfInsertedContent, selectTag)); | |
| 715 return VisiblePosition(enclosingSelect ? lastPositionInOrAfterNode(enclosing
Select) : m_endOfInsertedContent); | |
| 716 } | |
| 717 | |
| 718 VisiblePosition ReplaceSelectionCommand::positionAtStartOfInsertedContent() cons
t | |
| 719 { | |
| 720 return VisiblePosition(m_startOfInsertedContent); | |
| 721 } | |
| 722 | |
| 723 static void removeHeadContents(ReplacementFragment& fragment) | |
| 724 { | |
| 725 Node* next = nullptr; | |
| 726 for (Node* node = fragment.firstChild(); node; node = next) { | |
| 727 if (isHTMLBaseElement(*node) | |
| 728 || isHTMLLinkElement(*node) | |
| 729 || isHTMLMetaElement(*node) | |
| 730 || isHTMLStyleElement(*node) | |
| 731 || isHTMLTitleElement(*node)) { | |
| 732 next = NodeTraversal::nextSkippingChildren(*node); | |
| 733 fragment.removeNode(node); | |
| 734 } else { | |
| 735 next = NodeTraversal::next(*node); | |
| 736 } | |
| 737 } | |
| 738 } | |
| 739 | |
| 740 // Remove style spans before insertion if they are unnecessary. It's faster bec
ause we'll | |
| 741 // avoid doing a layout. | |
| 742 static bool handleStyleSpansBeforeInsertion(ReplacementFragment& fragment, const
Position& insertionPos) | |
| 743 { | |
| 744 Node* topNode = fragment.firstChild(); | |
| 745 | |
| 746 // Handling the case where we are doing Paste as Quotation or pasting into q
uoted content is more complicated (see handleStyleSpans) | |
| 747 // and doesn't receive the optimization. | |
| 748 if (isMailPasteAsQuotationHTMLBlockQuoteElement(topNode) || enclosingNodeOfT
ype(firstPositionInOrBeforeNode(topNode), isMailHTMLBlockquoteElement, CanCrossE
ditingBoundary)) | |
| 749 return false; | |
| 750 | |
| 751 // Either there are no style spans in the fragment or a WebKit client has ad
ded content to the fragment | |
| 752 // before inserting it. Look for and handle style spans after insertion. | |
| 753 if (!isLegacyAppleHTMLSpanElement(topNode)) | |
| 754 return false; | |
| 755 | |
| 756 HTMLSpanElement* wrappingStyleSpan = toHTMLSpanElement(topNode); | |
| 757 RefPtrWillBeRawPtr<EditingStyle> styleAtInsertionPos = EditingStyle::create(
insertionPos.parentAnchoredEquivalent()); | |
| 758 String styleText = styleAtInsertionPos->style()->asText(); | |
| 759 | |
| 760 // FIXME: This string comparison is a naive way of comparing two styles. | |
| 761 // We should be taking the diff and check that the diff is empty. | |
| 762 if (styleText != wrappingStyleSpan->getAttribute(styleAttr)) | |
| 763 return false; | |
| 764 | |
| 765 fragment.removeNodePreservingChildren(wrappingStyleSpan); | |
| 766 return true; | |
| 767 } | |
| 768 | |
| 769 // At copy time, WebKit wraps copied content in a span that contains the source
document's | |
| 770 // default styles. If the copied Range inherits any other styles from its ances
tors, we put | |
| 771 // those styles on a second span. | |
| 772 // This function removes redundant styles from those spans, and removes the span
s if all their | |
| 773 // styles are redundant. | |
| 774 // We should remove the Apple-style-span class when we're done, see <rdar://prob
lem/5685600>. | |
| 775 // We should remove styles from spans that are overridden by all of their childr
en, either here | |
| 776 // or at copy time. | |
| 777 void ReplaceSelectionCommand::handleStyleSpans(InsertedNodes& insertedNodes) | |
| 778 { | |
| 779 HTMLSpanElement* wrappingStyleSpan = nullptr; | |
| 780 // The style span that contains the source document's default style should b
e at | |
| 781 // the top of the fragment, but Mail sometimes adds a wrapper (for Paste As
Quotation), | |
| 782 // so search for the top level style span instead of assuming it's at the to
p. | |
| 783 | |
| 784 for (Node& node : NodeTraversal::startsAt(insertedNodes.firstNodeInserted())
) { | |
| 785 if (isLegacyAppleHTMLSpanElement(&node)) { | |
| 786 wrappingStyleSpan = toHTMLSpanElement(&node); | |
| 787 break; | |
| 788 } | |
| 789 } | |
| 790 | |
| 791 // There might not be any style spans if we're pasting from another applicat
ion or if | |
| 792 // we are here because of a document.execCommand("InsertHTML", ...) call. | |
| 793 if (!wrappingStyleSpan) | |
| 794 return; | |
| 795 | |
| 796 RefPtrWillBeRawPtr<EditingStyle> style = EditingStyle::create(wrappingStyleS
pan->inlineStyle()); | |
| 797 ContainerNode* context = wrappingStyleSpan->parentNode(); | |
| 798 | |
| 799 // If Mail wraps the fragment with a Paste as Quotation blockquote, or if yo
u're pasting into a quoted region, | |
| 800 // styles from blockquoteElement are allowed to override those from the sour
ce document, see <rdar://problem/4930986> and <rdar://problem/5089327>. | |
| 801 HTMLQuoteElement* blockquoteElement = isMailPasteAsQuotationHTMLBlockQuoteEl
ement(context) ? | |
| 802 toHTMLQuoteElement(context) : | |
| 803 toHTMLQuoteElement(enclosingNodeOfType(firstPositionInNode(context), isM
ailHTMLBlockquoteElement, CanCrossEditingBoundary)); | |
| 804 if (blockquoteElement) | |
| 805 context = document().documentElement(); | |
| 806 | |
| 807 // This operation requires that only editing styles to be removed from sourc
eDocumentStyle. | |
| 808 style->prepareToApplyAt(firstPositionInNode(context)); | |
| 809 | |
| 810 // Remove block properties in the span's style. This prevents properties tha
t probably have no effect | |
| 811 // currently from affecting blocks later if the style is cloned for a new bl
ock element during a future | |
| 812 // editing operation. | |
| 813 // FIXME: They *can* have an effect currently if blocks beneath the style sp
an aren't individually marked | |
| 814 // with block styles by the editing engine used to style them. WebKit doesn
't do this, but others might. | |
| 815 style->removeBlockProperties(); | |
| 816 | |
| 817 if (style->isEmpty() || !wrappingStyleSpan->hasChildren()) { | |
| 818 insertedNodes.willRemoveNodePreservingChildren(*wrappingStyleSpan); | |
| 819 removeNodePreservingChildren(wrappingStyleSpan); | |
| 820 } else { | |
| 821 setNodeAttribute(wrappingStyleSpan, styleAttr, AtomicString(style->style
()->asText())); | |
| 822 } | |
| 823 } | |
| 824 | |
| 825 void ReplaceSelectionCommand::mergeEndIfNeeded() | |
| 826 { | |
| 827 if (!m_shouldMergeEnd) | |
| 828 return; | |
| 829 | |
| 830 VisiblePosition startOfInsertedContent(positionAtStartOfInsertedContent()); | |
| 831 VisiblePosition endOfInsertedContent(positionAtEndOfInsertedContent()); | |
| 832 | |
| 833 // Bail to avoid infinite recursion. | |
| 834 if (m_movingParagraph) { | |
| 835 ASSERT_NOT_REACHED(); | |
| 836 return; | |
| 837 } | |
| 838 | |
| 839 // Merging two paragraphs will destroy the moved one's block styles. Always
move the end of inserted forward | |
| 840 // to preserve the block style of the paragraph already in the document, unl
ess the paragraph to move would | |
| 841 // include the what was the start of the selection that was pasted into, so
that we preserve that paragraph's | |
| 842 // block styles. | |
| 843 bool mergeForward = !(inSameParagraph(startOfInsertedContent, endOfInsertedC
ontent) && !isStartOfParagraph(startOfInsertedContent)); | |
| 844 | |
| 845 VisiblePosition destination = mergeForward ? endOfInsertedContent.next() : e
ndOfInsertedContent; | |
| 846 VisiblePosition startOfParagraphToMove = mergeForward ? startOfParagraph(end
OfInsertedContent) : endOfInsertedContent.next(); | |
| 847 | |
| 848 // Merging forward could result in deleting the destination anchor node. | |
| 849 // To avoid this, we add a placeholder node before the start of the paragrap
h. | |
| 850 if (endOfParagraph(startOfParagraphToMove).deepEquivalent() == destination.d
eepEquivalent()) { | |
| 851 RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(docum
ent()); | |
| 852 insertNodeBefore(placeholder, startOfParagraphToMove.deepEquivalent().an
chorNode()); | |
| 853 destination = VisiblePosition(positionBeforeNode(placeholder.get())); | |
| 854 } | |
| 855 | |
| 856 moveParagraph(startOfParagraphToMove, endOfParagraph(startOfParagraphToMove)
, destination); | |
| 857 | |
| 858 // Merging forward will remove m_endOfInsertedContent from the document. | |
| 859 if (mergeForward) { | |
| 860 if (m_startOfInsertedContent.isOrphan()) | |
| 861 m_startOfInsertedContent = endingSelection().visibleStart().deepEqui
valent(); | |
| 862 m_endOfInsertedContent = endingSelection().visibleEnd().deepEquivalent()
; | |
| 863 // If we merged text nodes, m_endOfInsertedContent could be null. If | |
| 864 // this is the case, we use m_startOfInsertedContent. | |
| 865 if (m_endOfInsertedContent.isNull()) | |
| 866 m_endOfInsertedContent = m_startOfInsertedContent; | |
| 867 } | |
| 868 } | |
| 869 | |
| 870 static Node* enclosingInline(Node* node) | |
| 871 { | |
| 872 while (ContainerNode* parent = node->parentNode()) { | |
| 873 if (isBlockFlowElement(*parent) || isHTMLBodyElement(*parent)) | |
| 874 return node; | |
| 875 // Stop if any previous sibling is a block. | |
| 876 for (Node* sibling = node->previousSibling(); sibling; sibling = sibling
->previousSibling()) { | |
| 877 if (isBlockFlowElement(*sibling)) | |
| 878 return node; | |
| 879 } | |
| 880 node = parent; | |
| 881 } | |
| 882 return node; | |
| 883 } | |
| 884 | |
| 885 static bool isInlineHTMLElementWithStyle(const Node* node) | |
| 886 { | |
| 887 // We don't want to skip over any block elements. | |
| 888 if (isBlock(node)) | |
| 889 return false; | |
| 890 | |
| 891 if (!node->isHTMLElement()) | |
| 892 return false; | |
| 893 | |
| 894 // We can skip over elements whose class attribute is | |
| 895 // one of our internal classes. | |
| 896 const HTMLElement* element = toHTMLElement(node); | |
| 897 const AtomicString& classAttributeValue = element->getAttribute(classAttr); | |
| 898 if (classAttributeValue == AppleTabSpanClass) { | |
| 899 UseCounter::count(element->document(), UseCounter::EditingAppleTabSpanCl
ass); | |
| 900 return true; | |
| 901 } | |
| 902 if (classAttributeValue == AppleConvertedSpace) { | |
| 903 UseCounter::count(element->document(), UseCounter::EditingAppleConverted
Space); | |
| 904 return true; | |
| 905 } | |
| 906 if (classAttributeValue == ApplePasteAsQuotation) { | |
| 907 UseCounter::count(element->document(), UseCounter::EditingApplePasteAsQu
otation); | |
| 908 return true; | |
| 909 } | |
| 910 | |
| 911 return EditingStyle::elementIsStyledSpanOrHTMLEquivalent(element); | |
| 912 } | |
| 913 | |
| 914 static inline HTMLElement* elementToSplitToAvoidPastingIntoInlineElementsWithSty
le(const Position& insertionPos) | |
| 915 { | |
| 916 Element* containingBlock = enclosingBlock(insertionPos.computeContainerNode(
)); | |
| 917 return toHTMLElement(highestEnclosingNodeOfType(insertionPos, isInlineHTMLEl
ementWithStyle, CannotCrossEditingBoundary, containingBlock)); | |
| 918 } | |
| 919 | |
| 920 void ReplaceSelectionCommand::doApply() | |
| 921 { | |
| 922 VisibleSelection selection = endingSelection(); | |
| 923 ASSERT(selection.isCaretOrRange()); | |
| 924 ASSERT(selection.start().anchorNode()); | |
| 925 if (!selection.isNonOrphanedCaretOrRange() || !selection.start().anchorNode(
)) | |
| 926 return; | |
| 927 | |
| 928 if (!selection.rootEditableElement()) | |
| 929 return; | |
| 930 | |
| 931 ReplacementFragment fragment(&document(), m_documentFragment.get(), selectio
n); | |
| 932 if (performTrivialReplace(fragment)) | |
| 933 return; | |
| 934 | |
| 935 // We can skip matching the style if the selection is plain text. | |
| 936 if ((selection.start().anchorNode()->layoutObject() && selection.start().anc
horNode()->layoutObject()->style()->userModify() == READ_WRITE_PLAINTEXT_ONLY) | |
| 937 && (selection.end().anchorNode()->layoutObject() && selection.end().anch
orNode()->layoutObject()->style()->userModify() == READ_WRITE_PLAINTEXT_ONLY)) | |
| 938 m_matchStyle = false; | |
| 939 | |
| 940 if (m_matchStyle) { | |
| 941 m_insertionStyle = EditingStyle::create(selection.start()); | |
| 942 m_insertionStyle->mergeTypingStyle(&document()); | |
| 943 } | |
| 944 | |
| 945 VisiblePosition visibleStart = selection.visibleStart(); | |
| 946 VisiblePosition visibleEnd = selection.visibleEnd(); | |
| 947 | |
| 948 bool selectionEndWasEndOfParagraph = isEndOfParagraph(visibleEnd); | |
| 949 bool selectionStartWasStartOfParagraph = isStartOfParagraph(visibleStart); | |
| 950 | |
| 951 Element* enclosingBlockOfVisibleStart = enclosingBlock(visibleStart.deepEqui
valent().anchorNode()); | |
| 952 | |
| 953 Position insertionPos = selection.start(); | |
| 954 bool startIsInsideMailBlockquote = enclosingNodeOfType(insertionPos, isMailH
TMLBlockquoteElement, CanCrossEditingBoundary); | |
| 955 bool selectionIsPlainText = !selection.isContentRichlyEditable(); | |
| 956 Element* currentRoot = selection.rootEditableElement(); | |
| 957 | |
| 958 if ((selectionStartWasStartOfParagraph && selectionEndWasEndOfParagraph && !
startIsInsideMailBlockquote) | |
| 959 || enclosingBlockOfVisibleStart == currentRoot | |
| 960 || isListItem(enclosingBlockOfVisibleStart) | |
| 961 || selectionIsPlainText) { | |
| 962 m_preventNesting = false; | |
| 963 } | |
| 964 | |
| 965 if (selection.isRange()) { | |
| 966 // When the end of the selection being pasted into is at the end of a pa
ragraph, and that selection | |
| 967 // spans multiple blocks, not merging may leave an empty line. | |
| 968 // When the start of the selection being pasted into is at the start of
a block, not merging | |
| 969 // will leave hanging block(s). | |
| 970 // Merge blocks if the start of the selection was in a Mail blockquote,
since we handle | |
| 971 // that case specially to prevent nesting. | |
| 972 bool mergeBlocksAfterDelete = startIsInsideMailBlockquote || isEndOfPara
graph(visibleEnd) || isStartOfBlock(visibleStart); | |
| 973 // FIXME: We should only expand to include fully selected special elemen
ts if we are copying a | |
| 974 // selection and pasting it on top of itself. | |
| 975 deleteSelection(false, mergeBlocksAfterDelete, false); | |
| 976 visibleStart = endingSelection().visibleStart(); | |
| 977 if (fragment.hasInterchangeNewlineAtStart()) { | |
| 978 if (isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleSta
rt)) { | |
| 979 if (!isEndOfEditableOrNonEditableContent(visibleStart)) | |
| 980 setEndingSelection(visibleStart.next()); | |
| 981 } else { | |
| 982 insertParagraphSeparator(); | |
| 983 } | |
| 984 } | |
| 985 insertionPos = endingSelection().start(); | |
| 986 } else { | |
| 987 ASSERT(selection.isCaret()); | |
| 988 if (fragment.hasInterchangeNewlineAtStart()) { | |
| 989 VisiblePosition next = visibleStart.next(CannotCrossEditingBoundary)
; | |
| 990 if (isEndOfParagraph(visibleStart) && !isStartOfParagraph(visibleSta
rt) && next.isNotNull()) { | |
| 991 setEndingSelection(next); | |
| 992 } else { | |
| 993 insertParagraphSeparator(); | |
| 994 visibleStart = endingSelection().visibleStart(); | |
| 995 } | |
| 996 } | |
| 997 // We split the current paragraph in two to avoid nesting the blocks fro
m the fragment inside the current block. | |
| 998 // For example paste <div>foo</div><div>bar</div><div>baz</div> into <di
v>x^x</div>, where ^ is the caret. | |
| 999 // As long as the div styles are the same, visually you'd expect: <div>
xbar</div><div>bar</div><div>bazx</div>, | |
| 1000 // not <div>xbar<div>bar</div><div>bazx</div></div>. | |
| 1001 // Don't do this if the selection started in a Mail blockquote. | |
| 1002 if (m_preventNesting && !startIsInsideMailBlockquote && !isEndOfParagrap
h(visibleStart) && !isStartOfParagraph(visibleStart)) { | |
| 1003 insertParagraphSeparator(); | |
| 1004 setEndingSelection(endingSelection().visibleStart().previous()); | |
| 1005 } | |
| 1006 insertionPos = endingSelection().start(); | |
| 1007 } | |
| 1008 | |
| 1009 // We don't want any of the pasted content to end up nested in a Mail blockq
uote, so first break | |
| 1010 // out of any surrounding Mail blockquotes. Unless we're inserting in a tabl
e, in which case | |
| 1011 // breaking the blockquote will prevent the content from actually being inse
rted in the table. | |
| 1012 if (startIsInsideMailBlockquote && m_preventNesting && !(enclosingNodeOfType
(insertionPos, &isTableStructureNode))) { | |
| 1013 applyCommandToComposite(BreakBlockquoteCommand::create(document())); | |
| 1014 // This will leave a br between the split. | |
| 1015 Node* br = endingSelection().start().anchorNode(); | |
| 1016 ASSERT(isHTMLBRElement(br)); | |
| 1017 // Insert content between the two blockquotes, but remove the br (since
it was just a placeholder). | |
| 1018 insertionPos = positionInParentBeforeNode(*br); | |
| 1019 removeNode(br); | |
| 1020 } | |
| 1021 | |
| 1022 // Inserting content could cause whitespace to collapse, e.g. inserting <div
>foo</div> into hello^ world. | |
| 1023 prepareWhitespaceAtPositionForSplit(insertionPos); | |
| 1024 | |
| 1025 // If the downstream node has been removed there's no point in continuing. | |
| 1026 if (!insertionPos.downstream().anchorNode()) | |
| 1027 return; | |
| 1028 | |
| 1029 // NOTE: This would be an incorrect usage of downstream() if downstream() we
re changed to mean the last position after | |
| 1030 // p that maps to the same visible position as p (since in the case where a
br is at the end of a block and collapsed | |
| 1031 // away, there are positions after the br which map to the same visible posi
tion as [br, 0]). | |
| 1032 HTMLBRElement* endBR = isHTMLBRElement(*insertionPos.downstream().anchorNode
()) ? toHTMLBRElement(insertionPos.downstream().anchorNode()) : 0; | |
| 1033 VisiblePosition originalVisPosBeforeEndBR; | |
| 1034 if (endBR) | |
| 1035 originalVisPosBeforeEndBR = VisiblePosition(positionBeforeNode(endBR), D
OWNSTREAM).previous(); | |
| 1036 | |
| 1037 RefPtrWillBeRawPtr<Element> enclosingBlockOfInsertionPos = enclosingBlock(in
sertionPos.anchorNode()); | |
| 1038 | |
| 1039 // Adjust insertionPos to prevent nesting. | |
| 1040 // If the start was in a Mail blockquote, we will have already handled adjus
ting insertionPos above. | |
| 1041 if (m_preventNesting && enclosingBlockOfInsertionPos && !isTableCell(enclosi
ngBlockOfInsertionPos.get()) && !startIsInsideMailBlockquote) { | |
| 1042 ASSERT(enclosingBlockOfInsertionPos != currentRoot); | |
| 1043 VisiblePosition visibleInsertionPos(insertionPos); | |
| 1044 if (isEndOfBlock(visibleInsertionPos) && !(isStartOfBlock(visibleInserti
onPos) && fragment.hasInterchangeNewlineAtEnd())) | |
| 1045 insertionPos = positionInParentAfterNode(*enclosingBlockOfInsertionP
os); | |
| 1046 else if (isStartOfBlock(visibleInsertionPos)) | |
| 1047 insertionPos = positionInParentBeforeNode(*enclosingBlockOfInsertion
Pos); | |
| 1048 } | |
| 1049 | |
| 1050 // Paste at start or end of link goes outside of link. | |
| 1051 insertionPos = positionAvoidingSpecialElementBoundary(insertionPos); | |
| 1052 | |
| 1053 // FIXME: Can this wait until after the operation has been performed? There
doesn't seem to be | |
| 1054 // any work performed after this that queries or uses the typing style. | |
| 1055 if (LocalFrame* frame = document().frame()) | |
| 1056 frame->selection().clearTypingStyle(); | |
| 1057 | |
| 1058 removeHeadContents(fragment); | |
| 1059 | |
| 1060 // We don't want the destination to end up inside nodes that weren't selecte
d. To avoid that, we move the | |
| 1061 // position forward without changing the visible position so we're still at
the same visible location, but | |
| 1062 // outside of preceding tags. | |
| 1063 insertionPos = positionAvoidingPrecedingNodes(insertionPos); | |
| 1064 | |
| 1065 // Paste into run of tabs splits the tab span. | |
| 1066 insertionPos = positionOutsideTabSpan(insertionPos); | |
| 1067 | |
| 1068 bool handledStyleSpans = handleStyleSpansBeforeInsertion(fragment, insertion
Pos); | |
| 1069 | |
| 1070 // We're finished if there is nothing to add. | |
| 1071 if (fragment.isEmpty() || !fragment.firstChild()) | |
| 1072 return; | |
| 1073 | |
| 1074 // If we are not trying to match the destination style we prefer a position | |
| 1075 // that is outside inline elements that provide style. | |
| 1076 // This way we can produce a less verbose markup. | |
| 1077 // We can skip this optimization for fragments not wrapped in one of | |
| 1078 // our style spans and for positions inside list items | |
| 1079 // since insertAsListItems already does the right thing. | |
| 1080 if (!m_matchStyle && !enclosingList(insertionPos.computeContainerNode())) { | |
| 1081 if (insertionPos.computeContainerNode()->isTextNode() && insertionPos.of
fsetInContainerNode() && !insertionPos.atLastEditingPositionForNode()) { | |
| 1082 splitTextNode(toText(insertionPos.computeContainerNode()), insertion
Pos.offsetInContainerNode()); | |
| 1083 insertionPos = firstPositionInNode(insertionPos.computeContainerNode
()); | |
| 1084 } | |
| 1085 | |
| 1086 if (RefPtrWillBeRawPtr<HTMLElement> elementToSplitTo = elementToSplitToA
voidPastingIntoInlineElementsWithStyle(insertionPos)) { | |
| 1087 if (insertionPos.computeContainerNode() != elementToSplitTo->parentN
ode()) { | |
| 1088 Node* splitStart = insertionPos.computeNodeAfterPosition(); | |
| 1089 if (!splitStart) | |
| 1090 splitStart = insertionPos.computeContainerNode(); | |
| 1091 RefPtrWillBeRawPtr<Node> nodeToSplitTo = splitTreeToNode(splitSt
art, elementToSplitTo->parentNode()).get(); | |
| 1092 insertionPos = positionInParentBeforeNode(*nodeToSplitTo); | |
| 1093 } | |
| 1094 } | |
| 1095 } | |
| 1096 | |
| 1097 // FIXME: When pasting rich content we're often prevented from heading down
the fast path by style spans. Try | |
| 1098 // again here if they've been removed. | |
| 1099 | |
| 1100 // 1) Insert the content. | |
| 1101 // 2) Remove redundant styles and style tags, this inner <b> for example: <b
>foo <b>bar</b> baz</b>. | |
| 1102 // 3) Merge the start of the added content with the content before the posit
ion being pasted into. | |
| 1103 // 4) Do one of the following: a) expand the last br if the fragment ends wi
th one and it collapsed, | |
| 1104 // b) merge the last paragraph of the incoming fragment with the paragraph t
hat contained the | |
| 1105 // end of the selection that was pasted into, or c) handle an interchange ne
wline at the end of the | |
| 1106 // incoming fragment. | |
| 1107 // 5) Add spaces for smart replace. | |
| 1108 // 6) Select the replacement if requested, and match style if requested. | |
| 1109 | |
| 1110 InsertedNodes insertedNodes; | |
| 1111 RefPtrWillBeRawPtr<Node> refNode = fragment.firstChild(); | |
| 1112 ASSERT(refNode); | |
| 1113 RefPtrWillBeRawPtr<Node> node = refNode->nextSibling(); | |
| 1114 | |
| 1115 fragment.removeNode(refNode); | |
| 1116 | |
| 1117 Element* blockStart = enclosingBlock(insertionPos.anchorNode()); | |
| 1118 if ((isHTMLListElement(refNode.get()) || (isLegacyAppleHTMLSpanElement(refNo
de.get()) && isHTMLListElement(refNode->firstChild()))) | |
| 1119 && blockStart && blockStart->layoutObject()->isListItem()) { | |
| 1120 refNode = insertAsListItems(toHTMLElement(refNode), blockStart, insertio
nPos, insertedNodes); | |
| 1121 } else { | |
| 1122 insertNodeAt(refNode, insertionPos); | |
| 1123 insertedNodes.respondToNodeInsertion(*refNode); | |
| 1124 } | |
| 1125 | |
| 1126 // Mutation events (bug 22634) may have already removed the inserted content | |
| 1127 if (!refNode->inDocument()) | |
| 1128 return; | |
| 1129 | |
| 1130 bool plainTextFragment = isPlainTextMarkup(refNode.get()); | |
| 1131 | |
| 1132 while (node) { | |
| 1133 RefPtrWillBeRawPtr<Node> next = node->nextSibling(); | |
| 1134 fragment.removeNode(node.get()); | |
| 1135 insertNodeAfter(node, refNode); | |
| 1136 insertedNodes.respondToNodeInsertion(*node); | |
| 1137 | |
| 1138 // Mutation events (bug 22634) may have already removed the inserted con
tent | |
| 1139 if (!node->inDocument()) | |
| 1140 return; | |
| 1141 | |
| 1142 refNode = node; | |
| 1143 if (node && plainTextFragment) | |
| 1144 plainTextFragment = isPlainTextMarkup(node.get()); | |
| 1145 node = next; | |
| 1146 } | |
| 1147 | |
| 1148 removeUnrenderedTextNodesAtEnds(insertedNodes); | |
| 1149 | |
| 1150 if (!handledStyleSpans) | |
| 1151 handleStyleSpans(insertedNodes); | |
| 1152 | |
| 1153 // Mutation events (bug 20161) may have already removed the inserted content | |
| 1154 if (!insertedNodes.firstNodeInserted() || !insertedNodes.firstNodeInserted()
->inDocument()) | |
| 1155 return; | |
| 1156 | |
| 1157 // Scripts specified in javascript protocol may remove |enclosingBlockOfInse
rtionPos| | |
| 1158 // during insertion, e.g. <iframe src="javascript:..."> | |
| 1159 if (enclosingBlockOfInsertionPos && !enclosingBlockOfInsertionPos->inDocumen
t()) | |
| 1160 enclosingBlockOfInsertionPos = nullptr; | |
| 1161 | |
| 1162 VisiblePosition startOfInsertedContent(firstPositionInOrBeforeNode(insertedN
odes.firstNodeInserted())); | |
| 1163 | |
| 1164 // We inserted before the enclosingBlockOfInsertionPos to prevent nesting, a
nd the content before the enclosingBlockOfInsertionPos wasn't in its own block a
nd | |
| 1165 // didn't have a br after it, so the inserted content ended up in the same p
aragraph. | |
| 1166 if (!startOfInsertedContent.isNull() && enclosingBlockOfInsertionPos && inse
rtionPos.anchorNode() == enclosingBlockOfInsertionPos->parentNode() && (unsigned
)insertionPos.computeEditingOffset() < enclosingBlockOfInsertionPos->nodeIndex()
&& !isStartOfParagraph(startOfInsertedContent)) | |
| 1167 insertNodeAt(createBreakElement(document()).get(), startOfInsertedConten
t.deepEquivalent()); | |
| 1168 | |
| 1169 if (endBR && (plainTextFragment || (shouldRemoveEndBR(endBR, originalVisPosB
eforeEndBR) && !(fragment.hasInterchangeNewlineAtEnd() && selectionIsPlainText))
)) { | |
| 1170 RefPtrWillBeRawPtr<ContainerNode> parent = endBR->parentNode(); | |
| 1171 insertedNodes.willRemoveNode(*endBR); | |
| 1172 removeNode(endBR); | |
| 1173 if (Node* nodeToRemove = highestNodeToRemoveInPruning(parent.get())) { | |
| 1174 insertedNodes.willRemoveNode(*nodeToRemove); | |
| 1175 removeNode(nodeToRemove); | |
| 1176 } | |
| 1177 } | |
| 1178 | |
| 1179 makeInsertedContentRoundTrippableWithHTMLTreeBuilder(insertedNodes); | |
| 1180 | |
| 1181 removeRedundantStylesAndKeepStyleSpanInline(insertedNodes); | |
| 1182 | |
| 1183 if (m_sanitizeFragment) | |
| 1184 applyCommandToComposite(SimplifyMarkupCommand::create(document(), insert
edNodes.firstNodeInserted(), insertedNodes.pastLastLeaf())); | |
| 1185 | |
| 1186 // Setup m_startOfInsertedContent and m_endOfInsertedContent. This should be
the last two lines of code that access insertedNodes. | |
| 1187 m_startOfInsertedContent = firstPositionInOrBeforeNode(insertedNodes.firstNo
deInserted()); | |
| 1188 m_endOfInsertedContent = lastPositionInOrAfterNode(insertedNodes.lastLeafIns
erted()); | |
| 1189 | |
| 1190 // Determine whether or not we should merge the end of inserted content with
what's after it before we do | |
| 1191 // the start merge so that the start merge doesn't effect our decision. | |
| 1192 m_shouldMergeEnd = shouldMergeEnd(selectionEndWasEndOfParagraph); | |
| 1193 | |
| 1194 if (shouldMergeStart(selectionStartWasStartOfParagraph, fragment.hasIntercha
ngeNewlineAtStart(), startIsInsideMailBlockquote)) { | |
| 1195 VisiblePosition startOfParagraphToMove = positionAtStartOfInsertedConten
t(); | |
| 1196 VisiblePosition destination = startOfParagraphToMove.previous(); | |
| 1197 // We need to handle the case where we need to merge the end | |
| 1198 // but our destination node is inside an inline that is the last in the
block. | |
| 1199 // We insert a placeholder before the newly inserted content to avoid be
ing merged into the inline. | |
| 1200 Node* destinationNode = destination.deepEquivalent().anchorNode(); | |
| 1201 if (m_shouldMergeEnd && destinationNode != enclosingInline(destinationNo
de) && enclosingInline(destinationNode)->nextSibling()) | |
| 1202 insertNodeBefore(createBreakElement(document()), refNode.get()); | |
| 1203 | |
| 1204 // Merging the the first paragraph of inserted content with the content
that came | |
| 1205 // before the selection that was pasted into would also move content aft
er | |
| 1206 // the selection that was pasted into if: only one paragraph was being p
asted, | |
| 1207 // and it was not wrapped in a block, the selection that was pasted into
ended | |
| 1208 // at the end of a block and the next paragraph didn't start at the star
t of a block. | |
| 1209 // Insert a line break just after the inserted content to separate it fr
om what | |
| 1210 // comes after and prevent that from happening. | |
| 1211 VisiblePosition endOfInsertedContent = positionAtEndOfInsertedContent(); | |
| 1212 if (startOfParagraph(endOfInsertedContent).deepEquivalent() == startOfPa
ragraphToMove.deepEquivalent()) { | |
| 1213 insertNodeAt(createBreakElement(document()).get(), endOfInsertedCont
ent.deepEquivalent()); | |
| 1214 // Mutation events (bug 22634) triggered by inserting the <br> might
have removed the content we're about to move | |
| 1215 if (!startOfParagraphToMove.deepEquivalent().inDocument()) | |
| 1216 return; | |
| 1217 } | |
| 1218 | |
| 1219 // FIXME: Maintain positions for the start and end of inserted content i
nstead of keeping nodes. The nodes are | |
| 1220 // only ever used to create positions where inserted content starts/ends
. | |
| 1221 moveParagraph(startOfParagraphToMove, endOfParagraph(startOfParagraphToM
ove), destination); | |
| 1222 m_startOfInsertedContent = endingSelection().visibleStart().deepEquivale
nt().downstream(); | |
| 1223 if (m_endOfInsertedContent.isOrphan()) | |
| 1224 m_endOfInsertedContent = endingSelection().visibleEnd().deepEquivale
nt().upstream(); | |
| 1225 } | |
| 1226 | |
| 1227 Position lastPositionToSelect; | |
| 1228 if (fragment.hasInterchangeNewlineAtEnd()) { | |
| 1229 VisiblePosition endOfInsertedContent = positionAtEndOfInsertedContent(); | |
| 1230 VisiblePosition next = endOfInsertedContent.next(CannotCrossEditingBound
ary); | |
| 1231 | |
| 1232 if (selectionEndWasEndOfParagraph || !isEndOfParagraph(endOfInsertedCont
ent) || next.isNull()) { | |
| 1233 if (!isStartOfParagraph(endOfInsertedContent)) { | |
| 1234 setEndingSelection(endOfInsertedContent); | |
| 1235 Element* enclosingBlockElement = enclosingBlock(endOfInsertedCon
tent.deepEquivalent().anchorNode()); | |
| 1236 if (isListItem(enclosingBlockElement)) { | |
| 1237 RefPtrWillBeRawPtr<HTMLLIElement> newListItem = createListIt
emElement(document()); | |
| 1238 insertNodeAfter(newListItem, enclosingBlockElement); | |
| 1239 setEndingSelection(VisiblePosition(firstPositionInNode(newLi
stItem.get()))); | |
| 1240 } else { | |
| 1241 // Use a default paragraph element (a plain div) for the emp
ty paragraph, using the last paragraph | |
| 1242 // block's style seems to annoy users. | |
| 1243 insertParagraphSeparator(true, !startIsInsideMailBlockquote
&& highestEnclosingNodeOfType(endOfInsertedContent.deepEquivalent(), | |
| 1244 isMailHTMLBlockquoteElement, CannotCrossEditingBoundary,
insertedNodes.firstNodeInserted()->parentNode())); | |
| 1245 } | |
| 1246 | |
| 1247 // Select up to the paragraph separator that was added. | |
| 1248 lastPositionToSelect = endingSelection().visibleStart().deepEqui
valent(); | |
| 1249 updateNodesInserted(lastPositionToSelect.anchorNode()); | |
| 1250 } | |
| 1251 } else { | |
| 1252 // Select up to the beginning of the next paragraph. | |
| 1253 lastPositionToSelect = next.deepEquivalent().downstream(); | |
| 1254 } | |
| 1255 } else { | |
| 1256 mergeEndIfNeeded(); | |
| 1257 } | |
| 1258 | |
| 1259 if (HTMLQuoteElement* mailBlockquote = toHTMLQuoteElement(enclosingNodeOfTyp
e(positionAtStartOfInsertedContent().deepEquivalent(), isMailPasteAsQuotationHTM
LBlockQuoteElement))) | |
| 1260 removeElementAttribute(mailBlockquote, classAttr); | |
| 1261 | |
| 1262 if (shouldPerformSmartReplace()) | |
| 1263 addSpacesForSmartReplace(); | |
| 1264 | |
| 1265 // If we are dealing with a fragment created from plain text | |
| 1266 // no style matching is necessary. | |
| 1267 if (plainTextFragment) | |
| 1268 m_matchStyle = false; | |
| 1269 | |
| 1270 completeHTMLReplacement(lastPositionToSelect); | |
| 1271 } | |
| 1272 | |
| 1273 bool ReplaceSelectionCommand::shouldRemoveEndBR(HTMLBRElement* endBR, const Visi
blePosition& originalVisPosBeforeEndBR) | |
| 1274 { | |
| 1275 if (!endBR || !endBR->inDocument()) | |
| 1276 return false; | |
| 1277 | |
| 1278 VisiblePosition visiblePos(positionBeforeNode(endBR)); | |
| 1279 | |
| 1280 // Don't remove the br if nothing was inserted. | |
| 1281 if (visiblePos.previous().deepEquivalent() == originalVisPosBeforeEndBR.deep
Equivalent()) | |
| 1282 return false; | |
| 1283 | |
| 1284 // Remove the br if it is collapsed away and so is unnecessary. | |
| 1285 if (!document().inNoQuirksMode() && isEndOfBlock(visiblePos) && !isStartOfPa
ragraph(visiblePos)) | |
| 1286 return true; | |
| 1287 | |
| 1288 // A br that was originally holding a line open should be displaced by inser
ted content or turned into a line break. | |
| 1289 // A br that was originally acting as a line break should still be acting as
a line break, not as a placeholder. | |
| 1290 return isStartOfParagraph(visiblePos) && isEndOfParagraph(visiblePos); | |
| 1291 } | |
| 1292 | |
| 1293 bool ReplaceSelectionCommand::shouldPerformSmartReplace() const | |
| 1294 { | |
| 1295 if (!m_smartReplace) | |
| 1296 return false; | |
| 1297 | |
| 1298 HTMLTextFormControlElement* textControl = enclosingTextFormControl(positionA
tStartOfInsertedContent().deepEquivalent()); | |
| 1299 if (isHTMLInputElement(textControl) && toHTMLInputElement(textControl)->type
() == InputTypeNames::password) | |
| 1300 return false; // Disable smart replace for password fields. | |
| 1301 | |
| 1302 return true; | |
| 1303 } | |
| 1304 | |
| 1305 static bool isCharacterSmartReplaceExemptConsideringNonBreakingSpace(UChar32 cha
racter, bool previousCharacter) | |
| 1306 { | |
| 1307 return isCharacterSmartReplaceExempt(character == noBreakSpaceCharacter ? '
' : character, previousCharacter); | |
| 1308 } | |
| 1309 | |
| 1310 void ReplaceSelectionCommand::addSpacesForSmartReplace() | |
| 1311 { | |
| 1312 VisiblePosition startOfInsertedContent = positionAtStartOfInsertedContent(); | |
| 1313 VisiblePosition endOfInsertedContent = positionAtEndOfInsertedContent(); | |
| 1314 | |
| 1315 Position endUpstream = endOfInsertedContent.deepEquivalent().upstream(); | |
| 1316 Node* endNode = endUpstream.computeNodeBeforePosition(); | |
| 1317 int endOffset = endNode && endNode->isTextNode() ? toText(endNode)->length()
: 0; | |
| 1318 if (endUpstream.isOffsetInAnchor()) { | |
| 1319 endNode = endUpstream.computeContainerNode(); | |
| 1320 endOffset = endUpstream.offsetInContainerNode(); | |
| 1321 } | |
| 1322 | |
| 1323 bool needsTrailingSpace = !isEndOfParagraph(endOfInsertedContent) && !isChar
acterSmartReplaceExemptConsideringNonBreakingSpace(endOfInsertedContent.characte
rAfter(), false); | |
| 1324 if (needsTrailingSpace && endNode) { | |
| 1325 bool collapseWhiteSpace = !endNode->layoutObject() || endNode->layoutObj
ect()->style()->collapseWhiteSpace(); | |
| 1326 if (endNode->isTextNode()) { | |
| 1327 insertTextIntoNode(toText(endNode), endOffset, collapseWhiteSpace ?
nonBreakingSpaceString() : " "); | |
| 1328 if (m_endOfInsertedContent.computeContainerNode() == endNode) | |
| 1329 m_endOfInsertedContent = Position(endNode, m_endOfInsertedConten
t.offsetInContainerNode() + 1); | |
| 1330 } else { | |
| 1331 RefPtrWillBeRawPtr<Text> node = document().createEditingTextNode(col
lapseWhiteSpace ? nonBreakingSpaceString() : " "); | |
| 1332 insertNodeAfter(node, endNode); | |
| 1333 updateNodesInserted(node.get()); | |
| 1334 } | |
| 1335 } | |
| 1336 | |
| 1337 document().updateLayout(); | |
| 1338 | |
| 1339 Position startDownstream = startOfInsertedContent.deepEquivalent().downstrea
m(); | |
| 1340 Node* startNode = startDownstream.computeNodeAfterPosition(); | |
| 1341 unsigned startOffset = 0; | |
| 1342 if (startDownstream.isOffsetInAnchor()) { | |
| 1343 startNode = startDownstream.computeContainerNode(); | |
| 1344 startOffset = startDownstream.offsetInContainerNode(); | |
| 1345 } | |
| 1346 | |
| 1347 bool needsLeadingSpace = !isStartOfParagraph(startOfInsertedContent) && !isC
haracterSmartReplaceExemptConsideringNonBreakingSpace(startOfInsertedContent.pre
vious().characterAfter(), true); | |
| 1348 if (needsLeadingSpace && startNode) { | |
| 1349 bool collapseWhiteSpace = !startNode->layoutObject() || startNode->layou
tObject()->style()->collapseWhiteSpace(); | |
| 1350 if (startNode->isTextNode()) { | |
| 1351 insertTextIntoNode(toText(startNode), startOffset, collapseWhiteSpac
e ? nonBreakingSpaceString() : " "); | |
| 1352 if (m_endOfInsertedContent.computeContainerNode() == startNode && m_
endOfInsertedContent.offsetInContainerNode()) | |
| 1353 m_endOfInsertedContent = Position(startNode, m_endOfInsertedCont
ent.offsetInContainerNode() + 1); | |
| 1354 } else { | |
| 1355 RefPtrWillBeRawPtr<Text> node = document().createEditingTextNode(col
lapseWhiteSpace ? nonBreakingSpaceString() : " "); | |
| 1356 // Don't updateNodesInserted. Doing so would set m_endOfInsertedCont
ent to be the node containing the leading space, | |
| 1357 // but m_endOfInsertedContent is supposed to mark the end of pasted
content. | |
| 1358 insertNodeBefore(node, startNode); | |
| 1359 m_startOfInsertedContent = firstPositionInNode(node.get()); | |
| 1360 } | |
| 1361 } | |
| 1362 } | |
| 1363 | |
| 1364 void ReplaceSelectionCommand::completeHTMLReplacement(const Position &lastPositi
onToSelect) | |
| 1365 { | |
| 1366 Position start = positionAtStartOfInsertedContent().deepEquivalent(); | |
| 1367 Position end = positionAtEndOfInsertedContent().deepEquivalent(); | |
| 1368 | |
| 1369 // Mutation events may have deleted start or end | |
| 1370 if (start.isNotNull() && !start.isOrphan() && end.isNotNull() && !end.isOrph
an()) { | |
| 1371 // FIXME (11475): Remove this and require that the creator of the fragme
nt to use nbsps. | |
| 1372 rebalanceWhitespaceAt(start); | |
| 1373 rebalanceWhitespaceAt(end); | |
| 1374 | |
| 1375 if (m_matchStyle) { | |
| 1376 ASSERT(m_insertionStyle); | |
| 1377 applyStyle(m_insertionStyle.get(), start, end); | |
| 1378 } | |
| 1379 | |
| 1380 if (lastPositionToSelect.isNotNull()) | |
| 1381 end = lastPositionToSelect; | |
| 1382 | |
| 1383 mergeTextNodesAroundPosition(start, end); | |
| 1384 } else if (lastPositionToSelect.isNotNull()) { | |
| 1385 start = end = lastPositionToSelect; | |
| 1386 } else { | |
| 1387 return; | |
| 1388 } | |
| 1389 | |
| 1390 if (m_selectReplacement) | |
| 1391 setEndingSelection(VisibleSelection(start, end, SEL_DEFAULT_AFFINITY, en
dingSelection().isDirectional())); | |
| 1392 else | |
| 1393 setEndingSelection(VisibleSelection(end, SEL_DEFAULT_AFFINITY, endingSel
ection().isDirectional())); | |
| 1394 } | |
| 1395 | |
| 1396 void ReplaceSelectionCommand::mergeTextNodesAroundPosition(Position& position, P
osition& positionOnlyToBeUpdated) | |
| 1397 { | |
| 1398 bool positionIsOffsetInAnchor = position.isOffsetInAnchor(); | |
| 1399 bool positionOnlyToBeUpdatedIsOffsetInAnchor = positionOnlyToBeUpdated.isOff
setInAnchor(); | |
| 1400 RefPtrWillBeRawPtr<Text> text = nullptr; | |
| 1401 if (positionIsOffsetInAnchor && position.computeContainerNode() && position.
computeContainerNode()->isTextNode()) { | |
| 1402 text = toText(position.computeContainerNode()); | |
| 1403 } else { | |
| 1404 Node* before = position.computeNodeBeforePosition(); | |
| 1405 if (before && before->isTextNode()) { | |
| 1406 text = toText(before); | |
| 1407 } else { | |
| 1408 Node* after = position.computeNodeAfterPosition(); | |
| 1409 if (after && after->isTextNode()) | |
| 1410 text = toText(after); | |
| 1411 } | |
| 1412 } | |
| 1413 if (!text) | |
| 1414 return; | |
| 1415 | |
| 1416 if (text->previousSibling() && text->previousSibling()->isTextNode()) { | |
| 1417 RefPtrWillBeRawPtr<Text> previous = toText(text->previousSibling()); | |
| 1418 insertTextIntoNode(text, 0, previous->data()); | |
| 1419 | |
| 1420 if (positionIsOffsetInAnchor) | |
| 1421 position = Position(position.computeContainerNode(), previous->lengt
h() + position.offsetInContainerNode()); | |
| 1422 else | |
| 1423 updatePositionForNodeRemoval(position, *previous); | |
| 1424 | |
| 1425 if (positionOnlyToBeUpdatedIsOffsetInAnchor) { | |
| 1426 if (positionOnlyToBeUpdated.computeContainerNode() == text) | |
| 1427 positionOnlyToBeUpdated = Position(text, previous->length() + po
sitionOnlyToBeUpdated.offsetInContainerNode()); | |
| 1428 else if (positionOnlyToBeUpdated.computeContainerNode() == previous) | |
| 1429 positionOnlyToBeUpdated = Position(text, positionOnlyToBeUpdated
.offsetInContainerNode()); | |
| 1430 } else { | |
| 1431 updatePositionForNodeRemoval(positionOnlyToBeUpdated, *previous); | |
| 1432 } | |
| 1433 | |
| 1434 removeNode(previous); | |
| 1435 } | |
| 1436 if (text->nextSibling() && text->nextSibling()->isTextNode()) { | |
| 1437 RefPtrWillBeRawPtr<Text> next = toText(text->nextSibling()); | |
| 1438 unsigned originalLength = text->length(); | |
| 1439 insertTextIntoNode(text, originalLength, next->data()); | |
| 1440 | |
| 1441 if (!positionIsOffsetInAnchor) | |
| 1442 updatePositionForNodeRemoval(position, *next); | |
| 1443 | |
| 1444 if (positionOnlyToBeUpdatedIsOffsetInAnchor && positionOnlyToBeUpdated.c
omputeContainerNode() == next) | |
| 1445 positionOnlyToBeUpdated = Position(text, originalLength + positionOn
lyToBeUpdated.offsetInContainerNode()); | |
| 1446 else | |
| 1447 updatePositionForNodeRemoval(positionOnlyToBeUpdated, *next); | |
| 1448 | |
| 1449 removeNode(next); | |
| 1450 } | |
| 1451 } | |
| 1452 | |
| 1453 EditAction ReplaceSelectionCommand::editingAction() const | |
| 1454 { | |
| 1455 return m_editAction; | |
| 1456 } | |
| 1457 | |
| 1458 // If the user is inserting a list into an existing list, instead of nesting the
list, | |
| 1459 // we put the list items into the existing list. | |
| 1460 Node* ReplaceSelectionCommand::insertAsListItems(PassRefPtrWillBeRawPtr<HTMLElem
ent> prpListElement, Element* insertionBlock, const Position& insertPos, Inserte
dNodes& insertedNodes) | |
| 1461 { | |
| 1462 RefPtrWillBeRawPtr<HTMLElement> listElement = prpListElement; | |
| 1463 | |
| 1464 while (listElement->hasOneChild() && isHTMLListElement(listElement->firstChi
ld())) | |
| 1465 listElement = toHTMLElement(listElement->firstChild()); | |
| 1466 | |
| 1467 bool isStart = isStartOfParagraph(VisiblePosition(insertPos)); | |
| 1468 bool isEnd = isEndOfParagraph(VisiblePosition(insertPos)); | |
| 1469 bool isMiddle = !isStart && !isEnd; | |
| 1470 Node* lastNode = insertionBlock; | |
| 1471 | |
| 1472 // If we're in the middle of a list item, we should split it into two separa
te | |
| 1473 // list items and insert these nodes between them. | |
| 1474 if (isMiddle) { | |
| 1475 int textNodeOffset = insertPos.offsetInContainerNode(); | |
| 1476 if (insertPos.anchorNode()->isTextNode() && textNodeOffset > 0) | |
| 1477 splitTextNode(toText(insertPos.anchorNode()), textNodeOffset); | |
| 1478 splitTreeToNode(insertPos.anchorNode(), lastNode, true); | |
| 1479 } | |
| 1480 | |
| 1481 while (RefPtrWillBeRawPtr<Node> listItem = listElement->firstChild()) { | |
| 1482 listElement->removeChild(listItem.get(), ASSERT_NO_EXCEPTION); | |
| 1483 if (isStart || isMiddle) { | |
| 1484 insertNodeBefore(listItem, lastNode); | |
| 1485 insertedNodes.respondToNodeInsertion(*listItem); | |
| 1486 } else if (isEnd) { | |
| 1487 insertNodeAfter(listItem, lastNode); | |
| 1488 insertedNodes.respondToNodeInsertion(*listItem); | |
| 1489 lastNode = listItem.get(); | |
| 1490 } else { | |
| 1491 ASSERT_NOT_REACHED(); | |
| 1492 } | |
| 1493 } | |
| 1494 if (isStart || isMiddle) { | |
| 1495 if (Node* node = lastNode->previousSibling()) | |
| 1496 return node; | |
| 1497 } | |
| 1498 return lastNode; | |
| 1499 } | |
| 1500 | |
| 1501 void ReplaceSelectionCommand::updateNodesInserted(Node *node) | |
| 1502 { | |
| 1503 if (!node) | |
| 1504 return; | |
| 1505 | |
| 1506 if (m_startOfInsertedContent.isNull()) | |
| 1507 m_startOfInsertedContent = firstPositionInOrBeforeNode(node); | |
| 1508 | |
| 1509 m_endOfInsertedContent = lastPositionInOrAfterNode(&NodeTraversal::lastWithi
nOrSelf(*node)); | |
| 1510 } | |
| 1511 | |
| 1512 // During simple pastes, where we're just pasting a text node into a run of text
, we insert the text node | |
| 1513 // directly into the text node that holds the selection. This is much faster th
an the generalized code in | |
| 1514 // ReplaceSelectionCommand, and works around <https://bugs.webkit.org/show_bug.c
gi?id=6148> since we don't | |
| 1515 // split text nodes. | |
| 1516 bool ReplaceSelectionCommand::performTrivialReplace(const ReplacementFragment& f
ragment) | |
| 1517 { | |
| 1518 if (!fragment.firstChild() || fragment.firstChild() != fragment.lastChild()
|| !fragment.firstChild()->isTextNode()) | |
| 1519 return false; | |
| 1520 | |
| 1521 // FIXME: Would be nice to handle smart replace in the fast path. | |
| 1522 if (m_smartReplace || fragment.hasInterchangeNewlineAtStart() || fragment.ha
sInterchangeNewlineAtEnd()) | |
| 1523 return false; | |
| 1524 | |
| 1525 // e.g. when "bar" is inserted after "foo" in <div><u>foo</u></div>, "bar" s
hould not be underlined. | |
| 1526 if (elementToSplitToAvoidPastingIntoInlineElementsWithStyle(endingSelection(
).start())) | |
| 1527 return false; | |
| 1528 | |
| 1529 RefPtrWillBeRawPtr<Node> nodeAfterInsertionPos = endingSelection().end().dow
nstream().anchorNode(); | |
| 1530 Text* textNode = toText(fragment.firstChild()); | |
| 1531 // Our fragment creation code handles tabs, spaces, and newlines, so we don'
t have to worry about those here. | |
| 1532 | |
| 1533 Position start = endingSelection().start(); | |
| 1534 Position end = replaceSelectedTextInNode(textNode->data()); | |
| 1535 if (end.isNull()) | |
| 1536 return false; | |
| 1537 | |
| 1538 if (nodeAfterInsertionPos && nodeAfterInsertionPos->parentNode() && isHTMLBR
Element(*nodeAfterInsertionPos) | |
| 1539 && shouldRemoveEndBR(toHTMLBRElement(nodeAfterInsertionPos.get()), Visib
lePosition(positionBeforeNode(nodeAfterInsertionPos.get())))) | |
| 1540 removeNodeAndPruneAncestors(nodeAfterInsertionPos.get()); | |
| 1541 | |
| 1542 VisibleSelection selectionAfterReplace(m_selectReplacement ? start : end, en
d); | |
| 1543 | |
| 1544 setEndingSelection(selectionAfterReplace); | |
| 1545 | |
| 1546 return true; | |
| 1547 } | |
| 1548 | |
| 1549 DEFINE_TRACE(ReplaceSelectionCommand) | |
| 1550 { | |
| 1551 visitor->trace(m_startOfInsertedContent); | |
| 1552 visitor->trace(m_endOfInsertedContent); | |
| 1553 visitor->trace(m_insertionStyle); | |
| 1554 visitor->trace(m_documentFragment); | |
| 1555 CompositeEditCommand::trace(visitor); | |
| 1556 } | |
| 1557 | |
| 1558 } // namespace blink | |
| OLD | NEW |