blob: 077d445042fccd826c5bd948fb562f25be185a45 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#ifndef UI_ACCESSIBILITY_AX_POSITION_H_
#define UI_ACCESSIBILITY_AX_POSITION_H_
#include <math.h>
#include <stdint.h>
#include <functional>
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/stack.h"
#include "base/export_template.h"
#include "base/i18n/break_iterator.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/accessibility/ax_common.h"
#include "ui/accessibility/ax_constants.mojom.h"
#include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_table_info.h"
#include "ui/accessibility/ax_text_attributes.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_manager.h"
#include "ui/gfx/utf16_indexing.h"
namespace ui {
class AXNodePosition;
class AXNode;
// 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 text boundary, (such as the start of a word or the
// end of a sentence), or whenever we are crossing the initial position's
// anchor. Note that the "anchor" is the node to which an AXPosition is attached
// to. It is provided when a position is created.
enum class AXBoundaryBehavior {
// Crosses all boundaries. If the bounds of the current window-like container,
// such as the current webpage, have been reached, returns a null position.
kCrossBoundary,
// Stops if the current anchor is crossed, regardless of how the resulting
// position has been computed. For example, even though in order to find the
// next or previous word start in a text field we need to descend to the leaf
// equivalent position, this behavior will only stop when the bounds of the
// original anchor, i.e. the text field, have been crossed.
kStopAtAnchorBoundary,
// Stops if we have reached the start or the end of of a window-like
// container, such as a webpage, a PDF, a dialog, the browser's UI (AKA
// Views), or the whole desktop.
kStopAtLastAnchorBoundary
};
// Defines whether moving to the next or previous position should consider the
// initial position before testing for the given boundary/behavior.
// kCheckInitialPosition should be used if the current position should be
// maintained if it meets the boundary criteria. Otherwise,
// kDontCheckInitialPosition will move to the next/previous position before
// testing for the specified boundary.
enum class AXBoundaryDetection {
kCheckInitialPosition,
kDontCheckInitialPosition,
};
struct AXMovementOptions {
AXMovementOptions(AXBoundaryBehavior boundary, AXBoundaryDetection detection)
: boundary_behavior(boundary), boundary_detection(detection) {}
AXBoundaryBehavior boundary_behavior;
AXBoundaryDetection boundary_detection;
// If true, indicates that an upstream position should not be crossed when
// moving forward and should skip its initial check when moving backward. This
// primarily applies to getting a pair of positions around a line from an
// upstream caret.
bool upstream_bounded = false;
};
// 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 the whole content, possibly spanning multiple accessibility
// trees.
kContentStart,
// At the end of the whole content, possibly spanning multiple accessibility
// trees.
kContentEnd
};
// 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 most objects, including empty objects, to be
// represented by an "embedded object 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 this special 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.
//
// However, there is a special case for UIA. kExposeCharacterForHypertext is
// used mainly to enable the hypertext logic and calculation for cases where the
// embedded object character is not needed. This logic is IA2 and ATK specific,
// and should not be used for UIA relevant calls and calculations. As a result,
// we have the kUIAExposeCharacterForTextContent which avoids the IA2/ATK
// specific logic for the text calculation but also keeps the same embedded
// object character behavior for cases when it is needed.
enum class AXEmbeddedObjectBehavior {
kExposeCharacterForHypertext,
kSuppressCharacter,
kUIAExposeCharacterForTextContent,
};
// Controls whether embedded objects are represented by a replacement
// character. This is initialized to a per-platform default but can be
// overridden for testing.
//
// On some platforms, most objects are represented in the text of their parents
// with a special "embedded object character" and not with their actual text
// contents. Also on the same platforms, if a node has only ignored descendants,
// i.e., it appears to be empty to assistive software, we need to treat it as a
// character and a word boundary. For example, an empty text field should act as
// a character and a word boundary when a screen reader user tries to navigate
// through it, otherwise the text field would be missed by the user.
//
// Tests should use ScopedAXEmbeddedObjectBehaviorSetter to change this.
// TODO(crbug.com/40764129) Don't export this so tests can't change it.
extern AX_EXPORT AXEmbeddedObjectBehavior g_ax_embedded_object_behavior;
class AX_EXPORT ScopedAXEmbeddedObjectBehaviorSetter {
public:
explicit ScopedAXEmbeddedObjectBehaviorSetter(
AXEmbeddedObjectBehavior behavior);
~ScopedAXEmbeddedObjectBehaviorSetter();
private:
AXEmbeddedObjectBehavior prev_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);
// 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>>;
using BoundaryConditionPredicate =
base::RepeatingCallback<bool(const AXPositionInstance&)>;
using BoundaryTextOffsetsFunc =
base::RepeatingCallback<const std::vector<int32_t>&(
const AXPositionInstance&)>;
static const int BEFORE_TEXT = -1;
static const int INVALID_INDEX = -2;
static const int INVALID_OFFSET = ax::mojom::kNoSelectionOffset;
static AXPositionInstance CreateNullPosition() {
AXPositionInstance new_position(new AXPositionType());
new_position->Initialize(AXPositionKind::NULL_POSITION, AXTreeIDUnknown(),
kInvalidAXNodeID, INVALID_INDEX, INVALID_OFFSET,
ax::mojom::TextAffinity::kDownstream);
return new_position;
}
static AXPositionInstance CreateTreePosition(const AXNode& anchor,
int child_index) {
DCHECK(anchor.tree());
DCHECK_NE(anchor.tree()->GetAXTreeID(), AXTreeIDUnknown());
DCHECK_NE(anchor.id(), kInvalidAXNodeID);
AXPositionInstance new_position(new AXPositionType());
new_position->Initialize(AXPositionKind::TREE_POSITION,
anchor.tree()->GetAXTreeID(), anchor.id(),
child_index, INVALID_OFFSET,
ax::mojom::TextAffinity::kDownstream);
return new_position;
}
static AXPositionInstance CreateTreePositionAtStartOfAnchor(
const AXNode& anchor) {
// Initialize the child index:
// - For a leaf, the child index will be BEFORE_TEXT.
// - Otherwise the child index will be 0.
int child_index = IsLeafNodeForTreePosition(anchor) ? BEFORE_TEXT : 0;
return CreateTreePosition(anchor, child_index);
}
static AXPositionInstance CreateTreePositionAtEndOfAnchor(
const AXNode& anchor) {
// Initialize the child index to the anchor's child count.
return CreateTreePosition(anchor,
anchor.GetChildCountCrossingTreeBoundary());
}
static AXPositionInstance CreateTextPosition(
const AXNode& anchor,
int text_offset,
ax::mojom::TextAffinity affinity) {
DCHECK(anchor.tree());
DCHECK_NE(anchor.tree()->GetAXTreeID(), AXTreeIDUnknown());
DCHECK_NE(anchor.id(), kInvalidAXNodeID);
AXPositionInstance new_position(new AXPositionType());
new_position->Initialize(AXPositionKind::TEXT_POSITION,
anchor.tree()->GetAXTreeID(), 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;
AXPositionInstance CloneWithDownstreamAffinity() const {
if (!IsTextPosition()) {
NOTREACHED() << "Only text positions have affinity.";
}
AXPositionInstance clone_with_downstream_affinity = Clone();
clone_with_downstream_affinity->affinity_ =
ax::mojom::TextAffinity::kDownstream;
return clone_with_downstream_affinity;
}
AXPositionInstance CloneWithUpstreamAffinity() const {
if (!IsTextPosition()) {
NOTREACHED() << "Only text positions have affinity.";
}
AXPositionInstance clone_with_upstream_affinity = Clone();
clone_with_upstream_affinity->affinity_ =
ax::mojom::TextAffinity::kUpstream;
return clone_with_upstream_affinity;
}
// 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;
AXNodeID 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();
DCHECK_LE(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());
// Use initialize without validation because this is used by ATs that
// used outdated information to generated a selection request.
new_position->InitializeWithoutValidation(
serialization.kind, 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_ < 0 || text_offset_ > MaxTextOffset())
return str;
const std::u16string& text = GetText();
DCHECK_GE(text_offset_, 0);
const size_t max_text_offset = text.size();
DCHECK_LE(text_offset_, static_cast<int>(max_text_offset)) << text;
std::u16string annotated_text;
if (text_offset_ == static_cast<int>(max_text_offset)) {
annotated_text = text + u"<>";
} else {
// TODO(aleventhal) This extra casting is only necessary to satisfy a
// compiler error that strangely occurs only when Initialize() contains
// SnapToMaxTextOffsetIfBeyond().
size_t unsigned_text_offset = static_cast<size_t>(text_offset_);
annotated_text = text.substr(0, unsigned_text_offset) + u"<" +
text[unsigned_text_offset] + u">" +
text.substr(unsigned_text_offset + 1);
}
return str + " annotated_text=" + base::UTF16ToUTF8(annotated_text);
}
// Helper for logging the position, the AXTreeManager and the anchor node.
std::string ToDebugString() const {
std::ostringstream str;
str << "* Position: " << ToString();
if (GetAnchor()) {
str << "\n* Anchor node: " << *GetAnchor();
if (IsTreePosition()) {
str << "\n* AnchorChildCount(): " << AnchorChildCount()
<< "\n* IsLeaf(): " << IsLeaf();
} else {
str << "\n* TextOffset: " << text_offset()
<< "\n* MaxTextOffset: " << MaxTextOffset();
}
}
if (GetManager())
str << "\n* Tree: " << GetManager()->ax_tree()->data().ToString();
return str.str();
}
AXPositionKind kind() const { return kind_; }
AXTreeID tree_id() const { return tree_id_; }
AXNodeID anchor_id() const { return anchor_id_; }
AXTreeManager* GetManager() const { return AXTreeManager::FromID(tree_id()); }
// Returns true if this position is within an "empty object", i.e. within a
// node that should contribute no text to the accessibility tree's text
// representation. For example, returns true if this position is within an
// empty control, such as an empty text field or (on Windows) a collapsed
// popup menu. On some platforms, such nodes need to be represented by an
// "object replacement character". This character is inserted purely for
// navigational purposes. This is because empty controls still need to act as
// a word and character boundary on those platforms.
static bool IsEmptyObject(const AXNode& node) {
// 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 descendants even though they are not
// ignored.
if (node.IsCollapsedMenuListSelect())
return true;
// All anchor nodes that are empty leaf nodes should be treated as empty
// objects. Empty leaf nodes are defined as nodes whose descendants are (A)
// not exposed to any platform accessibility APIs and (B) do not contribute
// any text to the tree's text representation. They may have unignored
// descendants however. They do not have any text content, hence they are
// empty from our perspective. For example, an empty text field may still
// have an unignored generic container inside it.
if (!node.IsEmptyLeaf())
return false;
// While atomic text fields from web content have a text node descendant,
// atomic text fields from Views don't. Their text value is set in the value
// attribute of the text field node directly.
if (node.IsView() && node.data().IsAtomicTextField() &&
!node.GetValueForControl().empty()) {
return false;
}
// One exception to the above rule that all empty leaf nodes are empty
// objects in AXPosition are <embed> and <object> elements that have
// children. They should not be treated as empty objects even when their
// descendants are all ignored so that text navigation won't stop on such
// nodes.
ax::mojom::Role role = node.GetRole();
if ((role == ax::mojom::Role::kEmbeddedObject ||
role == ax::mojom::Role::kPluginObject) &&
node.GetChildCountCrossingTreeBoundary()) {
return false;
}
// Nodes that are skipped during text navigation should also be "empty
// objects".
//
// Note that nodes that are skipped during text navigation could still have
// positions anchored to them, e.g. for determining if a paragraph boundary
// should be reported before or after such a node. Descending into the
// children of such objects could add unnecessary extra text boundaries.
if (node.IsIgnoredForTextNavigation())
return true;
// Another exception to the rule that all leaf nodes in the accessibility
// tree should be "empty objects" are kRootWebArea, kPdfRoot, kIframe,
// kIframePresentational, and text nodes. We don't want text navigation to
// stop on any of the above roles. On the other hand, nodes that only have
// ignored children (e.g., a button that contains only an empty ignored div)
// need to be treated as leaf nodes.
//
// Note that we have already determined that the anchor at this position
// doesn't have an unignored child, making this a leaf tree or text
// position, or a leaf's descendant.
return (!IsPlatformDocument(role) && !IsIframe(role) && !node.IsText());
}
// Return true if the node is a leaf, or has no selectable text content.
static bool IsLeafNodeForTreePosition(const AXNode& node) {
// Unignored text list markers expose text on their own, and all their
// descendants are ignored. Make sure they are treated as leaves, not empty
// containers.
if (node.GetRole() == ax::mojom::Role::kListMarker && !node.IsIgnored() &&
!node.GetUnignoredChildCountCrossingTreeBoundary()) {
return true;
}
return !node.GetChildCountCrossingTreeBoundary() || IsEmptyObject(node);
}
AXNode* GetAnchor() const {
if (tree_id_ == AXTreeIDUnknown() || anchor_id_ == kInvalidAXNodeID)
return nullptr;
const AXTreeManager* manager = GetManager();
if (manager)
return manager->GetNode(anchor_id());
return nullptr;
}
int GetAnchorSiblingCount() const {
if (IsNullPosition())
return 0;
AXPositionInstance parent_position = AsTreePosition()->CreateParentPosition(
ax::mojom::MoveDirection::kBackward);
if (!parent_position->IsNullPosition())
return parent_position->AnchorChildCount();
return 0;
}
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;
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:
NOTREACHED();
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.
DCHECK(child_index_ == BEFORE_TEXT ||
child_index_ == AnchorChildCount() || !IsLeaf())
<< "Leaf nodes can only have a position before or after, so they "
"must have a child index of BEFORE_TEXT or AnchorChildCount(): "
<< ToString() << " AnchorChildCount(): " << AnchorChildCount();
DCHECK(child_index_ != BEFORE_TEXT || IsLeaf())
<< "Non-leaf nodes cannot have a position of BEFORE_TEXT: "
<< *GetAnchor();
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.
//
// For example, in the following 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);
DCHECK(child_position && !child_position->IsNullPosition());
return child_position->IsNullPosition() ||
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;
AXNode* anchor = GetAnchor();
DCHECK(anchor);
return IsLeafNodeForTreePosition(*anchor);
}
// Returns true if this is a valid position, e.g. the child_index_ or
// text_offset_ is within a valid range.
//
// A position is always valid at creation time, but could become invalid after
// a tree update. For performance reasons, we don't check for validity every
// time a position is used, expecting clients to use this method instead.
bool IsValid() const {
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return tree_id_ == AXTreeIDUnknown() &&
anchor_id_ == kInvalidAXNodeID &&
child_index_ == INVALID_INDEX &&
text_offset_ == INVALID_OFFSET &&
affinity_ == ax::mojom::TextAffinity::kDownstream;
case AXPositionKind::TREE_POSITION:
if (!GetAnchor())
return false;
// The `BEFORE_TEXT` constant is only needed on leaf positions because
// on any other position a `child_index_` of 0 could be used. On leaf
// positions, however, often there are no child nodes and so a
// `child_index_` of 0 would confusingly indicate both a "before text"
// as well as an "after text" position. Note that some leaf positions,
// e.g. positions in empty objects, do have children.
if (IsLeaf()) {
// Leaf nodes can only have a position before or after, so they must
// have a child index of BEFORE_TEXT or AnchorChildCount().
return child_index_ == BEFORE_TEXT ||
child_index_ == AnchorChildCount();
}
return child_index_ >= 0 && child_index_ <= AnchorChildCount();
case AXPositionKind::TEXT_POSITION:
if (!GetAnchor())
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());
}
}
bool AtStartOfAnchor() const {
if (!GetAnchor())
return false;
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
if (IsLeaf())
return child_index_ == BEFORE_TEXT;
return child_index_ == 0;
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:
// A few positions are anchored to nodes that have children but we want
// to treat them as leaf positions. An example is an empty text field;
// it often has empty unignored divs coming from Blink inside it.
return child_index_ == AnchorChildCount();
case AXPositionKind::TEXT_POSITION:
return text_offset_ == MaxTextOffset();
}
}
bool AtStartOfWord() const {
AXPositionInstance text_position;
if (!AtEndOfAnchor()) {
// We could get a leaf text position at the end of its anchor, where word
// start offsets would surely not be present. In such cases, we need to
// normalize to the start of the next leaf anchor. We avoid making this
// change when we are at the end of our anchor because this could
// effectively shift the position forward.
text_position = AsLeafTextPositionBeforeCharacter();
} else {
text_position = AsLeafTextPosition();
}
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
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;
if (!AtStartOfAnchor()) {
// We could get a leaf text position at the start of its anchor, where
// word end offsets would surely not be present. In such cases, we need to
// normalize to the end of the previous leaf anchor. We avoid making this
// change when we are at the start of our anchor because this could
// effectively shift the position backward.
text_position = AsLeafTextPositionAfterCharacter();
} else {
text_position = AsLeafTextPosition();
}
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
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:
NOTREACHED();
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() &&
text_position->GetNextOnLineID() == kInvalidAXNodeID) {
return true;
}
// If the anchor is ignored, then by default it will not have a
// PreviousOnLineID set since we only set this on unignored nodes.
// However, it could still have something previous to it on the same
// line, like for example if we have some text on the same line, and a
// text node in the middle is set to aria-hidden.
return text_position->GetPreviousOnLineID() == kInvalidAXNodeID &&
text_position->AtStartOfAnchor() &&
!text_position->GetAnchor()->IsIgnored();
}
}
bool AtEndOfLine() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
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() == 0)
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 (text_position->GetNextOnLineID() == kInvalidAXNodeID) {
return (!text_position->AtEndOfTextSpan() &&
text_position->IsInWhiteSpace() &&
text_position->GetPreviousOnLineID() != kInvalidAXNodeID)
? 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();
}
}
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(
base::BindRepeating(&AbortMoveAtRootBoundary))
->IsNullPosition()) {
return AXBoundaryType::kContentStart;
}
// 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(
base::BindRepeating(&AbortMoveAtFormatBoundary));
} while (previous_position->IsIgnored());
if (previous_position->IsNullPosition())
return AXBoundaryType::kUnitBoundary;
return AXBoundaryType::kNone;
}
bool AtStartOfFormat() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
case AXPositionKind::TEXT_POSITION: {
const std::vector<int32_t>& format_starts =
text_position->GetFormatStartOffsets();
if (format_starts.size() <= 1) {
return GetFormatStartBoundaryType() != AXBoundaryType::kNone;
}
return base::Contains(format_starts,
int32_t{text_position->text_offset_});
}
}
}
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(
base::BindRepeating(&AbortMoveAtRootBoundary))
->IsNullPosition())
return AXBoundaryType::kContentEnd;
// 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(
base::BindRepeating(&AbortMoveAtFormatBoundary));
} while (next_position->IsIgnored());
if (next_position->IsNullPosition())
return AXBoundaryType::kUnitBoundary;
return AXBoundaryType::kNone;
}
bool AtEndOfFormat() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
case AXPositionKind::TEXT_POSITION: {
const std::vector<int32_t>& format_ends =
text_position->GetFormatEndOffsets();
if (format_ends.size() <= 1) {
return GetFormatEndBoundaryType() != AXBoundaryType::kNone;
}
return base::Contains(format_ends,
int32_t{text_position->text_offset_});
}
}
}
bool AtStartOfSentence() const {
AXPositionInstance text_position;
if (!AtEndOfAnchor()) {
// We could get a leaf text position at the end of its anchor, where
// sentence start offsets would surely not be present. In such cases, we
// need to normalize to the start of the next leaf anchor. We avoid making
// this change when we are at the end of our anchor because this could
// effectively shift the position forward.
text_position = AsLeafTextPositionBeforeCharacter();
} else {
text_position = AsLeafTextPosition();
}
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
case AXPositionKind::TEXT_POSITION: {
const std::vector<int32_t>& sentence_starts =
text_position->GetAnchor()->GetIntListAttribute(
ax::mojom::IntListAttribute::kSentenceStarts);
return base::Contains(sentence_starts,
int32_t{text_position->text_offset_});
}
}
}
bool AtEndOfSentence() const {
AXPositionInstance text_position;
if (!AtStartOfAnchor()) {
// We could get a leaf text position at the start of its anchor, where
// sentence end offsets would surely not be present. In such cases, we
// need to normalize to the end of the previous leaf anchor. We avoid
// making this change when we are at the start of our anchor because this
// could effectively shift the position backward.
text_position = AsLeafTextPositionAfterCharacter();
} else {
text_position = AsLeafTextPosition();
}
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
case AXPositionKind::TEXT_POSITION: {
const std::vector<int32_t>& sentence_ends =
text_position->GetAnchor()->GetIntListAttribute(
ax::mojom::IntListAttribute::kSentenceEnds);
return base::Contains(sentence_ends,
int32_t{text_position->text_offset_});
}
}
}
// `AtStartOfParagraph` is asymmetric from `AtEndOfParagraph` because line
// breaks could be present between paragraphs. The end of the paragraph is
// always before all such breaks, whilst the start of paragraph is always
// after.
//
// The start of a paragraph should be a leaf text position (or equivalent),
// either at the start of the whole content, or at the start of a leaf text
// position which is right after the one representing the end of the previous
// paragraph, or the one representing one or more line breaks that separate
// the two paragraphs.
//
// In other words, a position `AsLeafTextPosition` is the start of a paragraph
// if one of the following is true :
// 1. The current leaf text position must be at the start of an anchor, or
// after a '\n' character if white space is preserved (e.g. when using
// <pre>...</pre>, or when in an ARIA label), but not before a '\n' character
// in a <br> element unless multiple consecutive <br> elements are present and
// so empty paragraphs have been created.
// 2. Either (a) the current leaf text position is the first leaf text
// position in the whole content, or (b) there is a line breaking object
// between it and the previous leaf text position including any <br> element.
bool AtStartOfParagraph() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
case AXPositionKind::TEXT_POSITION: {
// 1. The current leaf text position must be at the start of an anchor,
// or after a '\n' character if white space is preserved (e.g. when
// using <pre>...</pre>, or when in an ARIA label), but not before a
// '\n' character in a <br> element unless multiple consecutive <br>
// elements are present and so empty paragraphs have been created.
//
// Note that, in theory, `!AtStartOfAnchor()` implies that
// `MaxTextOffset()` > 0 and `text_offset()` > 0. Therefore,
// `text_position->GetText().at(text_position->text_offset_ - 1)` should
// always be valid. However, as reported by https://crbug.com/1379716,
// this logic appears to have flaws.
//
// TODO(accessibility): Investigate what are these edge cases that lead
// to have a `text_offset_` greater than or equal to the `text` length.
if (!text_position->AtStartOfAnchor()) {
if (!text_position->IsPointingToLineBreak()) {
const std::u16string text = text_position->GetText();
if (static_cast<size_t>(text_position->text_offset_) <
text.length() &&
text.at(text_position->text_offset_) == '\n') {
return true;
}
}
return false;
}
// 2. Either (a) the current leaf text position is the first leaf text
// position in the whole content, or (b) there is a line breaking object
// between it and the previous leaf text position including any <br>
// element.
//
// 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. The
// predicate will return a null position when an anchor movement would
// cross a paragraph boundary, or the start of content has been reached.
const AbortMovePredicate abort_move_predicate =
base::BindRepeating(&AbortMoveAtParagraphBoundary,
ax::mojom::TextBoundary::kParagraphStart);
return text_position
->CreatePreviousLeafTextPosition(abort_move_predicate)
->IsNullPosition();
}
}
}
// `AtEndOfParagraph` is asymmetric from `AtStartOfParagraph` because line
// breaks could be present between paragraphs. The end of the paragraph is
// always before all such breaks, whilst the start of paragraph is always
// after.
//
// The end of a paragraph should be a leaf text position (or equivalent),
// either at the end of the whole content, or at the end of a leaf text
// position which is right before the one representing the start of the next
// paragraph, or the one representing one or more line breaks that separate
// the two paragraphs.
//
// In other words, a position `AsLeafTextPosition` is the end of a paragraph
// if one of the following is true :
// 1. The current leaf text position must be at the end of an anchor, or
// before a '\n' character if white space is preserved (e.g. when using
// <pre>...</pre>, or when in an ARIA label), but not after a '\n' character
// in a <br> element unless multiple consecutive <br> elements are present and
// so empty paragraphs have been created.
// 2. Either (a) the current leaf text position is the last leaf text position
// in the whole content, or (b) there is a line breaking object between it and
// the next leaf text position, including any <br> element.
bool AtEndOfParagraph() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
case AXPositionKind::TEXT_POSITION: {
// 1. The current leaf text position must be at the end of an anchor, or
// before a '\n' character if white space is preserved (e.g. when using
// <pre>...</pre>, or when in an ARIA label), but not after a '\n'
// character in a <br> element unless multiple consecutive <br> elements
// are present and so empty paragraphs have been created.
//
// Note that, in theory, `!AtEndOfAnchor()` implies
// `AtStartOfAnchor()` != `AtEndOfAnchor()` which in turn implies that
// `MaxTextOffset()` > 0 and `text_offset()` < `MaxTextOffset()`.
// Therefore, `text_position->GetText().at(text_position->text_offset_)`
// should always be valid. However, as reported by
// https://crbug.com/1379716, this logic appears to have flaws.
//
// TODO(accessibility): Investigate what are these edge cases that lead
// to have a `text_offset_` greater than or equal to the `text` length.
if (!text_position->AtEndOfAnchor()) {
if (!text_position->IsPointingToLineBreak()) {
const std::u16string text = text_position->GetText();
if (static_cast<size_t>(text_position->text_offset_) <
text.length() &&
text.at(text_position->text_offset_) == '\n') {
return true;
}
}
return false;
}
// 2. Either (a) the current leaf text position is the last leaf text
// position in the whole content, or (b) there is a line breaking object
// between it and the next leaf text position, including any <br>
// element.
//
// Search for the next text position within the current paragraph, using
// the paragraph boundary abort predicate. If a valid position was
// found, then this position cannot be the end of a paragraph. The
// predicate will return a null position when an anchor movement would
// cross a paragraph boundary, or the end of content has been reached.
const AbortMovePredicate abort_move_predicate =
base::BindRepeating(&AbortMoveAtParagraphBoundary,
ax::mojom::TextBoundary::kParagraphEnd);
return text_position->CreateNextLeafTextPosition(abort_move_predicate)
->IsNullPosition();
}
}
}
// Returns true if this position is at the start or right before content that
// is laid out using "display: inline-block".
bool AtStartOfInlineBlock() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
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(
base::BindRepeating(&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(
base::BindRepeating(&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;
}
}
}
// Page boundaries are only supported in certain content types, e.g. PDF
// documents.
bool AtStartOfPage() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
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 content was reached.
AXPositionInstance previous_text_position =
text_position->CreatePreviousLeafTextPosition(
base::BindRepeating(&AbortMoveAtPageBoundary));
return previous_text_position->IsNullPosition();
}
}
}
// Page boundaries are only supported in certain content types, e.g. PDF
// documents.
bool AtEndOfPage() const {
AXPositionInstance text_position = AsLeafTextPosition();
switch (text_position->kind_) {
case AXPositionKind::NULL_POSITION:
return false;
case AXPositionKind::TREE_POSITION:
NOTREACHED();
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 content was reached.
AXPositionInstance next_text_position =
text_position->CreateNextLeafTextPosition(
base::BindRepeating(&AbortMoveAtPageBoundary));
return next_text_position->IsNullPosition();
}
}
}
// Returns true if this position is at the start of the current accessibility
// tree, such as the current iframe, webpage, PDF document, dialog or window.
// Note that the current webpage could be made up of multiple accessibility
// trees stitched together, e.g. an out-of-process iframe will be in its own
// accessibility tree. For the purposes of this method, we don't distinguish
// between out-of-process and in-process iframes, treating them both as tree
// boundaries.
bool AtStartOfAXTree() const {
if (IsNullPosition() || !AtStartOfAnchor())
return false;
AXPositionInstance previous_anchor = CreatePreviousAnchorPosition();
// The start of the whole content should also be the start of an AXTree.
if (previous_anchor->IsNullPosition())
return true;
return previous_anchor->tree_id() != tree_id();
}
// Returns true if this position is at the end of the current accessibility
// tree, such as the current iframe, webpage, PDF document, dialog or window.
// Note that the current webpage could be made up of multiple accessibility
// trees stitched together, e.g. an out-of-process iframe will be in its own
// accessibility tree. For the purposes of this method, we don't distinguish
// between out-of-process and in-process iframes, treating them both as tree
// boundaries.
bool AtEndOfAXTree() const {
if (IsNullPosition() || !IsLeaf() || !AtEndOfAnchor())
return false;
return *CreatePositionAtEndOfAXTree() == *this;
}
// Returns true if this position is at the start of all content. This might
// refer to e.g. a single webpage (made up of multiple iframes), or a PDF
// document. Note that the current webpage could be made up of multiple
// accessibility trees stitched together, so even though a position could be
// at the start of a specific accessibility tree, it might not be at the start
// of the whole content.
bool AtStartOfContent() const {
if (IsNullPosition() || !AtStartOfAnchor())
return false;
return *CreatePositionAtStartOfContent() == *this;
}
// Returns true if this position is at the end of all content. This might
// refer to e.g. a single webpage (made up of multiple iframes), or a PDF
// document. Note that the current webpage could be made up of multiple
// accessibility trees stitched together, so even though a position could be
// at the end of a specific accessibility tree, it might not be at the end of
// the whole content.
bool AtEndOfContent() const {
if (IsNullPosition() || !AtEndOfAnchor())
return false;
return *CreatePositionAtEndOfContent() == *this;
}
// This method finds the lowest common ancestor node in the accessibility tree
// of this and |other| positions' anchor nodes.
AXNode* LowestCommonAnchor(const AXPosition& other) const {
if (IsNullPosition() || other.IsNullPosition())
return nullptr;
if (GetAnchor() == other.GetAnchor())
return GetAnchor();
base::stack<AXNode*> our_ancestors = GetAncestorAnchors();
base::stack<AXNode*> other_ancestors = other.GetAncestorAnchors();
AXNode* 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 LowestCommonAncestorPosition(
const AXPosition& other,
ax::mojom::MoveDirection move_direction) const {
return CreateAncestorPosition(LowestCommonAnchor(other), move_direction);
}
// See "CreateParentPosition" for an explanation of the use of
// |move_direction|.
AXPositionInstance CreateAncestorPosition(
const AXNode* ancestor_anchor,
ax::mojom::MoveDirection move_direction) 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();
const AXNode* leaf_node = GetEmptyObjectAncestorNode();
if (!leaf_node && position->IsLeaf()) {
// If there is no empty object ancestor, but the current position's
// anchor is a leaf, then use the same anchor, as it will be valid
// as long as a valid offset is used.
leaf_node = position->GetAnchor();
}
if (leaf_node) {
// In this class, we define the empty node as a leaf node (see
// `AXNode::IsLeaf()`) that doesn't have any content. On certain
// platforms, and so that such nodes will act as a character and a
// word boundary, we insert an "embedded object replacement character"
// in their text contents. This character is a string of length
// `AXNode::kEmbeddedObjectCharacterLengthUTF16`. For example, an
// empty text field should act as a character and a word boundary when
// a screen reader user tries to navigate through it, otherwise the
// text field would be missed by the user.
//
// Since we just explained that on certain platforms empty leaf nodes
// expose the "embedded object replacement character" in their text
// contents, and since we assume that all text is found only on leaf
// nodes, we should hide any descendants. Thus, a position on a
// descendant of an empty object is defined as invalid. To make it
// valid we move the position from the descendant to the empty leaf
// node itself. Otherwise, character and word navigation won't work
// properly.
AXPositionInstance new_position =
position->child_index() == BEFORE_TEXT
? CreateTreePositionAtStartOfAnchor(*leaf_node)
: CreateTreePositionAtEndOfAnchor(*leaf_node);
DCHECK(new_position->IsLeaf());
return new_position;
}
DCHECK(!position->IsLeaf());
// Not a leaf: use a child index from 0 to AnchorChilkdCount().
if (position->child_index_ == BEFORE_TEXT) {
position->child_index_ = 0;
return position;
}
DCHECK_GE(position->child_index_, 0);
if (position->child_index_ > position->AnchorChildCount())
position->child_index_ = position->AnchorChildCount();
break;
}
case AXPositionKind::TEXT_POSITION: {
if (!position->GetAnchor())
return CreateNullPosition();
if (const AXNode* empty_object_node = GetEmptyObjectAncestorNode()) {
// This is needed because an empty object as defined in this class 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 (on certain platforms) the
// length of the embedded object character here because the
// `MaxTextOffset()` of an empty object on those platforms is
// `AXNode::kEmbeddedObjectCharacterLengthUTF16`. If the invalid
// position was already at the start of the node, we set it to 0.
AXPositionInstance valid_position = CreateTextPosition(
*empty_object_node,
/* text_offset */ 0, ax::mojom::TextAffinity::kDownstream);
if (position->text_offset() > 0)
return valid_position->CreatePositionAtEndOfAnchor();
return std::move(valid_position);
}
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;
}
}
DCHECK(position->IsValid()) << *position;
return position;
}
AXPositionInstance AsTreePosition() const {
if (IsNullPosition() || IsTreePosition())
return Clone();
AXPositionInstance copy = Clone();
DCHECK_GE(copy->text_offset_, 0);
// Note that by design, `AXPosition::IsLeaf()` excludes the text found in
// ignored subtrees from the accessibility tree's text representation. (See
// `AXNode::IsEmptyLeaf()`.)
if (copy->IsLeaf()) {
// Even though leaf positions are generally not anchored to a node with a
// lot of descendants, still, there is the possibility that the leaf node
// is a text field with a large amount of text. We avoid computing
// `MaxTextOffset()` unless it is really necessary.
if (copy->text_offset_ == 0) {
copy->child_index_ = BEFORE_TEXT;
} else {
const int max_text_offset = copy->MaxTextOffset();
copy->child_index_ = copy->text_offset_ != max_text_offset
? BEFORE_TEXT
: AnchorChildCount();
}
copy->kind_ = AXPositionKind::TREE_POSITION;
DCHECK(copy->IsValid());
return copy;
}
// We stop at the first child that we can reach with the current text
// offset. We do not attempt to validate `MaxTextOffset()` in case it
// 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.
//
// Note that even though ignored children should not contribute any text
// content or hypertext to the tree's text representation, we have to
// include them because they might contain unignored descendants. We only
// exclude them if they are both ignored and contain no text content or
// hypertext. The latter is to avoid, as much as we can, the possibility
// that an unignored position will turn into an ignored one after calling
// this method.
int child_index = 0;
for (int current_offset = 0; child_index < copy->AnchorChildCount();
++child_index) {
AXPositionInstance child = copy->CreateChildPositionAt(child_index);
DCHECK(!child->IsNullPosition());
// 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.
//
// Note that in this context "adjacent children" excludes ignored
// children. Note also that children with no text content or no hypertext
// are not skipped, otherwise the following situation will produce an
// erroneous tree position:
// ++kTextField contenteditable=true "" (empty)
// ++++kStaticText "\n" ignored
// ++++++kInlineTextBox "\n" ignored
// ++++kStaticText "" (empty)
// ++++++kInlineTextOffset "" (empty)
// TextPosition anchor=kTextField text_offset=0 affinity=downstream
// AsTreePosition should produce:
// TreePosition anchor=kTextField child_index=1, and not child_index=0 or
// child_index=2
//
// See also `CreateLeafTextPositionBeforeCharacter` and
// `CreateLeafTextPositionAfterCharacter`.
const int child_length = child->MaxTextOffsetInParent();
const bool contributes_no_text_in_parent = !child_length;
const bool is_anchor_unignored = !child->GetAnchor()->IsIgnored();
if (copy->text_offset_ >= current_offset &&
(copy->text_offset_ < (current_offset + child_length) ||
((copy->affinity_ == ax::mojom::TextAffinity::kUpstream ||
(contributes_no_text_in_parent && is_anchor_unignored)) &&
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->AtEndOfAnchor()) {
tree_position =
tree_position
->CreateChildPositionAt(tree_position->child_index_ - 1)
->CreatePositionAtEndOfAnchor();
} else {
tree_position =
tree_position->CreateChildPositionAt(tree_position->child_index_);
}
DCHECK(!tree_position->IsNullPosition());
} while (!tree_position->IsLeaf());
DCHECK(tree_position->IsLeafTreePosition());
return tree_position;
}
AXPositionInstance AsTextPosition() const {
if (IsNullPosition() || IsTextPosition())
return Clone();
AXPositionInstance copy = Clone();
// Check if it is a "before text" position.
if (copy->child_index_ == BEFORE_TEXT) {
DCHECK(copy->IsLeaf())
<< "Before text positions can only appear on leaf nodes.";
// 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 and greater than 0 first, due to the
// possible performance cost of calling `MaxTextOffset()`. Also, if the
// text offset is already 0, we don't need to touch it, and if it is less
// than `MaxTextOffset()` we don't modify it as explained above.
DCHECK_GE(copy->text_offset_, INVALID_OFFSET)
<< "Unrecognized text offset.";
if (copy->text_offset_ == INVALID_OFFSET ||
(copy->text_offset_ > 0 &&
copy->text_offset_ >= copy->MaxTextOffset())) {
copy->text_offset_ = 0;
}
copy->kind_ = AXPositionKind::TEXT_POSITION;
return copy;
}
// Leaf nodes might have descendants that should be hidden for text
// navigation purposes, thus we can't rely solely on `AnchorChildCount()`.
// Any child index that is not `BEFORE_TEXT` should be treated as indicating
// an "after text" position. (See `IsInEmptyObject()` for more information.)
// ++kButton "<embedded_object_character>" (empty)
// ++++kGenericContainer ignored (Might sometimes be added by Blink.)
if (copy->IsLeaf() || copy->child_index_ == copy->AnchorChildCount()) {
copy->text_offset_ = copy->MaxTextOffset();
copy->kind_ = AXPositionKind::TEXT_POSITION;
return copy;
}
DCHECK_GE(copy->child_index_, 0);
DCHECK_LT(copy->child_index_, copy->AnchorChildCount());
int new_offset = 0;
for (int i = 0; i <= child_index_; ++i) {
AXPositionInstance child = copy->CreateChildPositionAt(i);
DCHECK(!child->IsNullPosition());
// 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.
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 whether 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();
AXPositionInstance text_position = Clone();
if (IsTreePosition()) {
DCHECK_NE(child_index(), BEFORE_TEXT)
<< "Before text positions should only be present on leaf anchor "
"nodes.";
DCHECK_GT(AnchorChildCount(), 0)
<< "Non-leaf positions should be anchored to nodes that have "
"children.";
// We can't go directly to a text position if we are initially dealing
// with a tree position, because empty child objects contribute no text to
// the tree's text representation and thus the existing child index
// information would be lost.
//
// ++kRootWebArea
// ++++kGenericContainer (empty object)
// ++++kGenericContainer (empty object)
// TreePosition anchor=kRootWebArea child_index=1 would turn into a text
// position on the same anchor but with a text offset of 0 if we call
// `AsTextPosition()` immediately before first anchoring ourselves to the
// selected child node.
if (child_index() > 0 && child_index() == AnchorChildCount()) {
text_position = CreateChildPositionAt(child_index() - 1)
->CreatePositionAtEndOfAnchor();
} else {
text_position = CreateChildPositionAt(child_index());
}
}
text_position = text_position->AsTextPosition();
DCHECK(!text_position->IsNullPosition());
int offset_in_parent = text_position->text_offset_;
// Determine the anchor and text offset of the leaf equivalent position by
// counting characters that are previous in tree order than
// `offset_in_parent`.
while (!text_position->IsLeaf()) {
AXPositionInstance child = text_position->CreateChildPositionAt(0);
DCHECK(!child->IsNullPosition());
// Note that even though ignored children should not contribute any text
// content or hypertext to the tree's text representation, we have to
// include them because they might contain unignored descendants. We only
// exclude them if they are both ignored and contain no text content or
// hypertext. The latter is to avoid, as much as we can, the possibility
// that an unignored position will turn into an ignored one after calling
// this method.
for (int i = 1;
i < text_position->AnchorChildCount() && offset_in_parent >= 0;
++i) {
const int child_length_in_parent = child->MaxTextOffsetInParent();
const bool contributes_no_text_in_parent =
(child_length_in_parent == 0);
const bool is_anchor_unignored = !child->GetAnchor()->IsIgnored();
if (offset_in_parent == 0 && contributes_no_text_in_parent &&
is_anchor_unignored) {
// If the text offset corresponds to multiple child positions because
// some of the children have no text content or hypertext, the above
// condition ensures that the first child will be chosen; unless it is
// ignored as explained before.
break;
}
if (offset_in_parent < child_length_in_parent)
break;
if (affinity_ == ax::mojom::TextAffinity::kUpstream &&
offset_in_parent == child_length_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->affinity_ = ax::mojom::TextAffinity::kUpstream;
break;
}
child = text_position->CreateChildPositionAt(i);
offset_in_parent -= child_length_in_parent;
}
// The text offset provided by our parent position might need to be
// adjusted, if this is an "after text" position and our anchor node is an
// embedded object (as determined by `IsEmbeddedObjectInParent()`).
// ++kRootWebArea "<embedded_object>"
// ++++kParagraph "Hello"
// TextPosition anchor=kRootWebArea text_offset=1
// should be translated into the following text position
// TextPosition anchor=kParagraph text_offset=5 annotated_text=Hello<>
// and not into the following one
// TextPosition anchor=kParagraph text_offset=1 annotated_text=<H>ello
if (child->IsEmbeddedObjectInParent() &&
offset_in_parent == child->MaxTextOffsetInParent()) {
offset_in_parent -= child->MaxTextOffsetInParent();
offset_in_parent += child->MaxTextOffset();
}
text_position = std::move(child);
}
DCHECK(text_position->IsLeafTextPosition());
text_position->text_offset_ = offset_in_parent;
// 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;
}
// Converts to a text position that is suitable for passing into the renderer
// as a selection endpoint. In other words, converts to a position that is
// suitable for setting as a DOM selection range endpoint.
//
// When blink is asked to set selection, it expects a text position to be
// anchored to the text node (otherwise a generic tree position is assumed
// and the offset is interpreted as a child index).
AXPositionInstance AsDomSelectionPosition() const {
if (IsNullPosition() || GetAnchor()->data().IsAtomicTextField())
return Clone();
AXPositionInstance text_position = AsLeafTextPosition();
if (text_position->GetAnchor() && text_position->GetAnchor()->GetRole() ==
ax::mojom::Role::kInlineTextBox) {
return text_position->CreateParentPosition();
}
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 text content 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 text content 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()) {
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;
}
// This method is similar to `AsUnignoredPosition`, but it will never cross
// an anchor boundary. This means that if the position is at the start or end
// of the anchor, it will return a position at the start or end of the anchor,
// respectively. This is useful when we want to ensure that the resulting
// position is still within the same anchor. If no unignored position can be
// found, it will return a null position.
AXPositionInstance TryAsUnignoredPositionPreservingAnchor(
AXPositionAdjustmentBehavior behavior) const {
if (IsNullPosition()) {
return Clone();
}
AXPositionInstance new_position = AsUnignoredPosition(behavior);
if (GetAnchor() == new_position->GetAnchor()) {
return new_position;
}
// As a last resort, AsUnignoredPosition() may return a position that is
// anchored at a different node. In such case, we need to create a new
// position that is anchored at the same node as this position. To do this,
// Try Calling AsUnignoredPosition in the other direction, starting from
// one of the ends, as there may not have been any unignored positions in
// the original direction.
switch (behavior) {
case AXPositionAdjustmentBehavior::kMoveBackward:
new_position = CreatePositionAtStartOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveForward);
break;
case AXPositionAdjustmentBehavior::kMoveForward:
new_position = CreatePositionAtEndOfAnchor()->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveBackward);
break;
}
if (GetAnchor() != new_position->GetAnchor()) {
// Check to see if the new position can be expressed in terms of the
// current anchor.
new_position = new_position->CreateAncestorPosition(
GetAnchor(), behavior == AXPositionAdjustmentBehavior::kMoveForward
? ax::mojom::MoveDirection::kForward
: ax::mojom::MoveDirection::kBackward);
}
// It could be that there are no unignored positions that can be rooted
// at the current anchor. In such case, we return a null position.
if (new_position->IsIgnored()) {
return CreateNullPosition();
}
// Retain original position type.
if (IsTextPosition()) {
new_position = new_position->AsTextPosition();
} else {
new_position = new_position->AsTreePosition();
}
return new_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 and the requested boundary type, i.e. the
// AXMovementOptions is set to `AXBoundaryBehavior::kStopAtAnchorBoundary` and
// `AXBoundaryDetection::kCheckInitialPosition`. 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.
//
// Observe that `expand_behavior` has an effect only when this position is
// between text units, e.g. between words, lines, paragraphs, etc. Also,
// 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 {
AXMovementOptions left_options{AXBoundaryBehavior::kStopAtAnchorBoundary,
AXBoundaryDetection::kCheckInitialPosition};
AXMovementOptions right_options{
AXBoundaryBehavior::kStopAtAnchorBoundary,
AXBoundaryDetection::kDontCheckInitialPosition};
if (boundary == ax::mojom::TextBoundary::kWebPage) {
left_options =
right_options = {AXBoundaryBehavior::kCrossBoundary,
AXBoundaryDetection::kDontCheckInitialPosition};
}
switch (expand_behavior) {
case AXRangeExpandBehavior::kLeftFirst: {
AXPositionInstance left_position = CreatePositionAtTextBoundary(
boundary, ax::mojom::MoveDirection::kBackward, left_options);
AXPositionInstance right_position =
left_position->CreatePositionAtTextBoundary(
boundary, ax::mojom::MoveDirection::kForward, right_options);
return AXRangeType(std::move(left_position), std::move(right_position));
}
case AXRangeExpandBehavior::kRightFirst: {
AXPositionInstance right_position = CreatePositionAtTextBoundary(
boundary, ax::mojom::MoveDirection::kForward, left_options);
AXPositionInstance left_position =
right_position->CreatePositionAtTextBoundary(
boundary, ax::mojom::MoveDirection::kBackward, right_options);
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,
AXMovementOptions options) const {
AXPositionInstance resulting_position = CreateNullPosition();
switch (boundary) {
case ax::mojom::TextBoundary::kNone:
NOTREACHED();
case ax::mojom::TextBoundary::kCharacter:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousCharacterPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextCharacterPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kFormatEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousFormatEndPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextFormatEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kFormatStart:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousFormatStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextFormatStartPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kFormatStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousFormatStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextFormatEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kLineEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousLineEndPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextLineEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kLineStart:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousLineStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextLineStartPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kLineStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousLineStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextLineEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kObject:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
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::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousPageEndPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextPageEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kPageStart:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousPageStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextPageStartPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kPageStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousPageStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextPageEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kParagraphEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousParagraphEndPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextParagraphEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kParagraphStart:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousParagraphStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextParagraphStartPosition(options);
break;
}
break;
// For UI Automation, empty lines after a paragraph should be merged into
// the preceding paragraph.
//
// See
// https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-uiautomationtextunits#paragraph
case ax::mojom::TextBoundary::kParagraphStartSkippingEmptyParagraphs:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position =
CreatePreviousParagraphStartPositionSkippingEmptyParagraphs(
options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position =
CreateNextParagraphStartPositionSkippingEmptyParagraphs(
options);
break;
}
break;
case ax::mojom::TextBoundary::kParagraphStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousParagraphStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextParagraphEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kSentenceEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousSentenceEndPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextSentenceEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kSentenceStart:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousSentenceStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextSentenceStartPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kSentenceStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousSentenceStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextSentenceEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kWebPage:
DCHECK_EQ(options.boundary_behavior, AXBoundaryBehavior::kCrossBoundary)
<< "We can't reach the start of the whole contents if we are "
"disallowed from crossing boundaries.";
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePositionAtStartOfContent();
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreatePositionAtEndOfContent();
break;
}
break;
case ax::mojom::TextBoundary::kWordEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousWordEndPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextWordEndPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kWordStart:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousWordStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextWordStartPosition(options);
break;
}
break;
case ax::mojom::TextBoundary::kWordStartOrEnd:
switch (direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
resulting_position = CreatePreviousWordStartPosition(options);
break;
case ax::mojom::MoveDirection::kForward:
resulting_position = CreateNextWordEndPosition(options);
break;
}
break;
}
return resulting_position;
}
AXPositionInstance CreatePositionAtStartOfAnchor() const {
const AXNode* anchor = GetAnchor();
if (!anchor)
return CreateNullPosition();
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return CreateNullPosition();
case AXPositionKind::TREE_POSITION:
return CreateTreePositionAtStartOfAnchor(*anchor);
case AXPositionKind::TEXT_POSITION:
return CreateTextPosition(*anchor, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
}
}
AXPositionInstance CreatePositionAtEndOfAnchor() const {
const AXNode* anchor = GetAnchor();
if (!anchor)
return CreateNullPosition();
switch (kind_) {
case AXPositionKind::NULL_POSITION:
return CreateNullPosition();
case AXPositionKind::TREE_POSITION:
return CreateTreePositionAtEndOfAnchor(*anchor);
case AXPositionKind::TEXT_POSITION:
return CreateTextPosition(*anchor, MaxTextOffset(),
ax::mojom::TextAffinity::kDownstream);
}
}
// Creates a position at the start of this position's accessibility tree, e.g.
// at the start of the current iframe, PDF plugin, Views tree, dialog, etc. We
// don't distinguish between out-of-process and in-process iframes, treating
// them both as tree boundaries.
//
// For a similar method that does not stop at iframe boundaries, see
// `CreatePositionAtStartOfContent()`.
AXPositionInstance CreatePositionAtStartOfAXTree() const {
AXPositionInstance root_position =
AsTreePosition()
->CreateAXTreeRootAncestorPosition(
ax::mojom::MoveDirection::kBackward)
->CreatePositionAtStartOfAnchor();
if (IsTextPosition())
root_position = root_position->AsTextPosition();
DCHECK_EQ(root_position->tree_id_, tree_id_)
<< "`CreatePositionAtStartOfAXTree` should not cross any tree "
"boundaries, neither return the null position.";
return root_position;
}
// Creates a position at the end of this position's accessibility tree, e.g.
// at the end of the current iframe, PDF plugin, Views tree, dialog, etc. We
// don't distinguish between out-of-process and in-process iframes, treating
// them both as tree boundaries.
//
// For a similar method that does not stop at iframe boundaries, see
// `CreatePositionAtEndOfContent()`.
AXPositionInstance CreatePositionAtEndOfAXTree() const {
AXPositionInstance root_position =
AsTreePosition()->CreateAXTreeRootAncestorPosition(
ax::mojom::MoveDirection::kBackward);
AXPositionInstance last_position =
root_position->CreatePositionAtEndOfAnchor()->AsLeafTreePosition();
if (IsTextPosition())
last_position = last_position->AsTextPosition();
return last_position;
}
// Creates a position at the start of all content, e.g. at the start of the
// whole webpage, PDF plugin, Views tree, dialog (native, ARIA or HTML),
// window, or the whole desktop.
//
// Note that this method will break out of an out-of-process iframe and return
// a position at the start of the top-level document, but it will not break
// into the Views tree if present. For a similar method that stops at all
// iframe boundaries, see `CreatePositionAtStartOfAXTree()`.
AXPositionInstance CreatePositionAtStartOfContent() const {
AXPositionInstance root_position =
AsTreePosition()
->CreateRootAncestorPosition(ax::mojom::MoveDirection::kBackward)
->CreatePositionAtStartOfAnchor();
if (IsTextPosition())
root_position = root_position->AsTextPosition();
return root_position;
}
// Creates a position at the end of all content, e.g. at the end of the whole
// webpage, PDF plugin, Views tree, dialog (native, ARIA or HTML), window, or
// the whole desktop.
//
// Note that this method will break out of an out-of-process iframe and return
// a position at the end of the top-level document, but it will not break into
// the Views tree if present. For a similar method that stops at all iframe
// boundaries, see `CreatePositionAtEndOfAXTree()`.
AXPositionInstance CreatePositionAtEndOfContent() const {
AXPositionInstance root_position =
AsTreePosition()->CreateRootAncestorPosition(
ax::mojom::MoveDirection::kBackward);
AXPositionInstance last_position =
root_position->CreatePositionAtEndOfAnchor()->AsLeafTreePosition();
if (IsTextPosition())
last_position = last_position->AsTextPosition();
return last_position;
}
AXPositionInstance CreateChildPositionAt(int child_index) const {
if (IsNullPosition() || IsLeaf())
return CreateNullPosition();
if (child_index < 0 || child_index >= AnchorChildCount())
return CreateNullPosition();
const AXNode* child_anchor =
GetAnchor()->GetChildAtIndexCrossingTreeBoundary(child_index);
if (!child_anchor)
return CreateNullPosition();
switch (kind_) {
case AXPositionKind::NULL_POSITION:
NOTREACHED();
case AXPositionKind::TREE_POSITION:
return CreateTreePositionAtStartOfAnchor(*child_anchor);
case AXPositionKind::TEXT_POSITION:
return CreateTextPosition(*child_anchor, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
}
return CreateNullPosition();
}
// Creates a parent equivalent position.
//
// Note that "move_direction" is only taken into consideration when all of
// these three conditions apply: This is a text position, we are in the
// process of searching for a text boundary, and this is a platform where
// child nodes are represented by "object replacement 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 "object replacement
// 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
// object replacement 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();
const AXNode* parent_anchor = GetAnchor()->GetParentCrossingTreeBoundary();
if (!parent_anchor)
return CreateNullPosition();
const AXTree* tree = parent_anchor->tree();
DCHECK(tree);
switch (kind_) {
case AXPositionKind::NULL_POSITION:
NOTREACHED();
case AXPositionKind::TREE_POSITION: {
if (IsLeafNodeForTreePosition(*parent_anchor)) {
if (AtEndOfAnchor() ||
move_direction == ax::mojom::MoveDirection::kForward) {
// If this position is an "after children" or an "after text"
// position inside of a leaf, or we are seeking a parent position
// for a forward movement operation with a parent leaf anchor,
// return a position at the end of the parent anchor.
return CreateTreePositionAtEndOfAnchor(*parent_anchor);
}
// If we are seeking a parent position for a backward movement
// operation, return a position at the start of the parent anchor.
return CreateTreePositionAtStartOfAnchor(*parent_anchor);
}
// If this position is an "after children" or an "after text" position,
// return either an "after children" position on the parent anchor, or a
// position anchored at the next child, depending on whether this is the
// last child in its parent anchor.
int child_index = AnchorIndexInParent();
if (AtEndOfAnchor())
return CreateTreePosition(*parent_anchor, (child_index + 1));
switch (move_direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
// "move_direction" is only important when this position is an
// "embedded object in parent", i.e., when this position's anchor is
// represented by an "object replacement character" in the text of
// its parent anchor. In this case we need to keep the child index
// to be right before the "object replacement character". If this is
// not an "embedded object in parent", then we simply need to use
// the "AnchorIndexInParent" for the child index. However, since
// "AnchorIndexInParent" always returns a child index that is before
// any "object replacement character" in our parent, we use that for
// both situations.
return CreateTreePosition(*parent_anchor, child_index);
case ax::mojom::MoveDirection::kForward:
// "move_direction" is only important when this position is an
// "embedded object in parent", i.e., when this position's anchor is
// represented by an "object replacement character" in the text of
// its parent anchor. In this case we need to move the child index
// to be after the "object replacement character" when this position
// is not at the start of its anchor. If this is not an "embedded
// object in parent", then we simply need to use the
// "AnchorIndexInParent" for the child index.
if (!AtStartOfAnchor() && IsEmbeddedObjectInParent())
++child_index;
return CreateTreePosition(*parent_anchor, child_index);
}
}
case AXPositionKind::TEXT_POSITION: {
// On some platforms, such as Android, Mac and Chrome OS, the text
// content of a node is made up by concatenating the text of child
// nodes. On other platforms, such as Windows IAccessible2 and Linux
// ATK, child nodes are represented by a single "object replacement
// character".
//
// If our parent's text content 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();
// TODO(crbug.com/40885940): temporary disabled until ax position
// autocorrection issue is fixed.
// DCHECK_LE(text_offset_, max_text_offset);
const int max_text_offset_in_parent =
IsEmbeddedObjectInParent()
? AXNode::kEmbeddedObjectCharacterLengthUTF16
: max_text_offset;
int parent_offset = AnchorTextOffsetInParent();
ax::mojom::TextAffinity parent_affinity = affinity_;
// "max_text_offset > 0" is required to filter out anchor nodes that are
// either ignored or empty, i.e. those that contribute no text content
// or hypertext to their parent's text representation. (See example in
// the "else" block.)
if (max_text_offset > 0 &&
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. It turns out, that even in
// the unusual case where there is a single character in our anchor's
// text content but our anchor is represented in our parent by an
// "embedded object replacement character" and not by our text
// content, the outcome is still correct.
parent_offset += text_offset_;
} else {
// Our parent represents our anchor node using an "object replacement"
// character in its text representation. Or, our anchor is a text node
// that is ignored or empty, and so contributes no text in its
// parent's text representation. For example:
// ++kTextField "Before after."
// ++++kStaticText "Before "
// ++++kStaticText "Ignored text" ignored
// ++++kStaticText "after."
// TextPosition anchor=kStaticText (ignored) text_offset=2
// annotated_text="Ig<n>ored text"
if (text_offset_ > 0 && text_offset_ < max_text_offset) {
// If this is a "before text" or an "after text" position, i.e. if
// "text_offset_" == 0 or "max_text_offset", then the child position
// is clearly before or clearly after any "object replacement
// character". No information would be lost when moving to a parent
// equivalent position, including affinity which can easily be
// computed. 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::kNone:
NOTREACHED();
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;
}
} else if (text_offset_ == max_text_offset) {
// Clearly, this is an "after text" position. The text offset should
// be after the "object replacement character". No information would
// be lost when moving to a parent equivalent position, including
// affinity which can easily be computed.
parent_offset += max_text_offset_in_parent;
}
// 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;
}
// There are two cases for which we need to set an upstream affinity on
// the parent position:
//
// Case 1:
// 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.
//
// Case 2:
// If the current position is followed by a generated newline character,
// which is a character that is actually not represented in the text
// content of the nodes but should still act as a stop when navigating
// to the previous/next character.
//
// When this is the case, we almost always want to set an upstream
// affinity on the `parent_position`. The only exception is when our
// current position is contained on a descendant of an empty object,
// because an empty object will hide the textual representation of its
// descendants, including the generated newline characters, by exposing
// a only the empty object character.
//
// Example:
// ++1 kLink "<embedded_object>"
// ++++2 kStaticText "hello" IsLineBreakingObject=true
// ++++++3 kInlineTextBox "hello"
// ++++4 kStaticText "world"
// ++++++5 kInlineTextBox "world"
//
// While there should be a generated newline character at the end of a
// position created on node 2, there won't be one represented to the
// user because node 1 simply exposes the empty object character and not
// its children's text.
AXPositionInstance parent_position =
CreateTextPosition(*parent_anchor, parent_offset, parent_affinity);
if ((AtEndOfAnchor() && !parent_position->AtStartOfAnchor() &&
!parent_position->AtEndOfAnchor() &&
parent_position->AtStartOfLine()) ||
(!IsEmbeddedObjectInParent() && IsFollowedByGeneratedNewline())) {
parent_position->affinity_ = ax::mojom::TextAffinity::kUpstream;
}
return parent_position;
}
}
}
// Creates the next tree position that is anchored at a leaf node of the
// AXTree.
AXPositionInstance CreateNextLeafTreePosition() const {
return CreateNextLeafTreePosition(
base::BindRepeating(&DefaultAbortMovePredicate));
}
// Creates the previous tree position that is anchored at a leaf node of the
// AXTree.
AXPositionInstance CreatePreviousLeafTreePosition() const {
return CreatePreviousLeafTreePosition(
base::BindRepeating(&DefaultAbortMovePredicate));
}
// Creates the next text position that is anchored at a leaf node of the
// AXTree.
AXPositionInstance CreateNextLeafTextPosition() const {
return CreateNextLeafTextPosition(
base::BindRepeating(&DefaultAbortMovePredicate));
}
// Creates the previous text position that is anchored at a leaf node of the
// AXTree.
AXPositionInstance CreatePreviousLeafTextPosition() const {
return CreatePreviousLeafTextPosition(
base::BindRepeating(&DefaultAbortMovePredicate));
}
AXPositionInstance CreateNextPositionAtAnchorWithText() const {
AXPositionInstance text_position = AsLeafTextPosition();
do {
text_position = text_position->CreateNextLeafTextPosition(
base::BindRepeating(&AbortMoveAtRootBoundary));
} while (!text_position->IsNullPosition() &&
(text_position->IsIgnored() || !text_position->MaxTextOffset()));
return text_position;
}
AXPositionInstance CreatePreviousPositionAtAnchorWithText() const {
AXPositionInstance text_position = AsLeafTextPosition();
do {
text_position = text_position->CreatePreviousLeafTextPosition(
base::BindRepeating(&AbortMoveAtRootBoundary));
} while (!text_position->IsNullPosition() &&
(text_position->IsIgnored() || !text_position->MaxTextOffset()));
return text_position->CreatePositionAtEndOfAnchor();
}
// Generated newline characters are not part of any AXNode in the AXTree. They
// are appended to the accessible textual representation exposed to ATs in
// AXRange::GetText. They are necessary to expose the implicit newlines
// created from the layout breaks to screen reader users. For example, the
// following HTML will create an implicit line break after "hello":
//
// <div contenteditable>
// <div>hello</div>
// <div>world</div>
// </div>
//
// Even though there is not explicit line break in this template, the text
// returned for this contenteditable is "hello\nworld". In order to allow
// screen reader users to navigate (either using the caret or the controls
// built-in the AT), we need to create character stops around these
// generated characters.
//
// We can only create character stops around generated newline characters
// when empty objects are represented in the accessible text (ie. when the
// behavior is set to
// `AXEmbeddedObjectBehavior::kExposeCharacterForHypertext`). Otherwise,
// there's a risk that `CreateParentPosition` will create a position that
// doesn't point to the same character. This is because a position located
// right before a generated newline character will be represented in the
// parent ancestor with an upstream affinity.
//
// Let's consider this AXTree:
// 1 root
// ++2 button
// ++3 checkbox
// ++4 static text
// ++++5 inline text box "abc"
//
// The text representation for the entire document, including the generated
// newlines, will be "\n\nabc" if the empty objects do not expose the empty
// object character. If we were to allow character stops at generated
// newline characters, it would be possible to create a next and previous
// position located before/after a generated newline character. However,
// creating an equivalent position in an ancestor would potentially lead to
// an incorrect position.
//
// Example:
// leaf_position_1: anchor=2, text_offset=0, affinity=downstream
// leaf_position_2: anchor=3, text_offset=0, affinity=downstream
//
// `leaf_position_1` and `leaf_position_2` should both return true for
// `AtStartOfParagraph` and `AtEndOfParagraph`. Calling
// `CreateParentPosition` on each of those will respectively create:
// parent_position_1: anchor=1, text_offset=0, affinity=upstream
// parent_position_1: anchor=1, text_offset=0, affinity=upstream
//
// ...which are both the same. `CreateParentPosition` is relatively important
// when it comes to moving the position by character because
// `CreatePreviousCharacterPosition` uses it in many cases to create the
// previous position on the same anchor as the original position.
//
// This is a quirk of the current implementation which cannot easily be fixed,
// because when object replacement characters are missing from empty objects
// (sucha as a checkbox without a label, etc.) any leaf equivalent position
// from one of the objects' ancestors would skip the empty object and create
// the child position at the first non-empty object. Consequently,
// CreateParentPosition cannot easily determine the correct affinity when
// computing parent equivalent positions from positions on empty objects, i.e.
// like the example positions given here. Skipping empty objects when creating
// leaf equivalent positions had to be done, because on platforms where they
// are not represented by an object replacement character, the AT does not
// even know they are there.
bool AllowsCharacterStopsOnGeneratedNewline() const {
return g_ax_embedded_object_behavior ==
AXEmbeddedObjectBehavior::kExposeCharacterForHypertext ||
g_ax_embedded_object_behavior ==
AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent ||
!IsInUnignoredEmptyObject();
}
bool IsFollowedByGeneratedNewline() const {
// Hard line breaks (such as <br> in HTML) are discounted because generated
// newlines are only inserted between neighboring block elements (such as
// <p>Hello</p><p>world</p>). Generated newlines are always a product of
// layout and have no corresponding AXNode to it. Hard line breaks have
// a matching AXNode and thus do not require to be treated differently.
AXPositionInstance leaf_text_position = AsLeafTextPosition();
if (!leaf_text_position->AllowsCharacterStopsOnGeneratedNewline() ||
leaf_text_position->affinity_ != ax::mojom::TextAffinity::kDownstream ||
leaf_text_position->GetAnchor()->IsLineBreak() ||
!leaf_text_position->AtEndOfParagraph()) {
return false;
}
AXPositionInstance next_position =
leaf_text_position->CreateNextPositionAtAnchorWithText();
return next_position->AllowsCharacterStopsOnGeneratedNewline() &&
!next_position->IsNullPosition() &&
!next_position->GetAnchor()->IsLineBreak() &&
next_position->AtStartOfParagraph();
}
bool IsPrecededByGeneratedNewline() const {
// Hard line breaks (such as <br> in HTML) are discounted because generated
// newlines are only inserted between neighboring block elements (such as
// <p>Hello</p><p>world</p>). Generated newlines are always a product of
// layout and have no corresponding AXNode to it. Hard line breaks have
// a matching AXNode and thus do not require to be treated differently.
AXPositionInstance leaf_text_position = AsLeafTextPosition();
if (!leaf_text_position->AllowsCharacterStopsOnGeneratedNewline() ||
leaf_text_position->GetAnchor()->IsLineBreak() ||
!leaf_text_position->AtStartOfParagraph()) {
return false;
}
AXPositionInstance previous_position =
leaf_text_position->CreatePreviousPositionAtAnchorWithText();
if (previous_position->IsNullPosition()) {
// When it's null, it's because we've reached the beginning of the tree.
// We need to make sure we didn't skip any generated newlines that could
// have been before the start of the page and our current position.
AXPositionInstance start_of_content =
CreatePositionAtStartOfContent()->AsLeafTextPosition();
return *start_of_content < *this &&
start_of_content->IsFollowedByGeneratedNewline();
}
return previous_position->AllowsCharacterStopsOnGeneratedNewline() &&
!previous_position->GetAnchor()->IsLineBreak() &&
previous_position->AtEndOfParagraph();
}
// 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 leaf_text_position = AsLeafTextPosition();
if (leaf_text_position->IsFollowedByGeneratedNewline()) {
return leaf_text_position;
}
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();
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()) {
std::unique_ptr<base::i18n::BreakIterator> grapheme_iterator =
text_position->GetGraphemeIterator();
// 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.
if (text_position->text_offset_ < 0 ||
text_position->text_offset_ > text_position->MaxTextOffset()) {
SANITIZER_NOTREACHED() << "Offset range error:\n" << ToDebugString();
return CreateNullPosition();
}
DCHECK_GE(text_position->text_offset_, 0);
DCHECK_LE(text_position->text_offset_, text_position->MaxTextOffset());
while (!text_position->AtStartOfAnchor() &&
(!gfx::IsValidCodePointIndex(
text_position->GetText(),
static_cast<size_t>(text_position->text_offset_)) ||
(grapheme_iterator &&
!grapheme_iterator->IsGraphemeBoundary(
static_cast<size_t>(text_position->text_offset_))))) {
--text_position->text_offset_;
}
return text_position;
}
return text_position->CreateNextPositionAtAnchorWithText();
}
// 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 leaf_text_position = AsLeafTextPosition();
if (leaf_text_position->IsPrecededByGeneratedNewline())
return leaf_text_position;
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();
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()) {
std::unique_ptr<base::i18n::BreakIterator> grapheme_iterator =
text_position->GetGraphemeIterator();
// 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.
if (text_position->text_offset_ < 0 ||
text_position->text_offset_ > text_position->MaxTextOffset()) {
SANITIZER_NOTREACHED() << "Offset range error:\n" << ToDebugString();
return CreateNullPosition();
}
DCHECK_GE(text_position->text_offset_, 0);
DCHECK_LE(text_position->text_offset_, text_position->MaxTextOffset());
while (!text_position->AtEndOfAnchor() &&
(!gfx::IsValidCodePointIndex(
text_position->GetText(),
static_cast<size_t>(text_position->text_offset_)) ||
(grapheme_iterator &&
!grapheme_iterator->IsGraphemeBoundary(
static_cast<size_t>(text_position->text_offset_))))) {
++text_position->text_offset_;
}
// 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;
}
return text_position->CreatePreviousPositionAtAnchorWithText();
}
// 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(
AXMovementOptions options) const {
if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary &&
AtEndOfAnchor()) {
return Clone();
}
AXPositionInstance text_position = AsLeafTextPositionBeforeCharacter();
if (text_position->IsNullPosition()) {
if (options.boundary_behavior != AXBoundaryBehavior::kCrossBoundary)
text_position = Clone();
return text_position;
}
if (text_position->IsFollowedByGeneratedNewline()) {
return CreateNextPositionAtAnchorWithText();
}
// Calling "AsLeafTextPositionBeforeCharacter" should have created a text
// position that is either at a grapheme boundary, or a null position. If
// our text offset is pointing to a position that is in the middle of a
// grapheme cluster, we should not erroneously assume that we are at a
// character boundary and stop because we had been asked to "stop if already
// at boundary". However, we should not modify our position if
// `AsLeafTextPositionBeforeCharacter` has simply moved us to the start of
// the next leaf anchor because we originally happened to be at the end of
// our current anchor. We also need to ensure that we are comparing two
// positions that have the same affinity, since
// `AsLeafTextPositionBeforeCharacter` resets the affinity to downstream,
// while the original affinity might have been upstream.
if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary &&
options.boundary_detection ==
AXBoundaryDetection::kCheckInitialPosition &&
(AtEndOfAnchor() || *text_position == *CloneWithDownstreamAffinity())) {
return Clone();
}
int max_text_offset = text_position->MaxTextOffset();
DCHECK_LT(text_position->text_offset_, max_text_offset);
std::unique_ptr<base::i18n::BreakIterator> grapheme_iterator =
text_position->GetGraphemeIterator();
do {
++text_position->text_offset_;
} while (text_position->text_offset_ < max_text_offset &&
grapheme_iterator &&
!grapheme_iterator->IsGraphemeBoundary(
static_cast<size_t>(text_position->text_offset_)));
DCHECK_GT(text_position->text_offset_, 0);
DCHECK_LE(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 AXNode* common_anchor = text_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
text_position = text_position->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kForward);
} else if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary) {
// If the next character position crosses the current anchor boundary
// with kStopAtAnchorBoundary, 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(
AXMovementOptions options) const {
if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary &&
AtStartOfAnchor()) {
return Clone();
}
AXPositionInstance text_position = AsLeafTextPositionAfterCharacter();
if (text_position->IsNullPosition()) {
if (options.boundary_behavior != AXBoundaryBehavior::kCrossBoundary)
text_position = Clone();
return text_position;
}
// Calling "AsLeafTextPositionAfterCharacter" should have created a text
// position that is either at the start of an anchor that is preceded by a
// generated newline, at a grapheme boundary or a null position.
if (text_position->IsPrecededByGeneratedNewline()) {
// When `text_position` is right after a generated newline character, we
// should create a position located at the end of the previous anchor.
text_position = CreatePreviousPositionAtAnchorWithText();
DCHECK(!text_position->IsNullPosition());
} else {
// If our text offset is pointing to a position that is in the middle of a
// grapheme cluster, we should not erroneously assume that we are at a
// character boundary and stop because we had been asked to "stop if
// already at boundary". However, we should not modify our position if
// `AsLeafTextPositionAfterCharacter` has simply moved us to the end of
// the previous leaf anchor because we originally happened to be at the
// start of our current anchor. We also need to ignore any differences
// that might be due to the affinity, because that should not be a
// determining factor as to whether we would stop if we are already at
// boundary or not.
if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary &&
options.boundary_detection ==
AXBoundaryDetection::kCheckInitialPosition &&
(AtStartOfAnchor() ||
*text_position == *CloneWithUpstreamAffinity() ||
*text_position == *CloneWithDownstreamAffinity())) {
return Clone();
}
DCHECK_GT(text_position->text_offset_, 0);
std::unique_ptr<base::i18n::BreakIterator> grapheme_iterator =
text_position->GetGraphemeIterator();
do {
--text_position->text_offset_;
} while (!text_position->AtStartOfAnchor() && grapheme_iterator &&
!grapheme_iterator->IsGraphemeBoundary(
static_cast<size_t>(text_position->text_offset_)));
DCHECK_GE(text_position->text_offset_, 0);
DCHECK_LT(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 AXNode* common_anchor = text_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
text_position = text_position->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kBackward);
} else if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary) {
// 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(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfWordPredicate),
base::BindRepeating(&AtEndOfWordPredicate),
base::BindRepeating(&GetWordStartOffsetsFunc));
}
AXPositionInstance CreatePreviousWordStartPosition(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfWordPredicate),
base::BindRepeating(&AtEndOfWordPredicate),
base::BindRepeating(&GetWordStartOffsetsFunc));
}
// Word end positions are one past the last character of the word.
AXPositionInstance CreateNextWordEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfWordPredicate),
base::BindRepeating(&AtEndOfWordPredicate),
base::BindRepeating(&GetWordEndOffsetsFunc));
}
// Word end positions are one past the last character of the word.
AXPositionInstance CreatePreviousWordEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfWordPredicate),
base::BindRepeating(&AtEndOfWordPredicate),
base::BindRepeating(&GetWordEndOffsetsFunc));
}
AXPositionInstance CreateNextLineStartPosition(
AXMovementOptions options) const {
options.upstream_bounded = true;
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfLinePredicate),
base::BindRepeating(&AtEndOfLinePredicate));
}
AXPositionInstance CreatePreviousLineStartPosition(
AXMovementOptions options) const {
options.upstream_bounded = true;
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfLinePredicate),
base::BindRepeating(&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(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfLinePredicate),
base::BindRepeating(&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(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfLinePredicate),
base::BindRepeating(&AtEndOfLinePredicate));
}
AXPositionInstance CreateNextFormatStartPosition(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfFormatPredicate),
base::BindRepeating(&AtEndOfFormatPredicate),
base::BindRepeating(&GetFormatStartOffsetsFunc));
}
AXPositionInstance CreatePreviousFormatStartPosition(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfFormatPredicate),
base::BindRepeating(&AtEndOfFormatPredicate),
base::BindRepeating(&GetFormatStartOffsetsFunc));
}
AXPositionInstance CreateNextFormatEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfFormatPredicate),
base::BindRepeating(&AtEndOfFormatPredicate),
base::BindRepeating(&GetFormatEndOffsetsFunc));
}
AXPositionInstance CreatePreviousFormatEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfFormatPredicate),
base::BindRepeating(&AtEndOfFormatPredicate),
base::BindRepeating(&GetFormatEndOffsetsFunc));
}
AXPositionInstance CreateNextSentenceStartPosition(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfSentencePredicate),
base::BindRepeating(&AtEndOfSentencePredicate),
base::BindRepeating(&GetSentenceStartOffsetsFunc));
}
AXPositionInstance CreatePreviousSentenceStartPosition(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfSentencePredicate),
base::BindRepeating(&AtEndOfSentencePredicate),
base::BindRepeating(&GetSentenceStartOffsetsFunc));
}
AXPositionInstance CreateNextSentenceEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfSentencePredicate),
base::BindRepeating(&AtEndOfSentencePredicate),
base::BindRepeating(&GetSentenceEndOffsetsFunc));
}
AXPositionInstance CreatePreviousSentenceEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfSentencePredicate),
base::BindRepeating(&AtEndOfSentencePredicate),
base::BindRepeating(&GetSentenceEndOffsetsFunc));
}
AXPositionInstance CreateNextParagraphStartPosition(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfParagraphPredicate),
base::BindRepeating(&AtEndOfParagraphPredicate));
}
AXPositionInstance CreateNextParagraphStartPositionSkippingEmptyParagraphs(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(
&AtStartOfParagraphExcludingEmptyParagraphsPredicate),
base::BindRepeating(
&AtStartOfParagraphExcludingEmptyParagraphsPredicate));
}
AXPositionInstance CreatePreviousParagraphStartPosition(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfParagraphPredicate),
base::BindRepeating(&AtEndOfParagraphPredicate));
}
AXPositionInstance
CreatePreviousParagraphStartPositionSkippingEmptyParagraphs(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(
&AtStartOfParagraphExcludingEmptyParagraphsPredicate),
base::BindRepeating(
&AtStartOfParagraphExcludingEmptyParagraphsPredicate));
}
AXPositionInstance CreateNextParagraphEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfParagraphPredicate),
base::BindRepeating(&AtEndOfParagraphPredicate));
}
AXPositionInstance CreatePreviousParagraphEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfParagraphPredicate),
base::BindRepeating(&AtEndOfParagraphPredicate));
}
AXPositionInstance CreateNextPageStartPosition(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfPagePredicate),
base::BindRepeating(&AtEndOfPagePredicate));
}
AXPositionInstance CreatePreviousPageStartPosition(
AXMovementOptions options) const {
return CreateBoundaryStartPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfPagePredicate),
base::BindRepeating(&AtEndOfPagePredicate));
}
AXPositionInstance CreateNextPageEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kForward,
base::BindRepeating(&AtStartOfPagePredicate),
base::BindRepeating(&AtEndOfPagePredicate));
}
AXPositionInstance CreatePreviousPageEndPosition(
AXMovementOptions options) const {
return CreateBoundaryEndPosition(
options, ax::mojom::MoveDirection::kBackward,
base::BindRepeating(&AtStartOfPagePredicate),
base::BindRepeating(&AtEndOfPagePredicate));
}
AXPositionInstance CreateBoundaryStartPosition(
AXMovementOptions options,
ax::mojom::MoveDirection move_direction,
BoundaryConditionPredicate at_start_condition,
BoundaryConditionPredicate at_end_condition,
BoundaryTextOffsetsFunc get_start_offsets =
BoundaryTextOffsetsFunc()) const {
AXPositionInstance text_position;
if (!AtEndOfAnchor()) {
// We could get a leaf text position at the end of its anchor, where
// boundary start offsets would surely not be present. In such cases, we
// need to normalize to the start of the next leaf anchor. We avoid making
// this change when we are at the end of our anchor because this could
// effectively shift the position forward.
text_position = AsLeafTextPositionBeforeCharacter();
} else {
text_position = AsLeafTextPosition();
}
if (text_position->IsNullPosition()) {
return text_position;
}
// If true, we should not move the position any further.
bool forward_upstream =
options.upstream_bounded &&
move_direction == ax::mojom::MoveDirection::kForward &&
affinity() == ax::mojom::TextAffinity::kUpstream;
// If true, we should skip the initial position and move at least once.
bool backward_upstream =
options.upstream_bounded &&
move_direction == ax::mojom::MoveDirection::kBackward &&
affinity() == ax::mojom::TextAffinity::kUpstream;
if (backward_upstream ||
(options.boundary_detection ==
AXBoundaryDetection::kDontCheckInitialPosition &&
!forward_upstream)) {
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 (options.boundary_behavior == AXBoundaryBehavior::kCrossBoundary) {
return text_position;
}
return Clone();
}
}
if (!forward_upstream && !at_start_condition.Run(text_position)) {
text_position = text_position->CreatePositionAtNextOffsetBoundary(
move_direction, get_start_offsets);
while (!at_start_condition.Run(text_position)) {
AXPositionInstance next_position;
switch (move_direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
if (text_position->AtStartOfAnchor()) {
next_position = text_position->CreatePreviousLeafTextPosition(
base::BindRepeating(&AbortMoveAtRootBoundary));
} else {
text_position = text_position->CreatePositionAtStartOfAnchor();
DCHECK(!text_position->IsNullPosition());
continue;
}
break;
case ax::mojom::MoveDirection::kForward:
next_position = text_position->CreateNextLeafTextPosition(
base::BindRepeating(&AbortMoveAtRootBoundary));
break;
}
if (next_position->IsNullPosition()) {
if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary) {
switch (move_direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
return CreatePositionAtStartOfAnchor()
->TryAsUnignoredPositionPreservingAnchor(
AXPositionAdjustmentBehavior::kMoveBackward);
case ax::mojom::MoveDirection::kForward:
return CreatePositionAtEndOfAnchor()
->TryAsUnignoredPositionPreservingAnchor(
AXPositionAdjustmentBehavior::kMoveForward);
}
}
if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtLastAnchorBoundary) {
// 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::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
text_position = text_position->CreatePositionAtStartOfAnchor();
break;
case ax::mojom::MoveDirection::kForward:
text_position = text_position->CreatePositionAtEndOfAnchor();
break;
}
break;
}
return next_position->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveForward);
}
// 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 AXNode* common_anchor = text_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
text_position =
text_position->CreateAncestorPosition(common_anchor, move_direction);
} else if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary) {
switch (move_direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
text_position = CreatePositionAtStartOfAnchor()
->TryAsUnignoredPositionPreservingAnchor(
AXPositionAdjustmentBehavior::kMoveBackward);
break;
case ax::mojom::MoveDirection::kForward:
text_position = CreatePositionAtEndOfAnchor()
->TryAsUnignoredPositionPreservingAnchor(
AXPositionAdjustmentBehavior::kMoveForward);
}
// Preserve affinity for forward upstream positions.
if (forward_upstream) {
text_position->affinity_ = ax::mojom::TextAffinity::kUpstream;
}
return text_position;
}
if (IsTreePosition()) {
text_position = text_position->AsTreePosition();
}
AXPositionInstance unignored_position = text_position->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveForward);
// If there are no unignored positions then `text_position` is anchored in
// ignored content at the end of the whole content. For
// `kStopAtLastAnchorBoundary`, try to adjust in the opposite direction to
// return a position within the whole content just before crossing into the
// ignored content. This will be the last unignored anchor boundary.
if (unignored_position->IsNullPosition() &&
options.boundary_behavior ==
AXBoundaryBehavior::kStopAtLastAnchorBoundary) {
unignored_position = text_position->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveBackward);
}
unignored_position->affinity_ = forward_upstream
? ax::mojom::TextAffinity::kUpstream
: ax::mojom::TextAffinity::kDownstream;
return unignored_position;
}
AXPositionInstance CreateBoundaryEndPosition(
AXMovementOptions options,
ax::mojom::MoveDirection move_direction,
BoundaryConditionPredicate at_start_condition,
BoundaryConditionPredicate at_end_condition,
BoundaryTextOffsetsFunc get_end_offsets =
BoundaryTextOffsetsFunc()) const {
AXPositionInstance text_position;
if (!AtStartOfAnchor()) {
// We could get a leaf text position at the start of its anchor, where
// boundary end offsets would surely not be present. In such cases, we
// need to normalize to the end of the previous leaf anchor. We avoid
// making this change when we are at the start of our anchor because this
// could effectively shift the position backward.
text_position = AsLeafTextPositionAfterCharacter();
} else {
text_position = AsLeafTextPosition();
}
if (text_position->IsNullPosition())
return text_position;
if (options.boundary_detection ==
AXBoundaryDetection::kDontCheckInitialPosition) {
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 (options.boundary_behavior == AXBoundaryBehavior::kCrossBoundary)
return text_position;
return Clone();
}
}
if (!at_end_condition.Run(text_position)) {
text_position = text_position->CreatePositionAtNextOffsetBoundary(
move_direction, get_end_offsets);
while (!at_end_condition.Run(text_position)) {
AXPositionInstance next_position;
switch (move_direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
next_position =
text_position
->CreatePreviousLeafTextPosition(
base::BindRepeating(&AbortMoveAtRootBoundary))
->CreatePositionAtEndOfAnchor();
break;
case ax::mojom::MoveDirection::kForward:
if (text_position->AtEndOfAnchor()) {
next_position = text_position->CreateNextLeafTextPosition(
base::BindRepeating(&AbortMoveAtRootBoundary));
} else {
text_position = text_position->CreatePositionAtEndOfAnchor();
DCHECK(!text_position->IsNullPosition());
continue;
}
break;
}
if (next_position->IsNullPosition()) {
if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary) {
switch (move_direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
return CreatePositionAtStartOfAnchor()
->TryAsUnignoredPositionPreservingAnchor(
AXPositionAdjustmentBehavior::kMoveBackward);
case ax::mojom::MoveDirection::kForward:
return CreatePositionAtEndOfAnchor()
->TryAsUnignoredPositionPreservingAnchor(
AXPositionAdjustmentBehavior::kMoveForward);
}
}
if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtLastAnchorBoundary) {
// 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::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
text_position = text_position->CreatePositionAtStartOfAnchor();
break;
case ax::mojom::MoveDirection::kForward:
text_position = text_position->CreatePositionAtEndOfAnchor();
break;
}
break;
}
return next_position->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveBackward);
}
// 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 AXNode* common_anchor = text_position->LowestCommonAnchor(*this);
if (GetAnchor() == common_anchor) {
text_position =
text_position->CreateAncestorPosition(common_anchor, move_direction);
} else if (options.boundary_behavior ==
AXBoundaryBehavior::kStopAtAnchorBoundary) {
switch (move_direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
return CreatePositionAtStartOfAnchor()
->TryAsUnignoredPositionPreservingAnchor(
AXPositionAdjustmentBehavior::kMoveBackward);
case ax::mojom::MoveDirection::kForward:
return CreatePositionAtEndOfAnchor()
->TryAsUnignoredPositionPreservingAnchor(
AXPositionAdjustmentBehavior::kMoveForward);
}
}
// 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->CloneWithDownstreamAffinity();
if (downstream_position->AtStartOfAnchor() ||
downstream_position->AtEndOfAnchor() ||
!downstream_position->AtStartOfLine()) {
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
}
}
if (IsTreePosition())
text_position = text_position->AsTreePosition();
AXPositionInstance unignored_position = text_position->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveBackward);
// If there are no unignored positions then `text_position` is anchored in
// ignored content at the start or end of the whole content. For
// `kStopAtLastAnchorBoundary`, try to adjust in the opposite direction to
// return a position within the whole content just before crossing into the
// ignored content. This will be the last unignored anchor boundary.
if (unignored_position->IsNullPosition() &&
options.boundary_behavior ==
AXBoundaryBehavior::kStopAtLastAnchorBoundary) {
unignored_position = text_position->AsUnignoredPosition(
AXPositionAdjustmentBehavior::kMoveForward);
}
return unignored_position;
}
// Uses depth-first pre-order traversal.
AXPositionInstance CreateNextAnchorPosition() const {
return CreateNextAnchorPosition(
base::BindRepeating(&DefaultAbortMovePredicate));
}
// Uses depth-first pre-order traversal.
AXPositionInstance CreatePreviousAnchorPosition() const {
return CreatePreviousAnchorPosition(
base::BindRepeating(&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 (IsNullPosition() || other.IsNullPosition()) {
if (IsNullPosition() && other.IsNullPosition())
return 0;
return std::nullopt;
}
// Valid positions are required for comparison. Use `AsValidPosition`
// or `SnapToMaxTextOffsetIfBeyond` before calling `CompareTo` or making
// comparisons.
DCHECK(IsValid());
DCHECK(other.IsValid());
if (GetAnchor() == other.GetAnchor())
return SlowCompareTo(other); // No optimization is necessary.
// 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. Obviously, this heuristic cannot be used if one position is
// the ancestor of the other.
//
// 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 a different, slower method which does away with many of our
// optimizations.
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. This will potentially make the
// two positions not be ancestors of one another, if they originally were.
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 whole
// content. There is no way that they could be ancestors of one another,
// so using the slow path is not required.
DCHECK_EQ(SlowCompareTo(other).value(), 0);
return 0;
}
// |this| normalized to a position past the end of the whole content.
// Since we don't know if one position is the ancestor of the other, we
// need to use the slow path.
return SlowCompareTo(other);
}
if (normalized_other_position->IsNullPosition()) {
// |other| normalized to a position past the end of the whole content.
// Since we don't know if one position is the ancestor of the other, we
// need to use the slow path.
return SlowCompareTo(other);
}
// Compute the ancestor stacks of both positions and walk them ourselves
// rather than calling `LowestCommonAnchor`. That way, we can discover the
// first uncommon ancestors which we need to use in order to compare the two
// positions.
const AXNode* common_anchor = nullptr;
base::stack<AXNode*> our_ancestors =
normalized_this_position->GetAncestorAnchors();
base::stack<AXNode*> 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::nullopt;
// If each position has an uncommon ancestor node, we can compare those
// instead of needing to compute ancestor positions. Otherwise we need to
// use "SlowCompareTo". Also, if the two positions became equivalent after
// being normalized above, we can't compare using this optimized method. We
// need to use "SlowCompareTo", because affinity information would have been
// lost during the normalization process. See comments in "SlowCompareTo"
// for an explanation of how affinity could affect the comparison. If one
// position is the ancestor of the other, we need to use "SlowCompareTo",
// especially if either or both positions are text positions, because the
// conversion to tree positions below would lose information that could
// affect the comparison. In the case where the positions are ancestors of
// one another, but they are both tree positions, using the "SlowCompareTo"
// method will not affect performance, so we still opt for that. Note that
// determining whether two positions are ancestors of one another could
// easily be accomplished by checking if there are any ancestors left after
// removing the common ancestor anchor from either position's ancestor
// stack.
if (our_ancestors.empty() || other_ancestors.empty())
return SlowCompareTo(other);
AXPositionInstance this_uncommon_tree_position =
CreateTreePositionAtStartOfAnchor(*our_ancestors.top());
int this_uncommon_ancestor_index =
this_uncommon_tree_position->AnchorIndexInParent();
AXPositionInstance other_uncommon_tree_position =
CreateTreePositionAtStartOfAnchor(*other_ancestors.top());
int other_uncommon_ancestor_index =
other_uncommon_tree_position->AnchorIndexInParent();
DCHECK_NE(this_uncommon_ancestor_index, other_uncommon_ancestor_index)
<< "Deepest uncommon ancestors should truly be uncommon, i.e. not "
"the same.";
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 or by word. The "SlowCompareTo" method can handle such corner
// cases. For some reproduction steps see https://crbug.com/1057831.
//
// 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.
//
// 1. 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
//
// 2. 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. We also don't need to perform an
// adjustment if one of the positions is not right after the "object
// replacement character" representing the object inside which the other
// position is located, hence the `AtStartOfAnchor()` and
// `IsEmbeddedObjectInParent()` checks.
if (abs(result) == 1 &&
((IsTextPosition() && !AtStartOfAnchor() &&
this_uncommon_tree_position->IsEmbeddedObjectInParent()) ||
(other.IsTextPosition() && !other.AtStartOfAnchor() &&
other_uncommon_tree_position->IsEmbeddedObjectInParent()))) {
return SlowCompareTo(other);
}
#if DCHECK_IS_ON()
// Validate the optimization against the non-optimized version of the
// method.
int slow_result = SlowCompareTo(other).value();
DCHECK((result == 0 && slow_result == 0) ||
(result < 0 && slow_result < 0) || (result > 0 && slow_result > 0))
<< result << " vs. " << slow_result;
#endif // DCHECK_IS_ON()
return result;
}
// A less optimized, but much slower version of "CompareTo". Should only be
// used when optimizations cannot be applied, e.g. when comparing ignored
// positions. See "CompareTo" for an explanation of the return values.
std::optional<int> SlowCompareTo(const AXPosition& other) const {
if (IsNullPosition() && other.IsNullPosition())
return 0;
if (IsNullPosition() || other.IsNullPosition())
return std::nullopt;
// If both positions share an anchor and either one is a text position, or
// both are tree positions, we can do a straight comparison of text offsets
// or child indices.
if (GetAnchor() == other.GetAnchor()) {
std::optional<int> optional_result;
ax::mojom::TextAffinity this_affinity;
ax::mojom::TextAffinity other_affinity;
if (IsTextPosition()) {
AXPositionInstance other_text_position = other.AsTextPosition();
optional_result = text_offset_ - other_text_position->text_offset_;
this_affinity = affinity();
other_affinity = other_text_position->affinity();
} else if (other.IsTextPosition()) {
AXPositionInstance this_text_position = AsTextPosition();
optional_result = this_text_position->text_offset_ - other.text_offset_;
this_affinity = this_text_position->affinity();
other_affinity = other.affinity();
}
if (optional_result) {
// Only when the two positions are otherwise equivalent will affinity
// play a role.
if (*optional_result != 0)
return optional_result;
if (this_affinity == ax::mojom::TextAffinity::kUpstream &&
other_affinity == ax::mojom::TextAffinity::kDownstream) {
return -1;
}
if (this_affinity == ax::mojom::TextAffinity::kDownstream &&
other_affinity == ax::mojom::TextAffinity::kUpstream) {
return 1;
}
return optional_result;
}
return child_index_ - other.child_index_;
}
// 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 position,
// especially if that ancestor is all the way up to the root of the tree,
// computing the parent position will need to be done repeatedly. We avoid
// the performance hit by converting both positions to tree positions and
// only falling back to computing ancestor text positions if at least one
// position is a text position and they don't have the same anchor.
//
// Essentially, the question we need to answer is: "When are two non
// equivalent positions going to erroneously have the same lowest common
// ancestor position when converted to tree positions as the ones they had
// before the conversion?" In other words, when will
// "this->AsTreePosition()->LowestCommonAncestorPosition(*other.AsTreePosition())
// ==
// other.AsTreePosition()->LowestCommonAncestorPosition(*this->AsTreePosition())"?
// The answer is either when they have the same anchor and at least one is a
// text position, (a case that was dealt with in the previous block), or
// when at least one is a text position and one is an ancestor position of
// the other. In all other cases, no information will be lost when
// converting to tree positions.
const AXNode* common_anchor = this->LowestCommonAnchor(other);
if (!common_anchor)
return std::nullopt;
// If either of the two positions is a text position, and if one position is
// an ancestor of the other, we need to compare using text positions,
// because converting to tree positions will potentially lose information if
// the text offset is anything other than 0 or `MaxTextOffset()`.
if (IsTextPosition() || other.IsTextPosition()) {
std::optional<int> optional_result;
ax::mojom::TextAffinity this_affinity;
ax::mojom::TextAffinity other_affinity;
// The following two "if" blocks deal with comparisons between two
// positions (one of which is a text position) that are ancestors of one
// another. The third "if" block deals with comparisons between two text
// positions that may or may not be ancestors of one another. Obviously,
// in the case of two text positions, affinity could always play a role
// (see comment in the relevant "if" block for an example). For the first
// two cases, affinity still needs to be taken into consideration because
// an "object replacement character" could be used to represent child
// nodes in the text of their parents. Here is an example of how affinity
// can influence a text/tree position comparison.
//
// 1 kRootWebArea
// ++2 kGenericContainer
// "<embedded_object_character><embedded_object_character>"
// ++3 kButton "Line 1"
// ++++++4 kStaticText "Line 1"
// ++++++++5 kInlineTextBox "Line 1"
// ++++6 kImage "<embedded_object_character>" kIsLineBreakingObject
//
// TextPosition anchor_id=5 text_offset=2 affinity=downstream
// annotated_text=Li<n>e 1
//
// TreePosition anchor_id=6 child_index=BEFORE_TEXT
//
// The `LowestCommonAncestor` for both will differ in its affinity:
// TextPosition anchor_id=2 text_offset=1 affinity=...
// annotated_text=embedded_object_character<embedded_object_character>
//
// The text position would create a kUpstream position, while the tree
// position would create a kDownstream position.
if (GetAnchor() == common_anchor) {
DCHECK_EQ(AsTextPosition()->GetAnchor(), common_anchor)
<< "AsTextPosition() should never modify the position's anchor.";
// This text position's anchor is the common ancestor of the other text
// position's anchor. We don't need to compute the ancestor position of
// this position at the common anchor, since we already have it.
//
// Note that we convert the other position to an ancestor text position
// using a forward direction, so that if there are any "object
// replacement characters", two positions one inside the character and
// one after it would compare as equivalent. Otherwise, screen readers
// might get stuck inside embedded objects while navigating by character
// or word. For some reproduction steps see https://crbug.com/1057831.
// Per the IAccessible2 Spec, any selection that partially selects text
// inside an embedded object, should select the entire "object
// replacement character" in the parent object where the character
// appears.
AXPositionInstance other_text_position =
other.AsTextPosition()->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kForward);
DCHECK_EQ(other_text_position->GetAnchor(), common_anchor);
other_affinity = other_text_position->affinity();
AXPositionInstance this_text_position = AsTextPosition();
this_affinity = this_text_position->affinity();
optional_result = this_text_position->text_offset() -
other_text_position->text_offset();
} else if (other.GetAnchor() == common_anchor) {
DCHECK_EQ(other.AsTextPosition()->GetAnchor(), common_anchor)
<< "AsTextPosition() should never modify the position's anchor.";
// The other text position's anchor is the common ancestor of this text
// position's anchor. We don't need to compute the ancestor position of
// the other position at the common anchor, since we already have it.
//
// Note that we convert this position to an ancestor text position using
// a forward direction, so that if there are any "object replacement
// characters", two positions one inside the character and one after it
// would compare as equivalent. Otherwise, screen readers might get
// stuck inside embedded objects while navigating by character or word.
// For some reproduction steps see https://crbug.com/1057831.
// Per the IAccessible2 Spec, any selection that partially selects text
// inside an embedded object, should select the entire "object
// replacement character" in the parent object where the character
// appears.
AXPositionInstance this_text_position =
AsTextPosition()->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kForward);
DCHECK_EQ(this_text_position->GetAnchor(), common_anchor);
this_affinity = this_text_position->affinity();
AXPositionInstance other_text_position = other.AsTextPosition();
other_affinity = other_text_position->affinity();
optional_result = this_text_position->text_offset() -
other_text_position->text_offset();
} else if (IsTextPosition() && other.IsTextPosition()) {
// We should compute and compare using the common ancestor text
// position. Computing an ancestor text position will automatically take
// affinity into consideration. It will also normalize text positions at
// the end of their anchors to equivalent positions at the start of the
// next anchor. Additionally, it would normalize positions within
// "object replacement characters" to before the character, because the
// two positions are not ancestors of one another and thus the special
// case (see previous block) defined in the IAccessible2 Spec doesn't
// apply. This process would maintain the characteristics of text
// position comparisons, since a particular offset in the tree's text
// representation could refer to multiple equivalent positions which are
// anchored to different nodes in the tree, i.e. nodes which are
// adjacent, or nodes that are at different levels of the tree.
//
// Here is an example of how affinity can influence a text position
// comparison when at a line boundary:
//
// 1 kRootWebArea
// ++2 kTextField "Line 1Line 2"
// ++++3 kStaticText "Line 1"
// ++++++4 kInlineTextBox "Line 1"
// ++++5 kGenericContainer kIsLineBreakingObject
// ++++++6 kStaticText "Line 2"
// ++++++++7 kInlineTextBox "Line 2"
//
// TextPosition anchor_id=4 text_offset=6 affinity=downstream
// annotated_text=Line 1<>
//
// TextPosition anchor_id=7 text_offset=0 affinity=downstream
// annotated_text=<L>ine 2
//
// The `LowestCommonAncestor` for both will differ only in its affinity:
// TextPosition anchor_id=2 text_offset=6 affinity=...
// annotated_text=Line 1<L>ine 2
//
// anchor_id=4 would create a kUpstream position, while anchor_id=7
// would create a kDownstream position.
AXPositionInstance this_text_position_ancestor =
LowestCommonAncestorPosition(other,
ax::mojom::MoveDirection::kBackward);
AXPositionInstance other_text_position_ancestor =
other.LowestCommonAncestorPosition(
*this, ax::mojom::MoveDirection::kBackward);
DCHECK(this_text_position_ancestor->IsTextPosition());
DCHECK(other_text_position_ancestor->IsTextPosition());
this_affinity = this_text_position_ancestor->affinity();
other_affinity = other_text_position_ancestor->affinity();
optional_result = this_text_position_ancestor->text_offset() -
other_text_position_ancestor->text_offset();
}
if (optional_result) {
// Only when the two positions are otherwise equivalent will affinity
// play a role.
if (*optional_result != 0)
return optional_result;
if (this_affinity == ax::mojom::TextAffinity::kUpstream &&
other_affinity == ax::mojom::TextAffinity::kDownstream) {
return -1;
}
if (this_affinity == ax::mojom::TextAffinity::kDownstream &&
other_affinity == ax::mojom::TextAffinity::kUpstream) {
return 1;
}
return optional_result;
}
}
// Both positions are tree positions. We should normalize all tree positions
// to the beginning of their anchors, unless one of the positions is the
// ancestor of the other. In the latter case, such a normalization would
// potentially lose information if performed on any of the two positions.
//
// ++kRootWebArea "<embedded_object><embedded_object>"
// ++++kParagraph "Paragraph1"
// ++++kParagraph "paragraph2"
// A tree position at the end of the root web area and a tree position at
// the end of the second paragraph should compare as equal. Normalizing any
// of the two positions to the start of their respective anchors would make
// the two positions unequal.
//
// Unlike text positions, two tree positions on two adjacent anchors, (the
// first position at the end of its anchor, (i.e. an "after children"
// position), and the other at its beginning), should not compare as equal.
// This is because each position in the tree is unique, unlike an offset in
// the tree's text representation which can refer to more than one tree
// position. Meanwhile, affinity does not play any role in this case, since
// except for "after children" positions, tree positions are collapsed to
// the beginning of their parent node when computing their parent position.
AXPositionInstance this_normalized_tree_position = AsTreePosition();
AXPositionInstance other_normalized_tree_position = other.AsTreePosition();
if (GetAnchor() != common_anchor &&
other_normalized_tree_position->GetAnchor() != common_anchor) {
// None of the positions is the ancestor of the other, so normalization
// could go ahead.
this_normalized_tree_position =
this_normalized_tree_position->CreatePositionAtStartOfAnchor();
other_normalized_tree_position =
other_normalized_tree_position->CreatePositionAtStartOfAnchor();
}
AXPositionInstance this_tree_position_ancestor =
this_normalized_tree_position->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kBackward);
AXPositionInstance other_tree_position_ancestor =
other_normalized_tree_position->CreateAncestorPosition(
common_anchor, ax::mojom::MoveDirection::kBackward);
DCHECK(this_tree_position_ancestor->IsTreePosition());
DCHECK(other_tree_position_ancestor->IsTreePosition());
return 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;
}
bool IsInEmptyObject() const {
if (IsNullPosition())
return false;
return IsEmptyObject(*GetAnchor());
}
bool IsInUnignoredEmptyObject() const {
return GetAnchor() && !GetAnchor()->IsIgnored() && IsInEmptyObject();
}
// Returns whether the position is anchored in an unignored and empty object,
// has an author specified name that is not empty, and it is not anchored in
// an image. This is because in UIA we want to expose embedded object
// characters for image elements, even if they have an author specified name.
// Only used for UIA.
bool EmptyObjectShouldProvideNameFromAttribute() const {
DCHECK(IsInUnignoredEmptyObject());
return GetAnchor()->GetNameFrom() == ax::mojom::NameFrom::kAttribute &&
!IsImage(GetAnchor()->GetRole()) &&
!GetAnchor()->GetNameUTF16().empty();
}
AXNode* GetEmptyObjectAncestorNode() const {
if (!GetAnchor())
return nullptr;
if (!GetAnchor()->IsIgnored()) {
// The only cases where a descendant of an empty object can be unignored
// is when on Windows we are inside of a collapsed popup button which is
// the parent of a menu list popup, or on all platforms inside a generic
// container that is the child of an empty text field.
if (AXNode* popup_button =
GetAnchor()->GetCollapsedMenuListSelectAncestor()) {
return popup_button;
}
if (GetAnchorRole() == ax::mojom::Role::kGenericContainer &&
!AnchorUnignoredChildCount()) {
return GetAnchor()->GetTextFieldAncestor();
}
return nullptr;
}
// The first unignored ancestor is necessarily the empty object if this node
// is the descendant of an empty object.
AXNode* ancestor_node = GetLowestUnignoredAncestor();
if (!ancestor_node)
return nullptr;
AXPositionInstance position =
CreateTextPosition(*ancestor_node, 0 /* text_offset */,
ax::mojom::TextAffinity::kDownstream);
if (position->IsInUnignoredEmptyObject())
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_);
// We explicitly don't swap any cached members.
name_ = std::u16string();
other.name_ = std::u16string();
}
// Returns the text (in UTF16 format) 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 most child nodes and empty
// objects.
std::u16string GetText(
const AXEmbeddedObjectBehavior embedded_object_behavior =
g_ax_embedded_object_behavior) const {
if (IsNullPosition()) {
return std::u16string();
}
static const base::NoDestructor<std::u16string> embedded_character_str(
AXNode::kEmbeddedObjectCharacterUTF16);
switch (embedded_object_behavior) {
case AXEmbeddedObjectBehavior::kSuppressCharacter:
return GetAnchor()->GetTextContentUTF16();
case AXEmbeddedObjectBehavior::kExposeCharacterForHypertext:
// Special case, if a position's anchor node has only ignored
// descendants, i.e., it appears to be empty to assistive software, on
// some platforms we need to still treat it as a character and a word
// boundary. We achieve this by adding an embedded object character in
// the text representation used by this class, but we don't expose that
// character to assistive software that tries to retrieve the node's
// text content.
if (IsInUnignoredEmptyObject()) {
return *embedded_character_str;
}
return GetAnchor()->GetHypertext();
case AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent:
// For UIA, we still have the notion of embedded object characters for
// text navigation purposes. I.e. when AT's need to navigate around
// nodes and elements which are empty and should then be exposed as
// embedded object characters.
//
// According to the spec, we should favor author supplied names over
// names from content. However, trying to fulfill this in every case
// leads to bugs in the UIA implementation in the TextRangeProvider
// since we create leaf text positions, which means that they will
// always have name from content. As such, for now we are
// implementing this special case where we will only return the author
// specified name if NameFrom is kAttribute and the name is not empty.
// Even though a case like:
// <button aria-label="label">hello</button>
// Should have its name exposed as "label" according to the spec
// but we will expose "hello" instead.
// Exposing the aria label here would make us expose text that isn't on
// a leaf position, and throughout our UIA implementation, we always
// assume and expect to be on leaf positions. Exposing the label
// when it has text from content would effectively hide the subtree
// from UIA ATs
// https://www.w3.org/TR/accname-1.1/#mapping_additional_nd_te
if (IsInUnignoredEmptyObject()) {
if (EmptyObjectShouldProvideNameFromAttribute()) {
return GetAnchor()->GetNameUTF16();
}
return *embedded_character_str;
}
// However, for UIA, we don't want to expose the Hypertext like the
// kExposeCharacterForHypertext case does, since that computation for
// Hypertext is IA2-specific. Instead, UIA needs the text contents of
// the node, which is what GetTextContentUTF16() returns.
return GetAnchor()->GetTextContentUTF16();
}
}
// Determines if this position is pointing to text inside a node that causes a
// line break. For example, a tree position pointing to a <br> element or a
// text node whose only content is the
// '\n' character, or a text position pointing to a '\n' character in its
// anchor's text representation.
bool IsPointingToLineBreak() const {
if (IsNullPosition())
return false;
// The position might be an ancestor position that does not currently point
// to a line break node, but once resolved to a leaf position, it might do
// so. This could only occur when we have a text position, because tree
// positions do not point to text unless they are anchored directly to a
// text node.
if (IsTextPosition()) {
AXPositionInstance leaf_text_position = AsLeafTextPosition();
DCHECK(leaf_text_position->GetAnchor());
if (leaf_text_position->GetAnchor()->IsLineBreak())
return true;
std::u16string text = leaf_text_position->GetText();
if (text.empty() ||
static_cast<size_t>(leaf_text_position->text_offset()) >=
text.length()) {
return false;
}
return text[leaf_text_position->text_offset()] == '\n';
}
// Tree position.
return GetAnchor()->IsLineBreak();
}
// Determines if the anchor containing this position is a text object.
bool IsInTextObject() const {
if (IsNullPosition())
return false;
return GetAnchor()->IsText();
}
// Determines if the anchor containing this position is a text field object.
bool IsInTextField() const {
if (IsNullPosition()) {
return false;
}
return GetAnchor()->data().IsTextField();
}
// 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". Note that
// by the above definition, if a position is pointing to a whitespace
// character, but not all of the text inside the position's anchor is
// whitespace, this method returns false.
bool IsInWhiteSpace() const {
if (IsNullPosition())
return false;
if (GetAnchor()->IsLineBreak())
return true; // A <br> or a text node whose contents is a single '\n'.
std::u16string text = GetText();
// `base::ContainsOnlyChars` returns true if the text is empty, which is not
// what we want here because the empty text is not the same as text with
// only whitespace characters. So, we explicitly exclude that possibility.
return !text.empty() &&
base::ContainsOnlyChars(text, base::kWhitespaceUTF16);
}
// 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 most child nodes and empty
// objects.
//
// Similar to "text_offset_", the length of the text is in UTF16 code units,
// not in grapheme clusters.
int MaxTextOffset(const AXEmbeddedObjectBehavior embedded_object_behavior =
g_ax_embedded_object_behavior) const {
if (IsNullPosition())
return INVALID_OFFSET;
switch (embedded_object_behavior) {
case AXEmbeddedObjectBehavior::kSuppressCharacter:
// TODO(nektar): Switch to anchor->GetTextContentLengthUTF8() after
// AXPosition switches to using UTF8.
return GetAnchor()->GetTextContentLengthUTF16();
case AXEmbeddedObjectBehavior::kExposeCharacterForHypertext:
// Special case: If a node has only ignored descendants, i.e., it
// appears to be empty to assistive software, on some platforms we need
// to still treat it as a character and a word boundary. We achieve this
// by adding an "object replacement character" in the accessibility
// tree's text representation, but we don't expose that character to
// assistive software that tries to retrieve the node's text content or
// hypertext.
if (IsInUnignoredEmptyObject())
return AXNode::kEmbeddedObjectCharacterLengthUTF16;
return static_cast<int>(GetAnchor()->GetHypertext().length());
case AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent:
// For UIA, we still have the notion of embedded object characters for
// text navigation purposes. I.e. when AT's need to navigate around
// nodes and elements which are empty and should then be exposed as
// embedded object characters, and as such we need to return the length
// of the embedded object character when calculating the `MaxTextOffset`
// for these nodes.
//
// According to the spec, we should favor author supplied names over
// names from content. However, trying to fulfill this in every case
// leads to bugs in the UIA implementation in the TextRangeProvider
// since we create leaf text positions, which means that they will
// always have name from content. As such, for now we are
// implementing this special case where we will only return the author
// specified name if NameFrom is kAttribute and the name is not empty.
if (IsInUnignoredEmptyObject()) {
if (EmptyObjectShouldProvideNameFromAttribute()) {
return (int)GetAnchor()->GetNameUTF16().length();
}
return AXNode::kEmbeddedObjectCharacterLengthUTF16;
}
// However, for UIA, we don't want to expose the Hypertext like the
// kExposeCharacterForHypertext case does, since that computation for
// Hypertext is IA2-specific. Instead, UIA needs the text contents of
// the node, so for `MaxTextOffset()` we should return the length of the
// text content.
return GetAnchor()->GetTextContentLengthUTF16();
}
}
// Returns the accessibility role of this position's anchor node. If this is a
// "null position", returns `ax::mojom::Role::kUnknown`.
ax::mojom::Role GetRole() const {
if (IsNullPosition())
return ax::mojom::Role::kUnknown;
return GetAnchor()->GetRole();
}
AXTextAttributes GetTextAttributes() const {
// Check either the current anchor or its parent for text attributes.
AXTextAttributes current_anchor_text_attributes =
!IsNullPosition() ? GetAnchor()->GetTextAttributes()
: AXTextAttributes();
if (current_anchor_text_attributes.IsUnset()) {
AXPositionInstance parent_position =
AsTreePosition()->CreateParentPosition(
ax::mojom::MoveDirection::kBackward);
if (!parent_position->IsNullPosition()) {
return parent_position->GetAnchor()->GetTextAttributes();
}
}
return current_anchor_text_attributes;
}
protected:
AXPosition()
: tree_id_(AXTreeIDUnknown()),
anchor_id_(kInvalidAXNodeID),
child_index_(INVALID_INDEX),
text_offset_(INVALID_OFFSET) {}
// 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.
//
// Work with a tree position so as not to incur any performance hit for
// calculating the corresponding text offset in the parent anchor on
// platforms that do not use an "object replacement character" to represent
// child nodes.
//
// Ignored positions are not visible to platform APIs. As a result, their
// text content or hypertext does not appear in their parent node, but the
// text of their unignored children does. (See `AXNode::GetHypertext()` for
// the meaning of "hypertext" in this context.
AXPositionInstance tree_position =
CreatePositionAtStartOfAnchor()->AsTreePosition();
DCHECK(!tree_position->IsNullPosition());
AXPositionInstance parent_position = tree_position->CreateParentPosition(
ax::mojom::MoveDirection::kBackward);
if (parent_position->IsNullPosition())
return 0; // There is only a single root node.
int offset_in_parent = 0;
for (int i = 0; i < parent_position->child_index(); ++i) {
AXPositionInstance child = parent_position->CreateChildPositionAt(i);
DCHECK(!child->IsNullPosition());
offset_in_parent += child->MaxTextOffsetInParent();
}
return offset_in_parent;
}
// In the case of a text position, lazily initializes or returns the existing
// grapheme iterator for the position's text. The grapheme iterator breaks at
// every grapheme cluster boundary.
//
// We only allow creating this iterator on leaf nodes. We currently don't need
// to move by grapheme boundaries on non-leaf nodes and computing plus caching
// the text content for all nodes is costly.
std::unique_ptr<base::i18n::BreakIterator> GetGraphemeIterator() const {
if (!IsLeafTextPosition())
return {};
// TODO(nektar): Remove member variable `name_` once hypertext has been
// migrated to AXNode. Currently, hypertext in AXNode gets updated every
// time the `AXNode::GetHypertext()` method is called which erroniously
// invalidates this AXPosition.
name_ = GetText();
auto grapheme_iterator = std::make_unique<base::i18n::BreakIterator>(
name_, base::i18n::BreakIterator::BREAK_CHARACTER);
if (!grapheme_iterator->Init())
return {};
return grapheme_iterator;
}
void InitializeWithoutValidation(AXPositionKind kind,
AXTreeID tree_id,
AXNodeID 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_ = kInvalidAXNodeID;
child_index_ = INVALID_INDEX;
text_offset_ = INVALID_OFFSET;
affinity_ = ax::mojom::TextAffinity::kDownstream;
}
}
void Initialize(AXPositionKind kind,
AXTreeID tree_id,
AXNodeID 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;
// TODO(accessibility) Consider using WeakPtr<AXTree> instead of an
// AXTreeID, which would be both faster and easier to use in combination
// with AXTreeSnapshotter, which does not use AXTreeManager to cache
// AXTreeIDs in a map.
SANITIZER_CHECK(GetManager() || kind_ == AXPositionKind::NULL_POSITION)
<< "Tree manager required, tree_id = " << tree_id.ToString()
<< " is unknown = " << (tree_id == AXTreeIDUnknown());
SANITIZER_CHECK(GetAnchor() || kind_ == AXPositionKind::NULL_POSITION)
<< "Creating a position without an anchor is disallowed:\n"
<< ToDebugString();
// TODO(crbug.com/40885940) Remove this line and let the below IsValid()
// assertion get triggered instead. We shouldn't be creating test positions
// with offsets that are too large. This seems to occur when the anchor node
// is ignored, and leads to a number of failing tests.
// Comment this line out as a known performance culprit (also see
// crbug.com/1401591).
// SnapToMaxTextOffsetIfBeyond();
#if defined(AX_EXTRA_MAC_NODES)
// Temporary hack to constrain child index when extra mac nodes are present.
// TODO(accessibility) Remove this hack that works around the fact that Mac
// can set a selection on extra mac nodes, which looks invalid because the
// child index is larger than AnchorChildCount(), which does not account
// for them. We need to get a child count that includes extra mac nodes,
// similar to how BrowserAccessibility::PlatformChildCount() does.
if (!IsValid() && IsTreePosition() && IsTableLike(GetAnchor()->GetRole()) &&
child_index > AnchorChildCount()) {
child_index_ = AnchorChildCount();
}
#endif
// TODO(crbug.com/40885940) see TODO above.
// Also look for the failures in
// AXPositionTest.AsLeafTextPositionBeforeCharacterIncludingGeneratedNewlines,
// AXPlatformNodeTextRangeProviderTest.TestNormalizeTextRangeForceSameAnchorOnDegenerateRange.
// SANITIZER_CHECK(IsValid()) << "Creating invalid positions is
// disallowed:\n"
// << ToDebugString();
}
int AnchorChildCount() const {
if (!GetAnchor())
return 0;
return static_cast<int>(GetAnchor()->GetChildCountCrossingTreeBoundary());
}
// When a child is ignored, it looks for unignored nodes of that child's
// children until there are no more descendants.
//
// For example:
// ++TextField
// ++++GenericContainer ignored
// ++++++StaticText "Hello"
// When we call the following method on TextField, it would return 1.
int AnchorUnignoredChildCount() const {
if (!GetAnchor())
return 0;
return static_cast<int>(
GetAnchor()->GetUnignoredChildCountCrossingTreeBoundary());
}
int AnchorIndexInParent() const {
// If this is the root tree, the index in parent will be 0.
return GetAnchor() ? static_cast<int>(GetAnchor()->GetIndexInParent())
: INVALID_INDEX;
}
base::stack<AXNode*> GetAncestorAnchors() const {
if (!GetAnchor())
return base::stack<AXNode*>();
return GetAnchor()->GetAncestorsCrossingTreeBoundaryAsStack();
}
AXNode* GetLowestUnignoredAncestor() const {
if (!GetAnchor())
return nullptr;
return GetAnchor()->GetLowestPlatformAncestor();
}
// Returns the length of text (in UTF16 code points) 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 {
if (IsNullPosition())
return 0;
// Ignored anchors are not visible to platform APIs. As a result, their
// text content or hypertext does not appear in their parent node, but the
// text of their unignored children does, if any. (See
// `AXNode::GetHypertext()` for the meaning of "hypertext" in this context.
if (!GetAnchor()->IsIgnored()) {
if (IsEmbeddedObjectInParent())
return AXNode::kEmbeddedObjectCharacterLengthUTF16;
} else {
// Ignored leaf (text) nodes might contain text content or hypertext, but
// it should not be exposed in their parent.
if (!AnchorUnignoredChildCount())
return 0;
}
return MaxTextOffset();
}
// Returns whether or not this anchor is represented in their parent with a
// single "object replacement character".
bool IsEmbeddedObjectInParent() const {
switch (g_ax_embedded_object_behavior) {
case AXEmbeddedObjectBehavior::kSuppressCharacter:
return false;
case AXEmbeddedObjectBehavior::kExposeCharacterForHypertext:
// We expose an "object replacement character" for all nodes except:
// A) Textual nodes, such as static text, inline text boxes and line
// breaks, and B) Nodes that are invisible to platform APIs.
//
// In the first case, textual nodes cannot be represented by an "object
// replacement character" in the hypertext of their unignored parents,
// because we want to maintain compatibility with how Firefox exposes
// text in IAccessibleText. In the second case, ignored nodes and nodes
// that are descendants of platform leaves should maintain the actual
// text of all their static text descendants, otherwise there would be
// loss of information while traversing the accessibility tree upwards.
// An example of a platform leaf is an <input> text field, because all
// of the accessibility subtree inside the text field is hidden from
// platform APIs. An example of how an ignored node can affect the
// hypertext of an unignored ancestor is shown below:
// ++kTextField "Hello"
// ++++kGenericContainer ignored "Hello"
// ++++++kStaticText "Hello"
// ++++++++kInlineTextBox "Hello"
// The generic container, even though it is ignored, should nevertheless
// maintain the text of its static text child and not use an "object
// replacement character". Otherwise, the value of the text field would
// be wrong.
//
// Please note that there is one more method that controls whether an
// "object replacement character" would be exposed. See
// `AXPosition::IsInUnignoredEmptyObject()`.
return !IsNullPosition() && !GetAnchor()->IsIgnored() &&
!GetAnchor()->IsText() && !GetAnchor()->IsChildOfLeaf();
case AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent:
return !IsNullPosition() && !GetAnchor()->IsIgnored() &&
GetAnchor()->IsLeaf() && IsInUnignoredEmptyObject();
}
}
// Determines if the anchor containing this position produces a hard line
// break in the text representation, e.g. the anchor is a block level element
// or a <br>.
bool IsInLineBreakingObject() const {
if (IsNullPosition())
return false;
return GetAnchor()->GetBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject);
}
ax::mojom::Role GetAnchorRole() const {
if (IsNullPosition())
return ax::mojom::Role::kUnknown;
return GetRole(GetAnchor());
}
ax::mojom::Role GetRole(AXNode* node) const { return node->GetRole(); }
const std::vector<int32_t>& GetWordStartOffsets() const {
if (IsNullPosition()) {
static const base::NoDestructor<std::vector<int32_t>> empty_word_starts;
return *empty_word_starts;
}
DCHECK(GetAnchor());
// An embedded object replacement character is exposed in a node's text
// representation when a control, such as a text field, is empty. Since the
// control has no text, no word start offsets are present in the
// `ax::mojom::IntListAttribute::kWordStarts` attribute, so we need to
// special case them here.
//
// For the kUIAExposeCharacterForHypertext case, we only want to return a
// vector with {0} if the empty object does not have an author specified
// name that we are exposing.
if (IsInUnignoredEmptyObject() &&
(g_ax_embedded_object_behavior ==
AXEmbeddedObjectBehavior::kExposeCharacterForHypertext ||
(g_ax_embedded_object_behavior ==
AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent &&
!EmptyObjectShouldProvideNameFromAttribute()))) {
// Using braces ensures that the vector will contain the given value, and
// not create a vector of size 0.
static const base::NoDestructor<std::vector<int32_t>>
embedded_word_starts{{0}};
return *embedded_word_starts;
}
return GetAnchor()->GetIntListAttribute(
ax::mojom::IntListAttribute::kWordStarts);
}
const std::vector<int32_t>& GetWordEndOffsets() const {
if (IsNullPosition()) {
static const base::NoDestructor<std::vector<int32_t>> empty_word_ends;
return *empty_word_ends;
}
DCHECK(GetAnchor());
// An embedded object replacement character is exposed in a node's text
// representation when a control, such as a text field, is empty. Since the
// control has no text, no word end offsets are present in the
// `ax::mojom::IntListAttribute::kWordEnds` attribute, so we need to special
// case them here.
//
// Since the whole text exposed inside of an embedded object is of
// length 1 (the embedded object replacement character), the word end offset
// is positioned at 1. Because we want to treat embedded object replacement
// characters as ordinary characters, it wouldn't be consistent to assume
// they have no length and return 0 instead of 1.
if (IsInUnignoredEmptyObject() &&
(g_ax_embedded_object_behavior ==
AXEmbeddedObjectBehavior::kExposeCharacterForHypertext ||
(g_ax_embedded_object_behavior ==
AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent &&
!EmptyObjectShouldProvideNameFromAttribute()))) {
// Using braces ensures that the vector will contain the given value, and
// not create a vector of size 1.
static const base::NoDestructor<std::vector<int32_t>> embedded_word_ends{
{1}};
return *embedded_word_ends;
}
return GetAnchor()->GetIntListAttribute(
ax::mojom::IntListAttribute::kWordEnds);
}
const std::vector<int32_t>& GetFormatStartOffsets() const {
if (IsNullPosition()) {
static const base::NoDestructor<std::vector<int32_t>> empty_format_starts;
return *empty_format_starts;
}
DCHECK(GetAnchor());
std::vector<int32_t> format_starts;
format_starts.push_back(0);
// Format is almost always consistent throughout any node -- the only
// exception are inline text boxes with CSS highlights. Therefore, unless
// the node is an inline text box with CSS highlights, we can assume the
// node's format starts only at index 0.
if (GetAnchor()->GetRole() != ax::mojom::Role::kInlineTextBox) {
static const base::NoDestructor<std::vector<int32_t>> format_starts_copy(
std::move(format_starts));
return *format_starts_copy;
}
AXNode* parent = GetAnchor()->GetUnignoredParent();
const std::vector<int32_t>& marker_types =
parent->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes);
const std::vector<int32_t>& highlight_types = parent->GetIntListAttribute(
ax::mojom::IntListAttribute::kHighlightTypes);
// Since, there are no highlights, there is no possibility of any spelling
// or grammar highlights.
if (highlight_types.empty()) {
static const base::NoDestructor<std::vector<int32_t>> format_starts_copy(
std::move(format_starts));
return *format_starts_copy;
}
CHECK_EQ(marker_types.size(), highlight_types.size());
const std::vector<int>& marker_starts =
parent->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts);
const std::vector<int>& marker_ends =
parent->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds);
CHECK_EQ(marker_types.size(), marker_starts.size());
CHECK_EQ(marker_types.size(), marker_ends.size());
int text_length = GetAnchor()->GetTextContentLengthUTF16();
for (size_t i = 0; i < marker_types.size(); ++i) {
if (HasSpellingOrGrammarErrorHighlight(
static_cast<ax::mojom::MarkerType>(marker_types[i]),
static_cast<ax::mojom::HighlightType>(highlight_types[i]))) {
if (marker_starts[i] != 0) { // 0 is already added
format_starts.push_back(marker_starts[i]);
}
if (marker_ends[i] < text_length - 1) {
format_starts.push_back(marker_ends[i]);
}
}
}
static const base::NoDestructor<std::vector<int32_t>> format_starts_copy(
std::move(format_starts));
return *format_starts_copy;
}
const std::vector<int32_t>& GetFormatEndOffsets() const {
if (IsNullPosition()) {
static const base::NoDestructor<std::vector<int32_t>> empty_format_ends;
return *empty_format_ends;
}
DCHECK(GetAnchor());
int text_length = GetAnchor()->GetTextContentLengthUTF16();
std::vector<int32_t> format_ends;
format_ends.push_back(text_length);
// Format is almost always consistent throughout any node -- the only
// exception are inline text boxes with CSS highlights. Therefore, unless
// the node is an inline text box with CSS highlights, we can assume the
// node's format ends only at the text length.
if (GetAnchor()->GetRole() != ax::mojom::Role::kInlineTextBox) {
static const base::NoDestructor<std::vector<int32_t>> format_ends_copy(
std::move(format_ends));
return *format_ends_copy;
}
AXNode* parent = GetAnchor()->GetUnignoredParent();
const std::vector<int32_t>& marker_types =
parent->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes);
const std::vector<int32_t>& highlight_types = parent->GetIntListAttribute(
ax::mojom::IntListAttribute::kHighlightTypes);
// Since, there are no highlights, there is no possibility of any spelling
// or grammar highlights.
if (highlight_types.empty()) {
static const base::NoDestructor<std::vector<int32_t>> format_ends_copy(
std::move(format_ends));
return *format_ends_copy;
}
CHECK_EQ(marker_types.size(), highlight_types.size());
const std::vector<int>& marker_starts =
parent->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts);
const std::vector<int>& marker_ends =
parent->GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds);
CHECK_EQ(marker_types.size(), marker_starts.size());
CHECK_EQ(marker_types.size(), marker_ends.size());
format_ends.clear();
for (size_t i = 0; i < marker_types.size(); ++i) {
if (HasSpellingOrGrammarErrorHighlight(
static_cast<ax::mojom::MarkerType>(marker_types[i]),
static_cast<ax::mojom::HighlightType>(highlight_types[i]))) {
if (marker_starts[i] > 0) {
format_ends.push_back(marker_starts[i]);
}
format_ends.push_back(marker_ends[i]);
}
}
if (format_ends.empty() || format_ends.back() != text_length) {
format_ends.push_back(text_length);
}
static const base::NoDestructor<std::vector<int32_t>> format_ends_copy(
std::move(format_ends));
return *format_ends_copy;
}
static bool HasSpellingOrGrammarErrorHighlight(
ax::mojom::MarkerType marker_type,
ax::mojom::HighlightType highlight_type) {
return marker_type == ax::mojom::MarkerType::kHighlight &&
(highlight_type == ax::mojom::HighlightType::kSpellingError ||
highlight_type == ax::mojom::HighlightType::kGrammarError);
}
AXNodeID GetNextOnLineID() const {
if (IsNullPosition())
return kInvalidAXNodeID;
DCHECK(GetAnchor());
if (GetAnchor()->HasIntAttribute(ax::mojom::IntAttribute::kNextOnLineId)) {
return static_cast<AXNodeID>(
GetAnchor()->GetIntAttribute(ax::mojom::IntAttribute::kNextOnLineId));
}
AXNode* parent = GetAnchor()->GetUnignoredParent();
if (!parent) {
return kInvalidAXNodeID;
}
// We should not need to bubble up to find the NextOnLine if we are not
// in an InlineTextBox, because the only cases where the relevant NextOnLine
// information is stored in the parent is in cases where we have text inside
// inline-block elements.
//
// We only want to bubble up to the parent to find the nextOnLine
// if we are in a leaf that is a last child.
// This is because if we have a structure where there are multiple
// InlineTextBox children that are in different lines, and the parent's
// NextOnLine only applies to the last child.
if (GetAnchor()->GetRole() != ax::mojom::Role::kInlineTextBox ||
parent->GetLastUnignoredChild() != GetAnchor()) {
return kInvalidAXNodeID;
}
while (parent &&
!parent->HasIntAttribute(ax::mojom::IntAttribute::kNextOnLineId)) {
parent = parent->GetUnignoredParent();
}
if (parent) {
return static_cast<AXNodeID>(
parent->GetIntAttribute(ax::mojom::IntAttribute::kNextOnLineId));
}
return kInvalidAXNodeID;
}
AXNodeID GetPreviousOnLineID() const {
if (IsNullPosition())
return kInvalidAXNodeID;
DCHECK(GetAnchor());
if (GetAnchor()->HasIntAttribute(
ax::mojom::IntAttribute::kPreviousOnLineId)) {
return static_cast<AXNodeID>(GetAnchor()->GetIntAttribute(
ax::mojom::IntAttribute::kPreviousOnLineId));
}
AXNode* parent = GetAnchor()->GetUnignoredParent();
if (!parent) {
return kInvalidAXNodeID;
}
// We should not need to bubble up to find the PreviousOnLine if we are not
// in an InlineTextBox, because the only cases where the relevant
// PreviousOnLine information is stored in the parent is in cases where we
// have text inside inline-block elements.
//
// We have some expectations that
// line break elements are not expected to have a previous on line element.
//
// We only want to bubble up to the parent to find the previousOnLine
// if we are in a leaf that is a first child.
// This is because if we have a structure where there are multiple
// InlineTextBox children that are in different lines, and the parent's
// PreviousOnLine only applies to the first child.
if (GetAnchor()->GetRole() != ax::mojom::Role::kInlineTextBox ||
parent->GetRole() == ax::mojom::Role::kLineBreak ||
parent->GetFirstUnignoredChild() != GetAnchor()) {
return kInvalidAXNodeID;
}
while (parent && !parent->HasIntAttribute(
ax::mojom::IntAttribute::kPreviousOnLineId)) {
parent = parent->GetUnignoredParent();
}
if (parent) {
return static_cast<AXNodeID>(
parent->GetIntAttribute(ax::mojom::IntAttribute::kPreviousOnLineId));
}
return kInvalidAXNodeID;
}
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.
using AbortMovePredicate =
base::RepeatingCallback<bool(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 its parent's children.
const bool is_last_child =
AnchorIndexInParent() == (GetAnchorSiblingCount() - 1);
DCHECK(GetAnchor());
return is_last_child &&
GetRole(GetAnchor()->GetParentCrossingTreeBoundary()) ==
ax::mojom::Role::kStaticText;
}
// Uses depth-first pre-order traversal.
AXPositionInstance CreateNextAnchorPosition(
const AbortMovePredicate& abort_predicate) const {
if (IsNullPosition())
return Clone();
AXPositionInstance current_position = AsTreePosition();
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.Run(*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);
DCHECK(!next_sibling->IsNullPosition());
if (abort_predicate.Run(*current_position, *next_sibling,
AXMoveType::kSibling,
AXMoveDirection::kNextInTree)) {
return CreateNullPosition();
}
return next_sibling;
}
if (abort_predicate.Run(*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(
const AbortMovePredicate& abort_predicate) const {
if (IsNullPosition())
return Clone();
AXPositionInstance current_position = AsTreePosition();
DCHECK(!current_position->IsNullPosition());
AXPositionInstance parent_position =
current_position->CreateParentPosition();
if (parent_position->IsNullPosition())
return parent_position;
// If there is no previous sibling, or the parent itself is a leaf, move up
// to the parent. The parent can be a leaf if we start with a tree position
// that is a descendant of a node that is an empty control represented by an
// "object replacement character" (see `IsInUnignoredEmptyObject()`).
const int index_in_parent = current_position->AnchorIndexInParent();
if (index_in_parent <= 0 || parent_position->IsLeaf()) {
if (abort_predicate.Run(*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);
DCHECK(!rightmost_leaf->IsNullPosition());
if (abort_predicate.Run(*current_position, *rightmost_leaf,
AXMoveType::kSibling,
AXMoveDirection::kPreviousInTree)) {
return CreateNullPosition();
}
CHECK(!rightmost_leaf->IsNullPosition());
while (!rightmost_leaf->IsLeaf()) {
parent_position = std::move(rightmost_leaf);
rightmost_leaf = parent_position->CreateChildPositionAt(
parent_position->AnchorChildCount() - 1);
DCHECK(!rightmost_leaf->IsNullPosition());
if (abort_predicate.Run(*parent_position, *rightmost_leaf,
AXMoveType::kDescendant,
AXMoveDirection::kPreviousInTree)) {
return CreateNullPosition();
}
CHECK(!rightmost_leaf->IsNullPosition());
}
return rightmost_leaf;
}
// Creates a text position using the next leaf node as its anchor.
// Nearly all of the text in the accessibility tree is contained in leaf
// nodes, so this method is mostly used to move through text nodes.
AXPositionInstance CreateNextLeafTextPosition(
const 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);
DCHECK(next_leaf);
return next_leaf->AsLeafTextPosition();
}
// Creates a text position using the previous leaf node as its anchor.
// Nearly all of the text in the accessibility tree is contained in leaf
// nodes, so this method is mostly used to move through text nodes.
AXPositionInstance CreatePreviousLeafTextPosition(
const 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);
}
DCHECK(previous_leaf);
return previous_leaf->AsLeafTextPosition();
}
// Creates a tree position using the next leaf node as its anchor.
// Nearly all of the text in the accessibility tree is contained in leaf
// nodes, so this method is mostly used to move through text nodes.
AXPositionInstance CreateNextLeafTreePosition(
const 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);
DCHECK(next_leaf);
return next_leaf;
}
// Creates a tree position using the previous leaf node as its anchor.
// Nearly all of the text in the accessibility tree is contained in leaf
// nodes, so this method is mostly used to move through text nodes.
AXPositionInstance CreatePreviousLeafTreePosition(
const 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);
}
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) {
// Sometimes, nodes that are used to signify paragraph boundaries are
// ignored, e.g. <div aria-hidden="true"></div>". We make the design
// decision to expose such boundaries to assistive software. Their
// associated ignored nodes are still not exposed. This ensures that
// navigation keys in text fields, such as Ctrl+Up/Down, will behave the
// same way as related screen reader commands.
return position->AtStartOfParagraph();
}
static bool AtStartOfParagraphExcludingEmptyParagraphsPredicate(
const AXPositionInstance& position) {
// For UI Automation, empty lines after a paragraph should be merged into
// the preceding paragraph.
//
// See
// https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-uiautomationtextunits#paragraph
const bool is_empty_paragraph =
position->IsPointingToLineBreak() ||
(position->IsInLineBreakingObject() &&
(position->GetAnchor()->IsEmptyLeaf() || position->GetText().empty()));
return !is_empty_paragraph && AtStartOfParagraphPredicate(position);
}
static bool AtEndOfParagraphPredicate(const AXPositionInstance& position) {
// Sometimes, nodes that are used to signify paragraph boundaries are
// ignored, e.g. <div aria-hidden="true"></div>". We make the design
// decision to expose such boundaries to assistive software. Their
// associated ignored nodes are still not exposed. This ensures that
// navigation keys in text fields, such as Ctrl+Up/Down, will behave the
// same way as related screen reader commands.
return position->AtEndOfParagraph();
}
static bool AtStartOfLinePredicate(const AXPositionInstance& position) {
// Sometimes, nodes that are used to signify line boundaries are ignored,
// e.g. <span contenteditable="false"> <br role="presentation"></span> which
// is used to make a hard line break appear as a soft one. We make the
// design decision to expose such boundaries to assistive software. Their
// associated ignored nodes are still not exposed.
return position->AtStartOfLine();
}
static bool AtEndOfLinePredicate(const AXPositionInstance& position) {
// Sometimes, nodes that are used to signify line boundaries are ignored,
// e.g. <span contenteditable="false"> <br role="presentation"></span> which
// is used to make a hard line break appear as a soft one. We make the
// design decision to expose such boundaries to assistive software. Their
// associated ignored nodes are still not exposed.
return position->AtEndOfLine();
}
static bool AtStartOfSentencePredicate(const AXPositionInstance& position) {
// Sentence 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->AtStartOfSentence();
}
static bool AtEndOfSentencePredicate(const AXPositionInstance& position) {
// Sentence 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->AtEndOfSentence();
}
static bool AtStartOfFormatPredicate(const AXPositionInstance& position) {
return position->AtStartOfFormat();
}
static bool AtEndOfFormatPredicate(const AXPositionInstance& position) {
return position->AtEndOfFormat();
}
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.IsInUnignoredEmptyObject() ||
move_to.IsInUnignoredEmptyObject()) {
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 attributes differ.
return move_from.AsLeafTreePosition()->GetTextAttributes() !=
move_to.AsLeafTreePosition()->GetTextAttributes();
}
static bool MoveCrossesLineBreakingObject(
const ax::mojom::TextBoundary paragraph_boundary,
const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
const AXPosition* proceeding_position = &move_from;
const AXPosition* trailing_position = &move_to;
switch (direction) {
case AXMoveDirection::kNextInTree:
break;
case AXMoveDirection::kPreviousInTree:
std::swap(proceeding_position, trailing_position);
break;
}
switch (paragraph_boundary) {
case ax::mojom::TextBoundary::kParagraphEnd: {
const bool trailing_block = trailing_position->IsInLineBreakingObject();
const bool trailing_line_break =
trailing_position->IsPointingToLineBreak();
return trailing_block || trailing_line_break;
}
case ax::mojom::TextBoundary::kParagraphStart: {
// The trailing object does not need to be a block or a line break for
// it to represent a start of a new paragraph.
//
// 1. Preceding block before "world" creates a paragraph start:
// <div><p>hello</p>world</div>
// 2. Preceding line break before "world" creates a paragraph start:
// <div>Hello<br>world</div>
const bool preceding_block =
proceeding_position->IsInLineBreakingObject();
const bool preceding_line_break =
proceeding_position->IsPointingToLineBreak();
return preceding_block || preceding_line_break;
}
default:
NOTREACHED();
}
}
// AbortMovePredicate function used to detect paragraph boundaries.
static bool AbortMoveAtParagraphBoundary(
const ax::mojom::TextBoundary paragraph_boundary,
const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
if (move_from.IsNullPosition() || move_to.IsNullPosition() ||
move_from.IsInUnignoredEmptyObject() ||
move_to.IsInUnignoredEmptyObject()) {
// We deliberately put empty objects, such as empty text fields, in their
// own paragraph for easier navigation. Otherwise, they could easily be
// missed by screen reader users.
return true;
}
return MoveCrossesLineBreakingObject(paragraph_boundary, move_from, move_to,
move_type, direction);
}
// AbortMovePredicate function used to detect page boundaries.
//
// Depending on the type of content, it might be separated into a number of
// pages. For example, a PDF document may expose multiple pages.
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 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;
}
}
// AbortMovePredicate function used to detect crossing through the boundaries
// of a window-like container, such as a webpage, a PDF, a dialog, the
// browser's UI (AKA Views), or the whole desktop. Window-like containers
// that are ignored should not cause us to abort. For example, a hidden dialog
// should not cause a break.
static bool AbortMoveAtRootBoundary(const AXPosition& move_from,
const AXPosition& move_to,
const AXMoveType move_type,
const AXMoveDirection direction) {
// Positions are null when moving past the whole content, therefore the root
// of a window-like container has certainly been crossed.
if (move_from.IsNullPosition() || move_to.IsNullPosition())
return true;
const ax::mojom::Role move_from_role = move_from.GetAnchorRole();
const ax::mojom::Role move_to_role = move_to.GetAnchorRole();
switch (move_type) {
case AXMoveType::kAncestor:
// For Ancestor moves, only abort when exiting an unignored window-like
// container. We don't care if the ancestor is the root of a window-like
// container or not, since the descendant is contained by it. However,
// we do care if the ancestor is an iframe because a webpage should be
// navigated as a single document together with all its iframes,
// (out-of-process or otherwise).
return IsRootLike(move_from_role) && !IsIframe(move_to_role) &&
!move_from.IsIgnored();
case AXMoveType::kDescendant:
// For Descendant moves, only abort when entering an unignored
// window-like container. We don't care if the ancestor is the root of a
// window-like container or not, since the descendant is contained by
// it. However, we do care if the ancestor is an iframe because a
// webpage should be navigated as a single document together with all
// its iframes, (out-of-process or otherwise).
return IsRootLike(move_to_role) && !IsIframe(move_from_role) &&
!move_to.IsIgnored();
case AXMoveType::kSibling:
// For Sibling moves, abort if both of the siblings are at the root of
// unignored window-like containers because that would mean exiting
// and/or entering a new window-like container. Iframes should not be
// present in this case because an iframe should never contain more than
// one kRootWebArea as its immediate child.
return IsRootLike(move_from_role) && IsRootLike(move_to_role) &&
!move_from.IsIgnored() && !move_to.IsIgnored();
}
}
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);
}
NOTREACHED();
}
static const std::vector<int32_t>& GetSentenceStartOffsetsFunc(
const AXPositionInstance& position) {
if (position->IsNullPosition()) {
static const base::NoDestructor<std::vector<int32_t>>
empty_sentence_starts;
return *empty_sentence_starts;
}
DCHECK(position->GetAnchor());
return position->GetAnchor()->GetIntListAttribute(
ax::mojom::IntListAttribute::kSentenceStarts);
}
static const std::vector<int32_t>& GetSentenceEndOffsetsFunc(
const AXPositionInstance& position) {
if (position->IsNullPosition()) {
static const base::NoDestructor<std::vector<int32_t>> empty_sentence_ends;
return *empty_sentence_ends;
}
DCHECK(position->GetAnchor());
return position->GetAnchor()->GetIntListAttribute(
ax::mojom::IntListAttribute::kSentenceEnds);
}
static const std::vector<int32_t>& GetWordStartOffsetsFunc(
const AXPositionInstance& position) {
return position->GetWordStartOffsets();
}
static const std::vector<int32_t>& GetWordEndOffsetsFunc(
const AXPositionInstance& position) {
return position->GetWordEndOffsets();
}
static const std::vector<int32_t>& GetFormatStartOffsetsFunc(
const AXPositionInstance& position) {
return position->GetFormatStartOffsets();
}
static const std::vector<int32_t>& GetFormatEndOffsetsFunc(
const AXPositionInstance& position) {
return position->GetFormatEndOffsets();
}
// Creates an ancestor equivalent position at the root node of this position's
// accessibility tree, e.g. at the root of the current iframe (out-of-process
// or not), PDF plugin, Views tree, dialog (native, ARIA or HTML), window, or
// the whole desktop.
//
// For a similar method that does not stop at all iframe boundaries, see
// `CreateRootAncestorPosition`.
//
// See `CreateParentPosition` for an explanation of the use of
// |move_direction|.
AXPositionInstance CreateAXTreeRootAncestorPosition(
ax::mojom::MoveDirection move_direction) const {
if (IsNullPosition())
return Clone();
AXPositionInstance root_position = Clone();
while (!IsRootLike(root_position->GetAnchorRole())) {
AXPositionInstance parent_position =
root_position->CreateParentPosition(move_direction);
if (parent_position->IsNullPosition())
break;
root_position = std::move(parent_position);
}
return root_position;
}
// Creates an ancestor equivalent position at the root node of all content,
// e.g. at the root of the whole webpage, PDF plugin, Views tree, dialog
// (native, ARIA or HTML), window, or the whole desktop.
//
// Note that this method will break out of an out-of-process iframe and return
// a position at the root of the top-level document, but it will not break
// into the Views tree if present. For a similar method that stops at all
// iframe boundaries, see `CreateAXTreeRootAncestorPosition`.
//
// See `CreateParentPosition` for an explanation of the use of
// |move_direction|.
AXPositionInstance CreateRootAncestorPosition(
ax::mojom::MoveDirection move_direction) const {
AXPositionInstance root_position =
CreateAXTreeRootAncestorPosition(move_direction);
AXPositionInstance web_root_position = CreateNullPosition();
for (; !root_position->IsNullPosition();
root_position =
root_position->CreateAXTreeRootAncestorPosition(move_direction)) {
// An "ax::mojom::Role::kRootWebArea" could also be present at the root of
// iframes or embedded objects, so we need to check that for that specific
// role the position is also at the top of the forest of accessibility
// trees making up the webpage. Note that the forest of accessibility
// trees would include Views and on Chrome OS the whole desktop, so in the
// case of a web root, checking if the parent position is the null
// position will not work.
if (root_position->GetAnchorRole() != ax::mojom::Role::kRootWebArea) {
if (web_root_position->IsNullPosition())
return root_position; // Original position is not in web contents.
// The previously saved web root is the shallowest in the forest of
// accessibility trees.
return web_root_position;
}
// Save this web root position and check if it is the shallowest in the
// forest of accessibility trees.
web_root_position = root_position->Clone();
root_position = root_position->CreateParentPosition(move_direction);
}
return web_root_position;
}
// 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.is_null())
return Clone();
AXPositionInstance text_position = AsTextPosition();
const std::vector<int32_t>& boundary_offsets =
get_offsets.Run(text_position);
if (boundary_offsets.empty())
return text_position;
switch (move_direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
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;
auto offsets_iterator_ref = *offsets_iterator;
text_position->text_offset_ = static_cast<int>(offsets_iterator_ref);
text_position->affinity_ = ax::mojom::TextAffinity::kDownstream;
}
break;
}
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()) {
auto offsets_iterator_ref = *offsets_iterator;
text_position->text_offset_ = static_cast<int>(offsets_iterator_ref);
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.is_null())
return Clone();
AXPositionInstance text_position = AsTextPosition();
const std::vector<int32_t>& boundary_offsets =
get_offsets.Run(text_position);
switch (move_direction) {
case ax::mojom::MoveDirection::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
if (boundary_offsets.empty()) {
return text_position->CreatePositionAtStartOfAnchor();
} else {
text_position->text_offset_ =
int{boundary_offsets[boundary_offsets.size() - 1]};
return text_position;
}
break;
case ax::mojom::MoveDirection::kForward:
if (boundary_offsets.empty()) {
return text_position->CreatePositionAtEndOfAnchor();
} else {
text_position->text_offset_ = int{boundary_offsets[0]};
return text_position;
}
break;
}
}
// Returns the next unignored leaf text position in the specified direction,
// also 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 `AXBoundaryBehavior::kStopAtAnchorBoundaryOrIfAlreadyAtBoundary` is
// not equivalent to the initial position. That's why ignored positions are
// also skipped. Otherwise, if a boundary is present on an ignored position,
// the search for the next or previous boundary would stop prematurely. Note
// that if there are multiple adjacent ignored positions and all of them
// create a boundary, we'll skip them all on purpose. For example, adjacent
// ignored paragraph boundaries could be created by using multiple aria-hidden
// divs next to one another. These should not contribute more than one
// paragraph boundary to the tree's text representation, otherwise this will
// create user confusion.
//
// Note that using the `CompareTo` method with text positions does not take
// into account position affinity or the order of their anchors in the tree:
// 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 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 a 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::kNone:
NOTREACHED();
case ax::mojom::MoveDirection::kBackward:
// If we are at a text offset greater than 0, we will simply decrease
// the offset by one; otherwise, we will create a position at the end of
// the previous unignored leaf node with non-empty text and decrease 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->AtStartOfAnchor() || text_position->IsIgnored()) {
text_position = text_position
->CreatePreviousLeafTextPosition(
base::BindRepeating(&AbortMoveAtRootBoundary))
->CreatePositionAtEndOfAnchor();
}
if (!text_position->IsNullPosition())
--text_position->text_offset_;
break;
case ax::mojom::MoveDirection::kForward:
// If we are at a text offset less than MaxTextOffset, we will simply
// increase the offset by one; otherwise, we will create a position at
// the start of the next unignored leaf node with non-empty text and
// increase its offset.
//
// Same as the comment above: using AtEndOfAnchor is enough to skip
// empty text nodes that are equivalent to the initial position.
while (text_position->AtEndOfAnchor() || text_position->IsIgnored()) {
text_position = text_position->CreateNextLeafTextPosition(
base::BindRepeating(&AbortMoveAtRootBoundary));
}
if (!text_position->IsNullPosition())
++text_position->text_offset_;
break;
}
DCHECK(text_position->IsValid());
return text_position;
}
AXPositionKind kind_ = AXPositionKind::NULL_POSITION;
// TODO(crbug.com/40864560): use weak pointers for the AXTree, so that
// AXPosition can be used without AXTreeManager support (and also faster than
// the slow AXTreeID).
AXTreeID tree_id_;
AXNodeID 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_ = ax::mojom::TextAffinity::kDownstream;
//
// Cached members that should be lazily created on first use.
//
// In the case of a leaf position, its text content (in UTF16 format). Used
// for initializing a grapheme break iterator.
mutable std::u16string name_;
};
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>
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();
}
extern template class EXPORT_TEMPLATE_DECLARE(AX_EXPORT)
AXPosition<AXNodePosition, AXNode>;
} // namespace ui
#endif // UI_ACCESSIBILITY_AX_POSITION_H_