blob: 2dc8cf8b43504ff3cd049f7925063a15f600b632 [file] [log] [blame]
/*
* 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/frame/web_feature.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 {
// --- ReplacementFragment helper class
class ReplacementFragment final {
STACK_ALLOCATED();
public:
ReplacementFragment(Document*, DocumentFragment*, const VisibleSelection&);
ReplacementFragment(const ReplacementFragment&) = delete;
ReplacementFragment& operator=(const ReplacementFragment&) = delete;
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);
Document* document_;
DocumentFragment* fragment_;
bool has_interchange_newline_at_start_;
bool has_interchange_newline_at_end_;
};
static bool IsInterchangeHTMLBRElement(const Node* node) {
DEFINE_STATIC_LOCAL(String, interchange_newline_class_string,
(AppleInterchangeNewline));
auto* html_br_element = DynamicTo<HTMLBRElement>(node);
if (!html_br_element ||
html_br_element->getAttribute(html_names::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.,
// <div>foo^</div>^
// 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(DocumentUpdateReason::kEditing);
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()
->IsTextControlIncludingNG()) &&
HasRichlyEditableStyle(*editable_root)) {
RemoveInterchangeNodes(fragment_);
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_);
String original_text = fragment_->textContent();
auto* event =
MakeGarbageCollected<BeforeTextInsertedEvent>(original_text);
editable_root->DispatchEvent(*event);
if (original_text != event->GetText()) {
fragment_ = CreateFragmentFromText(
selection.ToNormalizedEphemeralRange(), event->GetText());
RemoveInterchangeNodes(fragment_);
}
return;
}
}
HTMLElement* holder = InsertFragmentForTestRendering(editable_root);
if (!holder) {
RemoveInterchangeNodes(fragment_);
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<BeforeTextInsertedEvent>(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(DocumentUpdateReason::kEditing);
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_);
holder->AppendChild(fragment_);
root_editable_element->AppendChild(holder);
// TODO(editing-dev): Hoist this call to the call sites.
document_->UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
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<Member<Node>> 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_ == node)
first_node_inserted_ = NodeTraversal::Next(node);
if (last_node_inserted_ == node)
last_node_inserted_ = node.lastChild()
? node.lastChild()
: NodeTraversal::NextSkippingChildren(node);
if (ref_node_ == node)
ref_node_ = NodeTraversal::Next(node);
}
inline void ReplaceSelectionCommand::InsertedNodes::WillRemoveNode(Node& node) {
if (first_node_inserted_ == node && last_node_inserted_ == node) {
first_node_inserted_ = nullptr;
last_node_inserted_ = nullptr;
} else if (first_node_inserted_ == node) {
first_node_inserted_ =
NodeTraversal::NextSkippingChildren(*first_node_inserted_);
} else if (last_node_inserted_ == node) {
last_node_inserted_ =
NodeTraversal::PreviousAbsoluteSibling(*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_ == node)
first_node_inserted_ = &new_node;
if (last_node_inserted_ == node)
last_node_inserted_ = &new_node;
if (ref_node_ == 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) &&
!IsA<HTMLBRElement>(
*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) &&
!IsA<HTMLBRElement>(
*end_of_inserted_content.DeepEquivalent().AnchorNode()) &&
ShouldMerge(end_of_inserted_content, next);
}
static bool IsHTMLHeaderElement(const Node* a) {
const auto* element = DynamicTo<HTMLElement>(a);
if (!element)
return false;
return element->HasTagName(html_names::kH1Tag) ||
element->HasTagName(html_names::kH2Tag) ||
element->HasTagName(html_names::kH3Tag) ||
element->HasTagName(html_names::kH4Tag) ||
element->HasTagName(html_names::kH5Tag) ||
element->HasTagName(html_names::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(html_names::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: <rdar://problem/5371536> Style rules that match pasted content can
// change it's appearance
next = NodeTraversal::Next(*node);
if (!node->IsStyledElement())
continue;
auto* element = To<Element>(node);
const CSSPropertyValueSet* inline_style = element->InlineStyle();
EditingStyle* new_inline_style =
MakeGarbageCollected<EditingStyle>(inline_style);
if (inline_style) {
auto* html_element = DynamicTo<HTMLElement>(element);
if (html_element) {
Vector<QualifiedName> attributes;
DCHECK(html_element);
if (new_inline_style->ConflictsWithImplicitStyleOfElement(
html_element)) {
// e.g. <b style="font-weight: normal;"> is converted to <span
// style="font-weight: normal;">
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. <font size="3" style="font-size: 20px;"> is converted to <font
// style="font-size: 20px;">
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
// <rdar://problem/4930986> and <rdar://problem/5089327>.
auto* blockquote_element =
!context
? To<HTMLQuoteElement>(context)
: To<HTMLQuoteElement>(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(DocumentUpdateReason::kEditing);
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, html_names::kStyleAttr);
} else if (new_inline_style->Style()->PropertyCount() !=
inline_style->PropertyCount()) {
SetNodeAttribute(element, html_names::kStyleAttr,
AtomicString(new_inline_style->Style()->AsText()));
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// 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, html_names::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<AtomicString>, elements,
({
html_names::kAddressTag.LocalName(),
html_names::kArticleTag.LocalName(),
html_names::kAsideTag.LocalName(),
html_names::kBlockquoteTag.LocalName(),
html_names::kCaptionTag.LocalName(),
html_names::kCenterTag.LocalName(),
html_names::kColTag.LocalName(),
html_names::kColgroupTag.LocalName(),
html_names::kDdTag.LocalName(),
html_names::kDetailsTag.LocalName(),
html_names::kDirTag.LocalName(),
html_names::kDivTag.LocalName(),
html_names::kDlTag.LocalName(),
html_names::kDtTag.LocalName(),
html_names::kFieldsetTag.LocalName(),
html_names::kFigcaptionTag.LocalName(),
html_names::kFigureTag.LocalName(),
html_names::kFooterTag.LocalName(),
html_names::kFormTag.LocalName(),
html_names::kH1Tag.LocalName(),
html_names::kH2Tag.LocalName(),
html_names::kH3Tag.LocalName(),
html_names::kH4Tag.LocalName(),
html_names::kH5Tag.LocalName(),
html_names::kH6Tag.LocalName(),
html_names::kHeaderTag.LocalName(),
html_names::kHgroupTag.LocalName(),
html_names::kHrTag.LocalName(),
html_names::kLiTag.LocalName(),
html_names::kListingTag.LocalName(),
html_names::kMainTag.LocalName(), // Missing in the specification.
html_names::kMenuTag.LocalName(),
html_names::kNavTag.LocalName(),
html_names::kOlTag.LocalName(),
html_names::kPTag.LocalName(),
html_names::kPlaintextTag.LocalName(),
html_names::kPreTag.LocalName(),
html_names::kSectionTag.LocalName(),
html_names::kSummaryTag.LocalName(),
html_names::kTableTag.LocalName(),
html_names::kTbodyTag.LocalName(),
html_names::kTdTag.LocalName(),
html_names::kTfootTag.LocalName(),
html_names::kThTag.LocalName(),
html_names::kTheadTag.LocalName(),
html_names::kTrTag.LocalName(),
html_names::kUlTag.LocalName(),
html_names::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<HTMLElement>(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<HTMLElement>(EnclosingElementWithTag(
Position::InParentBeforeNode(*element), html_names::kPTag))) {
MoveElementOutOfAncestor(element, paragraph_element, editing_state);
if (editing_state->IsAborted())
return;
}
}
if (IsHTMLHeaderElement(element)) {
if (auto* header_element = To<HTMLElement>(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(DocumentUpdateReason::kEditing);
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(DocumentUpdateReason::kEditing);
auto* last_leaf_inserted = DynamicTo<Text>(inserted_nodes.LastLeafInserted());
if (last_leaf_inserted && !NodeHasVisibleLayoutText(*last_leaf_inserted) &&
!EnclosingElementWithTag(FirstPositionInOrBeforeNode(*last_leaf_inserted),
html_names::kSelectTag) &&
!EnclosingElementWithTag(FirstPositionInOrBeforeNode(*last_leaf_inserted),
html_names::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<Text>(inserted_nodes.FirstNodeInserted());
if (first_node_inserted) {
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
if (!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(DocumentUpdateReason::kEditing);
// 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.
auto* enclosing_select = To<HTMLSelectElement>(EnclosingElementWithTag(
end_of_inserted_content_, html_names::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(DocumentUpdateReason::kEditing);
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 (IsA<HTMLBaseElement>(*node) || IsA<HTMLLinkElement>(*node) ||
IsA<HTMLMetaElement>(*node) || IsA<HTMLStyleElement>(*node) ||
IsA<HTMLTitleElement>(*node) || IsA<SVGStyleElement>(*node)) {
next = NodeTraversal::NextSkippingChildren(*node);
fragment.RemoveNode(node);
} else {
next = NodeTraversal::Next(*node);
}
}
}
static bool FollowBlockElementStyle(const Node* node) {
const auto* element = DynamicTo<HTMLElement>(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() &&
IsA<HTMLBRElement>(node->firstChild())) {
return false;
}
return IsListItem(node) || IsTableCell(node) ||
element->HasTagName(html_names::kPreTag) ||
element->HasTagName(html_names::kH1Tag) ||
element->HasTagName(html_names::kH2Tag) ||
element->HasTagName(html_names::kH3Tag) ||
element->HasTagName(html_names::kH4Tag) ||
element->HasTagName(html_names::kH5Tag) ||
element->HasTagName(html_names::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 (!IsA<HTMLSpanElement>(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.
auto* wrapping_style_span = To<HTMLSpanElement>(top_node);
const Node* node = insertion_pos.AnchorNode();
// |node| can be an inline element like <br> under <li>
// 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<EditingStyle>(
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(html_names::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<HTMLBRElement>(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(DocumentUpdateReason::kEditing);
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(DocumentUpdateReason::kEditing);
// 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) || IsA<HTMLBodyElement>(*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<HTMLElement>(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<HTMLElement>(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<EditingStyle>(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(DocumentUpdateReason::kEditing);
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(DocumentUpdateReason::kEditing);
}
}
// We split the current paragraph in two to avoid nesting the blocks from
// the fragment inside the current block.
//
// For example, paste
// <div>foo</div><div>bar</div><div>baz</div>
// into
// <div>x^x</div>
// where ^ is the caret.
//
// As long as the div styles are the same, visually you'd expect:
// <div>xbar</div><div>bar</div><div>bazx</div>
// not
// <div>xbar<div>bar</div><div>bazx</div></div>
// 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(DocumentUpdateReason::kEditing);
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(DocumentUpdateReason::kEditing);
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<BreakBlockquoteCommand>(GetDocument()),
editing_state);
if (editing_state->IsAborted())
return;
// This will leave a br between the split.
Node* br = EndingVisibleSelection().Start().AnchorNode();
DCHECK(IsA<HTMLBRElement>(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
// <div>foo</div> into hello^ world.
PrepareWhitespaceAtPositionForSplit(insertion_pos);
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// 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]).
auto* end_br = DynamicTo<HTMLBRElement>(
*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(DocumentUpdateReason::kEditing);
// 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<Text>(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 <b> for example:
// <b>foo <b>bar</b> baz</b>.
// 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()->IsListItemIncludingNG() &&
HasEditableStyle(*block_start->parentNode())) {
inserted_nodes.SetRefNode(InsertAsListItems(
To<HTMLElement>(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(DocumentUpdateReason::kEditing);
// 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. <iframe
// src="javascript:...">
if (enclosing_block_of_insertion_pos &&
!enclosing_block_of_insertion_pos->isConnected())
enclosing_block_of_insertion_pos = nullptr;
VisiblePosition start_of_inserted_content = CreateVisiblePosition(
FirstPositionInOrBeforeNode(*inserted_nodes.FirstNodeInserted()));
// We inserted before the enclosingBlockOfInsertionPos to prevent nesting, and
// the content before the enclosingBlockOfInsertionPos wasn't in its own block
// and didn't have a br after it, so the inserted content ended up in the same
// paragraph.
if (!start_of_inserted_content.IsNull() && enclosing_block_of_insertion_pos &&
insertion_pos.AnchorNode() ==
enclosing_block_of_insertion_pos->parentNode() &&
(unsigned)insertion_pos.ComputeEditingOffset() <
enclosing_block_of_insertion_pos->NodeIndex() &&
!IsStartOfParagraph(start_of_inserted_content)) {
InsertNodeAt(MakeGarbageCollected<HTMLBRElement>(GetDocument()),
start_of_inserted_content.DeepEquivalent(), editing_state);
if (editing_state->IsAborted())
return;
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
if (end_br &&
(plain_text_fragment ||
(ShouldRemoveEndBR(end_br, original_vis_pos_before_end_br) &&
!(fragment.HasInterchangeNewlineAtEnd() && selection_is_plain_text)))) {
ContainerNode* parent = end_br->parentNode();
inserted_nodes.WillRemoveNode(*end_br);
ABORT_EDITING_COMMAND_IF(!inserted_nodes.RefNode());
RemoveNode(end_br, editing_state);
if (editing_state->IsAborted())
return;
if (Node* node_to_remove = HighestNodeToRemoveInPruning(parent)) {
inserted_nodes.WillRemoveNode(*node_to_remove);
ABORT_EDITING_COMMAND_IF(!inserted_nodes.RefNode());
RemoveNode(node_to_remove, editing_state);
if (editing_state->IsAborted())
return;
}
}
MakeInsertedContentRoundTrippableWithHTMLTreeBuilder(inserted_nodes,
editing_state);
if (editing_state->IsAborted())
return;
{
// TODO(dominicc): refNode may not be connected, for example in
// web_tests/editing/inserting/insert-table-in-paragraph-crash.html .
// Refactor this so there's a relationship between the conditions
// where refNode is dereferenced and refNode is connected.
bool ref_node_was_connected = inserted_nodes.RefNode()->isConnected();
RemoveRedundantStylesAndKeepStyleSpanInline(inserted_nodes, editing_state);
if (editing_state->IsAborted())
return;
DCHECK_EQ(inserted_nodes.RefNode()->isConnected(), ref_node_was_connected)
<< inserted_nodes.RefNode();
}
if (sanitize_fragment_ && inserted_nodes.FirstNodeInserted()) {
ApplyCommandToComposite(
MakeGarbageCollected<SimplifyMarkupCommand>(
GetDocument(), inserted_nodes.FirstNodeInserted(),
inserted_nodes.PastLastLeaf()),
editing_state);
if (editing_state->IsAborted())
return;
}
// Setup |start_of_inserted_content_| and |end_of_inserted_content_|.
// This should be the last two lines of code that access insertedNodes.
// TODO(editing-dev): The {First,Last}NodeInserted() nullptr checks may be
// unnecessary. Investigate.
start_of_inserted_content_ =
inserted_nodes.FirstNodeInserted()
? FirstPositionInOrBeforeNode(*inserted_nodes.FirstNodeInserted())
: Position();
end_of_inserted_content_ =
inserted_nodes.LastLeafInserted()
? LastPositionInOrAfterNode(*inserted_nodes.LastLeafInserted())
: Position();
// Determine whether or not we should merge the end of inserted content with
// what's after it before we do the start merge so that the start merge
// doesn't effect our decision.
should_merge_end_ = ShouldMergeEnd(selection_end_was_end_of_paragraph);
if (ShouldMergeStart(selection_start_was_start_of_paragraph,
fragment.HasInterchangeNewlineAtStart(),
start_is_inside_mail_blockquote)) {
VisiblePosition start_of_paragraph_to_move =
PositionAtStartOfInsertedContent();
VisiblePosition destination =
PreviousPositionOf(start_of_paragraph_to_move);
// Helpers for making the VisiblePositions valid again after DOM changes.
PositionWithAffinity start_of_paragraph_to_move_position =
start_of_paragraph_to_move.ToPositionWithAffinity();
PositionWithAffinity destination_position =
destination.ToPositionWithAffinity();
// We need to handle the case where we need to merge the end
// but our destination node is inside an inline that is the last in the
// block.
// We insert a placeholder before the newly inserted content to avoid being
// merged into the inline.
Node* destination_node = destination.DeepEquivalent().AnchorNode();
if (should_merge_end_ &&
destination_node != EnclosingInline(destination_node) &&
EnclosingInline(destination_node)->nextSibling()) {
InsertNodeBefore(MakeGarbageCollected<HTMLBRElement>(GetDocument()),
inserted_nodes.RefNode(), editing_state);
if (editing_state->IsAborted())
return;
}
// Merging the the first paragraph of inserted content with the content that
// came before the selection that was pasted into would also move content
// after the selection that was pasted into if: only one paragraph was being
// pasted, and it was not wrapped in a block, the selection that was pasted
// into ended at the end of a block and the next paragraph didn't start at
// the start of a block.
// Insert a line break just after the inserted content to separate it from
// what comes after and prevent that from happening.
VisiblePosition end_of_inserted_content = PositionAtEndOfInsertedContent();
if (StartOfParagraph(end_of_inserted_content).DeepEquivalent() ==
start_of_paragraph_to_move_position.GetPosition()) {
InsertNodeAt(MakeGarbageCollected<HTMLBRElement>(GetDocument()),
end_of_inserted_content.DeepEquivalent(), editing_state);
if (editing_state->IsAborted())
return;
// Mutation events (bug 22634) triggered by inserting the <br> might have
// removed the content we're about to move
if (!start_of_paragraph_to_move_position.IsConnected())
return;
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// Making the two VisiblePositions valid again.
start_of_paragraph_to_move =
CreateVisiblePosition(start_of_paragraph_to_move_position);
destination = CreateVisiblePosition(destination_position);
// FIXME: Maintain positions for the start and end of inserted content
// instead of keeping nodes. The nodes are only ever used to create
// positions where inserted content starts/ends.
MoveParagraph(start_of_paragraph_to_move,
EndOfParagraph(start_of_paragraph_to_move), destination,
editing_state);
if (editing_state->IsAborted())
return;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
const VisibleSelection& visible_selection_of_insterted_content =
EndingVisibleSelection();
start_of_inserted_content_ = MostForwardCaretPosition(
visible_selection_of_insterted_content.VisibleStart().DeepEquivalent());
if (end_of_inserted_content_.IsOrphan()) {
end_of_inserted_content_ = MostBackwardCaretPosition(
visible_selection_of_insterted_content.VisibleEnd().DeepEquivalent());
}
}
Position last_position_to_select;
if (fragment.HasInterchangeNewlineAtEnd()) {
VisiblePosition end_of_inserted_content = PositionAtEndOfInsertedContent();
VisiblePosition next =
NextPositionOf(end_of_inserted_content, kCannotCrossEditingBoundary);
if (selection_end_was_end_of_paragraph ||
!IsEndOfParagraph(end_of_inserted_content) || next.IsNull()) {
if (TextControlElement* text_control =
EnclosingTextControl(current_root)) {
if (!inserted_nodes.LastLeafInserted()->nextSibling()) {
InsertNodeAfter(text_control->CreatePlaceholderBreakElement(),
inserted_nodes.LastLeafInserted(), editing_state);
if (editing_state->IsAborted())
return;
}
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(
Position::AfterNode(*inserted_nodes.LastLeafInserted()))
.Build()));
// Select up to the paragraph separator that was added.
last_position_to_select =
EndingVisibleSelection().VisibleStart().DeepEquivalent();
} else if (!IsStartOfParagraph(end_of_inserted_content)) {
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(end_of_inserted_content.DeepEquivalent())
.Build()));
Element* enclosing_block_element = EnclosingBlock(
end_of_inserted_content.DeepEquivalent().AnchorNode());
if (IsListItem(enclosing_block_element)) {
auto* new_list_item =
MakeGarbageCollected<HTMLLIElement>(GetDocument());
InsertNodeAfter(new_list_item, enclosing_block_element,
editing_state);
if (editing_state->IsAborted())
return;
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(Position::FirstPositionInNode(*new_list_item))
.Build()));
} else {
// Use a default paragraph element (a plain div) for the empty
// paragraph, using the last paragraph block's style seems to annoy
// users.
InsertParagraphSeparator(
editing_state, true,
!start_is_inside_mail_blockquote &&
HighestEnclosingNodeOfType(
end_of_inserted_content.DeepEquivalent(),
IsMailHTMLBlockquoteElement, kCannotCrossEditingBoundary,
inserted_nodes.FirstNodeInserted()->parentNode()));
if (editing_state->IsAborted())
return;
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// Select up to the paragraph separator that was added.
last_position_to_select =
EndingVisibleSelection().VisibleStart().DeepEquivalent();
UpdateNodesInserted(last_position_to_select.AnchorNode());
}
} else {
// Select up to the beginning of the next paragraph.
last_position_to_select = MostForwardCaretPosition(next.DeepEquivalent());
}
} else {
MergeEndIfNeeded(editing_state);
if (editing_state->IsAborted())
return;
}
if (ShouldPerformSmartReplace()) {
AddSpacesForSmartReplace(editing_state);
if (editing_state->IsAborted())
return;
}
// If we are dealing with a fragment created from plain text
// no style matching is necessary.
if (plain_text_fragment)
match_style_ = false;
CompleteHTMLReplacement(last_position_to_select, editing_state);
}
bool ReplaceSelectionCommand::ShouldRemoveEndBR(
HTMLBRElement* end_br,
const VisiblePosition& original_vis_pos_before_end_br) {
if (!end_br || !end_br->isConnected())
return false;
VisiblePosition visible_pos = VisiblePosition::BeforeNode(*end_br);
// Don't remove the br if nothing was inserted.
if (PreviousPositionOf(visible_pos).DeepEquivalent() ==
original_vis_pos_before_end_br.DeepEquivalent())
return false;
// Remove the br if it is collapsed away and so is unnecessary.
if (!GetDocument().InNoQuirksMode() && IsEndOfBlock(visible_pos) &&
!IsStartOfParagraph(visible_pos))
return true;
// A br that was originally holding a line open should be displaced by
// inserted content or turned into a line break.
// A br that was originally acting as a line break should still be acting as a
// line break, not as a placeholder.
return IsStartOfParagraph(visible_pos) && IsEndOfParagraph(visible_pos);
}
bool ReplaceSelectionCommand::ShouldPerformSmartReplace() const {
if (!smart_replace_)
return false;
TextControlElement* text_control =
EnclosingTextControl(PositionAtStartOfInsertedContent().DeepEquivalent());
auto* html_input_element = DynamicTo<HTMLInputElement>(text_control);
if (html_input_element &&
html_input_element->type() == input_type_names::kPassword)
return false; // Disable smart replace for password fields.
return true;
}
static bool IsCharacterSmartReplaceExemptConsideringNonBreakingSpace(
UChar32 character,
bool previous_character) {
return IsCharacterSmartReplaceExempt(
character == kNoBreakSpaceCharacter ? ' ' : character,
previous_character);
}
void ReplaceSelectionCommand::AddSpacesForSmartReplace(
EditingState* editing_state) {
VisiblePosition end_of_inserted_content = PositionAtEndOfInsertedContent();
Position end_upstream =
MostBackwardCaretPosition(end_of_inserted_content.DeepEquivalent());
Node* end_node = end_upstream.ComputeNodeBeforePosition();
auto* end_text_node = DynamicTo<Text>(end_node);
int end_offset = end_text_node ? end_text_node->length() : 0;
if (end_upstream.IsOffsetInAnchor()) {
end_node = end_upstream.ComputeContainerNode();
end_offset = end_upstream.OffsetInContainerNode();
}
bool needs_trailing_space =
!IsEndOfParagraph(end_of_inserted_content) &&
!IsCharacterSmartReplaceExemptConsideringNonBreakingSpace(
CharacterAfter(end_of_inserted_content), false);
if (needs_trailing_space && end_node) {
bool collapse_white_space =
!end_node->GetLayoutObject() ||
end_node->GetLayoutObject()->Style()->CollapseWhiteSpace();
end_text_node = DynamicTo<Text>(end_node);
if (end_text_node) {
InsertTextIntoNode(end_text_node, end_offset,
collapse_white_space ? NonBreakingSpaceString() : " ");
if (end_of_inserted_content_.ComputeContainerNode() == end_node)
end_of_inserted_content_ = Position(
end_node, end_of_inserted_content_.OffsetInContainerNode() + 1);
} else {
Text* node = GetDocument().CreateEditingTextNode(
collapse_white_space ? NonBreakingSpaceString() : " ");
InsertNodeAfter(node, end_node, editing_state);
if (editing_state->IsAborted())
return;
// Make sure that |UpdateNodesInserted| does not change
// |start_of_inserted_content|.
DCHECK(start_of_inserted_content_.IsNotNull());
UpdateNodesInserted(node);
}
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
VisiblePosition start_of_inserted_content =
PositionAtStartOfInsertedContent();
Position start_downstream =
MostForwardCaretPosition(start_of_inserted_content.DeepEquivalent());
Node* start_node = start_downstream.ComputeNodeAfterPosition();
unsigned start_offset = 0;
if (start_downstream.IsOffsetInAnchor()) {
start_node = start_downstream.ComputeContainerNode();
start_offset = start_downstream.OffsetInContainerNode();
}
bool needs_leading_space =
!IsStartOfParagraph(start_of_inserted_content) &&
!IsCharacterSmartReplaceExemptConsideringNonBreakingSpace(
CharacterBefore(start_of_inserted_content), true);
if (needs_leading_space && start_node) {
bool collapse_white_space =
!start_node->GetLayoutObject() ||
start_node->GetLayoutObject()->Style()->CollapseWhiteSpace();
if (auto* start_text_node = DynamicTo<Text>(start_node)) {
InsertTextIntoNode(start_text_node, start_offset,
collapse_white_space ? NonBreakingSpaceString() : " ");
if (end_of_inserted_content_.ComputeContainerNode() == start_node &&
end_of_inserted_content_.OffsetInContainerNode())
end_of_inserted_content_ = Position(
start_node, end_of_inserted_content_.OffsetInContainerNode() + 1);
} else {
Text* node = GetDocument().CreateEditingTextNode(
collapse_white_space ? NonBreakingSpaceString() : " ");
// Don't UpdateNodesInserted. Doing so would set end_of_inserted_content_
// to be the node containing the leading space, but
// end_of_inserted_content_ issupposed to mark the end of pasted content.
InsertNodeBefore(node, start_node, editing_state);
if (editing_state->IsAborted())
return;
start_of_inserted_content_ = Position::FirstPositionInNode(*node);
}
}
}
void ReplaceSelectionCommand::CompleteHTMLReplacement(
const Position& last_position_to_select,
EditingState* editing_state) {
Position start = PositionAtStartOfInsertedContent().DeepEquivalent();
Position end = PositionAtEndOfInsertedContent().DeepEquivalent();
// Mutation events may have deleted start or end
if (start.IsNotNull() && !start.IsOrphan() && end.IsNotNull() &&
!end.IsOrphan()) {
// FIXME (11475): Remove this and require that the creator of the fragment
// to use nbsps.
RebalanceWhitespaceAt(start);
RebalanceWhitespaceAt(end);
if (match_style_) {
DCHECK(insertion_style_);
// Since |ApplyStyle()| changes contents of anchor node of |start| and
// |end|, we should relocate them.
auto* const range =
MakeGarbageCollected<Range>(GetDocument(), start, end);
ApplyStyle(insertion_style_.Get(), start, end, editing_state);
start = range->StartPosition();
end = range->EndPosition();
range->Dispose();
if (editing_state->IsAborted())
return;
}
if (last_position_to_select.IsNotNull())
end = last_position_to_select;
MergeTextNodesAroundPosition(start, end, editing_state);
if (editing_state->IsAborted())
return;
} else if (last_position_to_select.IsNotNull()) {
start = end = last_position_to_select;
} else {
return;
}
start_of_inserted_range_ = start;
end_of_inserted_range_ = end;
if (select_replacement_) {
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.SetBaseAndExtentDeprecated(start, end)
.Build()));
return;
}
if (end.IsNotNull()) {
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(end)
.Build()));
return;
}
SetEndingSelection(SelectionForUndoStep());
}
void ReplaceSelectionCommand::MergeTextNodesAroundPosition(
Position& position,
Position& position_only_to_be_updated,
EditingState* editing_state) {
bool position_is_offset_in_anchor = position.IsOffsetInAnchor();
bool position_only_to_be_updated_is_offset_in_anchor =
position_only_to_be_updated.IsOffsetInAnchor();
Text* text = nullptr;
auto* container_text_node = DynamicTo<Text>(position.ComputeContainerNode());
if (position_is_offset_in_anchor && container_text_node) {
text = container_text_node;
} else if (auto* before =
DynamicTo<Text>(position.ComputeNodeBeforePosition())) {
text = before;
} else if (auto* after =
DynamicTo<Text>(position.ComputeNodeAfterPosition())) {
text = after;
}
if (!text)
return;
// Merging Text nodes causes an additional layout. We'd like to skip it if the
// editable text is huge.
// TODO(tkent): 1024 was chosen by my intuition. We need data.
const unsigned kMergeSizeLimit = 1024;
bool has_incomplete_surrogate =
text->data().length() >= 1 &&
(U16_IS_TRAIL(text->data()[0]) ||
U16_IS_LEAD(text->data()[text->data().length() - 1]));
if (!has_incomplete_surrogate && text->data().length() > kMergeSizeLimit)
return;
if (auto* previous = DynamicTo<Text>(text->previousSibling())) {
if (has_incomplete_surrogate ||
previous->data().length() <= kMergeSizeLimit) {
InsertTextIntoNode(text, 0, previous->data());
if (position_is_offset_in_anchor) {
position =
Position(position.ComputeContainerNode(),
previous->length() + position.OffsetInContainerNode());
} else {
position = ComputePositionForNodeRemoval(position, *previous);
}
if (position_only_to_be_updated_is_offset_in_anchor) {
if (position_only_to_be_updated.ComputeContainerNode() == text)
position_only_to_be_updated = Position(
text, previous->length() +
position_only_to_be_updated.OffsetInContainerNode());
else if (position_only_to_be_updated.ComputeContainerNode() == previous)
position_only_to_be_updated = Position(
text, position_only_to_be_updated.OffsetInContainerNode());
} else {
position_only_to_be_updated = ComputePositionForNodeRemoval(
position_only_to_be_updated, *previous);
}
RemoveNode(previous, editing_state);
if (editing_state->IsAborted())
return;
}
}
if (auto* next = DynamicTo<Text>(text->nextSibling())) {
if (!has_incomplete_surrogate && next->data().length() > kMergeSizeLimit)
return;
unsigned original_length = text->length();
InsertTextIntoNode(text, original_length, next->data());
if (!position_is_offset_in_anchor)
position = ComputePositionForNodeRemoval(position, *next);
if (position_only_to_be_updated_is_offset_in_anchor &&
position_only_to_be_updated.ComputeContainerNode() == next) {
position_only_to_be_updated = Position(
text, original_length +
position_only_to_be_updated.OffsetInContainerNode());
} else {
position_only_to_be_updated =
ComputePositionForNodeRemoval(position_only_to_be_updated, *next);
}
RemoveNode(next, editing_state);
if (editing_state->IsAborted())
return;
}
}
InputEvent::InputType ReplaceSelectionCommand::GetInputType() const {
// |ReplaceSelectionCommand| could be used with Paste, Drag&Drop,
// InsertFragment and |TypingCommand|.
// 1. Paste, Drag&Drop, InsertFragment should rely on correct |input_type_|.
// 2. |TypingCommand| will supply the |GetInputType()|, so |input_type_| could
// default to |InputType::kNone|.
return input_type_;
}
// If the user is inserting a list into an existing list, instead of nesting the
// list, we put the list items into the existing list.
Node* ReplaceSelectionCommand::InsertAsListItems(HTMLElement* list_element,
Element* insertion_block,
const Position& insert_pos,
InsertedNodes& inserted_nodes,
EditingState* editing_state) {
while (list_element->HasOneChild() &&
IsHTMLListElement(list_element->firstChild()))
list_element = To<HTMLElement>(list_element->firstChild());
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
bool is_start = IsStartOfParagraph(CreateVisiblePosition(insert_pos));
bool is_end = IsEndOfParagraph(CreateVisiblePosition(insert_pos));
bool is_middle = !is_start && !is_end;
Node* last_node = insertion_block;
// If we're in the middle of a list item, we should split it into two separate
// list items and insert these nodes between them.
if (is_middle) {
int text_node_offset = insert_pos.OffsetInContainerNode();
auto* text_node = DynamicTo<Text>(insert_pos.AnchorNode());
if (text_node && text_node_offset > 0)
SplitTextNode(text_node, text_node_offset);
SplitTreeToNode(insert_pos.AnchorNode(), last_node, true);
}
while (Node* list_item = list_element->firstChild()) {
list_element->RemoveChild(list_item, ASSERT_NO_EXCEPTION);
if (is_start || is_middle) {
InsertNodeBefore(list_item, last_node, editing_state);
if (editing_state->IsAborted())
return nullptr;
inserted_nodes.RespondToNodeInsertion(*list_item);
} else if (is_end) {
InsertNodeAfter(list_item, last_node, editing_state);
if (editing_state->IsAborted())
return nullptr;
inserted_nodes.RespondToNodeInsertion(*list_item);
last_node = list_item;
} else {
NOTREACHED();
}
}
if (is_start || is_middle) {
if (Node* node = last_node->previousSibling())
return node;
}
return last_node;
}
void ReplaceSelectionCommand::UpdateNodesInserted(Node* node) {
if (!node)
return;
if (start_of_inserted_content_.IsNull())
start_of_inserted_content_ = FirstPositionInOrBeforeNode(*node);
end_of_inserted_content_ =
LastPositionInOrAfterNode(NodeTraversal::LastWithinOrSelf(*node));
}
// During simple pastes, where we're just pasting a text node into a run of
// text, we insert the text node directly into the text node that holds the
// selection. This is much faster than the generalized code in
// ReplaceSelectionCommand, and works around
// <https://bugs.webkit.org/show_bug.cgi?id=6148> since we don't split text
// nodes.
bool ReplaceSelectionCommand::PerformTrivialReplace(
const ReplacementFragment& fragment,
EditingState* editing_state) {
if (!fragment.FirstChild() || fragment.FirstChild() != fragment.LastChild() ||
!fragment.FirstChild()->IsTextNode())
return false;
// FIXME: Would be nice to handle smart replace in the fast path.
if (smart_replace_ || fragment.HasInterchangeNewlineAtStart() ||
fragment.HasInterchangeNewlineAtEnd())
return false;
// e.g. when "bar" is inserted after "foo" in <div><u>foo</u></div>, "bar"
// should not be underlined.
if (ElementToSplitToAvoidPastingIntoInlineElementsWithStyle(
EndingVisibleSelection().Start()))
return false;
// TODO(editing-dev): Use of UpdateStyleAndLayout
// needs to be audited. See http://crbug.com/590369 for more details.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
Node* node_after_insertion_pos =
MostForwardCaretPosition(EndingSelection().End()).AnchorNode();
auto* text_node = To<Text>(fragment.FirstChild());
// Our fragment creation code handles tabs, spaces, and newlines, so we don't
// have to worry about those here.
Position start = EndingVisibleSelection().Start();
Position end = ReplaceSelectedTextInNode(text_node->data());
if (end.IsNull())
return false;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
if (node_after_insertion_pos && node_after_insertion_pos->parentNode() &&
IsA<HTMLBRElement>(*node_after_insertion_pos) &&
ShouldRemoveEndBR(
To<HTMLBRElement>(node_after_insertion_pos),
VisiblePosition::BeforeNode(*node_after_insertion_pos))) {
RemoveNodeAndPruneAncestors(node_after_insertion_pos, editing_state);
if (editing_state->IsAborted())
return false;
}
start_of_inserted_range_ = start;
end_of_inserted_range_ = end;
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.SetBaseAndExtentDeprecated(select_replacement_ ? start : end, end)
.Build()));
return true;
}
bool ReplaceSelectionCommand::IsReplaceSelectionCommand() const {
return true;
}
EphemeralRange ReplaceSelectionCommand::InsertedRange() const {
return EphemeralRange(start_of_inserted_range_, end_of_inserted_range_);
}
void ReplaceSelectionCommand::Trace(Visitor* visitor) const {
visitor->Trace(start_of_inserted_content_);
visitor->Trace(end_of_inserted_content_);
visitor->Trace(insertion_style_);
visitor->Trace(document_fragment_);
visitor->Trace(start_of_inserted_range_);
visitor->Trace(end_of_inserted_range_);
CompositeEditCommand::Trace(visitor);
}
} // namespace blink