blob: a47421f734ebbc16f9b10705a183d36b683a11fe [file] [log] [blame]
/*
* Copyright (C) 2005 Apple Computer, 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/break_blockquote_command.h"
#include "third_party/blink/renderer/core/dom/node_traversal.h"
#include "third_party/blink/renderer/core/dom/text.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/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/visible_position.h"
#include "third_party/blink/renderer/core/editing/visible_selection.h"
#include "third_party/blink/renderer/core/editing/visible_units.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_quote_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/layout_list_item.h"
#include "third_party/blink/renderer/core/layout/ng/list/layout_ng_list_item.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
namespace blink {
namespace {
base::Optional<int> GetListItemNumber(const Node* node) {
if (!node)
return base::nullopt;
// Because of elements with "display:list-item" has list item number,
// we use layout object instead of checking |HTMLLIElement|.
const LayoutObject* const layout_object = node->GetLayoutObject();
if (!layout_object)
return base::nullopt;
if (layout_object->IsLayoutNGListItem())
return To<LayoutNGListItem>(layout_object)->Value();
if (layout_object->IsListItem())
return To<LayoutListItem>(layout_object)->Value();
return base::nullopt;
}
bool IsFirstVisiblePositionInNode(const VisiblePosition& visible_position,
const ContainerNode* node) {
if (visible_position.IsNull())
return false;
if (!visible_position.DeepEquivalent().ComputeContainerNode()->IsDescendantOf(
node))
return false;
VisiblePosition previous = PreviousPositionOf(visible_position);
return previous.IsNull() ||
!previous.DeepEquivalent().AnchorNode()->IsDescendantOf(node);
}
bool IsLastVisiblePositionInNode(const VisiblePosition& visible_position,
const ContainerNode* node) {
if (visible_position.IsNull())
return false;
if (!visible_position.DeepEquivalent().ComputeContainerNode()->IsDescendantOf(
node))
return false;
VisiblePosition next = NextPositionOf(visible_position);
return next.IsNull() ||
!next.DeepEquivalent().AnchorNode()->IsDescendantOf(node);
}
} // namespace
BreakBlockquoteCommand::BreakBlockquoteCommand(Document& document)
: CompositeEditCommand(document) {}
static HTMLQuoteElement* TopBlockquoteOf(const Position& start) {
// This is a position equivalent to the caret. We use |downstream()| so that
// |position| will be in the first node that we need to move (there are a few
// exceptions to this, see |doApply|).
const Position& position = MostForwardCaretPosition(start);
return To<HTMLQuoteElement>(
HighestEnclosingNodeOfType(position, IsMailHTMLBlockquoteElement));
}
void BreakBlockquoteCommand::DoApply(EditingState* editing_state) {
if (EndingSelection().IsNone())
return;
if (!TopBlockquoteOf(EndingVisibleSelection().Start()))
return;
// Delete the current selection.
if (EndingSelection().IsRange()) {
if (!DeleteSelection(editing_state, DeleteSelectionOptions::Builder()
.SetExpandForSpecialElements(true)
.SetSanitizeMarkup(true)
.Build()))
return;
}
// This is a scenario that should never happen, but we want to
// make sure we don't dereference a null pointer below.
DCHECK(!EndingSelection().IsNone());
if (EndingSelection().IsNone())
return;
const VisibleSelection& visible_selection = EndingVisibleSelection();
VisiblePosition visible_pos = visible_selection.VisibleStart();
// pos is a position equivalent to the caret. We use downstream() so that pos
// will be in the first node that we need to move (there are a few exceptions
// to this, see below).
Position pos = MostForwardCaretPosition(visible_selection.Start());
// Find the top-most blockquote from the start.
HTMLQuoteElement* const top_blockquote =
TopBlockquoteOf(visible_selection.Start());
if (!top_blockquote || !top_blockquote->parentNode())
return;
auto* break_element = MakeGarbageCollected<HTMLBRElement>(GetDocument());
bool is_last_vis_pos_in_node =
IsLastVisiblePositionInNode(visible_pos, top_blockquote);
// If the position is at the beginning of the top quoted content, we don't
// need to break the quote. Instead, insert the break before the blockquote,
// unless the position is as the end of the the quoted content.
if (IsFirstVisiblePositionInNode(visible_pos, top_blockquote) &&
!is_last_vis_pos_in_node) {
InsertNodeBefore(break_element, top_blockquote, editing_state);
if (editing_state->IsAborted())
return;
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(Position::BeforeNode(*break_element))
.Build()));
RebalanceWhitespace();
return;
}
// Insert a break after the top blockquote.
InsertNodeAfter(break_element, top_blockquote, editing_state);
if (editing_state->IsAborted())
return;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// If we're inserting the break at the end of the quoted content, we don't
// need to break the quote.
if (is_last_vis_pos_in_node) {
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(Position::BeforeNode(*break_element))
.Build()));
RebalanceWhitespace();
return;
}
// Don't move a line break just after the caret. Doing so would create an
// extra, empty paragraph in the new blockquote.
if (LineBreakExistsAtVisiblePosition(visible_pos)) {
pos = NextPositionOf(pos, PositionMoveType::kGraphemeCluster);
}
// Adjust the position so we don't split at the beginning of a quote.
while (IsFirstVisiblePositionInNode(CreateVisiblePosition(pos),
To<HTMLQuoteElement>(EnclosingNodeOfType(
pos, IsMailHTMLBlockquoteElement)))) {
pos = PreviousPositionOf(pos, PositionMoveType::kGraphemeCluster);
}
// startNode is the first node that we need to move to the new blockquote.
Node* start_node = pos.AnchorNode();
DCHECK(start_node);
// Split at pos if in the middle of a text node.
if (auto* text_node = DynamicTo<Text>(start_node)) {
int text_offset = pos.ComputeOffsetInContainerNode();
if ((unsigned)text_offset >= text_node->length()) {
start_node = NodeTraversal::Next(*start_node);
DCHECK(start_node);
} else if (text_offset > 0) {
SplitTextNode(text_node, text_offset);
}
} else if (pos.ComputeEditingOffset() > 0) {
Node* child_at_offset =
NodeTraversal::ChildAt(*start_node, pos.ComputeEditingOffset());
start_node =
child_at_offset ? child_at_offset : NodeTraversal::Next(*start_node);
DCHECK(start_node);
}
// If there's nothing inside topBlockquote to move, we're finished.
if (!start_node->IsDescendantOf(top_blockquote)) {
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(FirstPositionInOrBeforeNode(*start_node))
.Build()));
return;
}
// Build up list of ancestors in between the start node and the top
// blockquote.
HeapVector<Member<Element>> ancestors;
for (Element* node = start_node->parentElement();
node && node != top_blockquote; node = node->parentElement())
ancestors.push_back(node);
// Insert a clone of the top blockquote after the break.
Element& cloned_blockquote = top_blockquote->CloneWithoutChildren();
InsertNodeAfter(&cloned_blockquote, break_element, editing_state);
if (editing_state->IsAborted())
return;
// Clone startNode's ancestors into the cloned blockquote.
// On exiting this loop, clonedAncestor is the lowest ancestor
// that was cloned (i.e. the clone of either ancestors.last()
// or clonedBlockquote if ancestors is empty).
Element* cloned_ancestor = &cloned_blockquote;
for (wtf_size_t i = ancestors.size(); i != 0; --i) {
Element& cloned_child = ancestors[i - 1]->CloneWithoutChildren();
// Preserve list item numbering in cloned lists.
if (IsA<HTMLOListElement>(cloned_child)) {
Node* list_child_node = i > 1 ? ancestors[i - 2].Get() : start_node;
// The first child of the cloned list might not be a list item element,
// find the first one so that we know where to start numbering.
while (list_child_node && !IsA<HTMLLIElement>(*list_child_node))
list_child_node = list_child_node->nextSibling();
if (auto list_item_number = GetListItemNumber(list_child_node)) {
SetNodeAttribute(&cloned_child, html_names::kStartAttr,
AtomicString::Number(*list_item_number));
}
}
AppendNode(&cloned_child, cloned_ancestor, editing_state);
if (editing_state->IsAborted())
return;
cloned_ancestor = &cloned_child;
}
MoveRemainingSiblingsToNewParent(start_node, nullptr, cloned_ancestor,
editing_state);
if (editing_state->IsAborted())
return;
if (!ancestors.IsEmpty()) {
// Split the tree up the ancestor chain until the topBlockquote
// Throughout this loop, clonedParent is the clone of ancestor's parent.
// This is so we can clone ancestor's siblings and place the clones
// into the clone corresponding to the ancestor's parent.
Element* ancestor = nullptr;
Element* cloned_parent = nullptr;
for (ancestor = ancestors.front(),
cloned_parent = cloned_ancestor->parentElement();
ancestor && ancestor != top_blockquote;
ancestor = ancestor->parentElement(),
cloned_parent = cloned_parent->parentElement()) {
MoveRemainingSiblingsToNewParent(ancestor->nextSibling(), nullptr,
cloned_parent, editing_state);
if (editing_state->IsAborted())
return;
}
// If the startNode's original parent is now empty, remove it
Element* original_parent = ancestors.front().Get();
if (!original_parent->HasChildren()) {
RemoveNode(original_parent, editing_state);
if (editing_state->IsAborted())
return;
}
}
// Make sure the cloned block quote renders.
AddBlockPlaceholderIfNeeded(&cloned_blockquote, editing_state);
if (editing_state->IsAborted())
return;
// Put the selection right before the break.
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(Position::BeforeNode(*break_element))
.Build()));
RebalanceWhitespace();
}
} // namespace blink