blob: 783a72e5e66c238344222a1f2997a54514389cee [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/accessibility/ax_position.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
#include "third_party/blink/renderer/core/dom/container_node.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/node_traversal.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
#include "third_party/blink/renderer/modules/accessibility/ax_layout_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
namespace blink {
// static
const AXPosition AXPosition::CreatePositionBeforeObject(
const AXObject& child,
const AXPositionAdjustmentBehavior adjustment_behavior) {
if (child.IsDetached() || !child.AccessibilityIsIncludedInTree())
return {};
// If |child| is a text object, but not a text control, make behavior the same
// as |CreateFirstPositionInObject| so that equality would hold. Text controls
// behave differently because you should be able to set a position before the
// text control in case you want to e.g. select it as a whole.
if (child.IsTextObject())
return CreateFirstPositionInObject(child, adjustment_behavior);
const AXObject* parent = child.ParentObjectIncludedInTree();
if (!parent || parent->IsDetached())
return {};
DCHECK(parent);
AXPosition position(*parent);
position.text_offset_or_child_index_ = child.IndexInParent();
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(position.IsValid(&failure_reason)) << failure_reason;
#endif
return position.AsUnignoredPosition(adjustment_behavior);
}
// static
const AXPosition AXPosition::CreatePositionAfterObject(
const AXObject& child,
const AXPositionAdjustmentBehavior adjustment_behavior) {
if (child.IsDetached() || !child.AccessibilityIsIncludedInTree())
return {};
// If |child| is a text object, but not a text control, make behavior the same
// as |CreateLastPositionInObject| so that equality would hold. Text controls
// behave differently because you should be able to set a position after the
// text control in case you want to e.g. select it as a whole.
if (child.IsTextObject())
return CreateLastPositionInObject(child, adjustment_behavior);
const AXObject* parent = child.ParentObjectIncludedInTree();
if (!parent || parent->IsDetached())
return {};
DCHECK(parent);
AXPosition position(*parent);
position.text_offset_or_child_index_ = child.IndexInParent() + 1;
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(position.IsValid(&failure_reason)) << failure_reason;
#endif
return position.AsUnignoredPosition(adjustment_behavior);
}
// static
const AXPosition AXPosition::CreateFirstPositionInObject(
const AXObject& container,
const AXPositionAdjustmentBehavior adjustment_behavior) {
if (container.IsDetached())
return {};
if (container.IsTextObject() || container.IsNativeTextControl()) {
AXPosition position(container);
position.text_offset_or_child_index_ = 0;
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(position.IsValid(&failure_reason)) << failure_reason;
#endif
return position.AsUnignoredPosition(adjustment_behavior);
}
// If the container is not a text object, creating a position inside an
// object that is excluded from the accessibility tree will result in an
// invalid position, because child count is not always accurate for such
// objects.
const AXObject* unignored_container =
!container.AccessibilityIsIncludedInTree()
? container.ParentObjectIncludedInTree()
: &container;
DCHECK(unignored_container);
AXPosition position(*unignored_container);
position.text_offset_or_child_index_ = 0;
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(position.IsValid(&failure_reason)) << failure_reason;
#endif
return position.AsUnignoredPosition(adjustment_behavior);
}
// static
const AXPosition AXPosition::CreateLastPositionInObject(
const AXObject& container,
const AXPositionAdjustmentBehavior adjustment_behavior) {
if (container.IsDetached())
return {};
if (container.IsTextObject() || container.IsNativeTextControl()) {
AXPosition position(container);
position.text_offset_or_child_index_ = position.MaxTextOffset();
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(position.IsValid(&failure_reason)) << failure_reason;
#endif
return position.AsUnignoredPosition(adjustment_behavior);
}
// If the container is not a text object, creating a position inside an
// object that is excluded from the accessibility tree will result in an
// invalid position, because child count is not always accurate for such
// objects.
const AXObject* unignored_container =
!container.AccessibilityIsIncludedInTree()
? container.ParentObjectIncludedInTree()
: &container;
DCHECK(unignored_container);
AXPosition position(*unignored_container);
position.text_offset_or_child_index_ =
unignored_container->ChildCountIncludingIgnored();
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(position.IsValid(&failure_reason)) << failure_reason;
#endif
return position.AsUnignoredPosition(adjustment_behavior);
}
// static
const AXPosition AXPosition::CreatePositionInTextObject(
const AXObject& container,
const int offset,
const TextAffinity affinity,
const AXPositionAdjustmentBehavior adjustment_behavior) {
if (container.IsDetached() ||
!(container.IsTextObject() || container.IsTextControl())) {
return {};
}
AXPosition position(container);
position.text_offset_or_child_index_ = offset;
position.affinity_ = affinity;
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(position.IsValid(&failure_reason)) << failure_reason;
#endif
return position.AsUnignoredPosition(adjustment_behavior);
}
// static
const AXPosition AXPosition::FromPosition(
const Position& position,
const TextAffinity affinity,
const AXPositionAdjustmentBehavior adjustment_behavior) {
if (position.IsNull() || position.IsOrphan())
return {};
const Document* document = position.GetDocument();
// Non orphan positions always have a document.
DCHECK(document);
AXObjectCache* ax_object_cache = document->ExistingAXObjectCache();
if (!ax_object_cache)
return {};
auto* ax_object_cache_impl = static_cast<AXObjectCacheImpl*>(ax_object_cache);
const Position& parent_anchored_position = position.ToOffsetInAnchor();
const Node* container_node = parent_anchored_position.AnchorNode();
DCHECK(container_node);
const AXObject* container = ax_object_cache_impl->GetOrCreate(container_node);
if (!container)
return {};
if (container_node->IsTextNode()) {
if (!container->AccessibilityIsIncludedInTree()) {
// Find the closest DOM sibling that is unignored in the accessibility
// tree.
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveRight: {
const AXObject* next_container = FindNeighboringUnignoredObject(
*document, *container_node, container_node->parentNode(),
adjustment_behavior);
if (next_container) {
return CreatePositionBeforeObject(*next_container,
adjustment_behavior);
}
// Do the next best thing by moving up to the unignored parent if it
// exists.
if (!container || !container->ParentObjectIncludedInTree())
return {};
return CreateLastPositionInObject(
*container->ParentObjectIncludedInTree(), adjustment_behavior);
}
case AXPositionAdjustmentBehavior::kMoveLeft: {
const AXObject* previous_container = FindNeighboringUnignoredObject(
*document, *container_node, container_node->parentNode(),
adjustment_behavior);
if (previous_container) {
return CreatePositionAfterObject(*previous_container,
adjustment_behavior);
}
// Do the next best thing by moving up to the unignored parent if it
// exists.
if (!container || !container->ParentObjectIncludedInTree())
return {};
return CreateFirstPositionInObject(
*container->ParentObjectIncludedInTree(), adjustment_behavior);
}
}
}
AXPosition ax_position(*container);
// Convert from a DOM offset that may have uncompressed white space to a
// character offset.
//
// Note that NGOffsetMapping::GetInlineFormattingContextOf will reject DOM
// positions that it does not support, so we don't need to explicitly check
// this before calling the method.)
LayoutBlockFlow* formatting_context =
NGOffsetMapping::GetInlineFormattingContextOf(parent_anchored_position);
const NGOffsetMapping* container_offset_mapping =
formatting_context ? NGInlineNode::GetOffsetMapping(formatting_context)
: nullptr;
if (!container_offset_mapping) {
// We are unable to compute the text offset in the accessibility tree that
// corresponds to the DOM offset. We do the next best thing by returning
// either the first or the last AX position in |container| based on the
// |adjustment_behavior|.
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveRight:
return CreateLastPositionInObject(*container, adjustment_behavior);
case AXPositionAdjustmentBehavior::kMoveLeft:
return CreateFirstPositionInObject(*container, adjustment_behavior);
}
}
// We can now compute the text offset that corresponds to the given DOM
// position from the beginning of our formatting context. We also need to
// subtract the text offset of our |container| from the beginning of the
// same formatting context.
int container_offset = container->TextOffsetInFormattingContext(0);
int text_offset =
int{container_offset_mapping
->GetTextContentOffset(parent_anchored_position)
.value_or(static_cast<unsigned int>(container_offset))} -
container_offset;
DCHECK_GE(text_offset, 0);
ax_position.text_offset_or_child_index_ = text_offset;
ax_position.affinity_ = affinity;
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(ax_position.IsValid(&failure_reason)) << failure_reason;
#endif
return ax_position;
}
DCHECK(container_node->IsContainerNode());
if (!container->AccessibilityIsIncludedInTree()) {
container = container->ParentObjectIncludedInTree();
if (!container)
return {};
// |container_node| could potentially become nullptr if the unignored
// parent is an anonymous layout block.
container_node = container->GetNode();
}
AXPosition ax_position(*container);
// |ComputeNodeAfterPosition| returns nullptr for "after children"
// positions.
const Node* node_after_position = position.ComputeNodeAfterPosition();
if (!node_after_position) {
ax_position.text_offset_or_child_index_ =
container->ChildCountIncludingIgnored();
} else {
const AXObject* ax_child =
ax_object_cache_impl->GetOrCreate(node_after_position);
// |ax_child| might be nullptr because not all DOM nodes can have AX
// objects. For example, the "head" element has no corresponding AX
// object.
if (!ax_child || !ax_child->AccessibilityIsIncludedInTree()) {
// Find the closest DOM sibling that is present and unignored in the
// accessibility tree.
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveRight: {
const AXObject* next_child = FindNeighboringUnignoredObject(
*document, *node_after_position,
DynamicTo<ContainerNode>(container_node), adjustment_behavior);
if (next_child) {
return CreatePositionBeforeObject(*next_child,
adjustment_behavior);
}
return CreateLastPositionInObject(*container, adjustment_behavior);
}
case AXPositionAdjustmentBehavior::kMoveLeft: {
const AXObject* previous_child = FindNeighboringUnignoredObject(
*document, *node_after_position,
DynamicTo<ContainerNode>(container_node), adjustment_behavior);
if (previous_child) {
// |CreatePositionAfterObject| cannot be used here because it will
// try to create a position before the object that comes after
// |previous_child|, which in this case is the ignored object
// itself.
return CreateLastPositionInObject(*previous_child,
adjustment_behavior);
}
return CreateFirstPositionInObject(*container, adjustment_behavior);
}
}
}
if (!container->ChildrenIncludingIgnored().Contains(ax_child)) {
// The |ax_child| is aria-owned by another object.
return CreatePositionBeforeObject(*ax_child, adjustment_behavior);
}
if (ax_child->IsTextObject()) {
// The |ax_child| is a text object. In order that equality between
// seemingly identical positions would hold, i.e. a "before object"
// position before the text object and a "text position" before the
// first character of the text object, we would need to convert to the
// deep equivalent position.
return CreateFirstPositionInObject(*ax_child, adjustment_behavior);
}
ax_position.text_offset_or_child_index_ = ax_child->IndexInParent();
}
return ax_position;
}
// static
const AXPosition AXPosition::FromPosition(
const PositionWithAffinity& position_with_affinity,
const AXPositionAdjustmentBehavior adjustment_behavior) {
return FromPosition(position_with_affinity.GetPosition(),
position_with_affinity.Affinity(), adjustment_behavior);
}
AXPosition::AXPosition()
: container_object_(nullptr),
text_offset_or_child_index_(0),
affinity_(TextAffinity::kDownstream) {
#if DCHECK_IS_ON()
dom_tree_version_ = 0;
style_version_ = 0;
#endif
}
AXPosition::AXPosition(const AXObject& container)
: container_object_(&container),
text_offset_or_child_index_(0),
affinity_(TextAffinity::kDownstream) {
const Document* document = container_object_->GetDocument();
DCHECK(document);
#if DCHECK_IS_ON()
dom_tree_version_ = document->DomTreeVersion();
style_version_ = document->StyleVersion();
#endif
}
const AXObject* AXPosition::ChildAfterTreePosition() const {
if (!IsValid() || IsTextPosition())
return nullptr;
if (ChildIndex() == container_object_->ChildCountIncludingIgnored())
return nullptr;
DCHECK_LT(ChildIndex(), container_object_->ChildCountIncludingIgnored());
return container_object_->ChildAtIncludingIgnored(ChildIndex());
}
int AXPosition::ChildIndex() const {
if (!IsTextPosition())
return text_offset_or_child_index_;
NOTREACHED() << *this << " should be a tree position.";
return 0;
}
int AXPosition::TextOffset() const {
if (IsTextPosition())
return text_offset_or_child_index_;
NOTREACHED() << *this << " should be a text position.";
return 0;
}
int AXPosition::MaxTextOffset() const {
if (!IsTextPosition()) {
NOTREACHED() << *this << " should be a text position.";
return 0;
}
// TODO(nektar): Make AXObject::TextLength() public and use throughout this
// method.
if (container_object_->IsNativeTextControl())
return container_object_->StringValue().length();
const Node* container_node = container_object_->GetNode();
if (container_object_->IsAXInlineTextBox() || !container_node) {
// 1. The |Node| associated with an inline text box contains all the text in
// the static text object parent, whilst the inline text box might contain
// only part of it.
// 2. Some accessibility objects, such as those used for CSS "::before" and
// "::after" content, don't have an associated text node. We retrieve the
// text from the inline text box or layout object itself.
return container_object_->ComputedName().length();
}
const LayoutObject* layout_object = container_node->GetLayoutObject();
if (!layout_object)
return container_object_->ComputedName().length();
// TODO(nektar): Remove all this logic once we switch to
// AXObject::TextLength().
const bool is_atomic_inline_level =
layout_object->IsInline() && layout_object->IsAtomicInlineLevel();
if (!is_atomic_inline_level && !layout_object->IsText())
return container_object_->ComputedName().length();
// TODO(crbug.com/1149171): NGInlineOffsetMappingBuilder does not properly
// compute offset mappings for empty LayoutText objects. Other text objects
// (such as some list markers) are not affected.
if (const LayoutText* layout_text = DynamicTo<LayoutText>(layout_object)) {
if (layout_text->GetText().IsEmpty())
return container_object_->ComputedName().length();
}
LayoutBlockFlow* formatting_context =
NGOffsetMapping::GetInlineFormattingContextOf(*layout_object);
const NGOffsetMapping* container_offset_mapping =
formatting_context ? NGInlineNode::GetOffsetMapping(formatting_context)
: nullptr;
if (!container_offset_mapping)
return container_object_->ComputedName().length();
const base::span<const NGOffsetMappingUnit> mapping_units =
container_offset_mapping->GetMappingUnitsForNode(*container_node);
if (mapping_units.empty())
return container_object_->ComputedName().length();
return int{mapping_units.back().TextContentEnd() -
mapping_units.front().TextContentStart()};
}
TextAffinity AXPosition::Affinity() const {
if (!IsTextPosition()) {
NOTREACHED() << *this << " should be a text position.";
return TextAffinity::kDownstream;
}
return affinity_;
}
bool AXPosition::IsValid(String* failure_reason) const {
if (!container_object_) {
if (failure_reason)
*failure_reason = "\nPosition invalid: no container object.";
return false;
}
if (container_object_->IsDetached()) {
if (failure_reason)
*failure_reason = "\nPosition invalid: detached container object.";
return false;
}
if (!container_object_->GetDocument()) {
if (failure_reason) {
*failure_reason = "\nPosition invalid: no document for container object.";
}
return false;
}
// Some container objects, such as those for CSS "::before" and "::after"
// text, don't have associated DOM nodes.
if (container_object_->GetNode() &&
!container_object_->GetNode()->isConnected()) {
if (failure_reason) {
*failure_reason =
"\nPosition invalid: container object node is disconnected.";
}
return false;
}
const Document* document = container_object_->GetDocument();
DCHECK(document->IsActive());
DCHECK(!document->NeedsLayoutTreeUpdate());
if (!document->IsActive() || document->NeedsLayoutTreeUpdate()) {
if (failure_reason) {
*failure_reason =
"\nPosition invalid: document is either not active or it needs "
"layout tree update.";
}
return false;
}
if (IsTextPosition()) {
if (text_offset_or_child_index_ > MaxTextOffset()) {
if (failure_reason) {
*failure_reason = String::Format(
"\nPosition invalid: text offset too large.\n%d vs. %d.",
text_offset_or_child_index_, MaxTextOffset());
}
return false;
}
} else {
if (text_offset_or_child_index_ >
container_object_->ChildCountIncludingIgnored()) {
if (failure_reason) {
*failure_reason = String::Format(
"\nPosition invalid: child index too large.\n%d vs. %d.",
text_offset_or_child_index_,
container_object_->ChildCountIncludingIgnored());
}
return false;
}
}
#if DCHECK_IS_ON()
DCHECK_EQ(container_object_->GetDocument()->DomTreeVersion(),
dom_tree_version_);
DCHECK_EQ(container_object_->GetDocument()->StyleVersion(), style_version_);
#endif // DCHECK_IS_ON()
return true;
}
bool AXPosition::IsTextPosition() const {
// We don't call |IsValid| from here because |IsValid| uses this method.
if (!container_object_)
return false;
return container_object_->IsTextObject() ||
container_object_->IsNativeTextControl();
}
const AXPosition AXPosition::CreateNextPosition() const {
if (!IsValid())
return {};
if (IsTextPosition() && TextOffset() < MaxTextOffset()) {
return CreatePositionInTextObject(*container_object_, (TextOffset() + 1),
TextAffinity::kDownstream,
AXPositionAdjustmentBehavior::kMoveRight);
}
// Handles both an "after children" position, or a text position that is right
// after the last character.
const AXObject* child = ChildAfterTreePosition();
if (!child) {
// If this is a static text object, we should not descend into its inline
// text boxes when present, because we'll just be creating a text position
// in the same piece of text.
const AXObject* next_in_order =
container_object_->ChildCountIncludingIgnored()
? container_object_->DeepestLastChildIncludingIgnored()
->NextInPreOrderIncludingIgnored()
: container_object_->NextInPreOrderIncludingIgnored();
if (!next_in_order || !next_in_order->ParentObjectIncludedInTree())
return {};
return CreatePositionBeforeObject(*next_in_order,
AXPositionAdjustmentBehavior::kMoveRight);
}
if (!child->ParentObjectIncludedInTree())
return {};
return CreatePositionAfterObject(*child,
AXPositionAdjustmentBehavior::kMoveRight);
}
const AXPosition AXPosition::CreatePreviousPosition() const {
if (!IsValid())
return {};
if (IsTextPosition() && TextOffset() > 0) {
return CreatePositionInTextObject(*container_object_, (TextOffset() - 1),
TextAffinity::kDownstream,
AXPositionAdjustmentBehavior::kMoveLeft);
}
const AXObject* child = ChildAfterTreePosition();
const AXObject* object_before_position = nullptr;
// Handles both an "after children" position, or a text position that is
// before the first character.
if (!child) {
// If this is a static text object, we should not descend into its inline
// text boxes when present, because we'll just be creating a text position
// in the same piece of text.
if (!container_object_->IsTextObject() &&
container_object_->ChildCountIncludingIgnored()) {
const AXObject* last_child =
container_object_->LastChildIncludingIgnored();
// Dont skip over any intervening text.
if (last_child->IsTextObject() || last_child->IsNativeTextControl()) {
return CreatePositionAfterObject(
*last_child, AXPositionAdjustmentBehavior::kMoveLeft);
}
return CreatePositionBeforeObject(
*last_child, AXPositionAdjustmentBehavior::kMoveLeft);
}
object_before_position =
container_object_->PreviousInPreOrderIncludingIgnored();
} else {
object_before_position = child->PreviousInPreOrderIncludingIgnored();
}
if (!object_before_position ||
!object_before_position->ParentObjectIncludedInTree()) {
return {};
}
// Dont skip over any intervening text.
if (object_before_position->IsTextObject() ||
object_before_position->IsNativeTextControl()) {
return CreatePositionAfterObject(*object_before_position,
AXPositionAdjustmentBehavior::kMoveLeft);
}
return CreatePositionBeforeObject(*object_before_position,
AXPositionAdjustmentBehavior::kMoveLeft);
}
const AXPosition AXPosition::AsUnignoredPosition(
const AXPositionAdjustmentBehavior adjustment_behavior) const {
if (!IsValid())
return {};
// There are five possibilities:
//
// 1. The container object is ignored and this is not a text position or an
// "after children" position. Try to find the equivalent position in the
// unignored parent.
//
// 2. The position is a text position and the container object is ignored.
// Return a "before children" or an "after children" position anchored at the
// container's unignored parent.
//
// 3. The container object is ignored and this is an "after children"
// position. Find the previous or the next object in the tree and recurse.
//
// 4. The child after a tree position is ignored, but the container object is
// not. Return a "before children" or an "after children" position.
//
// 5. We arbitrarily decided to ignore positions that are anchored to before a
// text object. We move such positions to before the first character of the
// text object. This is in an effort to ensure that two positions, one a
// "before object" position anchored to a text object, and one a "text
// position" anchored to before the first character of the same text object,
// compare as equivalent.
const AXObject* container = container_object_;
const AXObject* child = ChildAfterTreePosition();
// Case 1.
// Neither text positions nor "after children" positions have a |child|
// object.
if (!container->AccessibilityIsIncludedInTree() && child) {
// |CreatePositionBeforeObject| already finds the unignored parent before
// creating the new position, so we don't need to replicate the logic here.
return CreatePositionBeforeObject(*child, adjustment_behavior);
}
// Cases 2 and 3.
if (!container->AccessibilityIsIncludedInTree()) {
// Case 2.
if (IsTextPosition()) {
if (!container->ParentObjectIncludedInTree())
return {};
// Calling |CreateNextPosition| or |CreatePreviousPosition| is not
// appropriate here because they will go through the text position
// character by character which is unnecessary, in addition to skipping
// any unignored siblings.
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveRight:
return CreateLastPositionInObject(
*container->ParentObjectIncludedInTree(), adjustment_behavior);
case AXPositionAdjustmentBehavior::kMoveLeft:
return CreateFirstPositionInObject(
*container->ParentObjectIncludedInTree(), adjustment_behavior);
}
}
// Case 3.
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveRight:
return CreateNextPosition().AsUnignoredPosition(adjustment_behavior);
case AXPositionAdjustmentBehavior::kMoveLeft:
return CreatePreviousPosition().AsUnignoredPosition(
adjustment_behavior);
}
}
// Case 4.
if (child && !child->AccessibilityIsIncludedInTree()) {
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveRight:
return CreateLastPositionInObject(*container);
case AXPositionAdjustmentBehavior::kMoveLeft:
return CreateFirstPositionInObject(*container);
}
}
// Case 5.
if (child && child->IsTextObject())
return CreateFirstPositionInObject(*child);
// The position is not ignored.
return *this;
}
const AXPosition AXPosition::AsValidDOMPosition(
const AXPositionAdjustmentBehavior adjustment_behavior) const {
if (!IsValid())
return {};
// We adjust to the next or previous position if the container or the child
// object after a tree position are mock or virtual objects, since mock or
// virtual objects will not be present in the DOM tree. Alternatively, in the
// case of an "after children" position, we need to check if the last child of
// the container object is mock or virtual and adjust accordingly. Abstract
// inline text boxes and static text nodes for CSS "::before" and "::after"
// positions are also considered to be virtual since they don't have an
// associated DOM node.
// In more detail:
// If the child after a tree position doesn't have an associated node in the
// DOM tree, we adjust to the next or previous position because a
// corresponding child node will not be found in the DOM tree. We need a
// corresponding child node in the DOM tree so that we can anchor the DOM
// position before it. We can't ask the layout tree for the child's container
// block node, because this might change the placement of the AX position
// drastically. However, if the container doesn't have a corresponding DOM
// node, we need to use the layout tree to find its corresponding container
// block node, because no AX positions inside an anonymous layout block could
// be represented in the DOM tree anyway.
const AXObject* container = container_object_;
DCHECK(container);
const AXObject* child = ChildAfterTreePosition();
const AXObject* last_child = container->LastChildIncludingIgnored();
if ((IsTextPosition() && (!container->GetNode() ||
container->GetNode()->IsMarkerPseudoElement())) ||
container->IsMockObject() || container->IsVirtualObject() ||
(!child && last_child &&
(!last_child->GetNode() ||
last_child->GetNode()->IsMarkerPseudoElement() ||
last_child->IsMockObject() || last_child->IsVirtualObject())) ||
(child &&
(!child->GetNode() || child->GetNode()->IsMarkerPseudoElement() ||
child->IsMockObject() || child->IsVirtualObject()))) {
AXPosition result;
if (adjustment_behavior == AXPositionAdjustmentBehavior::kMoveRight)
result = CreateNextPosition();
else
result = CreatePreviousPosition();
if (result && result != *this)
return result.AsValidDOMPosition(adjustment_behavior);
return {};
}
// At this point, if a DOM node is associated with our container, then the
// corresponding DOM position should be valid.
if (container->GetNode() && !container->GetNode()->IsMarkerPseudoElement())
return *this;
DCHECK(IsA<AXLayoutObject>(container))
<< "Non virtual and non mock AX objects that are not associated to a DOM "
"node should have an associated layout object.";
const Node* container_node =
To<AXLayoutObject>(container)->GetNodeOrContainingBlockNode();
DCHECK(container_node) << "All anonymous layout objects and list markers "
"should have a containing block element.";
DCHECK(!container->IsDetached());
if (!container_node || container->IsDetached())
return {};
auto& ax_object_cache_impl = container->AXObjectCache();
const AXObject* new_container =
ax_object_cache_impl.GetOrCreate(container_node);
DCHECK(new_container);
if (!new_container)
return {};
AXPosition position(*new_container);
if (new_container == container->ParentObjectIncludedInTree()) {
position.text_offset_or_child_index_ = container->IndexInParent();
} else {
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveRight:
position.text_offset_or_child_index_ =
new_container->ChildCountIncludingIgnored();
break;
case AXPositionAdjustmentBehavior::kMoveLeft:
position.text_offset_or_child_index_ = 0;
break;
}
}
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(position.IsValid(&failure_reason)) << failure_reason;
#endif
return position.AsValidDOMPosition(adjustment_behavior);
}
const PositionWithAffinity AXPosition::ToPositionWithAffinity(
const AXPositionAdjustmentBehavior adjustment_behavior) const {
const AXPosition adjusted_position = AsValidDOMPosition(adjustment_behavior);
if (!adjusted_position.IsValid())
return {};
const Node* container_node = adjusted_position.container_object_->GetNode();
DCHECK(container_node) << "AX positions that are valid DOM positions should "
"always be connected to their DOM nodes.";
if (!container_node)
return {};
if (!adjusted_position.IsTextPosition()) {
// AX positions that are unumbiguously at the start or end of a container,
// should convert to the corresponding DOM positions at the start or end of
// their parent node. Other child positions in the accessibility tree should
// recompute their parent in the DOM tree, because they might be ARIA owned
// by a different object in the accessibility tree than in the DOM tree, or
// their parent in the accessibility tree might be ignored.
const AXObject* child = adjusted_position.ChildAfterTreePosition();
if (child) {
const Node* child_node = child->GetNode();
DCHECK(child_node) << "AX objects used in AX positions that are valid "
"DOM positions should always be connected to their "
"DOM nodes.";
if (!child_node)
return {};
if (!child_node->previousSibling()) {
// Creates a |PositionAnchorType::kBeforeChildren| position.
container_node = child_node->parentNode();
DCHECK(container_node);
if (!container_node)
return {};
return PositionWithAffinity(
Position::FirstPositionInNode(*container_node), affinity_);
}
// Creates a |PositionAnchorType::kOffsetInAnchor| position.
return PositionWithAffinity(Position::InParentBeforeNode(*child_node),
affinity_);
}
// "After children" positions.
const AXObject* last_child = container_object_->LastChildIncludingIgnored();
if (last_child) {
const Node* last_child_node = last_child->GetNode();
DCHECK(last_child_node) << "AX objects used in AX positions that are "
"valid DOM positions should always be "
"connected to their DOM nodes.";
if (!last_child_node)
return {};
// Check if this is an "after children" position in the DOM as well.
if (!last_child_node->nextSibling()) {
// Creates a |PositionAnchorType::kAfterChildren| position.
container_node = last_child_node->parentNode();
DCHECK(container_node);
if (!container_node)
return {};
return PositionWithAffinity(
Position::LastPositionInNode(*container_node), affinity_);
}
// Do the next best thing by creating a
// |PositionAnchorType::kOffsetInAnchor| position after the last unignored
// child.
return PositionWithAffinity(Position::InParentAfterNode(*last_child_node),
affinity_);
}
// The |AXObject| container has no children. Do the next best thing by
// creating a |PositionAnchorType::kBeforeChildren| position.
return PositionWithAffinity(Position::FirstPositionInNode(*container_node),
affinity_);
}
// If NGOffsetMapping supports it, convert from a text offset, which may have
// white space collapsed, to a DOM offset which should have uncompressed white
// space. NGOffsetMapping supports layout text, layout replaced, ruby runs,
// list markers, and layout block flow at inline-level, i.e. "display=inline"
// or "display=inline-block". It also supports out-of-flow elements, which
// should not be relevant to text positions in the accessibility tree.
const LayoutObject* layout_object = container_node->GetLayoutObject();
// TODO(crbug.com/567964): LayoutObject::IsAtomicInlineLevel() also includes
// block-level replaced elements. We need to explicitly exclude them via
// LayoutObject::IsInline().
const bool supports_ng_offset_mapping =
layout_object &&
((layout_object->IsInline() && layout_object->IsAtomicInlineLevel()) ||
layout_object->IsText());
const NGOffsetMapping* container_offset_mapping = nullptr;
if (supports_ng_offset_mapping) {
LayoutBlockFlow* formatting_context =
NGOffsetMapping::GetInlineFormattingContextOf(*layout_object);
container_offset_mapping =
formatting_context ? NGInlineNode::GetOffsetMapping(formatting_context)
: nullptr;
}
if (!container_offset_mapping) {
// We are unable to compute the text offset in the accessibility tree that
// corresponds to the DOM offset. We do the next best thing by returning
// either the first or the last DOM position in |container_node| based on
// the |adjustment_behavior|.
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveRight:
return PositionWithAffinity(
Position::LastPositionInNode(*container_node), affinity_);
case AXPositionAdjustmentBehavior::kMoveLeft:
return PositionWithAffinity(
Position::FirstPositionInNode(*container_node), affinity_);
}
}
int text_offset_in_formatting_context =
adjusted_position.container_object_->TextOffsetInFormattingContext(
adjusted_position.TextOffset());
DCHECK_GE(text_offset_in_formatting_context, 0);
// An "after text" position in the accessibility tree should map to a text
// position in the DOM tree that is after the DOM node's text, but before any
// collapsed white space at the node's end. In all other cases, the text
// offset in the accessibility tree should be translated to a DOM offset that
// is after any collapsed white space. For example, look at the inline text
// box with the word "Hello" and observe how the white space in the DOM, both
// before and after the word, is mapped from the equivalent accessibility
// position.
//
// AX text position in "InlineTextBox" name="Hello", 0
// DOM position #text " Hello "@offsetInAnchor[3]
// AX text position in "InlineTextBox" name="Hello", 5
// DOM position #text " Hello "@offsetInAnchor[8]
Position dom_position =
adjusted_position.TextOffset() < adjusted_position.MaxTextOffset()
? container_offset_mapping->GetLastPosition(
static_cast<unsigned int>(text_offset_in_formatting_context))
: container_offset_mapping->GetFirstPosition(
static_cast<unsigned int>(text_offset_in_formatting_context));
// When there is no uncompressed white space at the end of our
// |container_node|, and this is an "after text" position, we might get back
// the NULL position if this is the last node in the DOM.
if (dom_position.IsNull())
dom_position = Position::LastPositionInNode(*container_node);
return PositionWithAffinity(dom_position, affinity_);
}
const Position AXPosition::ToPosition(
const AXPositionAdjustmentBehavior adjustment_behavior) const {
return ToPositionWithAffinity(adjustment_behavior).GetPosition();
}
String AXPosition::ToString() const {
if (!IsValid())
return "Invalid AXPosition";
StringBuilder builder;
if (IsTextPosition()) {
builder.Append("AX text position in ");
builder.Append(container_object_->ToString());
builder.AppendFormat(", %d", TextOffset());
return builder.ToString();
}
builder.Append("AX object anchored position in ");
builder.Append(container_object_->ToString());
builder.AppendFormat(", %d", ChildIndex());
return builder.ToString();
}
// static
const AXObject* AXPosition::FindNeighboringUnignoredObject(
const Document& document,
const Node& child_node,
const ContainerNode* container_node,
const AXPositionAdjustmentBehavior adjustment_behavior) {
AXObjectCache* ax_object_cache = document.ExistingAXObjectCache();
if (!ax_object_cache)
return nullptr;
auto* ax_object_cache_impl = static_cast<AXObjectCacheImpl*>(ax_object_cache);
switch (adjustment_behavior) {
case AXPositionAdjustmentBehavior::kMoveRight: {
const Node* next_node = &child_node;
while ((next_node = NodeTraversal::NextIncludingPseudo(*next_node,
container_node))) {
const AXObject* next_object =
ax_object_cache_impl->GetOrCreate(next_node);
if (next_object && next_object->AccessibilityIsIncludedInTree())
return next_object;
}
return nullptr;
}
case AXPositionAdjustmentBehavior::kMoveLeft: {
const Node* previous_node = &child_node;
// Since this is a pre-order traversal,
// "NodeTraversal::PreviousIncludingPseudo" will eventually reach
// |container_node| if |container_node| is not nullptr. We should exclude
// this as we are strictly interested in |container_node|'s unignored
// descendantsin the accessibility tree.
while ((previous_node = NodeTraversal::PreviousIncludingPseudo(
*previous_node, container_node)) &&
previous_node != container_node) {
const AXObject* previous_object =
ax_object_cache_impl->GetOrCreate(previous_node);
if (previous_object && previous_object->AccessibilityIsIncludedInTree())
return previous_object;
}
return nullptr;
}
}
}
bool operator==(const AXPosition& a, const AXPosition& b) {
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(a.IsValid(&failure_reason) && b.IsValid(&failure_reason))
<< failure_reason;
#endif
if (*a.ContainerObject() != *b.ContainerObject())
return false;
if (a.IsTextPosition() && b.IsTextPosition())
return a.TextOffset() == b.TextOffset() && a.Affinity() == b.Affinity();
if (!a.IsTextPosition() && !b.IsTextPosition())
return a.ChildIndex() == b.ChildIndex();
NOTREACHED() << "AXPosition objects having the same container object should "
"have the same type.";
return false;
}
bool operator!=(const AXPosition& a, const AXPosition& b) {
return !(a == b);
}
bool operator<(const AXPosition& a, const AXPosition& b) {
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(a.IsValid(&failure_reason) && b.IsValid(&failure_reason))
<< failure_reason;
#endif
if (a.ContainerObject() == b.ContainerObject()) {
if (a.IsTextPosition() && b.IsTextPosition())
return a.TextOffset() < b.TextOffset();
if (!a.IsTextPosition() && !b.IsTextPosition())
return a.ChildIndex() < b.ChildIndex();
NOTREACHED()
<< "AXPosition objects having the same container object should "
"have the same type.";
return false;
}
int index_in_ancestor1, index_in_ancestor2;
const AXObject* ancestor =
AXObject::LowestCommonAncestor(*a.ContainerObject(), *b.ContainerObject(),
&index_in_ancestor1, &index_in_ancestor2);
DCHECK_GE(index_in_ancestor1, -1);
DCHECK_GE(index_in_ancestor2, -1);
if (!ancestor)
return false;
if (ancestor == a.ContainerObject()) {
DCHECK(!a.IsTextPosition());
index_in_ancestor1 = a.ChildIndex();
}
if (ancestor == b.ContainerObject()) {
DCHECK(!b.IsTextPosition());
index_in_ancestor2 = b.ChildIndex();
}
return index_in_ancestor1 < index_in_ancestor2;
}
bool operator<=(const AXPosition& a, const AXPosition& b) {
return a < b || a == b;
}
bool operator>(const AXPosition& a, const AXPosition& b) {
#if DCHECK_IS_ON()
String failure_reason;
DCHECK(a.IsValid(&failure_reason) && b.IsValid(&failure_reason))
<< failure_reason;
#endif
if (a.ContainerObject() == b.ContainerObject()) {
if (a.IsTextPosition() && b.IsTextPosition())
return a.TextOffset() > b.TextOffset();
if (!a.IsTextPosition() && !b.IsTextPosition())
return a.ChildIndex() > b.ChildIndex();
NOTREACHED()
<< "AXPosition objects having the same container object should "
"have the same type.";
return false;
}
int index_in_ancestor1, index_in_ancestor2;
const AXObject* ancestor =
AXObject::LowestCommonAncestor(*a.ContainerObject(), *b.ContainerObject(),
&index_in_ancestor1, &index_in_ancestor2);
DCHECK_GE(index_in_ancestor1, -1);
DCHECK_GE(index_in_ancestor2, -1);
if (!ancestor)
return false;
if (ancestor == a.ContainerObject()) {
DCHECK(!a.IsTextPosition());
index_in_ancestor1 = a.ChildIndex();
}
if (ancestor == b.ContainerObject()) {
DCHECK(!b.IsTextPosition());
index_in_ancestor2 = b.ChildIndex();
}
return index_in_ancestor1 > index_in_ancestor2;
}
bool operator>=(const AXPosition& a, const AXPosition& b) {
return a > b || a == b;
}
std::ostream& operator<<(std::ostream& ostream, const AXPosition& position) {
return ostream << position.ToString().Utf8();
}
} // namespace blink