| /* |
| * 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 |