| /* |
| * Copyright (C) 2004, 2005, 2006, 2007 Apple 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. |
| */ |
| |
| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "core/editing/commands/EditingCommandsUtilities.h" |
| |
| #include "core/editing/EditingUtilities.h" |
| #include "core/editing/SelectionTemplate.h" |
| #include "core/editing/VisiblePosition.h" |
| #include "core/editing/VisibleSelection.h" |
| #include "core/frame/UseCounter.h" |
| #include "core/frame/WebFeatureForward.h" |
| #include "core/html/HTMLBodyElement.h" |
| #include "core/html/HTMLHtmlElement.h" |
| #include "core/inspector/ConsoleMessage.h" |
| #include "core/layout/LayoutObject.h" |
| |
| namespace blink { |
| |
| static bool HasARenderedDescendant(const Node* node, |
| const Node* excluded_node) { |
| for (const Node* n = node->firstChild(); n;) { |
| if (n == excluded_node) { |
| n = NodeTraversal::NextSkippingChildren(*n, node); |
| continue; |
| } |
| if (n->GetLayoutObject()) |
| return true; |
| n = NodeTraversal::Next(*n, node); |
| } |
| return false; |
| } |
| |
| Node* HighestNodeToRemoveInPruning(Node* node, const Node* exclude_node) { |
| Node* previous_node = nullptr; |
| Element* element = node ? RootEditableElement(*node) : nullptr; |
| for (; node; node = node->parentNode()) { |
| if (LayoutObject* layout_object = node->GetLayoutObject()) { |
| if (!layout_object->CanHaveChildren() || |
| HasARenderedDescendant(node, previous_node) || element == node || |
| exclude_node == node) |
| return previous_node; |
| } |
| previous_node = node; |
| } |
| return nullptr; |
| } |
| |
| Element* EnclosingTableCell(const Position& p) { |
| return ToElement(EnclosingNodeOfType(p, IsTableCell)); |
| } |
| |
| bool IsTableStructureNode(const Node* node) { |
| LayoutObject* layout_object = node->GetLayoutObject(); |
| return (layout_object && |
| (layout_object->IsTableCell() || layout_object->IsTableRow() || |
| layout_object->IsTableSection() || |
| layout_object->IsLayoutTableCol())); |
| } |
| |
| bool IsNodeRendered(const Node& node) { |
| LayoutObject* layout_object = node.GetLayoutObject(); |
| if (!layout_object) |
| return false; |
| |
| return layout_object->Style()->Visibility() == EVisibility::kVisible; |
| } |
| |
| // FIXME: This method should not need to call |
| // isStartOfParagraph/isEndOfParagraph |
| Node* EnclosingEmptyListItem(const VisiblePosition& visible_pos) { |
| DCHECK(visible_pos.IsValid()); |
| |
| // Check that position is on a line by itself inside a list item |
| Node* list_child_node = |
| EnclosingListChild(visible_pos.DeepEquivalent().AnchorNode()); |
| if (!list_child_node || !IsStartOfParagraph(visible_pos) || |
| !IsEndOfParagraph(visible_pos)) |
| return nullptr; |
| |
| VisiblePosition first_in_list_child = |
| CreateVisiblePosition(FirstPositionInOrBeforeNode(*list_child_node)); |
| VisiblePosition last_in_list_child = |
| CreateVisiblePosition(LastPositionInOrAfterNode(*list_child_node)); |
| |
| if (first_in_list_child.DeepEquivalent() != visible_pos.DeepEquivalent() || |
| last_in_list_child.DeepEquivalent() != visible_pos.DeepEquivalent()) |
| return nullptr; |
| |
| return list_child_node; |
| } |
| |
| bool AreIdenticalElements(const Node& first, const Node& second) { |
| if (!first.IsElementNode() || !second.IsElementNode()) |
| return false; |
| |
| const Element& first_element = ToElement(first); |
| const Element& second_element = ToElement(second); |
| if (!first_element.HasTagName(second_element.TagQName())) |
| return false; |
| |
| if (!first_element.HasEquivalentAttributes(&second_element)) |
| return false; |
| |
| return HasEditableStyle(first_element) && HasEditableStyle(second_element); |
| } |
| |
| // FIXME: need to dump this |
| static bool IsSpecialHTMLElement(const Node& n) { |
| if (!n.IsHTMLElement()) |
| return false; |
| |
| if (n.IsLink()) |
| return true; |
| |
| LayoutObject* layout_object = n.GetLayoutObject(); |
| if (!layout_object) |
| return false; |
| |
| if (layout_object->Style()->Display() == EDisplay::kTable || |
| layout_object->Style()->Display() == EDisplay::kInlineTable) |
| return true; |
| |
| if (layout_object->Style()->IsFloating()) |
| return true; |
| |
| return false; |
| } |
| |
| static HTMLElement* FirstInSpecialElement(const Position& pos) { |
| DCHECK(!NeedsLayoutTreeUpdate(pos)); |
| Element* element = RootEditableElement(*pos.ComputeContainerNode()); |
| for (Node& runner : NodeTraversal::InclusiveAncestorsOf(*pos.AnchorNode())) { |
| if (RootEditableElement(runner) != element) |
| break; |
| if (IsSpecialHTMLElement(runner)) { |
| HTMLElement* special_element = ToHTMLElement(&runner); |
| VisiblePosition v_pos = CreateVisiblePosition(pos); |
| VisiblePosition first_in_element = |
| CreateVisiblePosition(FirstPositionInOrBeforeNode(*special_element)); |
| if (IsDisplayInsideTable(special_element) && |
| !IsListItem(v_pos.DeepEquivalent().ComputeContainerNode()) && |
| v_pos.DeepEquivalent() == |
| NextPositionOf(first_in_element).DeepEquivalent()) |
| return special_element; |
| if (v_pos.DeepEquivalent() == first_in_element.DeepEquivalent()) |
| return special_element; |
| } |
| } |
| return nullptr; |
| } |
| |
| static HTMLElement* LastInSpecialElement(const Position& pos) { |
| DCHECK(!NeedsLayoutTreeUpdate(pos)); |
| Element* element = RootEditableElement(*pos.ComputeContainerNode()); |
| for (Node& runner : NodeTraversal::InclusiveAncestorsOf(*pos.AnchorNode())) { |
| if (RootEditableElement(runner) != element) |
| break; |
| if (IsSpecialHTMLElement(runner)) { |
| HTMLElement* special_element = ToHTMLElement(&runner); |
| VisiblePosition v_pos = CreateVisiblePosition(pos); |
| VisiblePosition last_in_element = |
| CreateVisiblePosition(LastPositionInOrAfterNode(*special_element)); |
| if (IsDisplayInsideTable(special_element) && |
| v_pos.DeepEquivalent() == |
| PreviousPositionOf(last_in_element).DeepEquivalent()) |
| return special_element; |
| if (v_pos.DeepEquivalent() == last_in_element.DeepEquivalent()) |
| return special_element; |
| } |
| } |
| return nullptr; |
| } |
| |
| Position PositionBeforeContainingSpecialElement( |
| const Position& pos, |
| HTMLElement** containing_special_element) { |
| DCHECK(!NeedsLayoutTreeUpdate(pos)); |
| HTMLElement* n = FirstInSpecialElement(pos); |
| if (!n) |
| return pos; |
| Position result = Position::InParentBeforeNode(*n); |
| if (result.IsNull() || RootEditableElement(*result.AnchorNode()) != |
| RootEditableElement(*pos.AnchorNode())) |
| return pos; |
| if (containing_special_element) |
| *containing_special_element = n; |
| return result; |
| } |
| |
| Position PositionAfterContainingSpecialElement( |
| const Position& pos, |
| HTMLElement** containing_special_element) { |
| DCHECK(!NeedsLayoutTreeUpdate(pos)); |
| HTMLElement* n = LastInSpecialElement(pos); |
| if (!n) |
| return pos; |
| Position result = Position::InParentAfterNode(*n); |
| if (result.IsNull() || RootEditableElement(*result.AnchorNode()) != |
| RootEditableElement(*pos.AnchorNode())) |
| return pos; |
| if (containing_special_element) |
| *containing_special_element = n; |
| return result; |
| } |
| |
| bool LineBreakExistsAtPosition(const Position& position) { |
| if (position.IsNull()) |
| return false; |
| |
| if (IsHTMLBRElement(*position.AnchorNode()) && |
| position.AtFirstEditingPositionForNode()) |
| return true; |
| |
| if (!position.AnchorNode()->GetLayoutObject()) |
| return false; |
| |
| if (!position.AnchorNode()->IsTextNode() || |
| !position.AnchorNode()->GetLayoutObject()->Style()->PreserveNewline()) |
| return false; |
| |
| const Text* text_node = ToText(position.AnchorNode()); |
| unsigned offset = position.OffsetInContainerNode(); |
| return offset < text_node->length() && text_node->data()[offset] == '\n'; |
| } |
| |
| // return first preceding DOM position rendered at a different location, or |
| // "this" |
| static Position PreviousCharacterPosition(const Position& position, |
| TextAffinity affinity) { |
| DCHECK(!NeedsLayoutTreeUpdate(position)); |
| if (position.IsNull()) |
| return Position(); |
| |
| Element* from_root_editable_element = |
| RootEditableElement(*position.AnchorNode()); |
| |
| bool at_start_of_line = |
| IsStartOfLine(CreateVisiblePosition(position, affinity)); |
| bool rendered = IsVisuallyEquivalentCandidate(position); |
| |
| Position current_pos = position; |
| while (!current_pos.AtStartOfTree()) { |
| // TODO(yosin) When we use |previousCharacterPosition()| other than |
| // finding leading whitespace, we should use |Character| instead of |
| // |CodePoint|. |
| current_pos = PreviousPositionOf(current_pos, PositionMoveType::kCodeUnit); |
| |
| if (RootEditableElement(*current_pos.AnchorNode()) != |
| from_root_editable_element) |
| return position; |
| |
| if (at_start_of_line || !rendered) { |
| if (IsVisuallyEquivalentCandidate(current_pos)) |
| return current_pos; |
| } else if (RendersInDifferentPosition(position, current_pos)) { |
| return current_pos; |
| } |
| } |
| |
| return position; |
| } |
| |
| // This assumes that it starts in editable content. |
| Position LeadingCollapsibleWhitespacePosition(const Position& position, |
| TextAffinity affinity, |
| WhitespacePositionOption option) { |
| DCHECK(!NeedsLayoutTreeUpdate(position)); |
| DCHECK(IsEditablePosition(position)) << position; |
| if (position.IsNull()) |
| return Position(); |
| |
| if (IsHTMLBRElement(*MostBackwardCaretPosition(position).AnchorNode())) |
| return Position(); |
| |
| const Position& prev = PreviousCharacterPosition(position, affinity); |
| if (prev == position) |
| return Position(); |
| const Node* const anchor_node = prev.AnchorNode(); |
| if (!anchor_node || !anchor_node->IsTextNode()) |
| return Position(); |
| if (EnclosingBlockFlowElement(*anchor_node) != |
| EnclosingBlockFlowElement(*position.AnchorNode())) |
| return Position(); |
| if (option == kNotConsiderNonCollapsibleWhitespace && |
| anchor_node->GetLayoutObject() && |
| !anchor_node->GetLayoutObject()->Style()->CollapseWhiteSpace()) |
| return Position(); |
| const String& string = ToText(anchor_node)->data(); |
| const UChar previous_character = string[prev.ComputeOffsetInContainerNode()]; |
| const bool is_space = option == kConsiderNonCollapsibleWhitespace |
| ? (IsSpaceOrNewline(previous_character) || |
| previous_character == kNoBreakSpaceCharacter) |
| : IsCollapsibleWhitespace(previous_character); |
| if (!is_space || !IsEditablePosition(prev)) |
| return Position(); |
| return prev; |
| } |
| |
| unsigned NumEnclosingMailBlockquotes(const Position& p) { |
| unsigned num = 0; |
| for (const Node* n = p.AnchorNode(); n; n = n->parentNode()) { |
| if (IsMailHTMLBlockquoteElement(n)) |
| num++; |
| } |
| return num; |
| } |
| |
| bool LineBreakExistsAtVisiblePosition(const VisiblePosition& visible_position) { |
| return LineBreakExistsAtPosition( |
| MostForwardCaretPosition(visible_position.DeepEquivalent())); |
| } |
| |
| HTMLElement* CreateHTMLElement(Document& document, const QualifiedName& name) { |
| DCHECK_EQ(name.NamespaceURI(), HTMLNames::xhtmlNamespaceURI) |
| << "Unexpected namespace: " << name; |
| return ToHTMLElement(document.CreateElement( |
| name, CreateElementFlags::ByCloneNode(), g_null_atom)); |
| } |
| |
| HTMLElement* EnclosingList(const Node* node) { |
| if (!node) |
| return nullptr; |
| |
| ContainerNode* root = HighestEditableRoot(FirstPositionInOrBeforeNode(*node)); |
| |
| for (Node& runner : NodeTraversal::AncestorsOf(*node)) { |
| if (IsHTMLUListElement(runner) || IsHTMLOListElement(runner)) |
| return ToHTMLElement(&runner); |
| if (runner == root) |
| return nullptr; |
| } |
| |
| return nullptr; |
| } |
| |
| Node* EnclosingListChild(const Node* node) { |
| if (!node) |
| return nullptr; |
| // Check for a list item element, or for a node whose parent is a list |
| // element. Such a node will appear visually as a list item (but without a |
| // list marker) |
| ContainerNode* root = HighestEditableRoot(FirstPositionInOrBeforeNode(*node)); |
| |
| // FIXME: This function is inappropriately named if it starts with node |
| // instead of node->parentNode() |
| for (Node* n = const_cast<Node*>(node); n && n->parentNode(); |
| n = n->parentNode()) { |
| if (IsHTMLLIElement(*n) || |
| (IsHTMLListElement(n->parentNode()) && n != root)) |
| return n; |
| if (n == root || IsTableCell(n)) |
| return nullptr; |
| } |
| |
| return nullptr; |
| } |
| |
| HTMLElement* OutermostEnclosingList(const Node* node, |
| const HTMLElement* root_list) { |
| HTMLElement* list = EnclosingList(node); |
| if (!list) |
| return nullptr; |
| |
| while (HTMLElement* next_list = EnclosingList(list)) { |
| if (next_list == root_list) |
| break; |
| list = next_list; |
| } |
| |
| return list; |
| } |
| |
| // Determines whether two positions are visibly next to each other (first then |
| // second) while ignoring whitespaces and unrendered nodes |
| static bool IsVisiblyAdjacent(const Position& first, const Position& second) { |
| return CreateVisiblePosition(first).DeepEquivalent() == |
| CreateVisiblePosition(MostBackwardCaretPosition(second)) |
| .DeepEquivalent(); |
| } |
| |
| bool CanMergeLists(const Element& first_list, const Element& second_list) { |
| if (!first_list.IsHTMLElement() || !second_list.IsHTMLElement()) |
| return false; |
| |
| DCHECK(!NeedsLayoutTreeUpdate(first_list)); |
| DCHECK(!NeedsLayoutTreeUpdate(second_list)); |
| return first_list.HasTagName( |
| second_list |
| .TagQName()) // make sure the list types match (ol vs. ul) |
| && HasEditableStyle(first_list) && |
| HasEditableStyle(second_list) // both lists are editable |
| && |
| RootEditableElement(first_list) == |
| RootEditableElement(second_list) // don't cross editing boundaries |
| && IsVisiblyAdjacent(Position::InParentAfterNode(first_list), |
| Position::InParentBeforeNode(second_list)); |
| // Make sure there is no visible content between this li and the previous list |
| } |
| |
| // Modifies selections that have an end point at the edge of a table |
| // that contains the other endpoint so that they don't confuse |
| // code that iterates over selected paragraphs. |
| VisibleSelection SelectionForParagraphIteration( |
| const VisibleSelection& original) { |
| VisibleSelection new_selection(original); |
| VisiblePosition start_of_selection(new_selection.VisibleStart()); |
| VisiblePosition end_of_selection(new_selection.VisibleEnd()); |
| |
| // If the end of the selection to modify is just after a table, and if the |
| // start of the selection is inside that table, then the last paragraph that |
| // we'll want modify is the last one inside the table, not the table itself (a |
| // table is itself a paragraph). |
| if (Element* table = TableElementJustBefore(end_of_selection)) { |
| if (start_of_selection.DeepEquivalent().AnchorNode()->IsDescendantOf( |
| table)) { |
| const VisiblePosition& new_end = |
| PreviousPositionOf(end_of_selection, kCannotCrossEditingBoundary); |
| if (new_end.IsNotNull()) { |
| new_selection = CreateVisibleSelection( |
| SelectionInDOMTree::Builder() |
| .Collapse(start_of_selection.ToPositionWithAffinity()) |
| .Extend(new_end.DeepEquivalent()) |
| .Build()); |
| } else { |
| new_selection = CreateVisibleSelection( |
| SelectionInDOMTree::Builder() |
| .Collapse(start_of_selection.ToPositionWithAffinity()) |
| .Build()); |
| } |
| } |
| } |
| |
| // If the start of the selection to modify is just before a table, and if the |
| // end of the selection is inside that table, then the first paragraph we'll |
| // want to modify is the first one inside the table, not the paragraph |
| // containing the table itself. |
| if (Element* table = TableElementJustAfter(start_of_selection)) { |
| if (end_of_selection.DeepEquivalent().AnchorNode()->IsDescendantOf(table)) { |
| const VisiblePosition new_start = |
| NextPositionOf(start_of_selection, kCannotCrossEditingBoundary); |
| if (new_start.IsNotNull()) { |
| new_selection = CreateVisibleSelection( |
| SelectionInDOMTree::Builder() |
| .Collapse(new_start.ToPositionWithAffinity()) |
| .Extend(end_of_selection.DeepEquivalent()) |
| .Build()); |
| } else { |
| new_selection = CreateVisibleSelection( |
| SelectionInDOMTree::Builder() |
| .Collapse(end_of_selection.ToPositionWithAffinity()) |
| .Build()); |
| } |
| } |
| } |
| |
| return new_selection; |
| } |
| |
| const String& NonBreakingSpaceString() { |
| DEFINE_STATIC_LOCAL(String, non_breaking_space_string, |
| (&kNoBreakSpaceCharacter, 1)); |
| return non_breaking_space_string; |
| } |
| |
| // TODO(tkent): This is a workaround of some crash bugs in the editing code, |
| // which assumes a document has a valid HTML structure. We should make the |
| // editing code more robust, and should remove this hack. crbug.com/580941. |
| void TidyUpHTMLStructure(Document& document) { |
| // hasEditableStyle() needs up-to-date ComputedStyle. |
| document.UpdateStyleAndLayoutTree(); |
| const bool needs_valid_structure = |
| HasEditableStyle(document) || |
| (document.documentElement() && |
| HasEditableStyle(*document.documentElement())); |
| if (!needs_valid_structure) |
| return; |
| |
| Element* const current_root = document.documentElement(); |
| if (current_root && IsHTMLHtmlElement(current_root)) |
| return; |
| Element* const existing_head = |
| current_root && IsHTMLHeadElement(current_root) ? current_root : nullptr; |
| Element* const existing_body = |
| current_root && (IsHTMLBodyElement(current_root) || |
| IsHTMLFrameSetElement(current_root)) |
| ? current_root |
| : nullptr; |
| // We ensure only "the root is <html>." |
| // documentElement as rootEditableElement is problematic. So we move |
| // non-<html> root elements under <body>, and the <body> works as |
| // rootEditableElement. |
| document.AddConsoleMessage(ConsoleMessage::Create( |
| kJSMessageSource, kWarningMessageLevel, |
| "document.execCommand() doesn't work with an invalid HTML structure. It " |
| "is corrected automatically.")); |
| UseCounter::Count(document, WebFeature::kExecCommandAltersHTMLStructure); |
| |
| Element* const root = HTMLHtmlElement::Create(document); |
| if (existing_head) |
| root->AppendChild(existing_head); |
| Element* const body = |
| existing_body ? existing_body : HTMLBodyElement::Create(document); |
| if (document.documentElement() && body != document.documentElement()) |
| body->AppendChild(document.documentElement()); |
| root->AppendChild(body); |
| DCHECK(!document.documentElement()); |
| document.AppendChild(root); |
| |
| // TODO(tkent): Should we check and move Text node children of <html>? |
| } |
| |
| } // namespace blink |