/* * Copyright (C) 2005, 2006, 2008 Apple Inc. All rights reserved. * Copyright (C) 2009, 2010, 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "third_party/blink/renderer/core/editing/commands/replace_selection_command.h" #include "base/macros.h" #include "third_party/blink/renderer/core/css/css_property_names.h" #include "third_party/blink/renderer/core/css/css_property_value_set.h" #include "third_party/blink/renderer/core/css/css_style_declaration.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/document_fragment.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/dom/text.h" #include "third_party/blink/renderer/core/editing/commands/apply_style_command.h" #include "third_party/blink/renderer/core/editing/commands/break_blockquote_command.h" #include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h" #include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h" #include "third_party/blink/renderer/core/editing/commands/simplify_markup_command.h" #include "third_party/blink/renderer/core/editing/commands/smart_replace.h" #include "third_party/blink/renderer/core/editing/editing_style.h" #include "third_party/blink/renderer/core/editing/editing_utilities.h" #include "third_party/blink/renderer/core/editing/editor.h" #include "third_party/blink/renderer/core/editing/ephemeral_range.h" #include "third_party/blink/renderer/core/editing/iterators/text_iterator.h" #include "third_party/blink/renderer/core/editing/selection_template.h" #include "third_party/blink/renderer/core/editing/serializers/html_interchange.h" #include "third_party/blink/renderer/core/editing/serializers/serialization.h" #include "third_party/blink/renderer/core/editing/visible_position.h" #include "third_party/blink/renderer/core/editing/visible_units.h" #include "third_party/blink/renderer/core/events/before_text_inserted_event.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/html/forms/html_input_element.h" #include "third_party/blink/renderer/core/html/forms/html_select_element.h" #include "third_party/blink/renderer/core/html/html_br_element.h" #include "third_party/blink/renderer/core/html/html_element.h" #include "third_party/blink/renderer/core/html/html_li_element.h" #include "third_party/blink/renderer/core/html/html_quote_element.h" #include "third_party/blink/renderer/core/html/html_span_element.h" #include "third_party/blink/renderer/core/html_names.h" #include "third_party/blink/renderer/core/input_type_names.h" #include "third_party/blink/renderer/core/layout/layout_object.h" #include "third_party/blink/renderer/core/layout/layout_text.h" #include "third_party/blink/renderer/core/svg/svg_style_element.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/heap/heap.h" #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" #include "third_party/blink/renderer/platform/wtf/std_lib_extras.h" #include "third_party/blink/renderer/platform/wtf/vector.h" namespace blink { using namespace html_names; // --- ReplacementFragment helper class class ReplacementFragment final { STACK_ALLOCATED(); public: ReplacementFragment(Document*, DocumentFragment*, const VisibleSelection&); Node* FirstChild() const; Node* LastChild() const; bool IsEmpty() const; bool HasInterchangeNewlineAtStart() const { return has_interchange_newline_at_start_; } bool HasInterchangeNewlineAtEnd() const { return has_interchange_newline_at_end_; } void RemoveNode(Node*); void RemoveNodePreservingChildren(ContainerNode*); private: HTMLElement* InsertFragmentForTestRendering(Element* root_editable_element); void RemoveUnrenderedNodes(ContainerNode*); void RestoreAndRemoveTestRenderingNodesToFragment(Element*); void RemoveInterchangeNodes(ContainerNode*); void InsertNodeBefore(Node*, Node* ref_node); Member document_; Member fragment_; bool has_interchange_newline_at_start_; bool has_interchange_newline_at_end_; DISALLOW_COPY_AND_ASSIGN(ReplacementFragment); }; static bool IsInterchangeHTMLBRElement(const Node* node) { DEFINE_STATIC_LOCAL(String, interchange_newline_class_string, (AppleInterchangeNewline)); if (!IsHTMLBRElement(node) || ToHTMLBRElement(node)->getAttribute(kClassAttr) != interchange_newline_class_string) return false; UseCounter::Count(node->GetDocument(), WebFeature::kEditingAppleInterchangeNewline); return true; } static Position PositionAvoidingPrecedingNodes(Position pos) { // If we're already on a break, it's probably a placeholder and we shouldn't // change our position. if (EditingIgnoresContent(*pos.AnchorNode())) return pos; // We also stop when changing block flow elements because even though the // visual position is the same. E.g., //
foo^
^ // The two positions above are the same visual position, but we want to stay // in the same block. Element* enclosing_block_element = EnclosingBlock(pos.ComputeContainerNode()); for (Position next_position = pos; next_position.ComputeContainerNode() != enclosing_block_element; pos = next_position) { if (LineBreakExistsAtPosition(pos)) break; if (pos.ComputeContainerNode()->NonShadowBoundaryParentNode()) next_position = Position::InParentAfterNode(*pos.ComputeContainerNode()); if (next_position == pos || EnclosingBlock(next_position.ComputeContainerNode()) != enclosing_block_element || CreateVisiblePosition(pos).DeepEquivalent() != CreateVisiblePosition(next_position).DeepEquivalent()) break; } return pos; } ReplacementFragment::ReplacementFragment(Document* document, DocumentFragment* fragment, const VisibleSelection& selection) : document_(document), fragment_(fragment), has_interchange_newline_at_start_(false), has_interchange_newline_at_end_(false) { if (!document_) return; if (!fragment_ || !fragment_->HasChildren()) return; TRACE_EVENT0("blink", "ReplacementFragment constructor"); Element* editable_root = selection.RootEditableElement(); DCHECK(editable_root); if (!editable_root) return; document_->UpdateStyleAndLayout(); Element* shadow_ancestor_element; if (editable_root->IsInShadowTree()) shadow_ancestor_element = editable_root->OwnerShadowHost(); else shadow_ancestor_element = editable_root; if (!editable_root->GetAttributeEventListener( event_type_names::kWebkitBeforeTextInserted) // FIXME: Remove these checks once textareas and textfields actually // register an event handler. && !(shadow_ancestor_element && shadow_ancestor_element->GetLayoutObject() && shadow_ancestor_element->GetLayoutObject()->IsTextControl()) && HasRichlyEditableStyle(*editable_root)) { RemoveInterchangeNodes(fragment_.Get()); return; } if (!HasRichlyEditableStyle(*editable_root)) { bool is_plain_text = true; for (Node& node : NodeTraversal::ChildrenOf(*fragment_)) { if (IsInterchangeHTMLBRElement(&node) && &node == fragment_->lastChild()) continue; if (!node.IsTextNode()) { is_plain_text = false; break; } } // We don't need TestRendering for plain-text editing + plain-text // insertion. if (is_plain_text) { RemoveInterchangeNodes(fragment_.Get()); String original_text = fragment_->textContent(); auto* event = MakeGarbageCollected(original_text); editable_root->DispatchEvent(*event); if (original_text != event->GetText()) { fragment_ = CreateFragmentFromText( selection.ToNormalizedEphemeralRange(), event->GetText()); RemoveInterchangeNodes(fragment_.Get()); } return; } } HTMLElement* holder = InsertFragmentForTestRendering(editable_root); if (!holder) { RemoveInterchangeNodes(fragment_.Get()); return; } const EphemeralRange range = CreateVisibleSelection( SelectionInDOMTree::Builder().SelectAllChildren(*holder).Build()) .ToNormalizedEphemeralRange(); const TextIteratorBehavior& behavior = TextIteratorBehavior::Builder() .SetEmitsOriginalText(true) .SetIgnoresStyleVisibility(true) .Build(); const String& text = PlainText(range, behavior); RemoveInterchangeNodes(holder); RemoveUnrenderedNodes(holder); RestoreAndRemoveTestRenderingNodesToFragment(holder); // Give the root a chance to change the text. auto* evt = MakeGarbageCollected(text); editable_root->DispatchEvent(*evt); if (text != evt->GetText() || !HasRichlyEditableStyle(*editable_root)) { RestoreAndRemoveTestRenderingNodesToFragment(holder); // TODO(editing-dev): Use of UpdateStyleAndLayout // needs to be audited. See http://crbug.com/590369 for more details. document->UpdateStyleAndLayout(); fragment_ = CreateFragmentFromText(selection.ToNormalizedEphemeralRange(), evt->GetText()); if (!fragment_->HasChildren()) return; holder = InsertFragmentForTestRendering(editable_root); RemoveInterchangeNodes(holder); RemoveUnrenderedNodes(holder); RestoreAndRemoveTestRenderingNodesToFragment(holder); } } bool ReplacementFragment::IsEmpty() const { return (!fragment_ || !fragment_->HasChildren()) && !has_interchange_newline_at_start_ && !has_interchange_newline_at_end_; } Node* ReplacementFragment::FirstChild() const { return fragment_ ? fragment_->firstChild() : nullptr; } Node* ReplacementFragment::LastChild() const { return fragment_ ? fragment_->lastChild() : nullptr; } void ReplacementFragment::RemoveNodePreservingChildren(ContainerNode* node) { if (!node) return; while (Node* n = node->firstChild()) { RemoveNode(n); InsertNodeBefore(n, node); } RemoveNode(node); } void ReplacementFragment::RemoveNode(Node* node) { if (!node) return; ContainerNode* parent = node->NonShadowBoundaryParentNode(); if (!parent) return; parent->RemoveChild(node); } void ReplacementFragment::InsertNodeBefore(Node* node, Node* ref_node) { if (!node || !ref_node) return; ContainerNode* parent = ref_node->NonShadowBoundaryParentNode(); if (!parent) return; parent->InsertBefore(node, ref_node); } HTMLElement* ReplacementFragment::InsertFragmentForTestRendering( Element* root_editable_element) { TRACE_EVENT0("blink", "ReplacementFragment::insertFragmentForTestRendering"); DCHECK(document_); HTMLElement* holder = CreateDefaultParagraphElement(*document_.Get()); holder->AppendChild(fragment_); root_editable_element->AppendChild(holder); // TODO(editing-dev): Hoist this call to the call sites. document_->UpdateStyleAndLayout(); return holder; } void ReplacementFragment::RestoreAndRemoveTestRenderingNodesToFragment( Element* holder) { if (!holder) return; while (Node* node = holder->firstChild()) { holder->RemoveChild(node); fragment_->AppendChild(node); } RemoveNode(holder); } void ReplacementFragment::RemoveUnrenderedNodes(ContainerNode* holder) { HeapVector> unrendered; for (Node& node : NodeTraversal::DescendantsOf(*holder)) { if (!IsNodeRendered(node) && !IsTableStructureNode(&node)) unrendered.push_back(&node); } for (auto& node : unrendered) RemoveNode(node); } void ReplacementFragment::RemoveInterchangeNodes(ContainerNode* container) { has_interchange_newline_at_start_ = false; has_interchange_newline_at_end_ = false; // Interchange newlines at the "start" of the incoming fragment must be // either the first node in the fragment or the first leaf in the fragment. Node* node = container->firstChild(); while (node) { if (IsInterchangeHTMLBRElement(node)) { has_interchange_newline_at_start_ = true; RemoveNode(node); break; } node = node->firstChild(); } if (!container->HasChildren()) return; // Interchange newlines at the "end" of the incoming fragment must be // either the last node in the fragment or the last leaf in the fragment. node = container->lastChild(); while (node) { if (IsInterchangeHTMLBRElement(node)) { has_interchange_newline_at_end_ = true; RemoveNode(node); break; } node = node->lastChild(); } } inline void ReplaceSelectionCommand::InsertedNodes::RespondToNodeInsertion( Node& node) { if (!first_node_inserted_) first_node_inserted_ = &node; last_node_inserted_ = &node; } inline void ReplaceSelectionCommand::InsertedNodes::WillRemoveNodePreservingChildren( Node& node) { if (first_node_inserted_.Get() == node) first_node_inserted_ = NodeTraversal::Next(node); if (last_node_inserted_.Get() == node) last_node_inserted_ = node.lastChild() ? node.lastChild() : NodeTraversal::NextSkippingChildren(node); if (ref_node_.Get() == node) ref_node_ = NodeTraversal::Next(node); } inline void ReplaceSelectionCommand::InsertedNodes::WillRemoveNode(Node& node) { if (first_node_inserted_.Get() == node && last_node_inserted_.Get() == node) { first_node_inserted_ = nullptr; last_node_inserted_ = nullptr; } else if (first_node_inserted_.Get() == node) { first_node_inserted_ = NodeTraversal::NextSkippingChildren(*first_node_inserted_); } else if (last_node_inserted_.Get() == node) { last_node_inserted_ = NodeTraversal::PreviousSkippingChildren(*last_node_inserted_); } if (node.contains(ref_node_)) ref_node_ = NodeTraversal::NextSkippingChildren(node); } inline void ReplaceSelectionCommand::InsertedNodes::DidReplaceNode( Node& node, Node& new_node) { if (first_node_inserted_.Get() == node) first_node_inserted_ = &new_node; if (last_node_inserted_.Get() == node) last_node_inserted_ = &new_node; if (ref_node_.Get() == node) ref_node_ = &new_node; } ReplaceSelectionCommand::ReplaceSelectionCommand( Document& document, DocumentFragment* fragment, CommandOptions options, InputEvent::InputType input_type) : CompositeEditCommand(document), select_replacement_(options & kSelectReplacement), smart_replace_(options & kSmartReplace), match_style_(options & kMatchStyle), document_fragment_(fragment), prevent_nesting_(options & kPreventNesting), moving_paragraph_(options & kMovingParagraph), input_type_(input_type), sanitize_fragment_(options & kSanitizeFragment), should_merge_end_(false) {} static bool HasMatchingQuoteLevel(VisiblePosition end_of_existing_content, VisiblePosition end_of_inserted_content) { Position existing = end_of_existing_content.DeepEquivalent(); Position inserted = end_of_inserted_content.DeepEquivalent(); bool is_inside_mail_blockquote = EnclosingNodeOfType( inserted, IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary); return is_inside_mail_blockquote && (NumEnclosingMailBlockquotes(existing) == NumEnclosingMailBlockquotes(inserted)); } bool ReplaceSelectionCommand::ShouldMergeStart( bool selection_start_was_start_of_paragraph, bool fragment_has_interchange_newline_at_start, bool selection_start_was_inside_mail_blockquote) { if (moving_paragraph_) return false; VisiblePosition start_of_inserted_content = PositionAtStartOfInsertedContent(); VisiblePosition prev = PreviousPositionOf(start_of_inserted_content, kCannotCrossEditingBoundary); if (prev.IsNull()) return false; // When we have matching quote levels, its ok to merge more frequently. // For a successful merge, we still need to make sure that the inserted // content starts with the beginning of a paragraph. And we should only merge // here if the selection start was inside a mail blockquote. This prevents // against removing a blockquote from newly pasted quoted content that was // pasted into an unquoted position. If that unquoted position happens to be // right after another blockquote, we don't want to merge and risk stripping a // valid block (and newline) from the pasted content. if (IsStartOfParagraph(start_of_inserted_content) && selection_start_was_inside_mail_blockquote && HasMatchingQuoteLevel(prev, PositionAtEndOfInsertedContent())) return true; return !selection_start_was_start_of_paragraph && !fragment_has_interchange_newline_at_start && IsStartOfParagraph(start_of_inserted_content) && !IsHTMLBRElement( *start_of_inserted_content.DeepEquivalent().AnchorNode()) && ShouldMerge(start_of_inserted_content, prev); } bool ReplaceSelectionCommand::ShouldMergeEnd( bool selection_end_was_end_of_paragraph) { VisiblePosition end_of_inserted_content(PositionAtEndOfInsertedContent()); VisiblePosition next = NextPositionOf(end_of_inserted_content, kCannotCrossEditingBoundary); if (next.IsNull()) return false; return !selection_end_was_end_of_paragraph && IsEndOfParagraph(end_of_inserted_content) && !IsHTMLBRElement( *end_of_inserted_content.DeepEquivalent().AnchorNode()) && ShouldMerge(end_of_inserted_content, next); } static bool IsHTMLHeaderElement(const Node* a) { const auto* element = DynamicTo(a); if (!element) return false; return element->HasTagName(kH1Tag) || element->HasTagName(kH2Tag) || element->HasTagName(kH3Tag) || element->HasTagName(kH4Tag) || element->HasTagName(kH5Tag) || element->HasTagName(kH6Tag); } static bool HaveSameTagName(Element* a, Element* b) { return a && b && a->tagName() == b->tagName(); } bool ReplaceSelectionCommand::ShouldMerge(const VisiblePosition& source, const VisiblePosition& destination) { if (source.IsNull() || destination.IsNull()) return false; Node* source_node = source.DeepEquivalent().AnchorNode(); Node* destination_node = destination.DeepEquivalent().AnchorNode(); Element* source_block = EnclosingBlock(source_node); Element* destination_block = EnclosingBlock(destination_node); return source_block && (!source_block->HasTagName(kBlockquoteTag) || IsMailHTMLBlockquoteElement(source_block)) && EnclosingListChild(source_block) == EnclosingListChild(destination_node) && EnclosingTableCell(source.DeepEquivalent()) == EnclosingTableCell(destination.DeepEquivalent()) && (!IsHTMLHeaderElement(source_block) || HaveSameTagName(source_block, destination_block)) // Don't merge to or from a position before or after a block because it // would be a no-op and cause infinite recursion. && !IsEnclosingBlock(source_node) && !IsEnclosingBlock(destination_node); } // Style rules that match just inserted elements could change their appearance, // like a div inserted into a document with div { display:inline; }. void ReplaceSelectionCommand::RemoveRedundantStylesAndKeepStyleSpanInline( InsertedNodes& inserted_nodes, EditingState* editing_state) { Node* past_end_node = inserted_nodes.PastLastLeaf(); Node* next = nullptr; for (Node* node = inserted_nodes.FirstNodeInserted(); node && node != past_end_node; node = next) { // FIXME: Style rules that match pasted content can // change it's appearance next = NodeTraversal::Next(*node); if (!node->IsStyledElement()) continue; auto* element = To(node); const CSSPropertyValueSet* inline_style = element->InlineStyle(); EditingStyle* new_inline_style = MakeGarbageCollected(inline_style); if (inline_style) { auto* html_element = DynamicTo(element); if (html_element) { Vector attributes; DCHECK(html_element); if (new_inline_style->ConflictsWithImplicitStyleOfElement( html_element)) { // e.g. is converted to element = ReplaceElementWithSpanPreservingChildrenAndAttributes( html_element); inline_style = element->InlineStyle(); inserted_nodes.DidReplaceNode(*html_element, *element); } else if (new_inline_style ->ExtractConflictingImplicitStyleOfAttributes( html_element, EditingStyle::kPreserveWritingDirection, nullptr, attributes, EditingStyle::kDoNotExtractMatchingStyle)) { // e.g. is converted to for (wtf_size_t i = 0; i < attributes.size(); i++) RemoveElementAttribute(html_element, attributes[i]); } } ContainerNode* context = element->parentNode(); // If Mail wraps the fragment with a Paste as Quotation blockquote, or if // you're pasting into a quoted region, styles from blockquoteNode are // allowed to override those from the source document, see // and . HTMLQuoteElement* blockquote_element = !context ? ToHTMLQuoteElement(context) : ToHTMLQuoteElement(EnclosingNodeOfType( Position::FirstPositionInNode(*context), IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary)); // EditingStyle::removeStyleFromRulesAndContext() uses StyleResolver, // which requires clean style. // TODO(editing-dev): There is currently no way to update style without // updating layout. We might want to have updateLifcycleToStyleClean() // similar to FrameView::updateLifecylceToLayoutClean() in Document. GetDocument().UpdateStyleAndLayout(); if (blockquote_element) new_inline_style->RemoveStyleFromRulesAndContext( element, GetDocument().documentElement()); new_inline_style->RemoveStyleFromRulesAndContext(element, context); } if (!inline_style || new_inline_style->IsEmpty()) { if (IsStyleSpanOrSpanWithOnlyStyleAttribute(element) || IsEmptyFontTag(element, kAllowNonEmptyStyleAttribute)) { inserted_nodes.WillRemoveNodePreservingChildren(*element); RemoveNodePreservingChildren(element, editing_state); if (editing_state->IsAborted()) return; continue; } RemoveElementAttribute(element, kStyleAttr); } else if (new_inline_style->Style()->PropertyCount() != inline_style->PropertyCount()) { SetNodeAttribute(element, kStyleAttr, AtomicString(new_inline_style->Style()->AsText())); } GetDocument().UpdateStyleAndLayout(); // FIXME: Tolerate differences in id, class, and style attributes. if (element->parentNode() && IsNonTableCellHTMLBlockElement(element) && AreIdenticalElements(*element, *element->parentNode()) && VisiblePosition::FirstPositionInNode(*element->parentNode()) .DeepEquivalent() == VisiblePosition::FirstPositionInNode(*element).DeepEquivalent() && VisiblePosition::LastPositionInNode(*element->parentNode()) .DeepEquivalent() == VisiblePosition::LastPositionInNode(*element).DeepEquivalent()) { inserted_nodes.WillRemoveNodePreservingChildren(*element); RemoveNodePreservingChildren(element, editing_state); if (editing_state->IsAborted()) return; continue; } if (element->parentNode() && HasRichlyEditableStyle(*element->parentNode()) && HasRichlyEditableStyle(*element)) { RemoveElementAttribute(element, kContenteditableAttr); } } } static bool IsProhibitedParagraphChild(const AtomicString& name) { // https://dvcs.w3.org/hg/editing/raw-file/57abe6d3cb60/editing.html#prohibited-paragraph-child DEFINE_STATIC_LOCAL( HashSet, elements, ({ kAddressTag.LocalName(), kArticleTag.LocalName(), kAsideTag.LocalName(), kBlockquoteTag.LocalName(), kCaptionTag.LocalName(), kCenterTag.LocalName(), kColTag.LocalName(), kColgroupTag.LocalName(), kDdTag.LocalName(), kDetailsTag.LocalName(), kDirTag.LocalName(), kDivTag.LocalName(), kDlTag.LocalName(), kDtTag.LocalName(), kFieldsetTag.LocalName(), kFigcaptionTag.LocalName(), kFigureTag.LocalName(), kFooterTag.LocalName(), kFormTag.LocalName(), kH1Tag.LocalName(), kH2Tag.LocalName(), kH3Tag.LocalName(), kH4Tag.LocalName(), kH5Tag.LocalName(), kH6Tag.LocalName(), kHeaderTag.LocalName(), kHgroupTag.LocalName(), kHrTag.LocalName(), kLiTag.LocalName(), kListingTag.LocalName(), kMainTag.LocalName(), // Missing in the specification. kMenuTag.LocalName(), kNavTag.LocalName(), kOlTag.LocalName(), kPTag.LocalName(), kPlaintextTag.LocalName(), kPreTag.LocalName(), kSectionTag.LocalName(), kSummaryTag.LocalName(), kTableTag.LocalName(), kTbodyTag.LocalName(), kTdTag.LocalName(), kTfootTag.LocalName(), kThTag.LocalName(), kTheadTag.LocalName(), kTrTag.LocalName(), kUlTag.LocalName(), kXmpTag.LocalName(), })); return elements.Contains(name); } void ReplaceSelectionCommand:: MakeInsertedContentRoundTrippableWithHTMLTreeBuilder( const InsertedNodes& inserted_nodes, EditingState* editing_state) { Node* past_end_node = inserted_nodes.PastLastLeaf(); Node* next = nullptr; for (Node* node = inserted_nodes.FirstNodeInserted(); node && node != past_end_node; node = next) { next = NodeTraversal::Next(*node); auto* element = DynamicTo(node); if (!element) continue; // moveElementOutOfAncestor() in a previous iteration might have failed, // and |node| might have been detached from the document tree. if (!node->isConnected()) continue; if (IsProhibitedParagraphChild(element->localName())) { if (HTMLElement* paragraph_element = To(EnclosingElementWithTag( Position::InParentBeforeNode(*element), kPTag))) { MoveElementOutOfAncestor(element, paragraph_element, editing_state); if (editing_state->IsAborted()) return; } } if (IsHTMLHeaderElement(element)) { if (auto* header_element = To(HighestEnclosingNodeOfType( Position::InParentBeforeNode(*element), IsHTMLHeaderElement))) { MoveElementOutOfAncestor(element, header_element, editing_state); if (editing_state->IsAborted()) return; } } } } void ReplaceSelectionCommand::MoveElementOutOfAncestor( Element* element, Element* ancestor, EditingState* editing_state) { DCHECK(element); if (!HasEditableStyle(*ancestor->parentNode())) return; GetDocument().UpdateStyleAndLayout(); VisiblePosition position_at_end_of_node = CreateVisiblePosition(LastPositionInOrAfterNode(*element)); VisiblePosition last_position_in_paragraph = VisiblePosition::LastPositionInNode(*ancestor); if (position_at_end_of_node.DeepEquivalent() == last_position_in_paragraph.DeepEquivalent()) { RemoveNode(element, editing_state); if (editing_state->IsAborted()) return; if (ancestor->nextSibling()) InsertNodeBefore(element, ancestor->nextSibling(), editing_state); else AppendNode(element, ancestor->parentNode(), editing_state); if (editing_state->IsAborted()) return; } else { Node* node_to_split_to = SplitTreeToNode(element, ancestor, true); RemoveNode(element, editing_state); if (editing_state->IsAborted()) return; InsertNodeBefore(element, node_to_split_to, editing_state); if (editing_state->IsAborted()) return; } if (!ancestor->HasChildren()) RemoveNode(ancestor, editing_state); } static inline bool NodeHasVisibleLayoutText(Text& text) { return text.GetLayoutObject() && text.GetLayoutObject()->ResolvedTextLength() > 0; } void ReplaceSelectionCommand::RemoveUnrenderedTextNodesAtEnds( InsertedNodes& inserted_nodes) { GetDocument().UpdateStyleAndLayout(); auto* last_leaf_inserted = DynamicTo(inserted_nodes.LastLeafInserted()); if (last_leaf_inserted && !NodeHasVisibleLayoutText(*last_leaf_inserted) && !EnclosingElementWithTag(FirstPositionInOrBeforeNode(*last_leaf_inserted), kSelectTag) && !EnclosingElementWithTag(FirstPositionInOrBeforeNode(*last_leaf_inserted), kScriptTag)) { inserted_nodes.WillRemoveNode(*last_leaf_inserted); // Removing a Text node won't dispatch synchronous events. RemoveNode(last_leaf_inserted, ASSERT_NO_EDITING_ABORT); } // We don't have to make sure that firstNodeInserted isn't inside a select or // script element, because it is a top level node in the fragment and the user // can't insert into those elements. auto* first_node_inserted = DynamicTo(inserted_nodes.FirstNodeInserted()); if (first_node_inserted && !NodeHasVisibleLayoutText(*first_node_inserted)) { inserted_nodes.WillRemoveNode(*first_node_inserted); // Removing a Text node won't dispatch synchronous events. RemoveNode(first_node_inserted, ASSERT_NO_EDITING_ABORT); } } VisiblePosition ReplaceSelectionCommand::PositionAtEndOfInsertedContent() const { // TODO(editing-dev): Hoist the call and change it into a DCHECK. GetDocument().UpdateStyleAndLayout(); // TODO(yosin): We should set |end_of_inserted_content_| not in SELECT // element, since contents of SELECT elements, e.g. OPTION, OPTGROUP, are // not editable, or SELECT element is an atomic on editing. HTMLSelectElement* enclosing_select = ToHTMLSelectElement( EnclosingElementWithTag(end_of_inserted_content_, kSelectTag)); if (enclosing_select) { return CreateVisiblePosition(LastPositionInOrAfterNode(*enclosing_select)); } if (end_of_inserted_content_.IsOrphan()) return VisiblePosition(); return CreateVisiblePosition(end_of_inserted_content_); } VisiblePosition ReplaceSelectionCommand::PositionAtStartOfInsertedContent() const { // TODO(editing-dev): Hoist the call and change it into a DCHECK. GetDocument().UpdateStyleAndLayout(); if (start_of_inserted_content_.IsOrphan()) return VisiblePosition(); return CreateVisiblePosition(start_of_inserted_content_); } static void RemoveHeadContents(ReplacementFragment& fragment) { Node* next = nullptr; for (Node* node = fragment.FirstChild(); node; node = next) { if (IsHTMLBaseElement(*node) || IsHTMLLinkElement(*node) || IsHTMLMetaElement(*node) || IsHTMLStyleElement(*node) || IsHTMLTitleElement(*node) || IsSVGStyleElement(*node)) { next = NodeTraversal::NextSkippingChildren(*node); fragment.RemoveNode(node); } else { next = NodeTraversal::Next(*node); } } } static bool FollowBlockElementStyle(const Node* node) { const auto* element = DynamicTo(node); if (!element) return false; // When content is inserted into an empty block, use the original style // instead of the block style. if (!node->firstChild()) return false; // A block with a placeholder BR appears the same as an empty block. if (node->firstChild() == node->lastChild() && IsHTMLBRElement(node->firstChild())) { return false; } return IsListItem(node) || IsTableCell(node) || element->HasTagName(kPreTag) || element->HasTagName(kH1Tag) || element->HasTagName(kH2Tag) || element->HasTagName(kH3Tag) || element->HasTagName(kH4Tag) || element->HasTagName(kH5Tag) || element->HasTagName(kH6Tag); } // Remove style spans before insertion if they are unnecessary. It's faster // because we'll avoid doing a layout. static void HandleStyleSpansBeforeInsertion(ReplacementFragment& fragment, const Position& insertion_pos) { Node* top_node = fragment.FirstChild(); if (!IsHTMLSpanElement(top_node)) return; // Handling the case where we are doing Paste as Quotation or pasting into // quoted content is more complicated (see handleStyleSpans) and doesn't // receive the optimization. if (EnclosingNodeOfType(FirstPositionInOrBeforeNode(*top_node), IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary)) return; // Remove style spans to follow the styles of parent block element when // |fragment| becomes a part of it. See bugs http://crbug.com/226941 and // http://crbug.com/335955. HTMLSpanElement* wrapping_style_span = ToHTMLSpanElement(top_node); const Node* node = insertion_pos.AnchorNode(); // |node| can be an inline element like
under
  • // e.g.) editing/execCommand/switch-list-type.html // editing/deleting/backspace-merge-into-block.html if (IsInline(node)) { node = EnclosingBlock(insertion_pos.AnchorNode()); if (!node) return; } if (FollowBlockElementStyle(node)) { fragment.RemoveNodePreservingChildren(wrapping_style_span); return; } EditingStyle* style_at_insertion_pos = MakeGarbageCollected( insertion_pos.ParentAnchoredEquivalent()); String style_text = style_at_insertion_pos->Style()->AsText(); // FIXME: This string comparison is a naive way of comparing two styles. // We should be taking the diff and check that the diff is empty. if (style_text != wrapping_style_span->getAttribute(kStyleAttr)) return; fragment.RemoveNodePreservingChildren(wrapping_style_span); } void ReplaceSelectionCommand::MergeEndIfNeeded(EditingState* editing_state) { if (!should_merge_end_) return; VisiblePosition start_of_inserted_content(PositionAtStartOfInsertedContent()); VisiblePosition end_of_inserted_content(PositionAtEndOfInsertedContent()); // Bail to avoid infinite recursion. if (moving_paragraph_) { NOTREACHED(); return; } // Merging two paragraphs will destroy the moved one's block styles. Always // move the end of inserted forward to preserve the block style of the // paragraph already in the document, unless the paragraph to move would // include the what was the start of the selection that was pasted into, so // that we preserve that paragraph's block styles. bool merge_forward = !(InSameParagraph(start_of_inserted_content, end_of_inserted_content) && !IsStartOfParagraph(start_of_inserted_content)); VisiblePosition destination = merge_forward ? NextPositionOf(end_of_inserted_content) : end_of_inserted_content; // TODO(editing-dev): Stop storing VisiblePositions through mutations. // See crbug.com/648949 for details. VisiblePosition start_of_paragraph_to_move = merge_forward ? StartOfParagraph(end_of_inserted_content) : NextPositionOf(end_of_inserted_content); // Merging forward could result in deleting the destination anchor node. // To avoid this, we add a placeholder node before the start of the paragraph. if (EndOfParagraph(start_of_paragraph_to_move).DeepEquivalent() == destination.DeepEquivalent()) { auto* placeholder = MakeGarbageCollected(GetDocument()); InsertNodeBefore(placeholder, start_of_paragraph_to_move.DeepEquivalent().AnchorNode(), editing_state); if (editing_state->IsAborted()) return; // TODO(editing-dev): Use of UpdateStyleAndLayout() // needs to be audited. See http://crbug.com/590369 for more details. GetDocument().UpdateStyleAndLayout(); destination = VisiblePosition::BeforeNode(*placeholder); start_of_paragraph_to_move = CreateVisiblePosition( start_of_paragraph_to_move.ToPositionWithAffinity()); } MoveParagraph(start_of_paragraph_to_move, EndOfParagraph(start_of_paragraph_to_move), destination, editing_state); if (editing_state->IsAborted()) return; GetDocument().UpdateStyleAndLayout(); // Merging forward will remove end_of_inserted_content from the document. if (merge_forward) { const VisibleSelection& visible_selection = EndingVisibleSelection(); if (start_of_inserted_content_.IsOrphan()) { start_of_inserted_content_ = visible_selection.VisibleStart().DeepEquivalent(); } end_of_inserted_content_ = visible_selection.VisibleEnd().DeepEquivalent(); // If we merged text nodes, end_of_inserted_content_ could be null. If // this is the case, we use start_of_inserted_content_. if (end_of_inserted_content_.IsNull()) end_of_inserted_content_ = start_of_inserted_content_; } } static Node* EnclosingInline(Node* node) { while (ContainerNode* parent = node->parentNode()) { if (IsBlockFlowElement(*parent) || IsHTMLBodyElement(*parent)) return node; // Stop if any previous sibling is a block. for (Node* sibling = node->previousSibling(); sibling; sibling = sibling->previousSibling()) { if (IsBlockFlowElement(*sibling)) return node; } node = parent; } return node; } static bool IsInlineHTMLElementWithStyle(const Node* node) { // We don't want to skip over any block elements. if (IsEnclosingBlock(node)) return false; const auto* element = DynamicTo(node); if (!element) return false; // We can skip over elements whose class attribute is // one of our internal classes. return EditingStyle::ElementIsStyledSpanOrHTMLEquivalent(element); } static inline HTMLElement* ElementToSplitToAvoidPastingIntoInlineElementsWithStyle( const Position& insertion_pos) { Element* containing_block = EnclosingBlock(insertion_pos.ComputeContainerNode()); return To(HighestEnclosingNodeOfType( insertion_pos, IsInlineHTMLElementWithStyle, kCannotCrossEditingBoundary, containing_block)); } void ReplaceSelectionCommand::SetUpStyle(const VisibleSelection& selection) { // We can skip matching the style if the selection is plain text. // TODO(editing-dev): Use IsEditablePosition instead of using UserModify // directly. if ((selection.Start().AnchorNode()->GetLayoutObject() && selection.Start() .AnchorNode() ->GetLayoutObject() ->Style() ->UserModify() == EUserModify::kReadWritePlaintextOnly) && (selection.End().AnchorNode()->GetLayoutObject() && selection.End().AnchorNode()->GetLayoutObject()->Style()->UserModify() == EUserModify::kReadWritePlaintextOnly)) match_style_ = false; if (match_style_) { insertion_style_ = MakeGarbageCollected(selection.Start()); insertion_style_->MergeTypingStyle(&GetDocument()); } } void ReplaceSelectionCommand::InsertParagraphSeparatorIfNeeds( const VisibleSelection& selection, const ReplacementFragment& fragment, EditingState* editing_state) { const VisiblePosition visible_start = selection.VisibleStart(); const VisiblePosition visible_end = selection.VisibleEnd(); const bool selection_end_was_end_of_paragraph = IsEndOfParagraph(visible_end); const bool selection_start_was_start_of_paragraph = IsStartOfParagraph(visible_start); Element* const enclosing_block_of_visible_start = EnclosingBlock(visible_start.DeepEquivalent().AnchorNode()); const bool start_is_inside_mail_blockquote = EnclosingNodeOfType( selection.Start(), IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary); const bool selection_is_plain_text = !IsRichlyEditablePosition(selection.Base()); Element* const current_root = selection.RootEditableElement(); if ((selection_start_was_start_of_paragraph && selection_end_was_end_of_paragraph && !start_is_inside_mail_blockquote) || enclosing_block_of_visible_start == current_root || IsListItem(enclosing_block_of_visible_start) || selection_is_plain_text) { prevent_nesting_ = false; } if (selection.IsRange()) { // When the end of the selection being pasted into is at the end of a // paragraph, and that selection spans multiple blocks, not merging may // leave an empty line. // When the start of the selection being pasted into is at the start of a // block, not merging will leave hanging block(s). // Merge blocks if the start of the selection was in a Mail blockquote, // since we handle that case specially to prevent nesting. bool merge_blocks_after_delete = start_is_inside_mail_blockquote || IsEndOfParagraph(visible_end) || IsStartOfBlock(visible_start); // FIXME: We should only expand to include fully selected special elements // if we are copying a selection and pasting it on top of itself. if (!DeleteSelection(editing_state, DeleteSelectionOptions::Builder() .SetMergeBlocksAfterDelete( merge_blocks_after_delete) .SetSanitizeMarkup(true) .Build())) return; if (fragment.HasInterchangeNewlineAtStart()) { GetDocument().UpdateStyleAndLayout(); VisiblePosition start_after_delete = EndingVisibleSelection().VisibleStart(); if (IsEndOfParagraph(start_after_delete) && !IsStartOfParagraph(start_after_delete) && !IsEndOfEditableOrNonEditableContent(start_after_delete)) { SetEndingSelection(SelectionForUndoStep::From( SelectionInDOMTree::Builder() .Collapse(NextPositionOf(start_after_delete).DeepEquivalent()) .Build())); } else { InsertParagraphSeparator(editing_state); } if (editing_state->IsAborted()) return; } } else { DCHECK(selection.IsCaret()); if (fragment.HasInterchangeNewlineAtStart()) { const VisiblePosition next = NextPositionOf(visible_start, kCannotCrossEditingBoundary); if (IsEndOfParagraph(visible_start) && !IsStartOfParagraph(visible_start) && next.IsNotNull()) { SetEndingSelection( SelectionForUndoStep::From(SelectionInDOMTree::Builder() .Collapse(next.DeepEquivalent()) .Build())); } else { InsertParagraphSeparator(editing_state); if (editing_state->IsAborted()) return; GetDocument().UpdateStyleAndLayout(); } } // We split the current paragraph in two to avoid nesting the blocks from // the fragment inside the current block. // // For example, paste //
    foo
    bar
    baz
    // into //
    x^x
    // where ^ is the caret. // // As long as the div styles are the same, visually you'd expect: //
    xbar
    bar
    bazx
    // not //
    xbar
    bar
    bazx
    // Don't do this if the selection started in a Mail blockquote. const VisiblePosition visible_start_position = EndingVisibleSelection().VisibleStart(); if (prevent_nesting_ && !start_is_inside_mail_blockquote && !IsEndOfParagraph(visible_start_position) && !IsStartOfParagraph(visible_start_position)) { InsertParagraphSeparator(editing_state); if (editing_state->IsAborted()) return; GetDocument().UpdateStyleAndLayout(); SetEndingSelection(SelectionForUndoStep::From( SelectionInDOMTree::Builder() .Collapse( PreviousPositionOf(EndingVisibleSelection().VisibleStart()) .DeepEquivalent()) .Build())); } } } void ReplaceSelectionCommand::DoApply(EditingState* editing_state) { TRACE_EVENT0("blink", "ReplaceSelectionCommand::doApply"); const VisibleSelection& selection = EndingVisibleSelection(); // ReplaceSelectionCommandTest.CrashWithNoSelection hits below abort // condition. ABORT_EDITING_COMMAND_IF(selection.IsNone()); ABORT_EDITING_COMMAND_IF(!selection.IsValidFor(GetDocument())); if (!selection.RootEditableElement()) return; ReplacementFragment fragment(&GetDocument(), document_fragment_.Get(), selection); bool trivial_replace_result = PerformTrivialReplace(fragment, editing_state); if (editing_state->IsAborted()) return; if (trivial_replace_result) return; GetDocument().UpdateStyleAndLayout(); SetUpStyle(selection); Element* const current_root = selection.RootEditableElement(); const bool start_is_inside_mail_blockquote = EnclosingNodeOfType( selection.Start(), IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary); const bool selection_is_plain_text = !IsRichlyEditablePosition(selection.Base()); const bool selection_end_was_end_of_paragraph = IsEndOfParagraph(selection.VisibleEnd()); const bool selection_start_was_start_of_paragraph = IsStartOfParagraph(selection.VisibleStart()); InsertParagraphSeparatorIfNeeds(selection, fragment, editing_state); if (editing_state->IsAborted()) return; Position insertion_pos = EndingVisibleSelection().Start(); // We don't want any of the pasted content to end up nested in a Mail // blockquote, so first break out of any surrounding Mail blockquotes. Unless // we're inserting in a table, in which case breaking the blockquote will // prevent the content from actually being inserted in the table. if (EnclosingNodeOfType(insertion_pos, IsMailHTMLBlockquoteElement, kCanCrossEditingBoundary) && prevent_nesting_ && !(EnclosingNodeOfType(insertion_pos, &IsTableStructureNode))) { ApplyCommandToComposite( MakeGarbageCollected(GetDocument()), editing_state); if (editing_state->IsAborted()) return; // This will leave a br between the split. Node* br = EndingVisibleSelection().Start().AnchorNode(); DCHECK(IsHTMLBRElement(br)) << br; // Insert content between the two blockquotes, but remove the br (since it // was just a placeholder). insertion_pos = Position::InParentBeforeNode(*br); RemoveNode(br, editing_state); if (editing_state->IsAborted()) return; } // Inserting content could cause whitespace to collapse, e.g. inserting //
    foo
    into hello^ world. PrepareWhitespaceAtPositionForSplit(insertion_pos); GetDocument().UpdateStyleAndLayout(); // If the downstream node has been removed there's no point in continuing. if (!MostForwardCaretPosition(insertion_pos).AnchorNode()) return; // NOTE: This would be an incorrect usage of downstream() if downstream() were // changed to mean the last position after 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 away, there are positions after the br which map to the same // visible position as [br, 0]). HTMLBRElement* end_br = ToHTMLBRElementOrNull( *MostForwardCaretPosition(insertion_pos).AnchorNode()); VisiblePosition original_vis_pos_before_end_br; if (end_br) { original_vis_pos_before_end_br = PreviousPositionOf(VisiblePosition::BeforeNode(*end_br)); } Element* enclosing_block_of_insertion_pos = EnclosingBlock(insertion_pos.AnchorNode()); // Adjust |enclosingBlockOfInsertionPos| to prevent nesting. // If the start was in a Mail blockquote, we will have already handled // adjusting |enclosingBlockOfInsertionPos| above. if (prevent_nesting_ && enclosing_block_of_insertion_pos && enclosing_block_of_insertion_pos != current_root && !IsTableCell(enclosing_block_of_insertion_pos) && !start_is_inside_mail_blockquote) { VisiblePosition visible_insertion_pos = CreateVisiblePosition(insertion_pos); if (IsEndOfBlock(visible_insertion_pos) && !(IsStartOfBlock(visible_insertion_pos) && fragment.HasInterchangeNewlineAtEnd())) insertion_pos = Position::InParentAfterNode(*enclosing_block_of_insertion_pos); else if (IsStartOfBlock(visible_insertion_pos)) insertion_pos = Position::InParentBeforeNode(*enclosing_block_of_insertion_pos); } // Paste at start or end of link goes outside of link. insertion_pos = PositionAvoidingSpecialElementBoundary(insertion_pos, editing_state); if (editing_state->IsAborted()) return; // FIXME: Can this wait until after the operation has been performed? There // doesn't seem to be any work performed after this that queries or uses the // typing style. if (LocalFrame* frame = GetDocument().GetFrame()) frame->GetEditor().ClearTypingStyle(); RemoveHeadContents(fragment); GetDocument().UpdateStyleAndLayout(); // We don't want the destination to end up inside nodes that weren't selected. // To avoid that, we move the position forward without changing the visible // position so we're still at the same visible location, but outside of // preceding tags. insertion_pos = PositionAvoidingPrecedingNodes(insertion_pos); // Paste into run of tabs splits the tab span. insertion_pos = PositionOutsideTabSpan(insertion_pos); HandleStyleSpansBeforeInsertion(fragment, insertion_pos); // We're finished if there is nothing to add. if (fragment.IsEmpty() || !fragment.FirstChild()) return; // If we are not trying to match the destination style we prefer a position // that is outside inline elements that provide style. // This way we can produce a less verbose markup. // We can skip this optimization for fragments not wrapped in one of // our style spans and for positions inside list items // since insertAsListItems already does the right thing. if (!match_style_ && !EnclosingList(insertion_pos.ComputeContainerNode())) { auto* text_node = DynamicTo(insertion_pos.ComputeContainerNode()); if (text_node && insertion_pos.OffsetInContainerNode() && !insertion_pos.AtLastEditingPositionForNode()) { SplitTextNode(text_node, insertion_pos.OffsetInContainerNode()); insertion_pos = Position::FirstPositionInNode(*insertion_pos.ComputeContainerNode()); } if (HTMLElement* element_to_split_to = ElementToSplitToAvoidPastingIntoInlineElementsWithStyle( insertion_pos)) { if (insertion_pos.ComputeContainerNode() != element_to_split_to->parentNode()) { Node* split_start = insertion_pos.ComputeNodeAfterPosition(); if (!split_start) split_start = insertion_pos.ComputeContainerNode(); Node* node_to_split_to = SplitTreeToNode(split_start, element_to_split_to->parentNode()); insertion_pos = Position::InParentBeforeNode(*node_to_split_to); } } } // FIXME: When pasting rich content we're often prevented from heading down // the fast path by style spans. Try again here if they've been removed. // 1) Insert the content. // 2) Remove redundant styles and style tags, this inner for example: // foo bar baz. // 3) Merge the start of the added content with the content before the // position being pasted into. // 4) Do one of the following: // a) expand the last br if the fragment ends with one and it collapsed, // b) merge the last paragraph of the incoming fragment with the paragraph // that contained the end of the selection that was pasted into, or // c) handle an interchange newline at the end of the incoming fragment. // 5) Add spaces for smart replace. // 6) Select the replacement if requested, and match style if requested. InsertedNodes inserted_nodes; inserted_nodes.SetRefNode(fragment.FirstChild()); DCHECK(inserted_nodes.RefNode()); Node* node = inserted_nodes.RefNode()->nextSibling(); fragment.RemoveNode(inserted_nodes.RefNode()); Element* block_start = EnclosingBlock(insertion_pos.AnchorNode()); if ((IsHTMLListElement(inserted_nodes.RefNode()) || (IsHTMLListElement(inserted_nodes.RefNode()->firstChild()))) && block_start && block_start->GetLayoutObject()->IsListItem() && HasEditableStyle(*block_start->parentNode())) { inserted_nodes.SetRefNode(InsertAsListItems( To(inserted_nodes.RefNode()), block_start, insertion_pos, inserted_nodes, editing_state)); if (editing_state->IsAborted()) return; } else { InsertNodeAt(inserted_nodes.RefNode(), insertion_pos, editing_state); if (editing_state->IsAborted()) return; inserted_nodes.RespondToNodeInsertion(*inserted_nodes.RefNode()); } // Mutation events (bug 22634) may have already removed the inserted content if (!inserted_nodes.RefNode()->isConnected()) return; bool plain_text_fragment = IsPlainTextMarkup(inserted_nodes.RefNode()); while (node) { Node* next = node->nextSibling(); fragment.RemoveNode(node); InsertNodeAfter(node, inserted_nodes.RefNode(), editing_state); if (editing_state->IsAborted()) return; inserted_nodes.RespondToNodeInsertion(*node); // Mutation events (bug 22634) may have already removed the inserted content if (!node->isConnected()) return; inserted_nodes.SetRefNode(node); if (node && plain_text_fragment) plain_text_fragment = IsPlainTextMarkup(node); node = next; } if (IsRichlyEditablePosition(insertion_pos)) { RemoveUnrenderedTextNodesAtEnds(inserted_nodes); ABORT_EDITING_COMMAND_IF(!inserted_nodes.RefNode()); } GetDocument().UpdateStyleAndLayout(); // Mutation events (bug 20161) may have already removed the inserted content if (!inserted_nodes.FirstNodeInserted() || !inserted_nodes.FirstNodeInserted()->isConnected()) return; // Scripts specified in javascript protocol may remove // |enclosingBlockOfInsertionPos| during insertion, e.g.