blob: 3e9cdcbe15468689cf0342e94e20bf3df58b1cec [file] [log] [blame]
// 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/text_offset_mapping.h"
#include <ostream>
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/iterators/character_iterator.h"
#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
#include "third_party/blink/renderer/core/layout/layout_block_flow.h"
namespace blink {
namespace {
// TODO(editing-dev): We may not need to do full-subtree traversal, but we're
// not sure, e.g. ::first-line. See |enum PseudoId| for list of pseudo elements
// used in Blink.
bool HasNonPsuedoNode(const LayoutObject& parent) {
if (parent.NonPseudoNode())
return true;
for (const LayoutObject* runner = &parent; runner;
runner = runner->NextInPreOrder(&parent)) {
if (runner->NonPseudoNode())
return true;
}
// Following HTML reach here:
// [1] <div style="columns: 5 31px">...</div>; http://crbug.com/832055
// [2] <select></select>; http://crbug.com/834623
return false;
}
bool CanBeInlineContentsContainer(const LayoutObject& layout_object) {
if (!layout_object.IsLayoutBlockFlow())
return false;
const LayoutBlockFlow& block_flow = ToLayoutBlockFlow(layout_object);
if (!block_flow.ChildrenInline() || block_flow.IsAtomicInlineLevel())
return false;
if (block_flow.NonPseudoNode()) {
// It is OK as long as |block_flow| is associated to non-pseudo |Node| even
// if it is empty block or containing only anonymous objects.
// See LinkSelectionClickEventsTest.SingleAndDoubleClickWillBeHandled
return true;
}
// Since we can't create |EphemeralRange|, we exclude a |LayoutBlockFlow| if
// its entire subtree is anonymous, e.g. |LayoutMultiColumnSet|,
// and with anonymous layout objects.
return HasNonPsuedoNode(block_flow);
}
// Returns outer most nested inline formatting context.
const LayoutBlockFlow& RootInlineContentsContainerOf(
const LayoutBlockFlow& block_flow) {
DCHECK(block_flow.ChildrenInline()) << block_flow;
const LayoutBlockFlow* root_block_flow = &block_flow;
for (const LayoutBlock* runner = block_flow.ContainingBlock(); runner;
runner = runner->ContainingBlock()) {
if (!runner->IsLayoutBlockFlow() || !runner->ChildrenInline())
break;
root_block_flow = ToLayoutBlockFlow(runner);
}
DCHECK(!root_block_flow->IsAtomicInlineLevel())
<< block_flow << ' ' << root_block_flow;
return *root_block_flow;
}
// TODO(editing-dev): We should have |ComputeInlineContents()| computing first
// and last layout objects representing a run of inline layout objects in
// |LayoutBlockFlow| instead of using |ComputeInlineContentsAsBlockFlow()|.
//
// For example "<p>a<b>CD<p>EF</p>G</b>h</p>", where b has display:inline-block.
// We should have three ranges:
// 1. aCD
// 2. EF
// 3. Gh
// See RangeWithNestedInlineBlock* tests.
// Note: Since "inline-block" and "float" are not considered as text segment
// boundary, we should not consider them as block for scanning.
// Example in selection text:
// <div>|ab<b style="display:inline-block">CD</b>ef</div>
// selection.modify('extent', 'forward', 'word')
// <div>^ab<b style="display:inline-block">CD</b>ef|</div>
// See also test cases for "inline-block" and "float" in |TextIterator|
//
// This is a helper function to compute inline layout object run from
// |LayoutBlockFlow|.
const LayoutBlockFlow* ComputeInlineContentsAsBlockFlow(
const LayoutObject& layout_object) {
const LayoutBlock* const block = layout_object.IsLayoutBlock()
? &ToLayoutBlock(layout_object)
: layout_object.ContainingBlock();
DCHECK(block) << layout_object;
if (!block->IsLayoutBlockFlow())
return nullptr;
const LayoutBlockFlow& block_flow = ToLayoutBlockFlow(*block);
if (!block_flow.ChildrenInline())
return nullptr;
if (block_flow.IsAtomicInlineLevel() ||
block_flow.IsFloatingOrOutOfFlowPositioned()) {
const LayoutBlockFlow& root_block_flow =
RootInlineContentsContainerOf(block_flow);
// Skip |root_block_flow| if it's an anonymous wrapper created for
// pseudo elements. See test AnonymousBlockFlowWrapperForFloatPseudo.
if (!CanBeInlineContentsContainer(root_block_flow))
return nullptr;
return &root_block_flow;
}
if (!CanBeInlineContentsContainer(block_flow))
return nullptr;
return &block_flow;
}
TextOffsetMapping::InlineContents CreateInlineContentsFromBlockFlow(
const LayoutBlockFlow& block_flow) {
const LayoutObject* first = nullptr;
for (const LayoutObject* runner = block_flow.FirstChild(); runner;
runner = runner->NextInPreOrder(&block_flow)) {
if (runner->NonPseudoNode()) {
first = runner;
break;
}
}
if (!first) {
DCHECK(block_flow.NonPseudoNode()) << block_flow;
return TextOffsetMapping::InlineContents(block_flow);
}
const LayoutObject* last = nullptr;
for (const LayoutObject* runner = block_flow.LastLeafChild(); runner;
runner = runner->PreviousInPreOrder(&block_flow)) {
if (runner->NonPseudoNode()) {
last = runner;
break;
}
}
DCHECK(last);
return TextOffsetMapping::InlineContents(block_flow, *first, *last);
}
TextOffsetMapping::InlineContents ComputeInlineContentsFromNode(
const Node& node) {
const LayoutObject* const layout_object = node.GetLayoutObject();
if (!layout_object)
return TextOffsetMapping::InlineContents();
const LayoutBlockFlow* const block_flow =
ComputeInlineContentsAsBlockFlow(*layout_object);
if (!block_flow)
return TextOffsetMapping::InlineContents();
return CreateInlineContentsFromBlockFlow(*block_flow);
}
String Ensure16Bit(const String& text) {
String text16(text);
text16.Ensure16Bit();
return text16;
}
} // namespace
TextOffsetMapping::TextOffsetMapping(const InlineContents& inline_contents,
const TextIteratorBehavior& behavior)
: behavior_(behavior),
range_(inline_contents.GetRange()),
text16_(Ensure16Bit(PlainText(range_, behavior_))) {}
TextOffsetMapping::TextOffsetMapping(const InlineContents& inline_contents)
: TextOffsetMapping(inline_contents,
TextIteratorBehavior::Builder()
.SetEmitsCharactersBetweenAllVisiblePositions(true)
.SetEmitsSmallXForTextSecurity(true)
.Build()) {}
int TextOffsetMapping::ComputeTextOffset(
const PositionInFlatTree& position) const {
if (position <= range_.StartPosition())
return 0;
if (position >= range_.EndPosition())
return text16_.length();
return TextIteratorInFlatTree::RangeLength(range_.StartPosition(), position,
behavior_);
}
PositionInFlatTree TextOffsetMapping::GetPositionBefore(unsigned offset) const {
DCHECK_LE(offset, text16_.length());
CharacterIteratorInFlatTree iterator(range_, behavior_);
if (offset >= 1 && offset == text16_.length()) {
iterator.Advance(offset - 1);
return iterator.GetPositionAfter();
}
iterator.Advance(offset);
return iterator.GetPositionBefore();
}
PositionInFlatTree TextOffsetMapping::GetPositionAfter(unsigned offset) const {
DCHECK_LE(offset, text16_.length());
CharacterIteratorInFlatTree iterator(range_, behavior_);
iterator.Advance(offset);
return iterator.GetPositionAfter();
}
EphemeralRangeInFlatTree TextOffsetMapping::ComputeRange(unsigned start,
unsigned end) const {
DCHECK_LE(end, text16_.length());
DCHECK_LE(start, end);
if (start == end)
return EphemeralRangeInFlatTree();
return EphemeralRangeInFlatTree(GetPositionBefore(start),
GetPositionAfter(end));
}
unsigned TextOffsetMapping::FindNonWhitespaceCharacterFrom(
unsigned offset) const {
for (unsigned runner = offset; runner < text16_.length(); ++runner) {
if (!IsWhitespace(text16_[runner]))
return runner;
}
return text16_.length();
}
// static
TextOffsetMapping::BackwardRange TextOffsetMapping::BackwardRangeOf(
const PositionInFlatTree& position) {
return BackwardRange(FindBackwardInlineContents(position));
}
// static
TextOffsetMapping::ForwardRange TextOffsetMapping::ForwardRangeOf(
const PositionInFlatTree& position) {
return ForwardRange(FindForwardInlineContents(position));
}
// static
template <typename Traverser>
TextOffsetMapping::InlineContents TextOffsetMapping::FindInlineContentsInternal(
const Node* start_node,
Traverser traverser) {
for (const Node* node = start_node; node; node = traverser(*node)) {
const InlineContents inline_contents = ComputeInlineContentsFromNode(*node);
if (inline_contents.IsNotNull())
return inline_contents;
}
return InlineContents();
}
// static
TextOffsetMapping::InlineContents TextOffsetMapping::FindBackwardInlineContents(
const PositionInFlatTree& position) {
const Node* previous_node = position.NodeAsRangeLastNode();
if (!previous_node)
return InlineContents();
if (const TextControlElement* enclosing_text_control =
EnclosingTextControl(position)) {
if (!FlatTreeTraversal::IsDescendantOf(*previous_node,
*enclosing_text_control)) {
// The first position in a text control reaches here.
return InlineContents();
}
return TextOffsetMapping::FindInlineContentsInternal(
previous_node, [enclosing_text_control](const Node& node) {
return FlatTreeTraversal::Previous(node, enclosing_text_control);
});
}
auto previous_skipping_text_control = [](const Node& node) -> const Node* {
DCHECK(!EnclosingTextControl(&node));
const Node* previous = FlatTreeTraversal::Previous(node);
const TextControlElement* previous_text_control =
EnclosingTextControl(previous);
if (!previous_text_control)
return previous;
return previous_text_control;
};
if (const TextControlElement* last_enclosing_text_control =
EnclosingTextControl(previous_node)) {
// Example, <input value=foo><span>bar</span>, span@beforeAnchor
return TextOffsetMapping::FindInlineContentsInternal(
last_enclosing_text_control, previous_skipping_text_control);
}
return TextOffsetMapping::FindInlineContentsInternal(
previous_node, previous_skipping_text_control);
}
// static
// Note: "doubleclick-whitespace-img-crash.html" call |NextWordPosition())
// with AfterNode(IMG) for <body><img></body>
TextOffsetMapping::InlineContents TextOffsetMapping::FindForwardInlineContents(
const PositionInFlatTree& position) {
const Node* next_node = position.NodeAsRangeFirstNode();
if (!next_node)
return InlineContents();
if (const TextControlElement* enclosing_text_control =
EnclosingTextControl(position)) {
if (!FlatTreeTraversal::IsDescendantOf(*next_node,
*enclosing_text_control)) {
// The last position in a text control reaches here.
return InlineContents();
}
return TextOffsetMapping::FindInlineContentsInternal(
next_node, [enclosing_text_control](const Node& node) {
return FlatTreeTraversal::Next(node, enclosing_text_control);
});
}
auto next_skipping_text_control = [](const Node& node) {
DCHECK(!EnclosingTextControl(&node));
if (IsTextControl(node))
return FlatTreeTraversal::NextSkippingChildren(node);
return FlatTreeTraversal::Next(node);
};
DCHECK(!EnclosingTextControl(next_node));
return TextOffsetMapping::FindInlineContentsInternal(
next_node, next_skipping_text_control);
}
// ----
TextOffsetMapping::InlineContents::InlineContents(
const LayoutBlockFlow& block_flow)
: block_flow_(&block_flow) {
DCHECK(block_flow_->NonPseudoNode());
DCHECK(CanBeInlineContentsContainer(*block_flow_)) << block_flow_;
}
// |first| and |last| should not be anonymous object.
// Note: "extend_selection_10_ltr_backward_word.html" has a block starts with
// collapsible whitespace with anonymous object.
TextOffsetMapping::InlineContents::InlineContents(
const LayoutBlockFlow& block_flow,
const LayoutObject& first,
const LayoutObject& last)
: block_flow_(&block_flow), first_(&first), last_(&last) {
DCHECK(first_->NonPseudoNode()) << first_;
DCHECK(last_->NonPseudoNode()) << last_;
DCHECK(CanBeInlineContentsContainer(*block_flow_)) << block_flow_;
DCHECK(first_->IsDescendantOf(block_flow_));
DCHECK(last_->IsDescendantOf(block_flow_));
}
bool TextOffsetMapping::InlineContents::operator==(
const InlineContents& other) const {
return block_flow_ == other.block_flow_;
}
const LayoutBlockFlow* TextOffsetMapping::InlineContents::GetEmptyBlock()
const {
DCHECK(block_flow_ && !first_ && !last_);
return block_flow_;
}
const LayoutObject& TextOffsetMapping::InlineContents::FirstLayoutObject()
const {
DCHECK(first_);
return *first_;
}
const LayoutObject& TextOffsetMapping::InlineContents::LastLayoutObject()
const {
DCHECK(last_);
return *last_;
}
EphemeralRangeInFlatTree TextOffsetMapping::InlineContents::GetRange() const {
DCHECK(block_flow_);
if (!first_) {
const Node& node = *block_flow_->NonPseudoNode();
return EphemeralRangeInFlatTree(
PositionInFlatTree::FirstPositionInNode(node),
PositionInFlatTree::LastPositionInNode(node));
}
const Node& first_node = *first_->NonPseudoNode();
const Node& last_node = *last_->NonPseudoNode();
return EphemeralRangeInFlatTree(
first_node.IsTextNode() ? PositionInFlatTree(first_node, 0)
: PositionInFlatTree::BeforeNode(first_node),
last_node.IsTextNode()
? PositionInFlatTree(last_node, ToText(last_node).length())
: PositionInFlatTree::AfterNode(last_node));
}
PositionInFlatTree
TextOffsetMapping::InlineContents::LastPositionBeforeBlockFlow() const {
DCHECK(block_flow_);
if (const Node* node = block_flow_->NonPseudoNode()) {
if (!FlatTreeTraversal::Parent(*node)) {
// Reached start of document.
return PositionInFlatTree();
}
return PositionInFlatTree::BeforeNode(*node);
}
DCHECK(first_);
DCHECK(first_->NonPseudoNode());
DCHECK(FlatTreeTraversal::Parent(*first_->NonPseudoNode()));
return PositionInFlatTree::BeforeNode(*first_->NonPseudoNode());
}
PositionInFlatTree
TextOffsetMapping::InlineContents::FirstPositionAfterBlockFlow() const {
DCHECK(block_flow_);
if (const Node* node = block_flow_->NonPseudoNode()) {
if (!FlatTreeTraversal::Parent(*node)) {
// Reached end of document.
return PositionInFlatTree();
}
return PositionInFlatTree::AfterNode(*node);
}
DCHECK(last_);
DCHECK(last_->NonPseudoNode());
DCHECK(FlatTreeTraversal::Parent(*last_->NonPseudoNode()));
return PositionInFlatTree::AfterNode(*last_->NonPseudoNode());
}
// static
TextOffsetMapping::InlineContents TextOffsetMapping::InlineContents::NextOf(
const InlineContents& inline_contents) {
const PositionInFlatTree position_after =
inline_contents.FirstPositionAfterBlockFlow();
if (position_after.IsNull())
return InlineContents();
return TextOffsetMapping::FindForwardInlineContents(position_after);
}
// static
TextOffsetMapping::InlineContents TextOffsetMapping::InlineContents::PreviousOf(
const InlineContents& inline_contents) {
const PositionInFlatTree position_before =
inline_contents.LastPositionBeforeBlockFlow();
if (position_before.IsNull())
return InlineContents();
return TextOffsetMapping::FindBackwardInlineContents(position_before);
}
std::ostream& operator<<(
std::ostream& ostream,
const TextOffsetMapping::InlineContents& inline_contents) {
return ostream << '[' << inline_contents.FirstLayoutObject() << ", "
<< inline_contents.LastLayoutObject() << ']';
}
// ----
TextOffsetMapping::InlineContents TextOffsetMapping::BackwardRange::Iterator::
operator*() const {
DCHECK(current_.IsNotNull());
return current_;
}
void TextOffsetMapping::BackwardRange::Iterator::operator++() {
DCHECK(current_.IsNotNull());
current_ = TextOffsetMapping::InlineContents::PreviousOf(current_);
}
// ----
TextOffsetMapping::InlineContents TextOffsetMapping::ForwardRange::Iterator::
operator*() const {
DCHECK(current_.IsNotNull());
return current_;
}
void TextOffsetMapping::ForwardRange::Iterator::operator++() {
DCHECK(current_.IsNotNull());
current_ = TextOffsetMapping::InlineContents::NextOf(current_);
}
} // namespace blink