blob: 71126d0224fd796469d7865cffe336ba3178e796 [file] [log] [blame]
// Copyright 2020 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 "content/browser/accessibility/accessibility_tree_formatter_utils_mac.h"
#include "base/strings/sys_string_conversions.h"
// error: 'accessibilityAttributeNames' is deprecated: first deprecated in
// macOS 10.10 - Use the NSAccessibility protocol methods instead (see
// NSAccessibilityProtocols.h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
using base::SysNSStringToUTF8;
namespace content {
namespace a11y {
namespace {
#define INT_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.name_or_value \
<< " to Int: " << msg; \
return nil;
#define INTARRAY_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.name_or_value \
<< " to IntArray: " << msg; \
return nil;
#define NSRANGE_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.name_or_value \
<< " to NSRange: " << msg; \
return nil;
#define UIELEMENT_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.name_or_value \
<< " to UIElement: " << msg; \
return nil;
#define TEXTMARKER_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.name_or_value \
<< " to AXTextMarker: " << msg \
<< ". Expected format: {anchor, offset, affinity}, where anchor " \
"is :line_num, offset is integer, affinity is either down, " \
"up or none"; \
return nil;
} // namespace
LineIndexesMap::LineIndexesMap(const BrowserAccessibilityCocoa* cocoa_node) {
int counter = 0;
Build(cocoa_node, &counter);
}
LineIndexesMap::~LineIndexesMap() {}
std::string LineIndexesMap::IndexBy(
const BrowserAccessibilityCocoa* cocoa_node) const {
std::string line_index = ":unknown";
if (map.find(cocoa_node) != map.end()) {
line_index = map.at(cocoa_node);
}
return line_index;
}
gfx::NativeViewAccessible LineIndexesMap::NodeBy(
const std::string& line_index) const {
for (std::pair<const gfx::NativeViewAccessible, std::string> item : map) {
if (item.second == line_index) {
return item.first;
}
}
return nil;
}
void LineIndexesMap::Build(const BrowserAccessibilityCocoa* cocoa_node,
int* counter) {
const std::string line_index =
std::string(1, ':') + base::NumberToString(++(*counter));
map.insert({cocoa_node, line_index});
for (BrowserAccessibilityCocoa* cocoa_child in [cocoa_node children]) {
Build(cocoa_child, counter);
}
}
// AttributeInvoker
AttributeInvoker::AttributeInvoker(const BrowserAccessibilityCocoa* cocoa_node,
const LineIndexesMap& line_indexes_map)
: cocoa_node(cocoa_node), line_indexes_map(line_indexes_map) {
attributes = [cocoa_node accessibilityAttributeNames];
parameterized_attributes =
[cocoa_node accessibilityParameterizedAttributeNames];
}
OptionalNSObject AttributeInvoker::Invoke(
const PropertyNode& property_node) const {
// Attributes
for (NSString* attribute : attributes) {
if (property_node.IsMatching(SysNSStringToUTF8(attribute))) {
return OptionalNSObject::NotNullOrNotApplicable(
[cocoa_node accessibilityAttributeValue:attribute]);
}
}
// Parameterized attributes
for (NSString* attribute : parameterized_attributes) {
if (property_node.IsMatching(SysNSStringToUTF8(attribute))) {
OptionalNSObject param = ParamByPropertyNode(property_node);
if (param.IsNotNil()) {
return OptionalNSObject([cocoa_node
accessibilityAttributeValue:attribute
forParameter:*param]);
}
return param;
}
}
return OptionalNSObject::NotApplicable();
}
OptionalNSObject AttributeInvoker::ParamByPropertyNode(
const PropertyNode& property_node) const {
// NSAccessibility attributes always take a single parameter.
if (property_node.parameters.size() != 1) {
LOG(ERROR) << "Failed to parse " << property_node.original_property
<< ": single parameter is expected";
return OptionalNSObject::Error();
}
// Nested attribute case: attempt to invoke an attribute for an argument node.
const PropertyNode& arg_node = property_node.parameters[0];
OptionalNSObject subvalue = Invoke(arg_node);
if (!subvalue.IsNotApplicable()) {
return subvalue;
}
// Otherwise parse argument node value.
const std::string& property_name = property_node.name_or_value;
if (property_name == "AXLineForIndex" ||
property_name == "AXTextMarkerForIndex") { // Int
return OptionalNSObject::NotNilOrError(PropertyNodeToInt(arg_node));
}
if (property_name == "AXCellForColumnAndRow") { // IntArray
return OptionalNSObject::NotNilOrError(PropertyNodeToIntArray(arg_node));
}
if (property_name == "AXStringForRange") { // NSRange
return OptionalNSObject::NotNilOrError(PropertyNodeToRange(arg_node));
}
if (property_name == "AXIndexForChildUIElement") { // UIElement
return OptionalNSObject::NotNilOrError(PropertyNodeToUIElement(arg_node));
}
if (property_name == "AXIndexForTextMarker") { // TextMarker
return OptionalNSObject::NotNilOrError(PropertyNodeToTextMarker(arg_node));
}
if (property_name == "AXStringForTextMarkerRange") { // TextMarkerRange
return OptionalNSObject::NotNilOrError(
PropertyNodeToTextMarkerRange(arg_node));
}
return OptionalNSObject::NotApplicable();
}
// NSNumber. Format: integer.
NSNumber* AttributeInvoker::PropertyNodeToInt(
const PropertyNode& intnode) const {
base::Optional<int> param = intnode.AsInt();
if (!param) {
INT_FAIL(intnode, "not a number")
}
return [NSNumber numberWithInt:*param];
}
// NSArray of two NSNumber. Format: [integer, integer].
NSArray* AttributeInvoker::PropertyNodeToIntArray(
const PropertyNode& arraynode) const {
if (arraynode.name_or_value != "[]") {
INTARRAY_FAIL(arraynode, "not array")
}
NSMutableArray* array =
[[NSMutableArray alloc] initWithCapacity:arraynode.parameters.size()];
for (const auto& paramnode : arraynode.parameters) {
base::Optional<int> param = paramnode.AsInt();
if (!param) {
INTARRAY_FAIL(arraynode, paramnode.name_or_value + " is not a number")
}
[array addObject:@(*param)];
}
return array;
}
// NSRange. Format: {loc: integer, len: integer}.
NSValue* AttributeInvoker::PropertyNodeToRange(
const PropertyNode& dictnode) const {
if (!dictnode.IsDict()) {
NSRANGE_FAIL(dictnode, "dictionary is expected")
}
base::Optional<int> loc = dictnode.FindIntKey("loc");
if (!loc) {
NSRANGE_FAIL(dictnode, "no loc or loc is not a number")
}
base::Optional<int> len = dictnode.FindIntKey("len");
if (!len) {
NSRANGE_FAIL(dictnode, "no len or len is not a number")
}
return [NSValue valueWithRange:NSMakeRange(*loc, *len)];
}
// UIElement. Format: :line_num.
gfx::NativeViewAccessible AttributeInvoker::PropertyNodeToUIElement(
const PropertyNode& uielement_node) const {
gfx::NativeViewAccessible uielement =
line_indexes_map.NodeBy(uielement_node.name_or_value);
if (!uielement) {
UIELEMENT_FAIL(uielement_node,
"no corresponding UIElement was found in the tree")
}
return uielement;
}
id AttributeInvoker::DictNodeToTextMarker(const PropertyNode& dictnode) const {
if (!dictnode.IsDict()) {
TEXTMARKER_FAIL(dictnode, "dictionary is expected")
}
if (dictnode.parameters.size() != 3) {
TEXTMARKER_FAIL(dictnode, "wrong number of dictionary elements")
}
BrowserAccessibilityCocoa* anchor_cocoa =
line_indexes_map.NodeBy(dictnode.parameters[0].name_or_value);
if (!anchor_cocoa) {
TEXTMARKER_FAIL(dictnode, "1st argument: wrong anchor")
}
base::Optional<int> offset = dictnode.parameters[1].AsInt();
if (!offset) {
TEXTMARKER_FAIL(dictnode, "2nd argument: wrong offset")
}
ax::mojom::TextAffinity affinity;
const std::string& affinity_str = dictnode.parameters[2].name_or_value;
if (affinity_str == "none") {
affinity = ax::mojom::TextAffinity::kNone;
} else if (affinity_str == "down") {
affinity = ax::mojom::TextAffinity::kDownstream;
} else if (affinity_str == "up") {
affinity = ax::mojom::TextAffinity::kUpstream;
} else {
TEXTMARKER_FAIL(dictnode, "3rd argument: wrong affinity")
}
return content::AXTextMarkerFrom(anchor_cocoa, *offset, affinity);
}
id AttributeInvoker::PropertyNodeToTextMarker(
const PropertyNode& dictnode) const {
return DictNodeToTextMarker(dictnode);
}
id AttributeInvoker::PropertyNodeToTextMarkerRange(
const PropertyNode& rangenode) const {
if (!rangenode.IsDict()) {
TEXTMARKER_FAIL(rangenode, "dictionary is expected")
}
const PropertyNode* anchornode = rangenode.FindKey("anchor");
if (!anchornode) {
TEXTMARKER_FAIL(rangenode, "no anchor")
}
id anchor_textmarker = DictNodeToTextMarker(*anchornode);
if (!anchor_textmarker) {
TEXTMARKER_FAIL(rangenode, "failed to parse anchor")
}
const PropertyNode* focusnode = rangenode.FindKey("focus");
if (!focusnode) {
TEXTMARKER_FAIL(rangenode, "no focus")
}
id focus_textmarker = DictNodeToTextMarker(*focusnode);
if (!focus_textmarker) {
TEXTMARKER_FAIL(rangenode, "failed to parse focus")
}
return content::AXTextMarkerRangeFrom(anchor_textmarker, focus_textmarker);
}
} // namespace a11y
} // namespace content
#pragma clang diagnostic pop