blob: 04a42aac14bdb41ee275e96bb519d981b513fe66 [file] [log] [blame]
/*
* 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 "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/commands/selection_for_undo_step.h"
#include "third_party/blink/renderer/core/editing/commands/typing_command.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/frame_selection.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/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/web_feature_forward.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/html/html_html_element.h"
#include "third_party/blink/renderer/core/inspector/console_message.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/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.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;
}
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;
}
bool IsInline(const Node* node) {
if (!node)
return false;
const ComputedStyle* style = node->GetComputedStyle();
return style && style->Display() == EDisplay::kInline;
}
// 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) {
const auto* first_element = DynamicTo<Element>(first);
const auto* second_element = DynamicTo<Element>(second);
if (!first_element || !second_element)
return false;
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()->IsDisplayTableBox())
return true;
if (layout_object->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)) {
auto* special_element = To<HTMLElement>(&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)) {
auto* special_element = To<HTMLElement>(&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 (IsA<HTMLBRElement>(*position.AnchorNode()) &&
position.AtFirstEditingPositionForNode())
return true;
if (!position.AnchorNode()->GetLayoutObject())
return false;
const auto* text_node = DynamicTo<Text>(position.AnchorNode());
if (!text_node || !text_node->GetLayoutObject()->Style()->PreserveNewline())
return false;
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 (IsA<HTMLBRElement>(*MostBackwardCaretPosition(position).AnchorNode()))
return Position();
const Position& prev = PreviousCharacterPosition(position, affinity);
if (prev == position)
return Position();
const Node* const anchor_node = prev.AnchorNode();
auto* anchor_text_node = DynamicTo<Text>(anchor_node);
if (!anchor_text_node)
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 = anchor_text_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(), html_names::xhtmlNamespaceURI)
<< "Unexpected namespace: " << name;
return To<HTMLElement>(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 (IsA<HTMLUListElement>(runner) || IsA<HTMLOListElement>(runner))
return To<HTMLElement>(&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 ((IsListItemTag(n) || IsListElementTag(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)) {
DCHECK(start_of_selection.IsNotNull()) << new_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)) {
DCHECK(end_of_selection.IsNotNull()) << new_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 && IsA<HTMLHtmlElement>(current_root))
return;
Element* const existing_head =
current_root && IsA<HTMLHeadElement>(current_root) ? current_root
: nullptr;
Element* const existing_body =
current_root && (IsA<HTMLBodyElement>(current_root) ||
IsA<HTMLFrameSetElement>(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(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning,
"document.execCommand() doesn't work with an invalid HTML structure. It "
"is corrected automatically."));
UseCounter::Count(document, WebFeature::kExecCommandAltersHTMLStructure);
auto* const root = MakeGarbageCollected<HTMLHtmlElement>(document);
if (existing_head)
root->AppendChild(existing_head);
auto* const body = existing_body
? existing_body
: MakeGarbageCollected<HTMLBodyElement>(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>?
}
InputEvent::InputType DeletionInputTypeFromTextGranularity(
DeleteDirection direction,
TextGranularity granularity) {
using InputType = InputEvent::InputType;
switch (direction) {
case DeleteDirection::kForward:
if (granularity == TextGranularity::kWord)
return InputType::kDeleteWordForward;
if (granularity == TextGranularity::kLineBoundary)
return InputType::kDeleteSoftLineForward;
if (granularity == TextGranularity::kParagraphBoundary)
return InputType::kDeleteHardLineForward;
return InputType::kDeleteContentForward;
case DeleteDirection::kBackward:
if (granularity == TextGranularity::kWord)
return InputType::kDeleteWordBackward;
if (granularity == TextGranularity::kLineBoundary)
return InputType::kDeleteSoftLineBackward;
if (granularity == TextGranularity::kParagraphBoundary)
return InputType::kDeleteHardLineBackward;
return InputType::kDeleteContentBackward;
default:
return InputType::kNone;
}
}
void DispatchEditableContentChangedEvents(Element* start_root,
Element* end_root) {
if (start_root) {
start_root->DispatchEvent(
*Event::Create(event_type_names::kWebkitEditableContentChanged));
}
if (end_root && end_root != start_root) {
end_root->DispatchEvent(
*Event::Create(event_type_names::kWebkitEditableContentChanged));
}
}
static void DispatchInputEvent(Element* target,
InputEvent::InputType input_type,
const String& data,
InputEvent::EventIsComposing is_composing) {
if (!target)
return;
// TODO(editing-dev): Pass appreciate |ranges| after it's defined on spec.
// http://w3c.github.io/editing/input-events.html#dom-inputevent-inputtype
InputEvent* const input_event =
InputEvent::CreateInput(input_type, data, is_composing, nullptr);
target->DispatchScopedEvent(*input_event);
}
void DispatchInputEventEditableContentChanged(
Element* start_root,
Element* end_root,
InputEvent::InputType input_type,
const String& data,
InputEvent::EventIsComposing is_composing) {
if (start_root)
DispatchInputEvent(start_root, input_type, data, is_composing);
if (end_root && end_root != start_root)
DispatchInputEvent(end_root, input_type, data, is_composing);
}
SelectionInDOMTree CorrectedSelectionAfterCommand(
const SelectionForUndoStep& passed_selection,
const Document* document) {
if (!passed_selection.Base().IsValidFor(*document) ||
!passed_selection.Extent().IsValidFor(*document))
return SelectionInDOMTree();
return passed_selection.AsSelection();
}
void ChangeSelectionAfterCommand(LocalFrame* frame,
const SelectionInDOMTree& new_selection,
const SetSelectionOptions& options) {
if (new_selection.IsNone())
return;
// See <rdar://problem/5729315> Some shouldChangeSelectedDOMRange contain
// Ranges for selections that are no longer valid
const bool selection_did_not_change_dom_position =
new_selection == frame->Selection().GetSelectionInDOMTree() &&
options.IsDirectional() == frame->Selection().IsDirectional();
const bool handle_visible =
frame->Selection().IsHandleVisible() && new_selection.IsRange();
frame->Selection().SetSelection(new_selection,
SetSelectionOptions::Builder(options)
.SetShouldShowHandle(handle_visible)
.SetIsDirectional(options.IsDirectional())
.Build());
// Some editing operations change the selection visually without affecting its
// position within the DOM. For example when you press return in the following
// (the caret is marked by ^):
// <div contentEditable="true"><div>^Hello</div></div>
// WebCore inserts <div><br></div> *before* the current block, which correctly
// moves the paragraph down but which doesn't change the caret's DOM position
// (["hello", 0]). In these situations the above FrameSelection::setSelection
// call does not call LocalFrameClient::DidChangeSelection(), which, on the
// Mac, sends selection change notifications and starts a new kill ring
// sequence, but we want to do these things (matches AppKit).
if (!selection_did_not_change_dom_position)
return;
frame->Client()->DidChangeSelection(
!frame->Selection().GetSelectionInDOMTree().IsRange());
}
InputEvent::EventIsComposing IsComposingFromCommand(
const CompositeEditCommand* command) {
auto* typing_command = DynamicTo<TypingCommand>(command);
if (typing_command &&
typing_command->CompositionType() != TypingCommand::kTextCompositionNone)
return InputEvent::EventIsComposing::kIsComposing;
return InputEvent::EventIsComposing::kNotComposing;
}
// ---------
VisiblePosition StartOfBlock(const VisiblePosition& visible_position,
EditingBoundaryCrossingRule rule) {
DCHECK(visible_position.IsValid()) << visible_position;
Position position = visible_position.DeepEquivalent();
Element* start_block =
position.ComputeContainerNode()
? EnclosingBlock(position.ComputeContainerNode(), rule)
: nullptr;
return start_block ? VisiblePosition::FirstPositionInNode(*start_block)
: VisiblePosition();
}
VisiblePosition EndOfBlock(const VisiblePosition& visible_position,
EditingBoundaryCrossingRule rule) {
DCHECK(visible_position.IsValid()) << visible_position;
Position position = visible_position.DeepEquivalent();
Element* end_block =
position.ComputeContainerNode()
? EnclosingBlock(position.ComputeContainerNode(), rule)
: nullptr;
return end_block ? VisiblePosition::LastPositionInNode(*end_block)
: VisiblePosition();
}
bool IsStartOfBlock(const VisiblePosition& pos) {
DCHECK(pos.IsValid()) << pos;
return pos.IsNotNull() &&
pos.DeepEquivalent() ==
StartOfBlock(pos, kCanCrossEditingBoundary).DeepEquivalent();
}
bool IsEndOfBlock(const VisiblePosition& pos) {
DCHECK(pos.IsValid()) << pos;
return pos.IsNotNull() &&
pos.DeepEquivalent() ==
EndOfBlock(pos, kCanCrossEditingBoundary).DeepEquivalent();
}
} // namespace blink