blob: bf05933828ec6aebc92e219d24a0734af4df45a1 [file] [log] [blame]
// Copyright 2016 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.
#ifndef UI_ACCESSIBILITY_AX_POSITION_H_
#define UI_ACCESSIBILITY_AX_POSITION_H_
#include <cmath>
#include <cstdint>
#include <functional>
#include <memory>
#include <ostream>
#include <stack>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "ax_enum_util.h"
#include "ax_enums.h"
#include "ax_node.h"
#include "ax_node_text_styles.h"
#include "ax_role_properties.h"
#include "ax_tree_id.h"
#include "base/container_utils.h"
#include "base/logging.h"
#include "base/string_utils.h"
namespace ui {
// Defines the type of position in the accessibility tree.
// A tree position is used when referring to a specific child of a node in the
// accessibility tree.
// A text position is used when referring to a specific character of text inside
// a particular node.
// A null position is used to signify that the provided data is invalid or that
// a boundary has been reached.
enum class AXPositionKind { NULL_POSITION, TREE_POSITION, TEXT_POSITION };
// Defines how creating the next or previous position should behave whenever we
// are at or are crossing a boundary, such as at the start of an anchor, a word
// or a line.
enum class AXBoundaryBehavior {
CrossBoundary,
StopAtAnchorBoundary,
StopIfAlreadyAtBoundary,
StopAtLastAnchorBoundary
};
// Describes in further detail what type of boundary a current position is on.
// For complex boundaries such as format boundaries, it can be useful to know
// why a particular boundary was chosen.
enum class AXBoundaryType {
// Not at a unit boundary.
kNone,
// At a unit boundary (e.g. a format boundary).
kUnitBoundary,
// At the start of a document.
kDocumentStart,
// At the end of a document.
kDocumentEnd
};
// When converting to an unignored position, determines how to adjust the new
// position in order to make it valid, either moving backward or forward in
// the accessibility tree.
enum class AXPositionAdjustmentBehavior { kMoveBackward, kMoveForward };
// Specifies how AXPosition::ExpandToEnclosingTextBoundary behaves.
//
// As an example, imagine we have the text "hello world" and a position before
// the space character. We want to expand to the surrounding word boundary.
// Since we are right at the end of the first word, we could either expand to
// the left first, find the start of the first word and then use that to find
// the corresponding word end, resulting in the word "Hello". Another
// possibility is to expand to the right first, find the end of the next word
// and use that as our starting point to find the previous word start, resulting
// in the word "world".
enum class AXRangeExpandBehavior {
// Expands to the left boundary first and then uses that position as the
// starting point to find the boundary to the right.
kLeftFirst,
// Expands to the right boundary first and then uses that position as the
// starting point to find the boundary to the left.
kRightFirst
};
// Some platforms require empty objects to be represented by a replacement
// character in order for text navigation to work correctly. This enum controls
// whether a replacement character will be exposed for such objects.
//
// When an embedded object is replaced by a real character, the expectations
// are the same with this character as with other ordinary characters.
// For example, with UIA on Windows, we need to be able to navigate inside and
// outside of this character as if it was an ordinary character, using the
// AXPlatformNodeTextRangeProvider methods. Since an embedded object character
// is the only character in a node, we also treat this character as a word.
enum class AXEmbeddedObjectBehavior {
kExposeCharacter,
kSuppressCharacter,
};
// Controls whether embedded objects are represented by a replacement
// character. This is initialized to a per-platform default but can be
// overridden for testing.
AX_EXPORT extern AXEmbeddedObjectBehavior g_ax_embedded_object_behavior;
// Forward declarations.
template <class AXPositionType, class AXNodeType>
class AXPosition;
template <class AXPositionType>
class AXRange;
template <class AXPositionType, class AXNodeType>
bool operator==(const AXPosition<AXPositionType, AXNodeType>& first,
const AXPosition<AXPositionType, AXNodeType>& second);
template <class AXPositionType, class AXNodeType>
bool operator!=(const AXPosition<AXPositionType, AXNodeType>& first,
const AXPosition<AXPositionType, AXNodeType>& second);
// A position in the accessibility tree.
//
// This class could either represent a tree position or a text position.
// Tree positions point to either a child of a specific node or at the end of a
// node (i.e. an "after children" position).
// Text positions point to either a character offset in the text inside a
// particular node including text from all its children, or to the end of the
// node's text, (i.e. an "after text" position).
// On tree positions that have a leaf node as their anchor, we also need to
// distinguish between "before text" and "after text" positions. To do this, if
// the child index is 0 and the anchor is a leaf node, then it's an "after text"
// position. If the child index is |BEFORE_TEXT| and the anchor is a leaf node,
// then this is a "before text" position.
// It doesn't make sense to have a "before text" position on a text position,
// because it is identical to setting its offset to the first character.
//
// To avoid re-computing either the text offset or the child index when
// converting between the two types of positions, both values are saved after
// the first conversion.
//
// This class template uses static polymorphism in order to allow sub-classes to
// be created from the base class without the base class knowing the type of the
// sub-class in advance.
// The template argument |AXPositionType| should always be set to the type of
// any class that inherits from this template, making this a
// "curiously recursive template".
//
// This class can be copied using the |Clone| method. It is designed to be
// immutable.
template <class AXPositionType, class AXNodeType>
class AXPosition {
public:
using AXPositionInstance =
std::unique_ptr<AXPosition<AXPositionType, AXNodeType>>;
using AXRangeType = AXRange<AXPosition<AXPositionType, AXNodeType>>;
typedef bool BoundaryConditionPredicate(const AXPositionInstance&);
typedef std::vector<int32_t> BoundaryTextOffsetsFunc(
const AXPositionInstance&);
static const int BEFORE_TEXT = -1;
static const int INVALID_INDEX = -2;
static const int INVALID_OFFSET = -1;
// Replacement character used to represent an empty object. See
// AXEmbeddedObjectBehavior for more information.
//
// Duplicate of AXPlatformNodeBase::kEmbeddedCharacter because we don't want
// to include platform specific code in here.
static constexpr char16_t kEmbeddedCharacter = L'\xfffc';
static AXPositionInstance CreateNullPosition() {
AXPositionInstance new_position(new AXPositionType());
new_position->Initialize(
AXPositionKind::NULL_POSITION, AXTreeIDUnknown(), AXNode::kInvalidAXID,
INVALID_INDEX, INVALID_OFFSET, ax::mojom::TextAffinity::kDownstream);
return new_position;
}
static AXPositionInstance CreateTreePosition(AXTreeID tree_id,
AXNode::AXID anchor_id,
int child_index) {
AXPositionInstance new_position(new AXPositionType());
new_position->Initialize(AXPositionKind::TREE_POSITION, tree_id, anchor_id,
child_index, INVALID_OFFSET,
ax::mojom::TextAffinity::kDownstream);
return new_position;
}
static AXPositionInstance CreateTextPosition(
AXTreeID tree_id,
AXNode::AXID anchor_id,
int text_offset,
ax::mojom::TextAffinity affinity) {
AXPositionInstance new_position(new AXPositionType());
new_position->Initialize(AXPositionKind::TEXT_POSITION, tree_id, anchor_id,
INVALID_INDEX, text_offset, affinity);
return new_position;
}
virtual ~AXPosition() = default;
// Implemented based on the copy and swap idiom.
AXPosition& operator=(const AXPosition& other) {
AXPositionInstance clone = other.Clone();
swap(*clone);
return *this;
}
virtual AXPositionInstance Clone() const = 0;
// A serialization of a position as POD. Not for sharing on disk or sharing
// across thread or process boundaries, just for passing a position to an
// API that works with positions as opaque objects.
struct SerializedPosition {
AXPositionKind kind;
AXNode::AXID anchor_id;
int child_index;
int text_offset;
ax::mojom::TextAffinity affinity;
char tree_id[33];
};
static_assert(std::is_trivially_copyable<SerializedPosition>::value,
"SerializedPosition must be POD");
SerializedPosition Serialize() {
SerializedPosition result;
result.kind = kind_;
// A tree ID can be serialized as a 32-byte string.
std::string tree_id_string = tree_id_.ToString();
BASE_DCHECK(tree_id_string.size() <= 32U);
strncpy(result.tree_id, tree_id_string.c_str(), 32);
result.tree_id[32] = 0;
result.anchor_id = anchor_id_;
result.child_index = child_index_;
result.text_offset = text_offset_;
result.affinity = affinity_;
return result;
}
static AXPositionInstance Unserialize(
const SerializedPosition& serialization) {
AXPositionInstance new_position(new AXPositionType());
new_position->Initialize(serialization.kind,
ui::AXTreeID::FromString(serialization.tree_id),
serialization.anchor_id, serialization.child_index,
serialization.text_offset, serialization.affinity);
return new_position;
}
std::string ToString() const {
std::string str;
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return "NullPosition";
case AXPositionKind::TREE_POSITION: {
std::string str_child_index;
if (child_index_ == BEFORE_TEXT) {
str_child_index = "before_text";
} else if (child_index_ == INVALID_INDEX) {
str_child_index = "invalid";
} else {
str_child_index = base::NumberToString(child_index_);
}
str = "TreePosition tree_id=" + tree_id_.ToString() +
" anchor_id=" + base::NumberToString(anchor_id_) +
" child_index=" + str_child_index;
break;
}
case AXPositionKind::TEXT_POSITION: {
std::string str_text_offset;
if (text_offset_ == INVALID_OFFSET) {
str_text_offset = "invalid";
} else {
str_text_offset = base::NumberToString(text_offset_);
}
str = "TextPosition anchor_id=" + base::NumberToString(anchor_id_) +
" text_offset=" + str_text_offset + " affinity=" +
ui::ToString(static_cast<ax::mojom::TextAffinity>(affinity_));
break;
}
}
if (!IsTextPosition() || text_offset_ > MaxTextOffset())
return str;
std::u16string text = GetText();
BASE_DCHECK(text_offset_ >= 0);
int max_text_offset = MaxTextOffset();
BASE_DCHECK(text_offset_ <= max_text_offset);
std::u16string annotated_text;
if (text_offset_ == max_text_offset) {
annotated_text = text + u"<>";
} else {
annotated_text = text.substr(0, text_offset_) + u"<" +
text[text_offset_] + u">" +
text.substr(text_offset_ + 1);
}
return str + " annotated_text=" + base::UTF16ToUTF8(annotated_text);
}
AXTreeID tree_id() const { return tree_id_; }
AXNode::AXID anchor_id() const { return anchor_id_; }
AXNodeType* GetAnchor() const {
if (tree_id_ == AXTreeIDUnknown() || anchor_id_ == AXNode::kInvalidAXID)
return nullptr;
return GetNodeInTree(tree_id_, anchor_id_);
}
AXPositionKind kind() const { return kind_; }
int child_index() const { return child_index_; }
int text_offset() const { return text_offset_; }
ax::mojom::TextAffinity affinity() const { return affinity_; }
bool IsIgnored() const {
if (IsNullPosition())
return false;
BASE_DCHECK(GetAnchor());
// If this position is anchored to an ignored node, then consider this
// position to be ignored.
if (GetAnchor()->IsIgnored())
return true;
switch (kind_) {
case AXPositionKind::NULL_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TREE_POSITION: {
// If this is a "before text" or an "after text" tree position, it's
// pointing to the anchor itself, which we've determined to be
// unignored.
BASE_DCHECK(!IsLeaf() || child_index_ == BEFORE_TEXT ||
child_index_ == 0)
<< "\"Before text\" and \"after text\" tree positions are only "
"valid on leaf nodes.";
if (child_index_ == BEFORE_TEXT || IsLeaf())
return false;
// If this position is an "after children" position, consider the
// position to be ignored if the last child is ignored. This is because
// the last child will not be visible in the unignored tree. If the
// position is not adjusted, the resulting position would erroneously
// point before the second child in the unignored subtree rooted at the
// last child.
//
// 1 kRootWebArea
// ++2 kGenericContainer ignored
// ++++3 kStaticText "Line 1."
// ++++4 kStaticText "Line 2."
//
// Tree position anchor=kGenericContainer, child_index=1.
//
// Alternatively, if there is a node at the position pointed to by
// "child_index_", i.e. this position is neither a leaf position nor an
// "after children" position, consider this tree position to be ignored
// if the child node is ignored.
int adjusted_child_index = child_index_ != AnchorChildCount()
? child_index_
: child_index_ - 1;
AXPositionInstance child_position =
CreateChildPositionAt(adjusted_child_index);
BASE_DCHECK(child_position && !child_position->IsNullPosition());
return child_position->GetAnchor()->IsIgnored();
}
case AXPositionKind::TEXT_POSITION:
// If the corresponding leaf position is ignored, the current text
// offset will point to ignored text. Therefore, consider this position
// to be ignored.
if (!IsLeaf())
return AsLeafTreePosition()->IsIgnored();
return false;
}
}
bool IsNullPosition() const {
return kind_ == AXPositionKind::NULL_POSITION || !GetAnchor();
}
bool IsTreePosition() const {
return GetAnchor() && kind_ == AXPositionKind::TREE_POSITION;
}
bool IsLeafTreePosition() const { return IsTreePosition() && IsLeaf(); }
bool IsTextPosition() const {
return GetAnchor() && kind_ == AXPositionKind::TEXT_POSITION;
}
bool IsLeafTextPosition() const { return IsTextPosition() && IsLeaf(); }
bool IsLeaf() const {
if (IsNullPosition())
return false;
return !AnchorChildCount() || IsEmptyObjectReplacedByCharacter();
}
// Returns true if this is a valid position, e.g. the child_index_ or
// text_offset_ is within a valid range.
bool IsValid() const {
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return tree_id_ == AXTreeIDUnknown() &&
anchor_id_ == AXNode::kInvalidAXID &&
child_index_ == INVALID_INDEX &&
text_offset_ == INVALID_OFFSET &&
affinity_ == ax::mojom::TextAffinity::kDownstream;
case AXPositionKind::TREE_POSITION:
return GetAnchor() &&
(child_index_ == BEFORE_TEXT ||
(child_index_ >= 0 && child_index_ <= AnchorChildCount())) &&
!IsInDescendantOfEmptyObject();
case AXPositionKind::TEXT_POSITION:
if (!GetAnchor() || IsInDescendantOfEmptyObject())
return false;
// For performance reasons we skip any validation of the text offset
// that involves retrieving the anchor's text, if the offset is set to
// 0, because 0 is frequently used and always valid regardless of the
// actual text.
return text_offset_ == 0 ||
(text_offset_ > 0 && text_offset_ <= MaxTextOffset());
}
}
// TODO(nektar): Update logic of AtStartOfAnchor() for text_offset_ == 0 and
// fix related bug.
bool AtStartOfAnchor() const {
if (!GetAnchor())
return false;
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
if (text_offset_ > 0)
return false;
if (!IsLeaf() || text_offset_ == 0)
return child_index_ == 0;
return child_index_ == BEFORE_TEXT;
case AXPositionKind::TEXT_POSITION:
return text_offset_ == 0;
}
}
bool AtEndOfAnchor() const {
if (!GetAnchor())
return false;
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
return child_index_ == AnchorChildCount();
case AXPositionKind::TEXT_POSITION:
return text_offset_ == MaxTextOffset();
}
}
bool AtStartOfWord() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TEXT_POSITION: {
const std::vector<int32_t> word_starts =
text_position->GetWordStartOffsets();
return base::Contains(word_starts,
int32_t{text_position->text_offset_});
}
}
}
bool AtEndOfWord() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TEXT_POSITION: {
const std::vector<int32_t> word_ends =
text_position->GetWordEndOffsets();
return base::Contains(word_ends, int32_t{text_position->text_offset_});
}
}
}
bool AtStartOfLine() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TEXT_POSITION:
// We treat a position after some white space that is not connected to
// any node after it via "next on line ID", to be equivalent to a
// position before the next line, and therefore as being at start of
// line.
//
// We assume that white space, including but not limited to hard line
// breaks, might be used to separate lines. For example, an inline text
// box with just a single space character inside it can be used to
// represent a soft line break. If an inline text box containing white
// space separates two lines, it should always be connected to the first
// line via "kPreviousOnLineId". This is guaranteed by the renderer. If
// there are multiple line breaks separating the two lines, then only
// the first line break is connected to the first line via
// "kPreviousOnLineId".
//
// Sometimes there might be an inline text box with a single space in it
// at the end of a text field. We should not mark positions that are at
// the end of text fields, or in general at the end of their anchor, as
// being at the start of line, except when that anchor is an inline text
// box that is in the middle of a text span. Note that in most but not
// all cases, the parent of an inline text box is a static text object,
// whose end signifies the end of the text span. One exception is line
// breaks.
if (text_position->AtEndOfAnchor() &&
!text_position->AtEndOfTextSpan() &&
text_position->IsInWhiteSpace() &&
GetNextOnLineID(text_position->anchor_id_) ==
AXNode::kInvalidAXID) {
return true;
}
return GetPreviousOnLineID(text_position->anchor_id_) ==
AXNode::kInvalidAXID &&
text_position->AtStartOfAnchor();
}
}
bool AtEndOfLine() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TEXT_POSITION:
// Text positions on objects with no text should not be considered at
// end of line because the empty position may share a text offset with
// a non-empty text position in which case the end of line iterators
// must move to the line end of the non-empty content. Specified next
// line IDs are ignored.
if (!text_position->MaxTextOffset())
return false;
// If affinity has been used to specify whether the caret is at the end
// of a line or at the start of the next one, this should have been
// reflected in the leaf text position we got via "AsLeafTextPosition".
// If affinity had been set to upstream, the leaf text position should
// be pointing to the end of the inline text box that ends the first
// line. If it had been set to downstream, the leaf text position should
// be pointing to the start of the inline text box that starts the
// second line.
//
// In other cases, we assume that white space, including but not limited
// to hard line breaks, might be used to separate lines. For example, an
// inline text box with just a single space character inside it can be
// used to represent a soft line break. If an inline text box containing
// white space separates two lines, it should always be connected to the
// first line via "kPreviousOnLineId". This is guaranteed by the
// renderer. If there are multiple line breaks separating the two lines,
// then only the first line break is connected to the first line via
// "kPreviousOnLineId".
//
// We don't treat a position that is at the start of white space that is
// on a line by itself as being at the end of the line. This is in order
// to enable screen readers to recognize and announce blank lines
// correctly. However, we do treat positions at the start of white space
// that end a line of text as being at the end of that line. We also
// treat positions at the end of white space that is on a line by
// itself, i.e. on a blank line, as being at the end of that line.
//
// Sometimes there might be an inline text box with a single space in it
// at the end of a text field. We should mark positions that are at the
// end of text fields, or in general at the end of an anchor with no
// "kNextOnLineId", as being at end of line, except when that anchor is
// an inline text box that is in the middle of a text span. Note that
// in most but not all cases, the parent of an inline text box is a
// static text object, whose end signifies the end of the text span. One
// exception is line breaks.
if (GetNextOnLineID(text_position->anchor_id_) ==
AXNode::kInvalidAXID) {
return (!text_position->AtEndOfTextSpan() &&
text_position->IsInWhiteSpace() &&
GetPreviousOnLineID(text_position->anchor_id_) !=
AXNode::kInvalidAXID)
? text_position->AtStartOfAnchor()
: text_position->AtEndOfAnchor();
}
// The current anchor might be followed by a soft line break.
return text_position->AtEndOfAnchor() &&
text_position->CreateNextLeafTextPosition()->AtEndOfLine();
}
}
// |AtStartOfParagraph| is asymmetric from |AtEndOfParagraph| because of
// trailing whitespace collapse rules.
// The start of a paragraph should be a leaf text position (or equivalent),
// either at the start of the document, or at the start of the next leaf text
// position from the one representing the end of the previous paragraph.
// A position |AsLeafTextPosition| is the start of a paragraph if all of the
// following are true :
// 1. The current leaf text position must be an unignored position at
// the start of an anchor.
// 2. The current position is not whitespace only, unless it is also
// the first leaf text position within the document.
// 3. Either (a) the current leaf text position is the first leaf text
// position in the document, or (b) there are no line breaking
// objects between it and the previous non-whitespace leaf text
// position.
bool AtStartOfParagraph() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TEXT_POSITION: {
// 1. The current leaf text position must be an unignored position at
// the start of an anchor.
if (text_position->IsIgnored() || !text_position->AtStartOfAnchor())
return false;
// 2. The current position is not whitespace only, unless it is also
// the first leaf text position within the document.
if (text_position->IsInWhiteSpace()) {
return text_position->CreatePreviousLeafTextPosition()
->IsNullPosition();
}
// 3. Either (a) the current leaf text position is the first leaf text
// position in the document, or (b) there are no line breaking
// objects between it and the previous non-whitespace leaf text
// position.
//
// Search for the previous text position within the current paragraph,
// using the paragraph boundary abort predicate.
// If a valid position was found, then this position cannot be
// the start of a paragraph.
// This will return a null position when an anchor movement would
// cross a paragraph boundary, or the start of document was reached.
bool crossed_line_breaking_object_token = false;
auto abort_move_predicate =
[&crossed_line_breaking_object_token](
const AXPosition& move_from, const AXPosition& move_to,
const AXMoveType type, const AXMoveDirection direction) {
return AbortMoveAtParagraphBoundary(
crossed_line_breaking_object_token, move_from, move_to, type,
direction);
};
AXPositionInstance previous_text_position = text_position->Clone();
do {
previous_text_position =
previous_text_position->CreatePreviousTextAnchorPosition(
abort_move_predicate);
// If the previous position is whitespace, then continue searching
// until a non-whitespace leaf text position is found within the
// current paragraph because whitespace is supposed to be collapsed.
// There's a chance that |CreatePreviousTextAnchorPosition| will
// return whitespace that should be appended to a previous paragraph
// rather than separating two pieces of the current paragraph.
} while (previous_text_position->IsInWhiteSpace() ||
previous_text_position->IsIgnored());
return previous_text_position->IsNullPosition();
}
}
}
// |AtEndOfParagraph| is asymmetric from |AtStartOfParagraph| because of
// trailing whitespace collapse rules.
// The end of a paragraph should be a leaf text position (or equivalent),
// either at the end of the document, or at the end of the previous leaf text
// position from the one representing the start of the next paragraph.
// A position |AsLeafTextPosition| is the end of a paragraph if all of the
// following are true :
// 1. The current leaf text position must be an unignored position at
// the end of an anchor.
// 2. Either (a) the current leaf text position is the last leaf text
// position in the document, or (b) there are no line breaking
// objects between it and the next leaf text position except when
// the next leaf text position is whitespace only since whitespace
// must be collapsed.
// 3. If there is a next leaf text position then it must not be
// whitespace only.
// 4. If there is a next leaf text position and it is not whitespace
// only, it must also be the start of a paragraph for the current
// position to be the end of a paragraph.
bool AtEndOfParagraph() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TEXT_POSITION: {
// 1. The current leaf text position must be an unignored position at
// the end of an anchor.
if (text_position->IsIgnored() || !text_position->AtEndOfAnchor())
return false;
// 2. Either (a) the current leaf text position is the last leaf text
// position in the document, or (b) there are no line breaking
// objects between it and the next leaf text position except when
// the next leaf text position is whitespace only since whitespace
// must be collapsed.
//
// Search for the next text position within the current paragraph,
// using the paragraph boundary abort predicate.
// If a null position was found, then this position must be the end of
// a paragraph.
// |CreateNextTextAnchorPosition| + |AbortMoveAtParagraphBoundary|
// will return a null position when an anchor movement would
// cross a paragraph boundary and there is no doubt that it is the end
// of a paragraph, or the end of document was reached.
// There are some fringe cases related to whitespace collapse that
// cannot be handled easily with only |AbortMoveAtParagraphBoundary|.
bool crossed_line_breaking_object_token = false;
auto abort_move_predicate =
[&crossed_line_breaking_object_token](
const AXPosition& move_from, const AXPosition& move_to,
const AXMoveType type, const AXMoveDirection direction) {
return AbortMoveAtParagraphBoundary(
crossed_line_breaking_object_token, move_from, move_to, type,
direction);
};
AXPositionInstance next_text_position = text_position->Clone();
do {
next_text_position = next_text_position->CreateNextTextAnchorPosition(
abort_move_predicate);
} while (next_text_position->IsIgnored());
if (next_text_position->IsNullPosition())
return true;
// 3. If there is a next leaf text position then it must not be
// whitespace only.
if (next_text_position->IsInWhiteSpace())
return false;
// 4. If there is a next leaf text position and it is not whitespace
// only, it must also be the start of a paragraph for the current
// position to be the end of a paragraph.
//
// Consider the following example :
// ++{1} kStaticText "First Paragraph"
// ++++{2} kInlineTextBox "First Paragraph"
// ++{3} kStaticText "\n Second Paragraph"
// ++++{4} kInlineTextBox "\n" kIsLineBreakingObject
// ++++{5} kInlineTextBox " "
// ++++{6} kInlineTextBox "Second Paragraph"
// A position at the end of {5} is the end of a paragraph, because
// the first paragraph must collapse trailing whitespace and contain
// leaf text anchors {2, 4, 5}. The second paragraph is only {6}.
return next_text_position->CreatePositionAtStartOfAnchor()
->AtStartOfParagraph();
}
}
}
bool AtStartOfPage() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TEXT_POSITION: {
if (!text_position->AtStartOfAnchor())
return false;
// Search for the previous text position within the current page,
// using the page boundary abort predicate.
// If a valid position was found, then this position cannot be
// the start of a page.
// This will return a null position when an anchor movement would
// cross a page boundary, or the start of document was reached.
AXPositionInstance previous_text_position =
text_position->CreatePreviousTextAnchorPosition(
AbortMoveAtPageBoundary);
return previous_text_position->IsNullPosition();
}
}
}
bool AtEndOfPage() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TEXT_POSITION: {
if (!text_position->AtEndOfAnchor())
return false;
// Search for the next text position within the current page,
// using the page boundary abort predicate.
// If a valid position was found, then this position cannot be
// the end of a page.
// This will return a null position when an anchor movement would
// cross a page boundary, or the end of document was reached.
AXPositionInstance next_text_position =
text_position->CreateNextTextAnchorPosition(
AbortMoveAtPageBoundary);
return next_text_position->IsNullPosition();
}
}
}
bool AtStartOfAXTree() const {
if (IsNullPosition())
return false;
if (AtStartOfAnchor()) {
AXPositionInstance previous_anchor = CreatePreviousAnchorPosition();
// Consider the start of the document as the start of an AXTree.
if (previous_anchor->IsNullPosition())
return true;
else
return previous_anchor->tree_id() != tree_id();
}
return false;
}
bool AtEndOfAXTree() const {
if (IsNullPosition())
return false;
if (AtEndOfAnchor()) {
AXPositionInstance next_anchor = CreateNextAnchorPosition();
// Consider the end of the document as the end of an AXTree.
if (next_anchor->IsNullPosition())
return true;
else
return next_anchor->tree_id() != tree_id();
}
return false;
}
AXBoundaryType GetFormatStartBoundaryType() const {
// Since formats are stored on text anchors, the start of a format boundary
// must be at the start of an anchor.
if (IsNullPosition() || !AtStartOfAnchor())
return AXBoundaryType::kNone;
// Treat the first iterable node as a format boundary.
if (CreatePreviousLeafTreePosition()->IsNullPosition())
return AXBoundaryType::kDocumentStart;
// Ignored positions cannot be format boundaries.
if (IsIgnored())
return AXBoundaryType::kNone;
// Iterate over anchors until a format boundary is found. This will return a
// null position upon crossing a boundary. Make sure the previous position
// is not on an ignored node.
AXPositionInstance previous_position = Clone();
do {
previous_position = previous_position->CreatePreviousLeafTreePosition(
AbortMoveAtFormatBoundary);
} while (previous_position->IsIgnored());
if (previous_position->IsNullPosition())
return AXBoundaryType::kUnitBoundary;
return AXBoundaryType::kNone;
}
bool AtStartOfFormat() const {
return GetFormatStartBoundaryType() != AXBoundaryType::kNone;
}
AXBoundaryType GetFormatEndBoundaryType() const {
// Since formats are stored on text anchors, the end of a format break must
// be at the end of an anchor.
if (IsNullPosition() || !AtEndOfAnchor())
return AXBoundaryType::kNone;
// Treat the last iterable node as a format boundary
if (CreateNextLeafTreePosition()->IsNullPosition())
return AXBoundaryType::kDocumentEnd;
// Ignored positions cannot be format boundaries.
if (IsIgnored())
return AXBoundaryType::kNone;
// Iterate over anchors until a format boundary is found. This will return a
// null position upon crossing a boundary. Make sure the next position is
// not on an ignored node.
AXPositionInstance next_position = Clone();
do {
next_position =
next_position->CreateNextLeafTreePosition(AbortMoveAtFormatBoundary);
} while (next_position->IsIgnored());
if (next_position->IsNullPosition())
return AXBoundaryType::kUnitBoundary;
return AXBoundaryType::kNone;
}
bool AtEndOfFormat() const {
return GetFormatEndBoundaryType() != AXBoundaryType::kNone;
}
bool AtStartOfInlineBlock() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
BASE_UNREACHABLE();
return false;
case AXPositionKind::TEXT_POSITION: {
if (text_position->AtStartOfAnchor()) {
AXPositionInstance previous_position =
text_position->CreatePreviousLeafTreePosition();
// Check that this position is not the start of the first anchor.
if (!previous_position->IsNullPosition()) {
previous_position = text_position->CreatePreviousLeafTreePosition(
&AbortMoveAtStartOfInlineBlock);
// If we get a null position here it means we have crossed an inline
// block's start, thus this position is located at such start.
if (previous_position->IsNullPosition())
return true;
}
}
if (text_position->AtEndOfAnchor()) {
AXPositionInstance next_position =
text_position->CreateNextLeafTreePosition();
// Check that this position is not the end of the last anchor.
if (!next_position->IsNullPosition()) {
next_position = text_position->CreateNextLeafTreePosition(
&AbortMoveAtStartOfInlineBlock);
// If we get a null position here it means we have crossed an inline
// block's start, thus this position is located at such start.
if (next_position->IsNullPosition())
return true;
}
}
return false;
}
}
}
bool AtStartOfDocument() const {
if (IsNullPosition())
return false;
return IsDocument(GetAnchorRole()) && AtStartOfAnchor();
}
bool AtEndOfDocument() const {
if (IsNullPosition())
return false;
return AtLastNodeInTree() && AtEndOfAnchor();
}
bool AtLastNodeInTree() const {
if (IsNullPosition())
return false;
// Avoid a potentionally expensive MaxTextOffset call by only using tree
// positions. The only thing that matters is whether our anchor_id_ is at
// the last anchor of the document, so we're free to ignore text_offset_.
AXPositionInstance tree_position =
CreateTreePosition(tree_id_, anchor_id_, 0);
return tree_position->CreateNextAnchorPosition()->IsNullPosition();
}
// This method finds the lowest common AXNodeType of |this| and |second|.
AXNodeType* LowestCommonAnchor(const AXPosition& second) const {
if (IsNullPosition() || second.IsNullPosition())
return nullptr;
if (GetAnchor() == second.GetAnchor())
return GetAnchor();
std::stack<AXNodeType*> our_ancestors = GetAncestorAnchors();
std::stack<AXNodeType*> other_ancestors = second.GetAncestorAnchors();
AXNodeType* common_anchor = nullptr;
while (!our_ancestors.empty() && !other_ancestors.empty() &&
our_ancestors.top() == other_ancestors.top()) {
common_anchor = our_ancestors.top();
our_ancestors.pop();
other_ancestors.pop();
}
return common_anchor;
}
// This method returns a position instead of a node because this allows us to
// return the corresponding text offset or child index in the ancestor that
// relates to the current position.
// Also, this method uses position instead of tree logic to traverse the tree,
// because positions can handle moving across multiple trees, while trees
// cannot.
AXPositionInstance LowestCommonAncestor(const AXPosition& second) const {
return CreateAncestorPosition(LowestCommonAnchor(second));
}
// See "CreateParentPosition" for an explanation of the use of
// |move_direction|.
AXPositionInstance CreateAncestorPosition(
const AXNodeType* ancestor_anchor,
ax::mojom::MoveDirection move_direction =
ax::mojom::MoveDirection::kForward) const {
if (!ancestor_anchor)
return CreateNullPosition();
AXPositionInstance ancestor_position = Clone();
while (!ancestor_position->IsNullPosition() &&
ancestor_position->GetAnchor() != ancestor_anchor) {
ancestor_position =
ancestor_position->CreateParentPosition(move_direction);
}
return ancestor_position;
}
// If the position is not valid, we return a new valid position that is
// closest to the original position if possible, or a null position otherwise.
AXPositionInstance AsValidPosition() const {
AXPositionInstance position = Clone();
switch (position->kind_) {
case AXPositionKind::NULL_POSITION:
// We avoid cloning to ensure that all fields will be valid.
return CreateNullPosition();
case AXPositionKind::TREE_POSITION: {
if (!position->GetAnchor())
return CreateNullPosition();
if (AXNodeType* empty_object_node = GetEmptyObjectAncestorNode()) {
// In this class and on certain platforms, we define the empty object
// as one that doesn't expose its underlying content. Its content is
// replaced by the empty object character (string of length 1). A
// position on a descendant of an empty object is invalid. To make it
// valid we move the position from the descendant to the empty object
// node itself.
return CreateTreePosition(
position->tree_id(), GetAnchorID(empty_object_node),
position->child_index() == BEFORE_TEXT ? BEFORE_TEXT : 0);
}
if (position->child_index_ == BEFORE_TEXT)
return position;
if (position->child_index_ < 0)
position->child_index_ = 0;
else if (position->child_index_ > position->AnchorChildCount())
position->child_index_ = position->AnchorChildCount();
break;
}
case AXPositionKind::TEXT_POSITION: {
if (!position->GetAnchor())
return CreateNullPosition();
if (AXNodeType* empty_object_node = GetEmptyObjectAncestorNode()) {
// This is needed because an empty object as defined in this class and
// on certain platforms can have descendants that should not be
// exposed. See comment above in similar implementation for
// AXPositionKind::TREE_POSITION.
//
// We set the |text_offset_| to either 0 or 1 here because the
// MaxTextOffset of an empty object is 1 (the empty object character,
// a string of length 1). If the invalid position was already at the
// start of the node, we set it to 0.
return CreateTextPosition(position->tree_id(),
GetAnchorID(empty_object_node),
position->text_offset() > 0 ? 1 : 0,
ax::mojom::TextAffinity::kDownstream);
}
if (position->text_offset_ <= 0) {
// 0 is always a valid offset, so skip calling MaxTextOffset in that
// case.
position->text_offset_ = 0;
position->affinity_ = ax::mojom::TextAffinity::kDownstream;
} else {
int max_text_offset = position->MaxTextOffset();
if (position->text_offset_ > max_text_offset) {
position->text_offset_ = max_text_offset;
position->affinity_ = ax::mojom::TextAffinity::kDownstream;
}
}
break;
}
}
BASE_DCHECK(position->IsValid());
return position;
}
AXPositionInstance AsTreePosition() const {
if (IsNullPosition() || IsTreePosition())
return Clone();
AXPositionInstance copy = Clone();
BASE_DCHECK(copy);
BASE_DCHECK(copy->text_offset_ >= 0);
if (copy->IsLeaf()) {
const int max_text_offset = copy->MaxTextOffset();
copy->child_index_ =
(max_text_offset != 0 && copy->text_offset_ != max_text_offset)
? BEFORE_TEXT
: 0;
copy->kind_ = AXPositionKind::TREE_POSITION;
return copy;
}
// We stop at the last child that we can reach with the current text offset
// and ignore any remaining children. This is for defensive programming
// purposes, in case "MaxTextOffset" doesn't match the total length of all
// our children. This may happen if, for example, there is a bug in the
// internal accessibility tree we get from the renderer. In contrast, the
// current offset could not be greater than the length of all our children
// because the position would have been invalid.
int current_offset = 0;
int child_index = 0;
for (; child_index < copy->AnchorChildCount(); ++child_index) {
AXPositionInstance child = copy->CreateChildPositionAt(child_index);
BASE_DCHECK(child);
int child_length = child->MaxTextOffsetInParent();
// If the text offset falls on the boundary between two adjacent children,
// we look at the affinity to decide whether to place the tree position on
// the first child vs. the second child. Upstream affinity would always
// choose the first child, whilst downstream affinity the second. This
// also has implications when converting the resulting tree position back
// to a text position. In that case, maintaining an upstream affinity
// would place the text position at the end of the first child, whilst
// maintaining a downstream affinity will place the text position at the
// beginning of the second child.
//
// This is vital for text positions on soft line breaks, as well as text
// positions before and after character, to work properly.
//
// See also `CreateLeafTextPositionBeforeCharacter` and
// `CreateLeafTextPositionAfterCharacter`.
if (copy->text_offset_ >= current_offset &&
(copy->text_offset_ < (current_offset + child_length) ||
(copy->affinity_ == ax::mojom::TextAffinity::kUpstream &&
copy->text_offset_ == (current_offset + child_length)))) {
break;
}
current_offset += child_length;
}
copy->child_index_ = child_index;
copy->kind_ = AXPositionKind::TREE_POSITION;
return copy;
}
// This is an optimization over "AsLeafTextPosition", in cases when computing
// the corresponding text offset on the leaf node is not needed. If this
// method is called on a text position, it will conservatively fall back to
// the non-optimized "AsLeafTextPosition", if the current text offset is
// greater than 0, or the affinity is upstream, since converting to a tree
// position at any point before reaching the leaf node could potentially lose
// information.
AXPositionInstance AsLeafTreePosition() const {
if (IsNullPosition() || IsLeaf())
return AsTreePosition();
// If our text offset is greater than 0, or if our affinity is set to
// upstream, we need to ensure that text offset and affinity will be taken
// into consideration during our descend to the leaves. Switching to a tree
// position early in this case will potentially lose information, so we
// descend using a text position instead.
//
// We purposely don't check whether this position is a text position, to
// allow for the possibility that this position has recently been converted
// from a text to a tree position and text offset or affinity information
// has been left intact.
if (text_offset_ > 0 || affinity_ == ax::mojom::TextAffinity::kUpstream)
return AsLeafTextPosition()->AsTreePosition();
AXPositionInstance tree_position = AsTreePosition();
do {
if (tree_position->child_index_ == tree_position->AnchorChildCount()) {
tree_position =
tree_position
->CreateChildPositionAt(tree_position->child_index_ - 1)
->CreatePositionAtEndOfAnchor();
} else {
tree_position =
tree_position->CreateChildPositionAt(tree_position->child_index_);
}
BASE_DCHECK(tree_position && !tree_position->IsNullPosition());
} while (!tree_position->IsLeaf());
BASE_DCHECK(tree_position && tree_position->IsLeafTreePosition());
return tree_position;
}
AXPositionInstance AsTextPosition() const {
if (IsNullPosition() || IsTextPosition())
return Clone();
AXPositionInstance copy = Clone();
BASE_DCHECK(copy);
// Check if it is a "before text" position.
if (copy->child_index_ == BEFORE_TEXT) {
// "Before text" positions can only appear on leaf nodes.
BASE_DCHECK(copy->IsLeaf());
// If the current text offset is valid, we don't touch it to potentially
// allow converting from a text position to a tree position and back
// without losing information.
//
// We test for INVALID_OFFSET first, due to the possible performance
// implications of calling MaxTextOffset().
BASE_DCHECK(copy->text_offset_ >= INVALID_OFFSET);
if (copy->text_offset_ == INVALID_OFFSET ||
(copy->text_offset_ > 0 &&
copy->text_offset_ >= copy->MaxTextOffset())) {
copy->text_offset_ = 0;
}
} else if (copy->child_index_ == copy->AnchorChildCount()) {
copy->text_offset_ = copy->MaxTextOffset();
} else {
BASE_DCHECK(copy->child_index_ >= 0);
BASE_DCHECK(copy->child_index_ < copy->AnchorChildCount());
int new_offset = 0;
for (int i = 0; i <= child_index_; ++i) {
AXPositionInstance child = copy->CreateChildPositionAt(i);
BASE_DCHECK(child);
// If the current text offset is valid, we don't touch it to
// potentially allow converting from a text position to a tree
// position and back without losing information. Otherwise, if the
// text_offset is invalid, equals to 0 or is smaller than
// |new_offset|, we reset it to the beginning of the current child
// node.
if (i == child_index_ && copy->text_offset_ <= new_offset) {
copy->text_offset_ = new_offset;
break;
}
int child_length = child->MaxTextOffsetInParent();
// Same comment as above: we don't touch the text offset if it's
// already valid.
if (i == child_index_ &&
(copy->text_offset_ > (new_offset + child_length) ||
// When the text offset is equal to the text's length but this is
// not an "after text" position.
(!copy->AtEndOfAnchor() &&
copy->text_offset_ == (new_offset + child_length)))) {
copy->text_offset_ = new_offset;
break;
}
new_offset += child_length;
}
}
// Affinity should always be left as downstream. The only case when the
// resulting text position is at the end of the line is when we get an
// "after text" leaf position, but even in this case downstream is
// appropriate because there is no ambiguity whetehr the position is at the
// end of the current line vs. the start of the next line. It would always
// be the former.
copy->kind_ = AXPositionKind::TEXT_POSITION;
return copy;
}
AXPositionInstance AsLeafTextPosition() const {
if (IsNullPosition() || IsLeaf())
return AsTextPosition();
// Adjust the text offset.
// No need to check for "before text" positions here because they are only
// present on leaf anchor nodes.
AXPositionInstance text_position = AsTextPosition();
int adjusted_offset = text_position->text_offset_;
do {
AXPositionInstance child_position =
text_position->CreateChildPositionAt(0);
BASE_DCHECK(child_position);
// If the text offset corresponds to multiple child positions because some
// of the children have empty text, the condition "adjusted_offset > 0"
// below ensures that the first child will be chosen.
for (int i = 1;
i < text_position->AnchorChildCount() && adjusted_offset > 0; ++i) {
const int max_text_offset_in_parent =
child_position->MaxTextOffsetInParent();
if (adjusted_offset < max_text_offset_in_parent) {
break;
}
if (affinity_ == ax::mojom::TextAffinity::kUpstream &&
adjusted_offset == max_text_offset_in_parent) {
// Maintain upstream affinity so that we'll be able to choose the
// correct leaf anchor if the text offset is right on the boundary
// between two leaves.
child_position->affinity_ = ax::mojom::TextAffinity::kUpstream;
break;
}
child_position = text_position->CreateChildPositionAt(i);
adjusted_offset -= max_text_offset_in_parent;
}
text_position = std::move(child_position);
} while (!text_position->IsLeaf());
BASE_DCHECK(text_position);
BASE_DCHECK(text_position->IsLeafTextPosition());
text_position->text_offset_ = adjusted_offset;
// A leaf Text position is always downstream since there is no ambiguity as
// to whether it refers to the end of the current or the start of the next
// line.
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
return text_position;
}
// We deploy three strategies in order to find the best match for an ignored
// position in the accessibility tree:
//
// 1. In the case of a text position, we move up the parent positions until we
// find the next unignored equivalent parent position. We don't do this for
// tree positions because, unlike text positions which maintain the
// corresponding text offset in the inner text of the parent node, tree
// positions would lose some information every time a parent position is
// computed. In other words, the parent position of a tree position is, in
// most cases, non-equivalent to the child position.
// 2. If no equivalent and unignored parent position can be computed, we try
// computing the leaf equivalent position. If this is unignored, we return it.
// This can happen both for tree and text positions, provided that the leaf
// node and its inner text is visible to platform APIs, i.e. it's unignored.
// 3. As a last resort, we move either to the next or previous unignored
// position in the accessibility tree, based on the "adjustment_behavior".
AXPositionInstance AsUnignoredPosition(
AXPositionAdjustmentBehavior adjustment_behavior) const {
if (IsNullPosition() || !IsIgnored())
return Clone();
AXPositionInstance leaf_tree_position = AsLeafTreePosition();
// If this is a text position, first try moving up to a parent equivalent
// position and check if the resulting position is still ignored. This
// won't result in the loss of any information. We can't do that in the
// case of tree positions, because we would be better off to move to the
// next or previous position within the same anchor, as this would lose
// less information than moving to a parent equivalent position.
//
// Text positions are considered ignored if either the current anchor is
// ignored, or if the equivalent leaf tree position is ignored.
// If this position is a leaf text position, or the equivalent leaf tree
// position is ignored, then it's not possible to create an ancestor text
// position that is unignored.
if (IsTextPosition() && !IsLeafTextPosition() &&
!leaf_tree_position->IsIgnored()) {
AXPositionInstance unignored_position = CreateParentPosition();
while (!unignored_position->IsNullPosition()) {
// Since the equivalent leaf tree position is unignored, search for the
// first unignored ancestor anchor and return that text position.
if (!unignored_position->GetAnchor()->IsIgnored()) {
BASE_DCHECK(!unignored_position->IsIgnored());
return unignored_position;
}
unignored_position = unignored_position->CreateParentPosition();
}
}
// There is a possibility that the position became unignored by moving to a
// leaf equivalent position. Otherwise, we have no choice but to move to the
// next or previous position and lose some information in the process.
while (leaf_tree_position->IsIgnored()) {
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveForward:
leaf_tree_position = leaf_tree_position->CreateNextLeafTreePosition();
break;
case AXPositionAdjustmentBehavior::kMoveBackward:
leaf_tree_position =
leaf_tree_position->CreatePreviousLeafTreePosition();
// in case the unignored leaf node contains some text, ensure that the
// resulting position is an "after text" position, as such a position
// would be the closest to the ignored one, given the fact that we are
// moving backwards through the tree.
leaf_tree_position =
leaf_tree_position->CreatePositionAtEndOfAnchor();
break;
}
}
if (IsTextPosition())
return leaf_tree_position->AsTextPosition();
return leaf_tree_position;
}
// Searches backward and forward from this position until it finds the given
// text boundary, and creates an AXRange that spans from the former to the
// latter. The resulting AXRange is always a forward range: its anchor always
// comes before its focus in document order. The resulting AXRange is bounded
// by the anchor of this position, i.e. the AXBoundaryBehavior is set to
// StopAtAnchorBoundary. The exception is ax::mojom::TextBoundary::kWebPage,
// where this behavior won't make sense. This behavior is based on current
// platform needs and might be relaxed if necessary in the future.
//
// Please note that |expand_behavior| should have no effect for
// ax::mojom::TextBoundary::kObject and ax::mojom::TextBoundary::kWebPage
// because the range should be the same regardless if we first move left or
// right.
AXRangeType ExpandToEnclosingTextBoundary(
ax::mojom::TextBoundary boundary,
AXRangeExpandBehavior expand_behavior) const {
AXBoundaryBehavior boundary_behavior =
AXBoundaryBehavior::StopAtAnchorBoundary;
if (boundary == ax::mojom::TextBoundary::kWebPage)
boundary_behavior = AXBoundaryBehavior::CrossBoundary;
switch (expand_behavior) {
case AXRangeExpandBehavior::kLeftFirst: {
AXPositionInstance left_position = CreatePositionAtTextBoundary(
boundary, ax::mojom::MoveDirection::kBackward, boundary_behavior);
AXPositionInstance right_position =
left_position->CreatePositionAtTextBoundary(
boundary, ax::mojom::MoveDirection::kForward,
boundary_behavior);
return AXRangeType(std::move(left_position), std::move(right_position));
}
case AXRangeExpandBehavior::kRightFirst: {
AXPositionInstance right_position = CreatePositionAtTextBoundary(
boundary, ax::mojom::MoveDirection::kForward, boundary_behavior);
AXPositionInstance left_position =
right_position->CreatePositionAtTextBoundary(
boundary, ax::mojom::MoveDirection::kBackward,
boundary_behavior);
return AXRangeType(std::move(left_position), std::move(right_position));
}
}
}
// Starting from this position, moves in the given direction until it finds
// the given text boundary, and creates a new position at that location.
//
// When a boundary has the "StartOrEnd" suffix, it means that this method will
// find the start boundary when moving in the backward direction, and the end
// boundary when moving in the forward direction.
AXPositionInstance CreatePositionAtTextBoundary(
ax::mojom::TextBoundary boundary,
ax::mojom::MoveDirection direction,
AXBoundaryBehavior boundary_behavior) const {
AXPositionInstance resulting_position = CreateNullPosition();
switch (boundary) {
case ax::mojom::TextBoundary::kCharacter:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousCharacterPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextCharacterPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kFormat:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousFormatStartPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextFormatEndPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kLineEnd:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousLineEndPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextLineEndPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kLineStart:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousLineStartPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextLineStartPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kLineStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousLineStartPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextLineEndPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kObject:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePositionAtStartOfAnchor();
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreatePositionAtEndOfAnchor();
break;
}
break;
case ax::mojom::TextBoundary::kPageEnd:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousPageEndPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextPageEndPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kPageStart:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousPageStartPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextPageStartPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kPageStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousPageStartPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextPageEndPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kParagraphEnd:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousParagraphEndPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position =
CreateNextParagraphEndPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kParagraphStart:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousParagraphStartPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position =
CreateNextParagraphStartPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kParagraphStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousParagraphStartPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position =
CreateNextParagraphEndPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kSentenceEnd:
BASE_LOG() << "Sentence boundaries are not yet supported.";
BASE_UNREACHABLE();
return CreateNullPosition();
case ax::mojom::TextBoundary::kSentenceStart:
BASE_LOG() << "Sentence boundaries are not yet supported.";
BASE_UNREACHABLE();
return CreateNullPosition();
case ax::mojom::TextBoundary::kSentenceStartOrEnd:
BASE_LOG() << "Sentence boundaries are not yet supported.";
BASE_UNREACHABLE();
return CreateNullPosition();
case ax::mojom::TextBoundary::kWebPage:
if (boundary_behavior != AXBoundaryBehavior::CrossBoundary) {
BASE_LOG() << "We can't reach the start of the document if we "
"are disallowed "
"from crossing boundaries.";
BASE_UNREACHABLE();
}
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePositionAtStartOfDocument();
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreatePositionAtEndOfDocument();
break;
}
break;
case ax::mojom::TextBoundary::kWordEnd:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousWordEndPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextWordEndPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kWordStart:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousWordStartPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextWordStartPosition(boundary_behavior);
break;
}
break;
case ax::mojom::TextBoundary::kWordStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousWordStartPosition(boundary_behavior);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextWordEndPosition(boundary_behavior);
break;
}
break;
}
return resulting_position;
}
AXPositionInstance CreatePositionAtStartOfAnchor() const {
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return CreateNullPosition();
case AXPositionKind::TREE_POSITION:
if (IsLeaf())
return CreateTreePosition(tree_id_, anchor_id_, BEFORE_TEXT);
return CreateTreePosition(tree_id_, anchor_id_, 0 /* child_index */);
case AXPositionKind::TEXT_POSITION:
return CreateTextPosition(tree_id_, anchor_id_, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
}
return CreateNullPosition();
}
AXPositionInstance CreatePositionAtEndOfAnchor() const {
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return CreateNullPosition();
case AXPositionKind::TREE_POSITION:
return CreateTreePosition(
tree_id_, anchor_id_,
IsEmptyObjectReplacedByCharacter() ? 0 : AnchorChildCount());
case AXPositionKind::TEXT_POSITION:
return CreateTextPosition(tree_id_, anchor_id_, MaxTextOffset(),
ax::mojom::TextAffinity::kDownstream);
}
return CreateNullPosition();
}
AXPositionInstance CreatePositionAtStartOfAXTree() const {
if (IsNullPosition() || AtStartOfAXTree())
return Clone();
// First check for positions on nodes which are AXTree boundaries, but where
// the text position on that node is not at the start of the anchor.
if (CreatePositionAtStartOfAnchor()->AtStartOfAXTree())
return CreatePositionAtStartOfAnchor();
// Iterate over tree positions until a boundary is reached.
AXPositionInstance previous_position = AsTreePosition();
do {
previous_position = previous_position->CreatePreviousAnchorPosition();
} while (!previous_position->AtStartOfAXTree());
// This method should not cross tree boundaries.
BASE_DCHECK(previous_position->tree_id() == tree_id());
if (IsTextPosition())
previous_position = previous_position->AsTextPosition();
return previous_position;
}
AXPositionInstance CreatePositionAtEndOfAXTree() const {
if (IsNullPosition() || AtEndOfAXTree())
return Clone();
// First check for positions on nodes which are AXTree boundaries, but where
// the text position on that node is not at the end of the anchor.
if (CreatePositionAtEndOfAnchor()->AtEndOfAXTree())
return CreatePositionAtEndOfAnchor();
// Iterate over tree positions until a boundary is reached.
AXPositionInstance next_position = AsTreePosition();
do {
next_position = next_position->CreateNextAnchorPosition()
->CreatePositionAtEndOfAnchor();
} while (!next_position->AtEndOfAXTree());
// This method should not cross tree boundaries.
BASE_DCHECK(next_position->tree_id() == tree_id());
if (IsTextPosition())
next_position = next_position->AsTextPosition();
return next_position->CreatePositionAtEndOfAnchor();
}
// "document" is defined here as a single, top-level, navigatable unit from
// a user's perspective. This means that all iframes are part of a single
// "document" that contains the top-level navigatable page. So this method
// will break out of an iframe and return a position at the start of the
// top-level document.
//
// Note that this definition is different than HTML's definition of
// "document", where each iframe has its own document object. For a similar
// method that stops at iframe boundaries, see
// CreatePositionAtStartOfAXTree().
AXPositionInstance CreatePositionAtStartOfDocument() const {
AXPositionInstance position =
AsTreePosition()->CreateDocumentAncestorPosition();
if (!position->IsNullPosition()) {
position = position->CreatePositionAtStartOfAnchor();
if (IsTextPosition())
position = position->AsTextPosition();
}
return position;
}
// "document" is defined here as a single, top-level, navigatable unit from
// a user's perspective. This means that all iframes are part of a single
// "document" that contains the top-level navigatable page. So this method
// will break out of an iframe and return a position at the end of the
// top-level document.
//
// Note that this definition is different than HTML's definition of
// "document", where each iframe has its own document object. For a similar
// method that stops at iframe boundaries, see CreatePositionAtEndOfAXTree().
AXPositionInstance CreatePositionAtEndOfDocument() const {
AXPositionInstance position =
AsTreePosition()->CreateDocumentAncestorPosition();
if (!position->IsNullPosition()) {
while (!position->IsLeaf()) {
position =
position->CreateChildPositionAt(position->AnchorChildCount() - 1);
}
position = position->CreatePositionAtEndOfAnchor();
if (IsTextPosition())
position = position->AsTextPosition();
}
return position;
}
AXPositionInstance CreateChildPositionAt(int child_index) const {
if (IsNullPosition() || IsLeaf())
return CreateNullPosition();
if (child_index < 0 || child_index >= AnchorChildCount())
return CreateNullPosition();
AXTreeID tree_id = AXTreeIDUnknown();
AXNode::AXID child_id = AXNode::kInvalidAXID;
AnchorChild(child_index, &tree_id, &child_id);
BASE_DCHECK(tree_id != AXTreeIDUnknown());
BASE_DCHECK(child_id != AXNode::kInvalidAXID);
switch (kind_) {
case AXPositionKind::NULL_POSITION:
BASE_UNREACHABLE();
return CreateNullPosition();
case AXPositionKind::TREE_POSITION: {
AXPositionInstance child_position =
CreateTreePosition(tree_id, child_id, 0 /* child_index */);
// If the child's anchor is a leaf node, make this a "before text"
// position.
if (child_position->IsLeaf())
child_position->child_index_ = BEFORE_TEXT;
return child_position;
}
case AXPositionKind::TEXT_POSITION:
return CreateTextPosition(tree_id, child_id, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
}
return CreateNullPosition();
}
// Creates a parent equivalent position.
//
// "move_direction" is used only in the case of a text position, when in
// the process of searching for a text boundary, and on platforms where child
// nodes are represented by embedded object characters. On such platforms, the
// "IsEmbeddedObjectInParent" method returns true. We need to decide whether
// to create a parent equivalent position that is before or after the child
// node, since moving to a parent position would always cause us to lose some
// information. We can't simply re-use the text offset of the child position
// because by definition the parent node doesn't include all the text of the
// child node, but only a single embedded object character.
//
// staticText name='Line one' IA2-hypertext='<embedded_object>'
// ++inlineTextBox name='Line one'
//
// If we are given a text position pointing to somewhere inside the
// inlineTextBox, and we move to the parent equivalent position, we need to
// decide whether the parent position would be set to point to before the
// embedded object character or after it. Both are valid, depending on the
// direction on motion, e.g. if we are trying to find the start of the line
// vs. the end of the line.
AXPositionInstance CreateParentPosition(
ax::mojom::MoveDirection move_direction =
ax::mojom::MoveDirection::kForward) const {
if (IsNullPosition())
return CreateNullPosition();
AXTreeID tree_id = AXTreeIDUnknown();
AXNode::AXID parent_id = AXNode::kInvalidAXID;
AnchorParent(&tree_id, &parent_id);
if (tree_id == AXTreeIDUnknown() || parent_id == AXNode::kInvalidAXID)
return CreateNullPosition();
switch (kind_) {
case AXPositionKind::NULL_POSITION:
BASE_UNREACHABLE();
return CreateNullPosition();
case AXPositionKind::TREE_POSITION:
return CreateTreePosition(tree_id, parent_id, AnchorIndexInParent());
case AXPositionKind::TEXT_POSITION: {
// On some platforms, such as Android, Mac and Chrome OS, the inner text
// of a node is made up by concatenating the text of child nodes. On
// other platforms, such as Windows IA2 and Linux ATK, child nodes are
// represented by a single embedded object character.
//
// If our parent's inner text is a concatenation of all its children's
// text, we need to maintain the affinity and compute the corresponding
// text offset. Otherwise, we have no choice but to return a position
// that is either before or after this child, losing some information in
// the process. Regardless to whether our parent contains all our text,
// we always recompute the affinity when the position is after the
// child.
//
// Recomputing the affinity in the latter situation is important because
// even though a text position might unambiguously be at the end of a
// line, its parent position might be the same as the parent position of
// a position that represents the start of the next line. For example:
//
// staticText name='Line oneLine two'
// ++inlineTextBox name='Line one'
// ++inlineTextBox name='Line two'
//
// If the original position is at the end of the inline text box for
// "Line one", then the resulting parent equivalent position would be
// the same as the one that would have been computed if the original
// position were at the start of the inline text box for "Line two".
const int max_text_offset = MaxTextOffset();
const int max_text_offset_in_parent =
IsEmbeddedObjectInParent() ? 1 : max_text_offset;
int parent_offset = AnchorTextOffsetInParent();
ax::mojom::TextAffinity parent_affinity = affinity_;
if (max_text_offset == max_text_offset_in_parent) {
// Our parent contains all our text. No information would be lost when
// moving to a parent equivalent position.
parent_offset += text_offset_;
} else if (text_offset_ > 0) {
// If "text_offset_" == 0, then the child position is clearly before
// any embedded object character. No information would be lost when
// moving to a parent equivalent position, including affinity
// information. Otherwise, we should decide whether to set the parent
// position to be before or after the child, based on the direction of
// motion, and also reset the affinity.
switch (move_direction) {
case ax::mojom::MoveDirection::kBackward:
// Keep the offset to be right before the embedded object
// character.
break;
case ax::mojom::MoveDirection::kForward:
// Set the offset to be after the embedded object character.
parent_offset += max_text_offset_in_parent;
break;
}
// The original affinity doesn't apply any more. In most cases, it
// should be downstream, unless there is an ambiguity as to whether
// the parent position is between the end of one line and the start of
// the next. We perform this check below.
parent_affinity = ax::mojom::TextAffinity::kDownstream;
}
AXPositionInstance parent_position = CreateTextPosition(
tree_id, parent_id, parent_offset, parent_affinity);
// If the current position is pointing at the end of its anchor, we need
// to check if the parent position has introduced ambiguity as to
// whether it refers to the end of a line or the start of the next.
// Ambiguity is only present when the parent position points to a text
// offset that is neither at the start nor at the end of its anchor. We
// check for ambiguity by creating the parent position and testing if it
// is erroneously at the start of the next line. Given that the current
// position, by the nature of being at the end of its anchor, could only
// be at end of line, the fact that the parent position is also
// determined to be at start of line demonstrates the presence of
// ambiguity which is resolved by setting its affinity to upstream.
//
// We could not have
// checked if the child was at the end of the line, because our
// "AtEndOfLine" predicate takes into account trailing line breaks,
// which would create false positives.
if (text_offset_ == max_text_offset &&
!parent_position->AtEndOfAnchor() &&
parent_position->AtStartOfLine()) {
parent_position->affinity_ = ax::mojom::TextAffinity::kUpstream;
}
return parent_position;
}
}
return CreateNullPosition();
}
// Creates a tree position using the next text-only node as its anchor.
// Assumes that text-only nodes are leaf nodes.
AXPositionInstance CreateNextLeafTreePosition() const {
return CreateNextLeafTreePosition(&DefaultAbortMovePredicate);
}
// Creates a tree position using the previous text-only node as its anchor.
// Assumes that text-only nodes are leaf nodes.
AXPositionInstance CreatePreviousLeafTreePosition() const {
return CreatePreviousLeafTreePosition(&DefaultAbortMovePredicate);
}
// Creates the next text position anchored at a leaf node of the AXTree.
//
// If a pointer |crossed_line_breaking_object| is provided, it'll be set to
// |true| if any line breaking object boundary was crossed by moving from this
// leaf text position to the next (if it exists), |false| otherwise.
AXPositionInstance CreateNextLeafTextPosition(
bool* crossed_line_breaking_object = nullptr) const {
if (crossed_line_breaking_object)
*crossed_line_breaking_object = false;
// If this is an ancestor text position, resolve to its leaf text position.
if (IsTextPosition() && !IsLeaf())
return AsLeafTextPosition();
std::function<AbortMovePredicate> abort_move_predicate;
if (crossed_line_breaking_object) {
abort_move_predicate = [crossed_line_breaking_object](
const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType type,
const AXMoveDirection direction) {
return UpdateCrossedLineBreakingObjectToken(
*crossed_line_breaking_object, move_from, move_to, type, direction);
};
} else {
abort_move_predicate =
[](const AXPosition& move_from, const AXPosition& move_to,
const AXMoveType type, const AXMoveDirection direction) {
return AXPosition::DefaultAbortMovePredicate(move_from, move_to,
type, direction);
};
}
return CreateNextLeafTreePosition(abort_move_predicate)->AsTextPosition();
}
// Creates a text position using the previous text-only node as its anchor.
// Assumes that text-only nodes are leaf nodes.
AXPositionInstance CreatePreviousLeafTextPosition() const {
return CreatePreviousTextAnchorPosition(DefaultAbortMovePredicate);
}
// Returns a text position located right before the next character (from this
// position) in the tree's text representation, following these conditions:
//
// - If this position is at the end of its anchor, normalize it to the start
// of the next text anchor, regardless of the position's affinity.
// Both text positions are equal when compared, but we consider the start of
// an anchor to be a position BEFORE its first character and the end of the
// previous to be AFTER its last character.
//
// - Skip any empty text anchors; they're "invisible" to the text
// representation and the next character could be ahead.
//
// - Return a null position if there is no next character forward.
//
// If possible, return a position anchored at the current position's anchor;
// this is necessary because we don't want to return any position that might
// be located in the shadow DOM or in a position anchored at a node that is
// not visible to a specific platform's APIs.
//
// Also, |text_offset| is adjusted to point to a valid character offset, i.e.
// it cannot be pointing to a low surrogate pair or to the middle of a
// grapheme cluster.
AXPositionInstance AsLeafTextPositionBeforeCharacter() const {
if (IsNullPosition())
return Clone();
AXPositionInstance text_position = AsTextPosition();
// In case the input affinity is upstream, reset it to downstream.
//
// This is to ensure that when we find the equivalent leaf text position, it
// will be at the start of anchor if the original position is anchored to a
// node higher up in the tree and pointing to a text offset that falls on
// the boundary between two leaf nodes. In other words, the returned
// position will always be "before character".
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
text_position = text_position->AsLeafTextPosition();
BASE_DCHECK(!text_position->IsNullPosition())
<< "Adjusting to a leaf position should never turn a non-null position "
"into a null one.";
if (!text_position->IsIgnored() && !text_position->AtEndOfAnchor()) {
BASE_DCHECK(text_position->text_offset_ >= 0);
return text_position;
}
text_position = text_position->CreateNextLeafTextPosition();
while (!text_position->IsNullPosition() &&
(text_position->IsIgnored() || !text_position->MaxTextOffset())) {
text_position = text_position->CreateNextLeafTextPosition();
}
return text_position;
}
// Returns a text position located right after the previous character (from
// this position) in the tree's text representation.
//
// See `AsLeafTextPositionBeforeCharacter`, as this is its "reversed" version.
AXPositionInstance AsLeafTextPositionAfterCharacter() const {
if (IsNullPosition())
return Clone();
AXPositionInstance text_position = AsTextPosition();
// Temporarily set the affinity to upstream.
//
// This is to ensure that when we find the equivalent leaf text position, it
// will be at the end of anchor if the original position is anchored to a
// node higher up in the tree and pointing to a text offset that falls on
// the boundary between two leaf nodes. In other words, the returned
// position will always be "after character".
text_position->affinity_ = ax::mojom::TextAffinity::kUpstream;
text_position = text_position->AsLeafTextPosition();
BASE_DCHECK(!text_position->IsNullPosition())
<< "Adjusting to a leaf position should never turn a non-null position "
"into a null one.";
if (!text_position->IsIgnored() && !text_position->AtStartOfAnchor()) {
// The following situation should not be possible but there are existing
// crashes in the field.
//
// TODO(nektar): Remove this workaround as soon as the source of the bug
// is identified.
BASE_DCHECK(text_position->text_offset_ >= 0);
// TODO(chunhtai): handles grapheme.
// Reset the affinity to downstream, because an upstream affinity doesn't
// make sense on a leaf anchor.
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
return text_position;
}
text_position = text_position->CreatePreviousLeafTextPosition();
while (!text_position->IsNullPosition() &&
(text_position->IsIgnored() || !text_position->MaxTextOffset())) {
text_position = text_position->CreatePreviousLeafTextPosition();
}
return text_position->CreatePositionAtEndOfAnchor();
}
// Creates a position pointing to before the next character, which is defined
// as the start of the next grapheme cluster. Also, ensures that the created
// position will not point to a low surrogate pair.
//
// A grapheme cluster is what an end-user would consider a character and it
// could include a letter with additional diacritics. It could be more than
// one Unicode code unit in length.
//
// See also http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
AXPositionInstance CreateNextCharacterPosition(
AXBoundaryBehavior boundary_behavior) const {
if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary &&
AtEndOfAnchor()) {
return Clone();
}
// There is no next character position.
AXPositionInstance text_position = AsLeafTextPositionBeforeCharacter();
if (text_position->IsNullPosition()) {
if (boundary_behavior == AXBoundaryBehavior::StopIfAlreadyAtBoundary ||
boundary_behavior == AXBoundaryBehavior::StopAtLastAnchorBoundary) {
text_position = Clone();
}
return text_position;
}
if (boundary_behavior == AXBoundaryBehavior::StopIfAlreadyAtBoundary &&
*text_position == *this) {
return Clone();
}
BASE_DCHECK(text_position->text_offset_ < text_position->MaxTextOffset());
// TODO(chunhtai): Need to consider grapheme cluster.
++text_position->text_offset_;
BASE_DCHECK(text_position->text_offset_ > 0);
BASE_DCHECK(text_position->text_offset_ <= text_position->MaxTextOffset());
// If the character boundary is in the same subtree, return a position
// rooted at this position's anchor. This is necessary because we don't want
// to return a position that might be in the shadow DOM when this position
// is not.
const AXNodeType* common_anchor = text_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
text_position = text_position->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kForward);
} else if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary) {
// If the next character position crosses the current anchor boundary
// with StopAtAnchorBoundary, snap to the end of the current anchor.
return CreatePositionAtEndOfAnchor();
}
// Even if the resulting position is right on a soft line break, affinity is
// defaulted to downstream so that this method will always produce the same
// result regardless of the direction of motion or the input affinity.
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
if (IsTreePosition())
return text_position->AsTreePosition();
return text_position;
}
// Creates a position pointing to before the previous character, which is
// defined as the start of the previous grapheme cluster. Also, ensures that
// the created position will not point to a low surrogate pair.
//
// See the comment above `CreateNextCharacterPosition` for the definition of a
// grapheme cluster.
AXPositionInstance CreatePreviousCharacterPosition(
AXBoundaryBehavior boundary_behavior) const {
if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary &&
AtStartOfAnchor()) {
return Clone();
}
// There is no previous character position.
AXPositionInstance text_position = AsLeafTextPositionAfterCharacter();
if (text_position->IsNullPosition()) {
if (boundary_behavior == AXBoundaryBehavior::StopIfAlreadyAtBoundary ||
boundary_behavior == AXBoundaryBehavior::StopAtLastAnchorBoundary) {
text_position = Clone();
}
return text_position;
}
if (boundary_behavior == AXBoundaryBehavior::StopIfAlreadyAtBoundary &&
*text_position == *this) {
return Clone();
}
BASE_DCHECK(text_position->text_offset_ > 0);
// TODO(chunhtai): Need to consider grapheme cluster.
--text_position->text_offset_;
BASE_DCHECK(text_position->text_offset_ >= 0);
BASE_DCHECK(text_position->text_offset_ < text_position->MaxTextOffset());
// The character boundary should be in the same subtree. Return a position
// rooted at this position's anchor. This is necessary because we don't want
// to return a position that might be in the shadow DOM when this position
// is not.
const AXNodeType* common_anchor = text_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
text_position = text_position->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kBackward);
} else if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary) {
// If the previous character position crosses the current anchor boundary
// with StopAtAnchorBoundary, snap to the start of the current anchor.
return CreatePositionAtStartOfAnchor();
}
// Even if the resulting position is right on a soft line break, affinity is
// defaulted to downstream so that this method will always produce the same
// result regardless of the direction of motion or the input affinity.
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
if (IsTreePosition())
return text_position->AsTreePosition();
return text_position;
}
AXPositionInstance CreateNextWordStartPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryStartPosition(
boundary_behavior, ax::mojom::MoveDirection::kForward,
&AtStartOfWordPredicate, &AtEndOfWordPredicate,
&GetWordStartOffsetsFunc);
}
AXPositionInstance CreatePreviousWordStartPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryStartPosition(
boundary_behavior, ax::mojom::MoveDirection::kBackward,
&AtStartOfWordPredicate, &AtEndOfWordPredicate,
&GetWordStartOffsetsFunc);
}
// Word end positions are one past the last character of the word.
AXPositionInstance CreateNextWordEndPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryEndPosition(
boundary_behavior, ax::mojom::MoveDirection::kForward,
&AtStartOfWordPredicate, &AtEndOfWordPredicate, &GetWordEndOffsetsFunc);
}
// Word end positions are one past the last character of the word.
AXPositionInstance CreatePreviousWordEndPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryEndPosition(
boundary_behavior, ax::mojom::MoveDirection::kBackward,
&AtStartOfWordPredicate, &AtEndOfWordPredicate, &GetWordEndOffsetsFunc);
}
AXPositionInstance CreateNextLineStartPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryStartPosition(
boundary_behavior, ax::mojom::MoveDirection::kForward,
&AtStartOfLinePredicate, &AtEndOfLinePredicate);
}
AXPositionInstance CreatePreviousLineStartPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryStartPosition(
boundary_behavior, ax::mojom::MoveDirection::kBackward,
&AtStartOfLinePredicate, &AtEndOfLinePredicate);
}
// Line end positions are one past the last character of the line, excluding
// any white space or newline characters that separate the lines.
AXPositionInstance CreateNextLineEndPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryEndPosition(
boundary_behavior, ax::mojom::MoveDirection::kForward,
&AtStartOfLinePredicate, &AtEndOfLinePredicate);
}
// Line end positions are one past the last character of the line, excluding
// any white space or newline characters separating the lines.
AXPositionInstance CreatePreviousLineEndPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryEndPosition(
boundary_behavior, ax::mojom::MoveDirection::kBackward,
&AtStartOfLinePredicate, &AtEndOfLinePredicate);
}
AXPositionInstance CreatePreviousFormatStartPosition(
AXBoundaryBehavior boundary_behavior) const {
if (IsNullPosition())
return Clone();
AXBoundaryType boundary_type = GetFormatStartBoundaryType();
if (boundary_type != AXBoundaryType::kNone) {
if (boundary_behavior == AXBoundaryBehavior::StopIfAlreadyAtBoundary ||
(boundary_behavior == AXBoundaryBehavior::StopAtLastAnchorBoundary &&
boundary_type == AXBoundaryType::kDocumentStart)) {
AXPositionInstance clone = Clone();
// In order to make equality checks simpler, affinity should be reset so
// that we would get consistent output from this function regardless of
// input affinity.
clone->affinity_ = ax::mojom::TextAffinity::kDownstream;
return clone;
} else if (boundary_behavior == AXBoundaryBehavior::CrossBoundary &&
boundary_type == AXBoundaryType::kDocumentStart) {
// If we're at a format boundary and there are no more text positions
// to traverse, return a null position for cross-boundary moves.
return CreateNullPosition();
}
}
AXPositionInstance tree_position =
AsTreePosition()->CreatePositionAtStartOfAnchor();
AXPositionInstance previous_tree_position =
tree_position->CreatePreviousLeafTreePosition();
// If moving to the start of the current anchor hasn't changed our position
// from the original position, we need to test the previous leaf tree
// position.
if (AtStartOfAnchor() &&
boundary_behavior != AXBoundaryBehavior::StopIfAlreadyAtBoundary) {
tree_position = std::move(previous_tree_position);
previous_tree_position = tree_position->CreatePreviousLeafTreePosition();
}
// The first position in the document is also a format start boundary, so we
// should not return NullPosition unless we started from that location.
while (boundary_type != AXBoundaryType::kDocumentStart &&
!previous_tree_position->IsNullPosition() &&
!tree_position->AtStartOfFormat()) {
tree_position = std::move(previous_tree_position);
previous_tree_position = tree_position->CreatePreviousLeafTreePosition();
}
// If the format boundary is in the same subtree, return a position rooted
// at the current position.
// This is necessary because we don't want to return any position that might
// be in the shadow DOM if the original position was not.
const AXNodeType* common_anchor = tree_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
tree_position = tree_position->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kBackward);
} else if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary) {
return CreatePositionAtStartOfAnchor();
}
if (IsTextPosition())
return tree_position->AsTextPosition();
return tree_position;
}
AXPositionInstance CreateNextFormatEndPosition(
AXBoundaryBehavior boundary_behavior) const {
if (IsNullPosition())
return Clone();
AXBoundaryType boundary_type = GetFormatEndBoundaryType();
if (boundary_type != AXBoundaryType::kNone) {
if (boundary_behavior == AXBoundaryBehavior::StopIfAlreadyAtBoundary ||
(boundary_behavior == AXBoundaryBehavior::StopAtLastAnchorBoundary &&
boundary_type == AXBoundaryType::kDocumentEnd)) {
AXPositionInstance clone = Clone();
// In order to make equality checks simpler, affinity should be reset so
// that we would get consistent output from this function regardless of
// input affinity.
clone->affinity_ = ax::mojom::TextAffinity::kDownstream;
return clone;
} else if (boundary_behavior == AXBoundaryBehavior::CrossBoundary &&
boundary_type == AXBoundaryType::kDocumentEnd) {
// If we're at a format boundary and there are no more text positions
// to traverse, return a null position for cross-boundary moves.
return CreateNullPosition();
}
}
AXPositionInstance tree_position =
AsTreePosition()->CreatePositionAtEndOfAnchor();
AXPositionInstance next_tree_position =
tree_position->CreateNextLeafTreePosition()
->CreatePositionAtEndOfAnchor();
// If moving to the end of the current anchor hasn't changed our original
// position, we need to test the next leaf tree position.
if (AtEndOfAnchor() &&
boundary_behavior != AXBoundaryBehavior::StopIfAlreadyAtBoundary) {
tree_position = std::move(next_tree_position);
next_tree_position = tree_position->CreateNextLeafTreePosition()
->CreatePositionAtEndOfAnchor();
}
// The last position in the document is also a format end boundary, so we
// should not return NullPosition unless we started from that location.
while (boundary_type != AXBoundaryType::kDocumentEnd &&
!next_tree_position->IsNullPosition() &&
!tree_position->AtEndOfFormat()) {
tree_position = std::move(next_tree_position);
next_tree_position = tree_position->CreateNextLeafTreePosition()
->CreatePositionAtEndOfAnchor();
}
// If the format boundary is in the same subtree, return a position
// rooted at the current position.
// This is necessary because we don't want to return any position that might
// be in the shadow DOM if the original position was not.
const AXNodeType* common_anchor = tree_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
tree_position = tree_position->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kForward);
} else if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary) {
return CreatePositionAtEndOfAnchor();
}
if (IsTextPosition())
return tree_position->AsTextPosition();
return tree_position;
}
AXPositionInstance CreateNextParagraphStartPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryStartPosition(
boundary_behavior, ax::mojom::MoveDirection::kForward,
&AtStartOfParagraphPredicate, &AtEndOfParagraphPredicate);
}
AXPositionInstance CreatePreviousParagraphStartPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryStartPosition(
boundary_behavior, ax::mojom::MoveDirection::kBackward,
&AtStartOfParagraphPredicate, &AtEndOfParagraphPredicate);
}
AXPositionInstance CreateNextParagraphEndPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryEndPosition(
boundary_behavior, ax::mojom::MoveDirection::kForward,
&AtStartOfParagraphPredicate, &AtEndOfParagraphPredicate);
}
AXPositionInstance CreatePreviousParagraphEndPosition(
AXBoundaryBehavior boundary_behavior) const {
AXPositionInstance previous_position = CreateBoundaryEndPosition(
boundary_behavior, ax::mojom::MoveDirection::kBackward,
&AtStartOfParagraphPredicate, &AtEndOfParagraphPredicate);
if (boundary_behavior == AXBoundaryBehavior::CrossBoundary ||
boundary_behavior == AXBoundaryBehavior::StopAtLastAnchorBoundary) {
// This is asymmetric with CreateNextParagraphEndPosition due to
// asymmetries in text anchor movement. Consider:
//
// ++1 rootWebArea
// ++++2 staticText name="FIRST"
// ++++3 genericContainer isLineBreakingObject=true
// ++++++4 genericContainer isLineBreakingObject=true
// ++++++5 staticText name="SECOND"
//
// Node 2 offset 5 FIRST<> is a paragraph end since node 3 is a line-
// breaking object that's not collapsible (since it's not a leaf). When
// looking for the next text anchor position from there, we advance to
// sibling node 3, then since that node has descendants, we convert to a
// tree position to find the leaf node that maps to "node 3 offset 0".
// Since node 4 has no text, we skip it and land on node 5. We end up at
// node 5 offset 6 SECOND<> as our next paragraph end.
//
// The set of paragraph ends should be consistent when moving in the
// reverse direction. But starting from node 5 offset 6, the previous text
// anchor position is previous sibling node 4. We'll consider that a
// paragraph end since it's a leaf line-breaking object and stop.
//
// Essentially, we have two consecutive line-breaking objects, each of
// which stops movement in the "outward" direction, for different reasons.
//
// We handle this by looking back one more step after finding a candidate
// for previous paragraph end, then testing a forward step from the look-
// back position. That will land us on the candidate position if it's a
// valid paragraph boundary.
//
while (!previous_position->IsNullPosition()) {
AXPositionInstance look_back_position =
previous_position->AsLeafTextPosition()
->CreatePreviousLeafTextPosition()
->CreatePositionAtEndOfAnchor();
if (look_back_position->IsNullPosition()) {
// Nowhere to look back to, so our candidate must be a valid paragraph
// boundary.
break;
}
AXPositionInstance forward_step_position =
look_back_position->CreateNextLeafTextPosition()
->CreatePositionAtEndOfAnchor();
if (*forward_step_position == *previous_position)
break;
previous_position = previous_position->CreateBoundaryEndPosition(
boundary_behavior, ax::mojom::MoveDirection::kBackward,
&AtStartOfParagraphPredicate, &AtEndOfParagraphPredicate);
}
}
return previous_position;
}
AXPositionInstance CreateNextPageStartPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryStartPosition(
boundary_behavior, ax::mojom::MoveDirection::kForward,
&AtStartOfPagePredicate, &AtEndOfPagePredicate);
}
AXPositionInstance CreatePreviousPageStartPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryStartPosition(
boundary_behavior, ax::mojom::MoveDirection::kBackward,
&AtStartOfPagePredicate, &AtEndOfPagePredicate);
}
AXPositionInstance CreateNextPageEndPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryEndPosition(
boundary_behavior, ax::mojom::MoveDirection::kForward,
&AtStartOfPagePredicate, &AtEndOfPagePredicate);
}
AXPositionInstance CreatePreviousPageEndPosition(
AXBoundaryBehavior boundary_behavior) const {
return CreateBoundaryEndPosition(
boundary_behavior, ax::mojom::MoveDirection::kBackward,
&AtStartOfPagePredicate, &AtEndOfPagePredicate);
}
AXPositionInstance CreateBoundaryStartPosition(
AXBoundaryBehavior boundary_behavior,
ax::mojom::MoveDirection move_direction,
BoundaryConditionPredicate at_start_condition,
BoundaryConditionPredicate at_end_condition,
BoundaryTextOffsetsFunc get_start_offsets = {}) const {
AXPositionInstance text_position = AsLeafTextPosition();
if (text_position->IsNullPosition())
return text_position;
if (boundary_behavior != AXBoundaryBehavior::StopIfAlreadyAtBoundary) {
text_position =
text_position->CreateAdjacentLeafTextPosition(move_direction);
if (text_position->IsNullPosition()) {
// There is no adjacent position to move to; in such case, CrossBoundary
// behavior shall return a null position, while any other behavior shall
// fallback to return the initial position.
if (boundary_behavior == AXBoundaryBehavior::CrossBoundary)
return text_position;
return Clone();
}
}
if (!at_start_condition(text_position)) {
text_position = text_position->CreatePositionAtNextOffsetBoundary(
move_direction, get_start_offsets);
while (!at_start_condition(text_position)) {
AXPositionInstance next_position;
if (move_direction == ax::mojom::MoveDirection::kForward) {
next_position = text_position->CreateNextLeafTextPosition();
} else {
if (text_position->AtStartOfAnchor()) {
next_position = text_position->CreatePreviousLeafTextPosition();
} else {
text_position = text_position->CreatePositionAtStartOfAnchor();
BASE_DCHECK(!text_position->IsNullPosition());
continue;
}
}
if (next_position->IsNullPosition()) {
if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary) {
switch (move_direction) {
case ax::mojom::MoveDirection::kForward:
return CreatePositionAtEndOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveForward);
case ax::mojom::MoveDirection::kBackward:
return CreatePositionAtStartOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveBackward);
}
}
if (boundary_behavior ==
AXBoundaryBehavior::StopAtLastAnchorBoundary) {
// We can't simply return the following position; break and after
// this loop we'll try to do some adjustments to text_position.
switch (move_direction) {
case ax::mojom::MoveDirection::kForward:
text_position = text_position->CreatePositionAtEndOfAnchor();
break;
case ax::mojom::MoveDirection::kBackward:
text_position = text_position->CreatePositionAtStartOfAnchor();
break;
}
break;
}
return next_position->AsUnignoredPosition(
AdjustmentBehaviorFromBoundaryDirection(move_direction));
}
// Continue searching for the next boundary start in the specified
// direction until the next logical text position is reached.
text_position = next_position->CreatePositionAtFirstOffsetBoundary(
move_direction, get_start_offsets);
}
}
// If the boundary is in the same subtree, return a position rooted at this
// position's anchor. This is necessary because we don't want to return a
// position that might be in the shadow DOM when this position is not.
const AXNodeType* common_anchor = text_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
text_position =
text_position->CreateAncestorPosition(common_anchor, move_direction);
} else if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary) {
switch (move_direction) {
case ax::mojom::MoveDirection::kForward:
return CreatePositionAtEndOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveForward);
case ax::mojom::MoveDirection::kBackward:
return CreatePositionAtStartOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveBackward);
}
}
// Affinity is only upstream at the end of a line, and so a start boundary
// will never have an upstream affinity.
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
if (IsTreePosition())
text_position = text_position->AsTreePosition();
AXPositionInstance unignored_position = text_position->AsUnignoredPosition(
AdjustmentBehaviorFromBoundaryDirection(move_direction));
// If there are no unignored positions in |move_direction| then
// |text_position| is anchored in ignored content at the start or end
// of the document.
// For StopAtLastAnchorBoundary, try to adjust in the opposite direction
// to return a position within the document just before crossing into
// the ignored content. This will be the last unignored anchor boundary.
if (unignored_position->IsNullPosition() &&
boundary_behavior == AXBoundaryBehavior::StopAtLastAnchorBoundary) {
unignored_position =
text_position->AsUnignoredPosition(OppositeAdjustmentBehavior(
AdjustmentBehaviorFromBoundaryDirection(move_direction)));
}
return unignored_position;
}
AXPositionInstance CreateBoundaryEndPosition(
AXBoundaryBehavior boundary_behavior,
ax::mojom::MoveDirection move_direction,
BoundaryConditionPredicate at_start_condition,
BoundaryConditionPredicate at_end_condition,
BoundaryTextOffsetsFunc get_end_offsets = {}) const {
AXPositionInstance text_position = AsLeafTextPosition();
if (text_position->IsNullPosition())
return text_position;
if (boundary_behavior != AXBoundaryBehavior::StopIfAlreadyAtBoundary) {
text_position =
text_position->CreateAdjacentLeafTextPosition(move_direction);
if (text_position->IsNullPosition()) {
// There is no adjacent position to move to; in such case, CrossBoundary
// behavior shall return a null position, while any other behavior shall
// fallback to return the initial position.
if (boundary_behavior == AXBoundaryBehavior::CrossBoundary)
return text_position;
return Clone();
}
}
if (!at_end_condition(text_position)) {
text_position = text_position->CreatePositionAtNextOffsetBoundary(
move_direction, get_end_offsets);
while (!at_end_condition(text_position)) {
AXPositionInstance next_position;
if (move_direction == ax::mojom::MoveDirection::kForward) {
if (text_position->AtEndOfAnchor()) {
next_position = text_position->CreateNextLeafTextPosition();
} else {
text_position = text_position->CreatePositionAtEndOfAnchor();
BASE_DCHECK(!text_position->IsNullPosition());
continue;
}
} else {
next_position = text_position->CreatePreviousLeafTextPosition()
->CreatePositionAtEndOfAnchor();
}
if (next_position->IsNullPosition()) {
if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary) {
switch (move_direction) {
case ax::mojom::MoveDirection::kForward:
return CreatePositionAtEndOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveForward);
case ax::mojom::MoveDirection::kBackward:
return CreatePositionAtStartOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveBackward);
}
}
if (boundary_behavior ==
AXBoundaryBehavior::StopAtLastAnchorBoundary) {
// We can't simply return the following position; break and after
// this loop we'll try to do some adjustments to text_position.
switch (move_direction) {
case ax::mojom::MoveDirection::kForward:
text_position = text_position->CreatePositionAtEndOfAnchor();
break;
case ax::mojom::MoveDirection::kBackward:
text_position = text_position->CreatePositionAtStartOfAnchor();
break;
}
break;
}
return next_position->AsUnignoredPosition(
AdjustmentBehaviorFromBoundaryDirection(move_direction));
}
// Continue searching for the next boundary end in the specified
// direction until the next logical text position is reached.
text_position = next_position->CreatePositionAtFirstOffsetBoundary(
move_direction, get_end_offsets);
}
}
// If the boundary is in the same subtree, return a position rooted at this
// position's anchor. This is necessary because we don't want to return a
// position that might be in the shadow DOM when this position is not.
const AXNodeType* common_anchor = text_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
text_position =
text_position->CreateAncestorPosition(common_anchor, move_direction);
} else if (boundary_behavior == AXBoundaryBehavior::StopAtAnchorBoundary) {
switch (move_direction) {
case ax::mojom::MoveDirection::kForward:
return CreatePositionAtEndOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveForward);
case ax::mojom::MoveDirection::kBackward:
return CreatePositionAtStartOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveBackward);
}
}
// If there is no ambiguity as to whether the position is at the end of
// the current boundary or the start of the next boundary, an upstream
// affinity should be reset to downstream in order to get consistent output
// from this method, regardless of input affinity.
//
// Note that there could be no ambiguity if the boundary is either at the
// start or the end of the current anchor, so we should always reset to
// downstream affinity in those cases.
if (text_position->affinity_ == ax::mojom::TextAffinity::kUpstream) {
AXPositionInstance downstream_position = text_position->Clone();
downstream_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
if (downstream_position->AtStartOfAnchor() ||
downstream_position->AtEndOfAnchor() ||
!at_start_condition(downstream_position)) {
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
}
}
if (IsTreePosition())
text_position = text_position->AsTreePosition();
AXPositionInstance unignored_position = text_position->AsUnignoredPosition(
AdjustmentBehaviorFromBoundaryDirection(move_direction));
// If there are no unignored positions in |move_direction| then
// |text_position| is anchored in ignored content at the start or end
// of the document.
// For StopAtLastAnchorBoundary, try to adjust in the opposite direction
// to return a position within the document just before crossing into
// the ignored content. This will be the last unignored anchor boundary.
if (unignored_position->IsNullPosition() &&
boundary_behavior == AXBoundaryBehavior::StopAtLastAnchorBoundary) {
unignored_position =
text_position->AsUnignoredPosition(OppositeAdjustmentBehavior(
AdjustmentBehaviorFromBoundaryDirection(move_direction)));
}
return unignored_position;
}
// TODO(nektar): Add sentence navigation methods.
// Uses depth-first pre-order traversal.
AXPositionInstance CreateNextAnchorPosition() const {
return CreateNextAnchorPosition(&DefaultAbortMovePredicate);
}
// Uses depth-first pre-order traversal.
AXPositionInstance CreatePreviousAnchorPosition() const {
return CreatePreviousAnchorPosition(&DefaultAbortMovePredicate);
}
// Returns an optional integer indicating the logical order of this position
// compared to another position or returns an empty optional if the positions
// are not comparable. Any text position at the same character location is
// logically equivalent although they may be on different anchors or have
// different text offsets. Positions are not comparable when one position is
// null and the other is not or if the positions do not have any common
// ancestor.
// 0: if this position is logically equivalent to the other position
// <0: if this position is logically less than the other position
// >0: if this position is logically greater than the other position
std::optional<int> CompareTo(const AXPosition& other) const {
if (this->IsNullPosition() && other.IsNullPosition())
return std::optional<int>(0);
if (this->IsNullPosition() || other.IsNullPosition())
return std::optional<int>(std::nullopt);
// If both positions share an anchor and are of the same type, we can do a
// straight compare of text offsets or child indices.
if (GetAnchor() == other.GetAnchor()) {
if (IsTextPosition() && other.IsTextPosition())
return text_offset() - other.text_offset();
if (IsTreePosition() && other.IsTreePosition())
return child_index() - other.child_index();
}
// Ancestor positions are expensive to compute. If possible, we will avoid
// doing so by computing the ancestor chain of the two positions' anchors.
// If the lowest common ancestor is neither position's anchor, we can use
// the order of the first uncommon ancestors as a proxy for the order of the
// positions.
//
// In order to do that, we need to normalize text positions at the end of an
// anchor to equivalent positions at the start of the next anchor. Ignored
// positions are a special case in that they need to be shifted to the
// nearest unignored position in order to be normalized. That shifting can
// change the comparison result, so if we have an ignored position, we must
// use the slow path.
if (IsIgnored() || other.IsIgnored())
return SlowCompareTo(other);
// Normalize any text positions at the end of an anchor to equivalent
// positions at the start of the next anchor.
AXPositionInstance normalized_this_position = Clone();
if (normalized_this_position->IsTextPosition()) {
normalized_this_position =
normalized_this_position->AsLeafTextPositionBeforeCharacter();
}
AXPositionInstance normalized_other_position = other.Clone();
if (normalized_other_position->IsTextPosition()) {
normalized_other_position =
normalized_other_position->AsLeafTextPositionBeforeCharacter();
}
if (normalized_this_position->IsNullPosition()) {
if (normalized_other_position->IsNullPosition()) {
// Both positions normalized to a position past the end of the document.
BASE_DCHECK(SlowCompareTo(other).value() == 0);
return 0;
}
// |this| normalized to a position past the end of the document.
BASE_DCHECK(SlowCompareTo(other).value() > 0);
return 1;
} else if (normalized_other_position->IsNullPosition()) {
// |other| normalized to a position past the end of the document.
BASE_DCHECK(SlowCompareTo(other).value() < 0);
return -1;
}
// Compute the ancestor stacks of both positions and walk them ourselves
// rather than calling LowestCommonAnchor(). That way, we can discover the
// first uncommon ancestors.
const AXNodeType* common_anchor = nullptr;
std::stack<AXNodeType*> our_ancestors =
normalized_this_position->GetAncestorAnchors();
std::stack<AXNodeType*> other_ancestors =
normalized_other_position->GetAncestorAnchors();
while (!our_ancestors.empty() && !other_ancestors.empty() &&
our_ancestors.top() == other_ancestors.top()) {
common_anchor = our_ancestors.top();
our_ancestors.pop();
other_ancestors.pop();
}
if (!common_anchor)
return std::optional<int>(std::nullopt);
// If each position has an uncommon ancestor node, we can compare those
// instead of needing to compute ancestor positions.
if (!our_ancestors.empty() && !other_ancestors.empty()) {
AXPositionInstance this_uncommon_tree_position = CreateTreePosition(
GetTreeID(our_ancestors.top()), GetAnchorID(our_ancestors.top()),
0 /*child_index*/);
int this_uncommon_ancestor_index =
this_uncommon_tree_position->AnchorIndexInParent();
AXPositionInstance other_uncommon_tree_position = CreateTreePosition(
GetTreeID(other_ancestors.top()), GetAnchorID(other_ancestors.top()),
0 /*child_index*/);
int other_uncommon_ancestor_index =
other_uncommon_tree_position->AnchorIndexInParent();
BASE_DCHECK(this_uncommon_ancestor_index !=
other_uncommon_ancestor_index);
int result = this_uncommon_ancestor_index - other_uncommon_ancestor_index;
// On platforms that support embedded objects, if a text position is
// within an embedded object and if it is not at the start of that object,
// the resulting ancestor position should be adjusted to point after the
// embedded object. Otherwise, assistive software will not be able to get
// out of the embedded object if its text is not editable when navigating
// by character.
//
// For example, look at the following accessibility tree and the two
// example text positions together with their equivalent ancestor
// positions.
// ++1 kRootWebArea
// ++++2 kTextField "Before<embedded_object>after"
// ++++++3 kStaticText "Before"
// ++++++++4 kInlineTextBox "Before"
// ++++++5 kImage "Test image"
// ++++++6 kStaticText "after"
// ++++++++7 kInlineTextBox "after"
//
// Note that the alt text of an image cannot be navigated with cursor
// left/right, even when the rest of the contents are in a
// contenteditable.
//
// Ancestor position should not be adjusted:
// TextPosition anchor_id=kImage text_offset=0 affinity=downstream
// annotated_text=<T>est image AncestorTextPosition anchor_id=kTextField
// text_offset=6 affinity=downstream
// annotated_text=Before<embedded_object>after
//
// Ancestor position should be adjusted:
// TextPosition anchor_id=kImage text_offset=1 affinity=downstream
// annotated_text=T<e>st image AncestorTextPosition anchor_id=kTextField
// text_offset=7 affinity=downstream
// annotated_text=Beforeembedded_object<a>fter
//
// Note that since the adjustment to the distance between the ancestor
// positions could at most be by one, we skip doing this check if the
// ancestor positions have a distance of more than one since it can never
// change the outcome of the comparison. Note too that if both ancestor
// positions need to be adjusted, the adjustments will cancel out.
if (abs(result) == 1) {
if (!normalized_this_position->AtStartOfAnchor() &&
this_uncommon_tree_position->IsEmbeddedObjectInParent()) {
result += 1;
}
if (!normalized_other_position->AtStartOfAnchor() &&
other_uncommon_tree_position->IsEmbeddedObjectInParent()) {
result -= 1;
}
}
#ifndef NDEBUG
// Validate the optimization.
int slow_result = SlowCompareTo(other).value();
BASE_DCHECK((result == 0 && slow_result == 0) ||
(result < 0 && slow_result < 0) ||
(result > 0 && slow_result > 0));
#endif
return result;
}
return SlowCompareTo(other);
}
std::optional<int> SlowCompareTo(const AXPosition& other) const {
// It is potentially costly to compute the parent position of a text
// position, whilst computing the parent position of a tree position is
// really inexpensive. In order to find the lowest common ancestor,
// especially if that ancestor is all the way up to the root of the tree,
// this will need to be done repeatedly. We avoid the performance hit by
// converting both positions to tree positions and only falling back to text
// positions if both are text positions and the lowest common ancestor is
// not one of their anchors. Essentially, the question we need to answer is:
// "When are two non equivalent positions going to have the same lowest
// common ancestor position when converted to tree positions?" The answer is
// when they are both text positions and they either have the same anchor,
// or one is the ancestor of the other.
const AXNodeType* common_anchor = this->LowestCommonAnchor(other);
if (!common_anchor)
return std::optional<int>(std::nullopt);
// Attempt to avoid recomputing the lowest common ancestor because we may
// already have its anchor in which case just find the text offset.
if (this->IsTextPosition() && other.IsTextPosition()) {
// This text position's anchor is the common ancestor of the other text
// position's anchor.
if (this->GetAnchor() == common_anchor) {
AXPositionInstance other_text_position =
other.CreateAncestorPosition(common_anchor);
return std::optional<int>(this->text_offset_ -
other_text_position->text_offset_);
}
// The other text position's anchor is the common ancestor of this text
// position's anchor.
if (other.GetAnchor() == common_anchor) {
AXPositionInstance this_text_position =
this->CreateAncestorPosition(common_anchor);
return std::optional<int>(this_text_position->text_offset_ -
other.text_offset_);
}
// All optimizations failed. Fall back to comparing text positions with
// the common text position ancestor.
AXPositionInstance this_text_position_ancestor =
this->CreateAncestorPosition(common_anchor);
AXPositionInstance other_text_position_ancestor =
other.CreateAncestorPosition(common_anchor);
BASE_DCHECK(this_text_position_ancestor->IsTextPosition());
BASE_DCHECK(other_text_position_ancestor->IsTextPosition());
BASE_DCHECK(common_anchor == this_text_position_ancestor->GetAnchor());
BASE_DCHECK(common_anchor == other_text_position_ancestor->GetAnchor());
// TODO - This does not take into account |affinity_|, so we may return
// a false positive when comparing at the end of a line.
// For example :
// ++1 kRootWebArea
// ++++2 kTextField "Line 1\nLine 2"
// ++++++3 kStaticText "Line 1"
// ++++++++4 kInlineTextBox "Line 1"
// ++++++5 kLineBreak "\n"
// ++++++6 kStaticText "Line 2"
// ++++++++7 kInlineTextBox "Line 2"
//
// TextPosition anchor_id=5 text_offset=1
// affinity=downstream annotated_text=\n<>
//
// TextPosition anchor_id=7 text_offset=0
// affinity=downstream annotated_text=<L>ine 2
//
// |LowestCommonAncestor| for both will be :
// TextPosition anchor_id=2 text_offset=7
// ... except anchor_id=5 creates a kUpstream position, while
// anchor_id=7 creates a kDownstream position.
return std::optional<int>(this_text_position_ancestor->text_offset_ -
other_text_position_ancestor->text_offset_);
}
// All optimizations failed. Fall back to comparing child index with
// the common tree position ancestor.
AXPositionInstance this_tree_position_ancestor =
this->AsTreePosition()->CreateAncestorPosition(common_anchor);
AXPositionInstance other_tree_position_ancestor =
other.AsTreePosition()->CreateAncestorPosition(common_anchor);
BASE_DCHECK(this_tree_position_ancestor->IsTreePosition());
BASE_DCHECK(other_tree_position_ancestor->IsTreePosition());
BASE_DCHECK(common_anchor == this_tree_position_ancestor->GetAnchor());
BASE_DCHECK(common_anchor == other_tree_position_ancestor->GetAnchor());
return std::optional<int>(this_tree_position_ancestor->child_index() -
other_tree_position_ancestor->child_index());
}
// A valid position can become invalid if the underlying tree structure
// changes. This is expected behavior, but it is sometimes necessary to
// maintain valid positions. This method modifies an invalid position that is
// beyond MaxTextOffset to snap to MaxTextOffset.
void SnapToMaxTextOffsetIfBeyond() {
int max_text_offset = MaxTextOffset();
if (text_offset_ > max_text_offset)
text_offset_ = max_text_offset;
}
// Returns true if this position is on an empty object node that needs to
// be represented by an empty object replacement character. It does when the
// node is a collapsed menu list popup button or has no unignored child and is
// not a text object. This feature is only enabled on some platforms.
bool IsEmptyObjectReplacedByCharacter() const {
if (g_ax_embedded_object_behavior ==
AXEmbeddedObjectBehavior::kSuppressCharacter ||
IsNullPosition()) {
return false;
}
// A collapsed popup button that contains a menu list popup (i.e, the exact
// subtree representation we get from a collapsed <select> element on
// Windows) should not expose its children even though they are not ignored.
if (GetAnchor()->IsCollapsedMenuListPopUpButton())
return true;
// All other elements that have unignored descendants should not be treated
// as empty objects.
if (AnchorUnignoredChildCount())
return false;
// All unignored leaf nodes in the AXTree except document and text
// nodes should be replaced by the embedded object character. Also, nodes
// that only have ignored children (e.g., a button that contains only an
// empty div) need to be treated as leaf nodes.
//
// Calling AXPosition::IsIgnored here is not possible as it would create an
// infinite loop. However, GetAnchor()->IsIgnored() is sufficient here
// because we know that the anchor at this position doesn't have an
// unignored child, making this a leaf tree or text position.
return !GetAnchor()->IsIgnored() && !IsDocument(GetAnchorRole()) &&
!IsInTextObject() && !IsIframe(GetAnchorRole());
}
bool IsInDescendantOfEmptyObject() const {
if (g_ax_embedded_object_behavior ==
AXEmbeddedObjectBehavior::kSuppressCharacter ||
IsNullPosition()) {
return false;
}
// Empty objects are only possible on a collapsed popup button parent of a
// menu list popup or a node that only has ignored descendants. If it has no
// empty object ancestor, it can't be inside of an empty object.
return GetEmptyObjectAncestorNode();
}
AXNodeType* GetEmptyObjectAncestorNode() const {
if (g_ax_embedded_object_behavior ==
AXEmbeddedObjectBehavior::kSuppressCharacter ||
!GetAnchor()) {
return nullptr;
}
if (!GetAnchor()->IsIgnored()) {
// The only case where a descendant of an empty object can be unignored is
// when we are inside of a collapsed popup button parent of a menu list
// popup.
if (AXNodeType* popup_button =
GetAnchor()->GetCollapsedMenuListPopUpButtonAncestor()) {
return popup_button;
}
return nullptr;
}
// The first unignored ancestor is necessarily the empty object if this node
// is the descendant of an empty object.
AXNodeType* ancestor_node = GetLowestUnignoredAncestor();
if (!ancestor_node)
return nullptr;
AXPositionInstance position = CreateTextPosition(
tree_id_, GetAnchorID(ancestor_node), 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
if (position && position->IsEmptyObjectReplacedByCharacter())
return ancestor_node;
return nullptr;
}
void swap(AXPosition& other) {
std::swap(kind_, other.kind_);
std::swap(tree_id_, other.tree_id_);
std::swap(anchor_id_, other.anchor_id_);
std::swap(child_index_, other.child_index_);
std::swap(text_offset_, other.text_offset_);
std::swap(affinity_, other.affinity_);
}
// Abstract methods.
// Returns the text that is present inside the anchor node, including any text
// found in descendant text nodes, based on the platform's text
// representation. Some platforms use an embedded object replacement character
// that replaces the text coming from each child node.
virtual std::u16string GetText() const = 0;
// Determines if the anchor containing this position is a <br> or a text
// object whose parent's anchor is an enclosing <br>.
virtual bool IsInLineBreak() const = 0;
// Determines if the anchor containing this position is a text object.
virtual bool IsInTextObject() const = 0;
// Determines if the text representation of this position's anchor contains
// only whitespace characters; <br> objects span a single '\n' character, so
// positions inside line breaks are also considered "in whitespace".
virtual bool IsInWhiteSpace() const = 0;
// Returns the length of the text that is present inside the anchor node,
// including any text found in descendant text nodes. This is based on the
// platform's text representation. Some platforms use an embedded object
// character that replaces the text coming from each child node.
//
// Similar to "text_offset_", the length of the text is in UTF16 code units,
// not in grapheme clusters.
virtual int MaxTextOffset() const {
if (IsNullPosition())
return INVALID_OFFSET;
return static_cast<int>(GetText().length());
}
protected:
AXPosition()
: kind_(AXPositionKind::NULL_POSITION),
tree_id_(AXTreeIDUnknown()),
anchor_id_(AXNode::kInvalidAXID),
child_index_(INVALID_INDEX),
text_offset_(INVALID_OFFSET),
affinity_(ax::mojom::TextAffinity::kDownstream) {}
// We explicitly don't copy any cached members.
AXPosition(const AXPosition& other)
: kind_(other.kind_),
tree_id_(other.tree_id_),
anchor_id_(other.anchor_id_),
child_index_(other.child_index_),
text_offset_(other.text_offset_),
affinity_(other.affinity_) {}
// Returns the character offset inside our anchor's parent at which our text
// starts.
int AnchorTextOffsetInParent() const {
if (IsNullPosition())
return INVALID_OFFSET;
// Calculate how much text there is to the left of this anchor.
AXPositionInstance tree_position = AsTreePosition();
BASE_DCHECK(tree_position);
AXPositionInstance parent_position = tree_position->CreateParentPosition();
BASE_DCHECK(parent_position);
if (parent_position->IsNullPosition())
return 0;
int offset_in_parent = 0;
for (int i = 0; i < parent_position->child_index(); ++i) {
AXPositionInstance child = parent_position->CreateChildPositionAt(i);
BASE_DCHECK(child);
offset_in_parent += child->MaxTextOffsetInParent();
}
return offset_in_parent;
}
void Initialize(AXPositionKind kind,
AXTreeID tree_id,
int32_t anchor_id,
int child_index,
int text_offset,
ax::mojom::TextAffinity affinity) {
kind_ = kind;
tree_id_ = tree_id;
anchor_id_ = anchor_id;
child_index_ = child_index;
text_offset_ = text_offset;
affinity_ = affinity;
if (!IsValid()) {
// Reset to the null position.
kind_ = AXPositionKind::NULL_POSITION;
tree_id_ = AXTreeIDUnknown();
anchor_id_ = AXNode::kInvalidAXID;
child_index_ = INVALID_INDEX;
text_offset_ = INVALID_OFFSET;
affinity_ = ax::mojom::TextAffinity::kDownstream;
}
}
// Abstract methods.
virtual void AnchorChild(int child_index,
AXTreeID* tree_id,
int32_t* child_id) const = 0;
virtual int AnchorChildCount() const = 0;
// When a child is ignored, it looks for unignored nodes of that child's
// children until there are no more descendants.
//
// E.g.
// ++TextField
// ++++GenericContainer ignored
// ++++++StaticText "Hello"
// When we call the following method on TextField, it would return 1.
virtual int AnchorUnignoredChildCount() const = 0;
virtual int AnchorIndexInParent() const = 0;
virtual int AnchorSiblingCount() const = 0;
virtual std::stack<AXNodeType*> GetAncestorAnchors() const = 0;
virtual AXNodeType* GetLowestUnignoredAncestor() const = 0;
virtual void AnchorParent(AXTreeID* tree_id, int32_t* parent_id) const = 0;
virtual AXNodeType* GetNodeInTree(AXTreeID tree_id,
int32_t node_id) const = 0;
virtual int32_t GetAnchorID(AXNodeType* node) const = 0;
virtual AXTreeID GetTreeID(AXNodeType* node) const = 0;
// Returns the length of text that this anchor node takes up in its parent.
// On some platforms, embedded objects are represented in their parent with a
// single embedded object character.
int MaxTextOffsetInParent() const {
return IsEmbeddedObjectInParent() ? 1 : MaxTextOffset();
}
// Returns whether or not this anchor is represented in their parent with a
// single embedded object character.
virtual bool IsEmbeddedObjectInParent() const = 0;
// Determines if the anchor containing this position produces a hard line
// break in the text representation, e.g. a block level element or a <br>.
virtual bool IsInLineBreakingObject() const = 0;
virtual ax::mojom::Role GetAnchorRole() const = 0;
virtual ax::mojom::Role GetRole(AXNodeType* node) const = 0;
virtual AXNodeTextStyles GetTextStyles() const = 0;
virtual std::vector<int32_t> GetWordStartOffsets() const = 0;
virtual std::vector<int32_t> GetWordEndOffsets() const = 0;
virtual int32_t GetNextOnLineID(int32_t node_id) const = 0;
virtual int32_t GetPreviousOnLineID(int32_t node_id) const = 0;
private:
// Defines the relationship between positions during traversal.
// For example, moving from a descendant to an ancestor, is a kAncestor move.
enum class AXMoveType {
kAncestor,
kDescendant,
kSibling,
};
// Defines the direction of position movement, either next / previous in tree.
enum class AXMoveDirection {
kNextInTree,
kPreviousInTree,
};
// Type of predicate function called during anchor navigation.
// When the predicate returns |true|, the navigation stops and returns a
// null position object.
typedef bool AbortMovePredicate(const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType type,
const AXMoveDirection direction);
// A text span is defined by a series of inline text boxes that make up a
// single static text object.
bool AtEndOfTextSpan() const {
if (GetAnchorRole() != ax::mojom::Role::kInlineTextBox || !AtEndOfAnchor())
return false;
// We are at the end of text span if |this| position has
// role::kInlineTextBox, the parent of |this| has role::kStaticText, and the
// anchor node of |this| is the last child of parent's children.
const bool is_last_child =
AnchorIndexInParent() == (AnchorSiblingCount() - 1);
return is_last_child && GetRole(GetLowestUnignoredAncestor()) ==
ax::mojom::Role::kStaticText;
}
// Uses depth-first pre-order traversal.
AXPositionInstance CreateNextAnchorPosition(
std::function<AbortMovePredicate> abort_predicate) const {
if (IsNullPosition())
return Clone();
AXPositionInstance current_position = AsTreePosition();
BASE_DCHECK(!current_position->IsNullPosition());
if (!IsLeaf()) {
const int child_index = current_position->child_index_;
if (child_index < current_position->AnchorChildCount()) {
AXPositionInstance child_position =
current_position->CreateChildPositionAt(child_index);
if (abort_predicate(*current_position, *child_position,
AXMoveType::kDescendant,
AXMoveDirection::kNextInTree)) {
return CreateNullPosition();
}
return child_position;
}
}
AXPositionInstance parent_position =
current_position->CreateParentPosition();
// Get the next sibling if it exists, otherwise move up the AXTree to the
// lowest next sibling of this position's ancestors.
while (!parent_position->IsNullPosition()) {
const int index_in_parent = current_position->AnchorIndexInParent();
if (index_in_parent + 1 < parent_position->AnchorChildCount()) {
AXPositionInstance next_sibling =
parent_position->CreateChildPositionAt(index_in_parent + 1);
BASE_DCHECK(!next_sibling->IsNullPosition());
if (abort_predicate(*current_position, *next_sibling,
AXMoveType::kSibling,
AXMoveDirection::kNextInTree)) {
return CreateNullPosition();
}
return next_sibling;
}
if (abort_predicate(*current_position, *parent_position,
AXMoveType::kAncestor,
AXMoveDirection::kNextInTree)) {
return CreateNullPosition();
}
current_position = std::move(parent_position);
parent_position = current_position->CreateParentPosition();
}
return CreateNullPosition();
}
// Uses depth-first pre-order traversal.
AXPositionInstance CreatePreviousAnchorPosition(
std::function<AbortMovePredicate> abort_predicate) const {
if (IsNullPosition())
return Clone();
AXPositionInstance current_position = AsTreePosition();
BASE_DCHECK(!current_position->IsNullPosition());
AXPositionInstance parent_position =
current_position->CreateParentPosition();
if (parent_position->IsNullPosition())
return parent_position;
// If there is no previous sibling, move up to the parent.
const int index_in_parent = current_position->AnchorIndexInParent();
if (index_in_parent <= 0) {
if (abort_predicate(*current_position, *parent_position,
AXMoveType::kAncestor,
AXMoveDirection::kPreviousInTree)) {
return CreateNullPosition();
}
return parent_position;
}
// Get the previous sibling's deepest last child.
AXPositionInstance rightmost_leaf =
parent_position->CreateChildPositionAt(index_in_parent - 1);
BASE_DCHECK(!rightmost_leaf->IsNullPosition());
if (abort_predicate(*current_position, *rightmost_leaf,
AXMoveType::kSibling,
AXMoveDirection::kPreviousInTree)) {
return CreateNullPosition();
}
while (!rightmost_leaf->IsLeaf()) {
parent_position = std::move(rightmost_leaf);
rightmost_leaf = parent_position->CreateChildPositionAt(
parent_position->AnchorChildCount() - 1);
BASE_DCHECK(!rightmost_leaf->IsNullPosition());
if (abort_predicate(*parent_position, *rightmost_leaf,
AXMoveType::kDescendant,
AXMoveDirection::kPreviousInTree)) {
return CreateNullPosition();
}
}
return rightmost_leaf;
}
// Creates a position using the next text-only node as its anchor.
// Assumes that text-only nodes are leaf nodes.
AXPositionInstance CreateNextTextAnchorPosition(
std::function<AbortMovePredicate> abort_predicate) const {
// If this is an ancestor text position, resolve to its leaf text position.
if (IsTextPosition() && !IsLeaf())
return AsLeafTextPosition();
AXPositionInstance next_leaf = CreateNextAnchorPosition(abort_predicate);
while (!next_leaf->IsNullPosition() && !next_leaf->IsLeaf())
next_leaf = next_leaf->CreateNextAnchorPosition(abort_predicate);
BASE_DCHECK(next_leaf);
return next_leaf->AsLeafTextPosition();
}
// Creates a position using the previous text-only node as its anchor.
// Assumes that text-only nodes are leaf nodes.
AXPositionInstance CreatePreviousTextAnchorPosition(
std::function<AbortMovePredicate> abort_predicate) const {
// If this is an ancestor text position, resolve to its leaf text position.
if (IsTextPosition() && !IsLeaf())
return AsLeafTextPosition();
AXPositionInstance previous_leaf =
CreatePreviousAnchorPosition(abort_predicate);
while (!previous_leaf->IsNullPosition() && !previous_leaf->IsLeaf()) {
previous_leaf =
previous_leaf->CreatePreviousAnchorPosition(abort_predicate);
}
BASE_DCHECK(previous_leaf);
return previous_leaf->AsLeafTextPosition();
}
// Creates a tree position using the next text-only node as its anchor.
// Assumes that text-only nodes are leaf nodes.
AXPositionInstance CreateNextLeafTreePosition(
std::function<AbortMovePredicate> abort_predicate) const {
AXPositionInstance next_leaf =
AsTreePosition()->CreateNextAnchorPosition(abort_predicate);
while (!next_leaf->IsNullPosition() && !next_leaf->IsLeaf())
next_leaf = next_leaf->CreateNextAnchorPosition(abort_predicate);
BASE_DCHECK(next_leaf);
return next_leaf;
}
// Creates a tree position using the previous text-only node as its anchor.
// Assumes that text-only nodes are leaf nodes.
AXPositionInstance CreatePreviousLeafTreePosition(
std::function<AbortMovePredicate> abort_predicate) const {
AXPositionInstance previous_leaf =
AsTreePosition()->CreatePreviousAnchorPosition(abort_predicate);
while (!previous_leaf->IsNullPosition() && !previous_leaf->IsLeaf()) {
previous_leaf =
previous_leaf->CreatePreviousAnchorPosition(abort_predicate);
}
BASE_DCHECK(previous_leaf);
return previous_leaf;
}
//
// Static helpers for lambda usage.
//
static bool AtStartOfPagePredicate(const AXPositionInstance& position) {
// If a page boundary is ignored, then it should not be exposed to assistive
// software.
return !position->IsIgnored() && position->AtStartOfPage();
}
static bool AtEndOfPagePredicate(const AXPositionInstance& position) {
// If a page boundary is ignored, then it should not be exposed to assistive
// software.
return !position->IsIgnored() && position->AtEndOfPage();
}
static bool AtStartOfParagraphPredicate(const AXPositionInstance& position) {
// The "AtStartOfParagraph" method already excludes ignored nodes.
return position->AtStartOfParagraph();
}
static bool AtEndOfParagraphPredicate(const AXPositionInstance& position) {
// The "AtEndOfParagraph" method already excludes ignored nodes.
return position->AtEndOfParagraph();
}
static bool AtStartOfLinePredicate(const AXPositionInstance& position) {
// Sometimes, nodes that are used to signify line boundaries are ignored.
return position->AtStartOfLine();
}
static bool AtEndOfLinePredicate(const AXPositionInstance& position) {
// Sometimes, nodes that are used to signify line boundaries are ignored.
return position->AtEndOfLine();
}
static bool AtStartOfWordPredicate(const AXPositionInstance& position) {
// Word boundaries should be at specific text offsets that are "visible" to
// assistive software, hence not ignored. Ignored nodes are often used for
// additional layout information, such as line and paragraph boundaries.
// Their text is not currently processed.
return !position->IsIgnored() && position->AtStartOfWord();
}
static bool AtEndOfWordPredicate(const AXPositionInstance& position) {
// Word boundaries should be at specific text offsets that are "visible" to
// assistive software, hence not ignored. Ignored nodes are often used for
// additional layout information, such as line and paragraph boundaries.
// Their text is not currently processed.
return !position->IsIgnored() && position->AtEndOfWord();
}
static bool DefaultAbortMovePredicate(const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
// Default behavior is to never abort.
return false;
}
// AbortMovePredicate function used to detect format boundaries.
static bool AbortMoveAtFormatBoundary(const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
if (move_from.IsNullPosition() || move_to.IsNullPosition() ||
move_from.IsEmptyObjectReplacedByCharacter() ||
move_to.IsEmptyObjectReplacedByCharacter()) {
return true;
}
// Treat moving into or out of nodes with certain roles as a format break.
ax::mojom::Role from_role = move_from.GetAnchorRole();
ax::mojom::Role to_role = move_to.GetAnchorRole();
if (from_role != to_role) {
if (IsFormatBoundary(from_role) || IsFormatBoundary(to_role))
return true;
}
// Stop moving when text styles differ.
return move_from.AsLeafTreePosition()->GetTextStyles() !=
move_to.AsLeafTreePosition()->GetTextStyles();
}
static bool MoveCrossesLineBreakingObject(const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
const bool move_from_break = move_from.IsInLineBreakingObject();
const bool move_to_break = move_to.IsInLineBreakingObject();
switch (move_type) {
case AXMoveType::kAncestor:
// For Ancestor moves, only abort when exiting a block descendant.
// We don't care if the ancestor is a block or not, since the
// descendant is contained by it.
return move_from_break;
case AXMoveType::kDescendant:
// For Descendant moves, only abort when entering a block descendant.
// We don't care if the ancestor is a block or not, since the
// descendant is contained by it.
return move_to_break;
case AXMoveType::kSibling:
// For Sibling moves, abort if at least one of the siblings are a block,
// because that would mean exiting and/or entering a block.
return move_from_break || move_to_break;
}
BASE_UNREACHABLE();
return false;
}
// AbortMovePredicate function used to detect paragraph boundaries.
// We don't want to abort immediately after crossing a line breaking object
// boundary if the anchor we're moving to is not a leaf, this is necessary to
// avoid aborting if the next leaf position is whitespace-only; update
// |crossed_line_breaking_object_token| and wait until a leaf anchor is
// reached in order to correctly determine paragraph boundaries.
static bool AbortMoveAtParagraphBoundary(
bool& crossed_line_breaking_object_token,
const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
if (move_from.IsNullPosition() || move_to.IsNullPosition() ||
move_from.IsEmptyObjectReplacedByCharacter() ||
move_to.IsEmptyObjectReplacedByCharacter()) {
return true;
}
if (!crossed_line_breaking_object_token) {
crossed_line_breaking_object_token = MoveCrossesLineBreakingObject(
move_from, move_to, move_type, direction);
}
if (crossed_line_breaking_object_token && move_to.IsLeaf()) {
// If there's a sequence of whitespace-only anchors, collapse so only the
// last whitespace-only anchor is considered a paragraph boundary.
return direction != AXMoveDirection::kNextInTree ||
!move_to.IsInWhiteSpace();
}
return false;
}
// This AbortMovePredicate never aborts, but detects whether a sequence of
// consecutive moves cross any line breaking object boundary.
static bool UpdateCrossedLineBreakingObjectToken(
bool& crossed_line_breaking_object_token,
const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
if (!crossed_line_breaking_object_token) {
crossed_line_breaking_object_token = MoveCrossesLineBreakingObject(
move_from, move_to, move_type, direction);
}
return false;
}
// AbortMovePredicate function used to detect page boundaries.
static bool AbortMoveAtPageBoundary(const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
if (move_from.IsNullPosition() || move_to.IsNullPosition())
return true;
const bool move_from_break = move_from.GetAnchor()->GetBoolAttribute(
ax::mojom::BoolAttribute::kIsPageBreakingObject);
const bool move_to_break = move_to.GetAnchor()->GetBoolAttribute(
ax::mojom::BoolAttribute::kIsPageBreakingObject);
switch (move_type) {
case AXMoveType::kAncestor:
// For Ancestor moves, only abort when exiting a page break.
// We don't care if the ancestor is a page break or not, since the
// descendant is contained by it.
return move_from_break;
case AXMoveType::kDescendant:
// For Descendant moves, only abort when entering a page break
// descendant. We don't care if the ancestor is a page break or not,
// since the descendant is contained by it.
return move_to_break;
case AXMoveType::kSibling:
// For Sibling moves, abort if at both of the siblings are a page
// break, because that would mean exiting and/or entering a page break.
return move_from_break && move_to_break;
}
BASE_UNREACHABLE();
return false;
}
static bool AbortMoveAtStartOfInlineBlock(const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
if (move_from.IsNullPosition() || move_to.IsNullPosition())
return true;
// These will only be available if AXMode has kHTML set.
const bool move_from_is_inline_block =
move_from.GetAnchor()->GetStringAttribute(
ax::mojom::StringAttribute::kDisplay) == "inline-block";
const bool move_to_is_inline_block =
move_to.GetAnchor()->GetStringAttribute(
ax::mojom::StringAttribute::kDisplay) == "inline-block";
switch (direction) {
case AXMoveDirection::kNextInTree:
// When moving forward, break if we enter an inline block.
return move_to_is_inline_block &&
(move_type == AXMoveType::kDescendant ||
move_type == AXMoveType::kSibling);
case AXMoveDirection::kPreviousInTree:
// When moving backward, break if we exit an inline block.
return move_from_is_inline_block &&
(move_type == AXMoveType::kAncestor ||
move_type == AXMoveType::kSibling);
}
BASE_UNREACHABLE();
return false;
}
static AXPositionAdjustmentBehavior AdjustmentBehaviorFromBoundaryDirection(
ax::mojom::MoveDirection move_direction) {
switch (move_direction) {
case ax::mojom::MoveDirection::kForward:
return AXPositionAdjustmentBehavior::kMoveForward;
case ax::mojom::MoveDirection::kBackward:
return AXPositionAdjustmentBehavior::kMoveBackward;
}
}
static AXPositionAdjustmentBehavior OppositeAdjustmentBehavior(
AXPositionAdjustmentBehavior adjustment_behavior) {
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveForward:
return AXPositionAdjustmentBehavior::kMoveBackward;
case AXPositionAdjustmentBehavior::kMoveBackward:
return AXPositionAdjustmentBehavior::kMoveForward;
}
}
static std::vector<int32_t> GetWordStartOffsetsFunc(
const AXPositionInstance& position) {
return position->GetWordStartOffsets();
}
static std::vector<int32_t> GetWordEndOffsetsFunc(
const AXPositionInstance& position) {
return position->GetWordEndOffsets();
}
AXPositionInstance CreateDocumentAncestorPosition() const {
AXPositionInstance iterator = Clone();
while (!iterator->IsNullPosition()) {
if (IsDocument(iterator->GetAnchorRole()) &&
iterator->CreateParentPosition()->IsNullPosition()) {
break;
}
iterator = iterator->CreateParentPosition();
}
return iterator;
}
// Creates a text position that is in the same anchor as the current
// position, but starting from the current text offset, adjusts to the next
// or the previous boundary offset depending on the boundary direction. If
// there is no next / previous offset, the current text offset is unchanged.
AXPositionInstance CreatePositionAtNextOffsetBoundary(
ax::mojom::MoveDirection move_direction,
BoundaryTextOffsetsFunc get_offsets) const {
if (IsNullPosition() || !get_offsets)
return Clone();
AXPositionInstance text_position = AsTextPosition();
const std::vector<int32_t> boundary_offsets = get_offsets(text_position);
if (boundary_offsets.empty())
return text_position;
switch (move_direction) {
case ax::mojom::MoveDirection::kForward: {
const auto offsets_iterator =
std::upper_bound(boundary_offsets.begin(), boundary_offsets.end(),
int32_t{text_position->text_offset_});
// If there is no next offset, the current offset should be unchanged.
if (offsets_iterator < boundary_offsets.end()) {
text_position->text_offset_ = static_cast<int>(*offsets_iterator);
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
}
break;
}
case ax::mojom::MoveDirection::kBackward: {
auto offsets_iterator =
std::lower_bound(boundary_offsets.begin(), boundary_offsets.end(),
int32_t{text_position->text_offset_});
// If there is no previous offset, the current offset should be
// unchanged.
if (offsets_iterator > boundary_offsets.begin()) {
// Since we already checked if "boundary_offsets" are non-empty, we
// can safely move the iterator one position back, even if it's
// currently at the vector's end.
--offsets_iterator;
text_position->text_offset_ = static_cast<int>(*offsets_iterator);
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
}
break;
}
}
return text_position;
}
// Creates a text position that is in the same anchor as the current
// position, but adjusts its text offset to be either at the first or last
// offset boundary, based on the boundary direction. When moving forward,
// the text position is adjusted to point to the first offset boundary, or
// to the end of its anchor if there are no offset boundaries. When moving
// backward, it is adjusted to point to the last offset boundary, or to the
// start of its anchor if there are no offset boundaries.
AXPositionInstance CreatePositionAtFirstOffsetBoundary(
ax::mojom::MoveDirection move_direction,
BoundaryTextOffsetsFunc get_offsets) const {
if (IsNullPosition() || !get_offsets)
return Clone();
AXPositionInstance text_position = AsTextPosition();
const std::vector<int32_t> boundary_offsets = get_offsets(text_position);
switch (move_direction) {
case ax::mojom::MoveDirection::kForward:
if (boundary_offsets.empty()) {
return text_position->CreatePositionAtEndOfAnchor();
} else {
text_position->text_offset_ = static_cast<int>(boundary_offsets[0]);
return text_position;
}
break;
case ax::mojom::MoveDirection::kBackward:
if (boundary_offsets.empty()) {
return text_position->CreatePositionAtStartOfAnchor();
} else {
text_position->text_offset_ =
static_cast<int>(boundary_offsets[boundary_offsets.size() - 1]);
return text_position;
}
break;
}
}
// Returns the next leaf text position in the specified direction ensuring
// that *AsLeafTextPosition() != *CreateAdjacentLeafTextPosition() is true;
// returns a null position if no adjacent position exists.
//
// This method is the first step for CreateBoundary[Start|End]Position to
// guarantee that the resulting position when using a boundary behavior other
// than StopIfAlreadyAtBoundary is not equivalent to the initial position.
//
// Note that using CompareTo with text positions does not take into account
// position affinity or tree pre-order, two text positions are considered
// equivalent if their offsets in the text representation of the entire AXTree
// are the same. As such, using Create[Next|Previous]LeafTextPosition is not
// enough to create adjacent positions, e.g. the end of an anchor and the
// start of the next one are equivalent; furthermore, there could be nodes
// with no text representation between them, all of them being equivalent too.
//
// IMPORTANT: This method basically moves the given position one character
// forward/backward, but it could end up at the middle of a grapheme cluster,
// so it shouldn't be used to move by ax::mojom::TextBoundary::kCharacter (for
// such purpose use Create[Next|Previous]CharacterPosition instead).
AXPositionInstance CreateAdjacentLeafTextPosition(
ax::mojom::MoveDirection move_direction) const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (move_direction) {
case ax::mojom::MoveDirection::kForward:
// If we are at a text offset less than MaxTextOffset, we will simply
// increase the offset by one; otherwise, create a position at the start
// of the next leaf node with non-empty text and increase its offset.
//
// Note that a position located at offset 0 of an empty text node is
// considered both, at the start and at the end of its anchor, so the
// following loop skips over empty text leaf nodes, which is expected
// since those positions are equivalent to both, the previous non-empty
// leaf node's end and the next non-empty leaf node's start.
while (text_position->AtEndOfAnchor()) {
text_position = text_position->CreateNextLeafTextPosition();
}
if (!text_position->IsNullPosition())
++text_position->text_offset_;
break;
case ax::mojom::MoveDirection::kBackward:
// If we are at a text offset greater than 0, we will simply decrease
// the offset by one; otherwise, create a position at the end of the
// previous leaf node with non-empty text and decrease its offset.
//
// Same as the comment above, using AtStartOfAnchor is enough to skip
// empty text nodes that are equivalent to the initial position.
while (text_position->AtStartOfAnchor()) {
text_position = text_position->CreatePreviousLeafTextPosition()
->CreatePositionAtEndOfAnchor();
}
if (!text_position->IsNullPosition())
--text_position->text_offset_;
break;
}
BASE_DCHECK(text_position->IsValid());
return text_position;
}
AXPositionKind kind_;
AXTreeID tree_id_;
AXNode::AXID anchor_id_;
// For text positions, |child_index_| is initially set to |-1| and only
// computed on demand. The same with tree positions and |text_offset_|.
int child_index_;
// "text_offset_" represents the number of UTF16 code units before this
// position. It doesn't count grapheme clusters.
int text_offset_;
// Affinity is used to distinguish between two text positions that point to
// the same text offset, but which happens to fall on a soft line break. A
// soft line break doesn't insert any white space in the accessibility tree,
// so without affinity there would be no way to determine whether a text
// position is before or after the soft line break. An upstream affinity
// means that the position is before the soft line break, whilst a
// downstream affinity means that the position is after the soft line break.
//
// Please note that affinity could only be set to upstream for positions
// that are anchored to non-leaf nodes. When on a leaf node, there could
// never be an ambiguity as to which line a position points to because Blink
// creates separate inline text boxes for each line of text. Therefore, a
// leaf text position before the soft line break would be pointing to the
// end of its anchor node, whilst a leaf text position after the soft line
// break would be pointing to the start of the next node.
ax::mojom::TextAffinity affinity_;
};
template <class AXPositionType, class AXNodeType>
const int AXPosition<AXPositionType, AXNodeType>::BEFORE_TEXT;
template <class AXPositionType, class AXNodeType>
const int AXPosition<AXPositionType, AXNodeType>::INVALID_INDEX;
template <class AXPositionType, class AXNodeType>
const int AXPosition<AXPositionType, AXNodeType>::INVALID_OFFSET;
template <class AXPositionType, class AXNodeType>
bool operator==(const AXPosition<AXPositionType, AXNodeType>& first,
const AXPosition<AXPositionType, AXNodeType>& second) {
const std::optional<int> compare_to_optional = first.CompareTo(second);
return compare_to_optional.has_value() && compare_to_optional.value() == 0;
}
template <class AXPositionType, class AXNodeType>
bool operator!=(const AXPosition<AXPositionType, AXNodeType>& first,
const AXPosition<AXPositionType, AXNodeType>& second) {
const std::optional<int> compare_to_optional = first.CompareTo(second);
return compare_to_optional.has_value() && compare_to_optional.value() != 0;
}
template <class AXPositionType, class AXNodeType>
bool operator<(const AXPosition<AXPositionType, AXNodeType>& first,
const AXPosition<AXPositionType, AXNodeType>& second) {
const std::optional<int> compare_to_optional = first.CompareTo(second);
return compare_to_optional.has_value() && compare_to_optional.value() < 0;
}
template <class AXPositionType, class AXNodeType>
bool operator<=(const AXPosition<AXPositionType, AXNodeType>& first,
const AXPosition<AXPositionType, AXNodeType>& second) {
const std::optional<int> compare_to_optional = first.CompareTo(second);
return compare_to_optional.has_value() && compare_to_optional.value() <= 0;
}
template <class AXPositionType, class AXNodeType>
bool operator>(const AXPosition<AXPositionType, AXNodeType>& first,
const AXPosition<AXPositionType, AXNodeType>& second) {
const std::optional<int> compare_to_optional = first.CompareTo(second);
return compare_to_optional.has_value() && compare_to_optional.value() > 0;
}
template <class AXPositionType, class AXNodeType>
bool operator>=(const AXPosition<AXPositionType, AXNodeType>& first,
const AXPosition<AXPositionType, AXNodeType>& second) {
const std::optional<int> compare_to_optional = first.CompareTo(second);
return compare_to_optional.has_value() && compare_to_optional.value() >= 0;
}
template <class AXPositionType, class AXNodeType>
void swap(AXPosition<AXPositionType, AXNodeType>& first,
AXPosition<AXPositionType, AXNodeType>& second) {
first.swap(second);
}
template <class AXPositionType, class AXNodeType>
std::ostream& operator<<(
std::ostream& stream,
const AXPosition<AXPositionType, AXNodeType>& position) {
return stream << position.ToString();
}
} // namespace ui
#endif // UI_ACCESSIBILITY_AX_POSITION_H_