/* * Copyright (C) 2008, 2009, 2010, 2011 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "WebAccessibilityObjectWrapperBase.h" #if ENABLE(ACCESSIBILITY) #import "AXObjectCache.h" #import "AccessibilityARIAGridRow.h" #import "AccessibilityList.h" #import "AccessibilityListBox.h" #import "AccessibilityObjectInterface.h" #import "AccessibilityRenderObject.h" #import "AccessibilityScrollView.h" #import "AccessibilitySpinButton.h" #import "AccessibilityTable.h" #import "AccessibilityTableCell.h" #import "AccessibilityTableColumn.h" #import "AccessibilityTableRow.h" #import "ColorMac.h" #import "ContextMenuController.h" #import "Editing.h" #import "FrameSelection.h" #import "HTMLNames.h" #import "LayoutRect.h" #import "LocalizedStrings.h" #import "Page.h" #import "RenderTextControl.h" #import "RenderView.h" #import "RenderWidget.h" #import "ScrollView.h" #import "TextCheckerClient.h" #import "TextIterator.h" #import "VisibleUnits.h" #import #if PLATFORM(MAC) #import "WebAccessibilityObjectWrapperMac.h" #else #import "WebAccessibilityObjectWrapperIOS.h" #endif using namespace WebCore; using namespace HTMLNames; // Search Keys #ifndef NSAccessibilityAnyTypeSearchKey #define NSAccessibilityAnyTypeSearchKey @"AXAnyTypeSearchKey" #endif #ifndef NSAccessibilityArticleSearchKey #define NSAccessibilityArticleSearchKey @"AXArticleSearchKey" #endif #ifndef NSAccessibilityBlockquoteSameLevelSearchKey #define NSAccessibilityBlockquoteSameLevelSearchKey @"AXBlockquoteSameLevelSearchKey" #endif #ifndef NSAccessibilityBlockquoteSearchKey #define NSAccessibilityBlockquoteSearchKey @"AXBlockquoteSearchKey" #endif #ifndef NSAccessibilityBoldFontSearchKey #define NSAccessibilityBoldFontSearchKey @"AXBoldFontSearchKey" #endif #ifndef NSAccessibilityButtonSearchKey #define NSAccessibilityButtonSearchKey @"AXButtonSearchKey" #endif #ifndef NSAccessibilityCheckBoxSearchKey #define NSAccessibilityCheckBoxSearchKey @"AXCheckBoxSearchKey" #endif #ifndef NSAccessibilityControlSearchKey #define NSAccessibilityControlSearchKey @"AXControlSearchKey" #endif #ifndef NSAccessibilityDifferentTypeSearchKey #define NSAccessibilityDifferentTypeSearchKey @"AXDifferentTypeSearchKey" #endif #ifndef NSAccessibilityFontChangeSearchKey #define NSAccessibilityFontChangeSearchKey @"AXFontChangeSearchKey" #endif #ifndef NSAccessibilityFontColorChangeSearchKey #define NSAccessibilityFontColorChangeSearchKey @"AXFontColorChangeSearchKey" #endif #ifndef NSAccessibilityFrameSearchKey #define NSAccessibilityFrameSearchKey @"AXFrameSearchKey" #endif #ifndef NSAccessibilityGraphicSearchKey #define NSAccessibilityGraphicSearchKey @"AXGraphicSearchKey" #endif #ifndef NSAccessibilityHeadingLevel1SearchKey #define NSAccessibilityHeadingLevel1SearchKey @"AXHeadingLevel1SearchKey" #endif #ifndef NSAccessibilityHeadingLevel2SearchKey #define NSAccessibilityHeadingLevel2SearchKey @"AXHeadingLevel2SearchKey" #endif #ifndef NSAccessibilityHeadingLevel3SearchKey #define NSAccessibilityHeadingLevel3SearchKey @"AXHeadingLevel3SearchKey" #endif #ifndef NSAccessibilityHeadingLevel4SearchKey #define NSAccessibilityHeadingLevel4SearchKey @"AXHeadingLevel4SearchKey" #endif #ifndef NSAccessibilityHeadingLevel5SearchKey #define NSAccessibilityHeadingLevel5SearchKey @"AXHeadingLevel5SearchKey" #endif #ifndef NSAccessibilityHeadingLevel6SearchKey #define NSAccessibilityHeadingLevel6SearchKey @"AXHeadingLevel6SearchKey" #endif #ifndef NSAccessibilityHeadingSameLevelSearchKey #define NSAccessibilityHeadingSameLevelSearchKey @"AXHeadingSameLevelSearchKey" #endif #ifndef NSAccessibilityHeadingSearchKey #define NSAccessibilityHeadingSearchKey @"AXHeadingSearchKey" #endif #ifndef NSAccessibilityHighlightedSearchKey #define NSAccessibilityHighlightedSearchKey @"AXHighlightedSearchKey" #endif #ifndef NSAccessibilityKeyboardFocusableSearchKey #define NSAccessibilityKeyboardFocusableSearchKey @"AXKeyboardFocusableSearchKey" #endif #ifndef NSAccessibilityItalicFontSearchKey #define NSAccessibilityItalicFontSearchKey @"AXItalicFontSearchKey" #endif #ifndef NSAccessibilityLandmarkSearchKey #define NSAccessibilityLandmarkSearchKey @"AXLandmarkSearchKey" #endif #ifndef NSAccessibilityLinkSearchKey #define NSAccessibilityLinkSearchKey @"AXLinkSearchKey" #endif #ifndef NSAccessibilityListSearchKey #define NSAccessibilityListSearchKey @"AXListSearchKey" #endif #ifndef NSAccessibilityLiveRegionSearchKey #define NSAccessibilityLiveRegionSearchKey @"AXLiveRegionSearchKey" #endif #ifndef NSAccessibilityMisspelledWordSearchKey #define NSAccessibilityMisspelledWordSearchKey @"AXMisspelledWordSearchKey" #endif #ifndef NSAccessibilityOutlineSearchKey #define NSAccessibilityOutlineSearchKey @"AXOutlineSearchKey" #endif #ifndef NSAccessibilityPlainTextSearchKey #define NSAccessibilityPlainTextSearchKey @"AXPlainTextSearchKey" #endif #ifndef NSAccessibilityRadioGroupSearchKey #define NSAccessibilityRadioGroupSearchKey @"AXRadioGroupSearchKey" #endif #ifndef NSAccessibilitySameTypeSearchKey #define NSAccessibilitySameTypeSearchKey @"AXSameTypeSearchKey" #endif #ifndef NSAccessibilityStaticTextSearchKey #define NSAccessibilityStaticTextSearchKey @"AXStaticTextSearchKey" #endif #ifndef NSAccessibilityStyleChangeSearchKey #define NSAccessibilityStyleChangeSearchKey @"AXStyleChangeSearchKey" #endif #ifndef NSAccessibilityTableSameLevelSearchKey #define NSAccessibilityTableSameLevelSearchKey @"AXTableSameLevelSearchKey" #endif #ifndef NSAccessibilityTableSearchKey #define NSAccessibilityTableSearchKey @"AXTableSearchKey" #endif #ifndef NSAccessibilityTextFieldSearchKey #define NSAccessibilityTextFieldSearchKey @"AXTextFieldSearchKey" #endif #ifndef NSAccessibilityUnderlineSearchKey #define NSAccessibilityUnderlineSearchKey @"AXUnderlineSearchKey" #endif #ifndef NSAccessibilityUnvisitedLinkSearchKey #define NSAccessibilityUnvisitedLinkSearchKey @"AXUnvisitedLinkSearchKey" #endif #ifndef NSAccessibilityVisitedLinkSearchKey #define NSAccessibilityVisitedLinkSearchKey @"AXVisitedLinkSearchKey" #endif // Search #ifndef NSAccessibilityImmediateDescendantsOnly #define NSAccessibilityImmediateDescendantsOnly @"AXImmediateDescendantsOnly" #endif static NSArray *convertMathPairsToNSArray(const AccessibilityObject::AccessibilityMathMultiscriptPairs& pairs, NSString *subscriptKey, NSString *superscriptKey) { return createNSArray(pairs, [&] (auto& pair) { WebAccessibilityObjectWrapper *wrappers[2]; NSString *keys[2]; NSUInteger count = 0; if (pair.first && pair.first->wrapper() && !pair.first->accessibilityIsIgnored()) { wrappers[0] = pair.first->wrapper(); keys[0] = subscriptKey; count = 1; } if (pair.second && pair.second->wrapper() && !pair.second->accessibilityIsIgnored()) { wrappers[count] = pair.second->wrapper(); keys[count] = superscriptKey; count += 1; } return adoptNS([[NSDictionary alloc] initWithObjects:wrappers forKeys:keys count:count]); }).autorelease(); } NSArray *makeNSArray(const WebCore::AXCoreObject::AccessibilityChildrenVector& children) { return createNSArray(children, [] (const auto& child) -> id { if (!child) return nil; auto wrapper = child->wrapper(); // We want to return the attachment view instead of the object representing the attachment, // otherwise, we get palindrome errors in the AX hierarchy. if (child->isAttachment() && wrapper.attachmentView) return wrapper.attachmentView; return wrapper; }).autorelease(); } @implementation WebAccessibilityObjectWrapperBase @synthesize identifier = _identifier; - (id)initWithAccessibilityObject:(AXCoreObject*)axObject { ASSERT(isMainThread()); if (!(self = [super init])) return nil; [self attachAXObject:axObject]; return self; } - (void)attachAXObject:(AXCoreObject*)axObject { ASSERT(axObject && (!_identifier.isValid() || _identifier == axObject->objectID())); m_axObject = axObject; if (!_identifier.isValid()) _identifier = m_axObject->objectID(); } #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) - (void)attachIsolatedObject:(AXCoreObject*)isolatedObject { ASSERT(isolatedObject && (!_identifier.isValid() || _identifier == isolatedObject->objectID())); m_isolatedObject = isolatedObject; if (isMainThread()) m_isolatedObjectInitialized = true; if (!_identifier.isValid()) _identifier = m_isolatedObject->objectID(); } #endif - (void)detach { ASSERT(isMainThread()); _identifier = { }; m_axObject = nullptr; } #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) - (void)detachIsolatedObject:(AccessibilityDetachmentType)detachmentType { ASSERT_WITH_MESSAGE_UNUSED( detachmentType, detachmentType == AccessibilityDetachmentType::ElementChanged ? _identifier.isValid() && m_axObject : true, "isolated object was detached due to element change, but ID %s was invalid (%d) and/or m_axObject was nullptr (%d)", _identifier.loggingString().utf8().data(), !_identifier.isValid(), !m_axObject ); m_isolatedObject = nullptr; } #endif #if PLATFORM(MAC) - (WebCore::AXCoreObject*)updateObjectBackingStore { // Calling updateBackingStore() can invalidate this element so self must be retained. // If it does become invalidated, self.axBackingObject will be nil. retainPtr(self).autorelease(); auto* backingObject = self.axBackingObject; if (!backingObject) return nil; backingObject->updateBackingStore(); return self.axBackingObject; } #else - (BOOL)_prepareAccessibilityCall { // rdar://7980318 if we start a call, then block in WebThreadLock(), then we're dealloced on another, thread, we could // crash, so we should retain ourself for the duration of usage here. retainPtr(self).autorelease(); WebThreadLock(); // If we came back from our thread lock and we were detached, we will no longer have an self.axBackingObject. if (!self.axBackingObject) return NO; self.axBackingObject->updateBackingStore(); if (!self.axBackingObject) return NO; return YES; } #endif - (id)attachmentView { return nil; } - (WebCore::AXCoreObject*)axBackingObject { if (isMainThread()) return m_axObject; #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) ASSERT(AXObjectCache::isIsolatedTreeEnabled()); return m_isolatedObject; #else ASSERT_NOT_REACHED(); return nullptr; #endif } - (BOOL)isIsolatedObject { #if ENABLE(ACCESSIBILITY_ISOLATED_TREE) auto* backingObject = self.axBackingObject; return backingObject && backingObject->isAXIsolatedObjectInstance(); #else return NO; #endif } - (NSString *)baseAccessibilityDescription { return self.axBackingObject->descriptionAttributeValue(); } - (NSArray *)baseAccessibilitySpeechHint { return [(NSString *)self.axBackingObject->speechHintAttributeValue() componentsSeparatedByString:@" "]; } - (NSString *)baseAccessibilityHelpText { return self.axBackingObject->helpTextAttributeValue(); } struct PathConversionInfo { WebAccessibilityObjectWrapperBase *wrapper; CGMutablePathRef path; }; static void convertPathToScreenSpaceFunction(PathConversionInfo& conversion, const PathElement& element) { WebAccessibilityObjectWrapperBase *wrapper = conversion.wrapper; CGMutablePathRef newPath = conversion.path; FloatRect rect; switch (element.type) { case PathElement::Type::MoveToPoint: { rect = FloatRect(element.points[0], FloatSize()); CGPoint newPoint = [wrapper convertRectToSpace:rect space:AccessibilityConversionSpace::Screen].origin; CGPathMoveToPoint(newPath, nil, newPoint.x, newPoint.y); break; } case PathElement::Type::AddLineToPoint: { rect = FloatRect(element.points[0], FloatSize()); CGPoint newPoint = [wrapper convertRectToSpace:rect space:AccessibilityConversionSpace::Screen].origin; CGPathAddLineToPoint(newPath, nil, newPoint.x, newPoint.y); break; } case PathElement::Type::AddQuadCurveToPoint: { rect = FloatRect(element.points[0], FloatSize()); CGPoint newPoint1 = [wrapper convertRectToSpace:rect space:AccessibilityConversionSpace::Screen].origin; rect = FloatRect(element.points[1], FloatSize()); CGPoint newPoint2 = [wrapper convertRectToSpace:rect space:AccessibilityConversionSpace::Screen].origin; CGPathAddQuadCurveToPoint(newPath, nil, newPoint1.x, newPoint1.y, newPoint2.x, newPoint2.y); break; } case PathElement::Type::AddCurveToPoint: { rect = FloatRect(element.points[0], FloatSize()); CGPoint newPoint1 = [wrapper convertRectToSpace:rect space:AccessibilityConversionSpace::Screen].origin; rect = FloatRect(element.points[1], FloatSize()); CGPoint newPoint2 = [wrapper convertRectToSpace:rect space:AccessibilityConversionSpace::Screen].origin; rect = FloatRect(element.points[2], FloatSize()); CGPoint newPoint3 = [wrapper convertRectToSpace:rect space:AccessibilityConversionSpace::Screen].origin; CGPathAddCurveToPoint(newPath, nil, newPoint1.x, newPoint1.y, newPoint2.x, newPoint2.y, newPoint3.x, newPoint3.y); break; } case PathElement::Type::CloseSubpath: { CGPathCloseSubpath(newPath); break; } } } - (CGPathRef)convertPathToScreenSpace:(const Path&)path { auto convertedPath = adoptCF(CGPathCreateMutable()); PathConversionInfo conversion = { self, convertedPath.get() }; path.apply([&conversion](const PathElement& pathElement) { convertPathToScreenSpaceFunction(conversion, pathElement); }); return convertedPath.autorelease(); } // Determine the visible range by checking intersection of unobscuredContentRect and a range of text by // advancing forward by line from top and backwards by line from the bottom, until we have a visible range. - (NSRange)accessibilityVisibleCharacterRange { return Accessibility::retrieveValueFromMainThread([protectedSelf = retainPtr(self)] () -> NSRange { auto backingObject = protectedSelf.get().baseUpdateBackingStore; if (!backingObject) return NSMakeRange(NSNotFound, 0); auto elementRange = makeNSRange(backingObject->elementRange()); if (elementRange.location == NSNotFound) return elementRange; auto visibleRange = makeNSRange(backingObject->visibleCharacterRange()); if (visibleRange.location == NSNotFound) return visibleRange; return NSMakeRange(visibleRange.location - elementRange.location, visibleRange.length); }); } - (id)_accessibilityWebDocumentView { ASSERT_NOT_REACHED(); // Overridden by sub-classes return nil; } - (CGRect)convertRectToSpace:(const WebCore::FloatRect&)rect space:(AccessibilityConversionSpace)space { auto* backingObject = self.axBackingObject; if (!backingObject) return CGRectZero; return backingObject->convertRectToPlatformSpace(rect, space); } static BOOL addObjectWrapperToArray(AccessibilityObject* axObject, NSMutableArray *array) { if (!axObject) return NO; auto* wrapper = axObject->wrapper(); if (!wrapper) return NO; // Don't add the same object twice, but since this has already been added, we should return // YES because we want to inform that it's in the array if ([array containsObject:wrapper]) return YES; #if PLATFORM(IOS_FAMILY) // Explicitly set that this is a now element, in case other logic tries to override. [wrapper setValue:@YES forKey:@"isAccessibilityElement"]; #endif [array addObject:wrapper]; return YES; } static int blockquoteLevel(RenderObject* renderer) { if (!renderer) return 0; int result = 0; for (Node* node = renderer->node(); node; node = node->parentNode()) { if (node->hasTagName(blockquoteTag)) result += 1; } return result; } static void AXAttributeStringSetLanguage(NSMutableAttributedString* attrString, RenderObject* renderer, NSRange range) { if (!renderer) return; AccessibilityObject* axObject = renderer->document().axObjectCache()->getOrCreate(renderer); NSString *language = axObject->language(); if ([language length]) [attrString addAttribute:UIAccessibilityTokenLanguage value:language range:range]; else [attrString removeAttribute:UIAccessibilityTokenLanguage range:range]; } static void AXAttributeStringSetBlockquoteLevel(NSMutableAttributedString* attrString, RenderObject* renderer, NSRange range) { int quoteLevel = blockquoteLevel(renderer); if (quoteLevel) [attrString addAttribute:UIAccessibilityTokenBlockquoteLevel value:@(quoteLevel) range:range]; else [attrString removeAttribute:UIAccessibilityTokenBlockquoteLevel range:range]; } static void AXAttributeStringSetHeadingLevel(NSMutableAttributedString* attrString, RenderObject* renderer, NSRange range) { if (!renderer) return; AccessibilityObject* parentObject = renderer->document().axObjectCache()->getOrCreate(renderer->parent()); int parentHeadingLevel = parentObject->headingLevel(); if (parentHeadingLevel) [attrString addAttribute:UIAccessibilityTokenHeadingLevel value:@(parentHeadingLevel) range:range]; else [attrString removeAttribute:UIAccessibilityTokenHeadingLevel range:range]; } // When modifying attributed strings, the range can come from a source which may provide faulty information (e.g. the spell checker). // To protect against such cases, the range should be validated before adding or removing attributes. bool AXAttributedStringRangeIsValid(NSAttributedString *attributedString, const NSRange& range) { return NSMaxRange(range) <= [attributedString length]; } void AXAttributedStringSetFont(NSMutableAttributedString *attributedString, CTFontRef font, const NSRange& range) { if (!AXAttributedStringRangeIsValid(attributedString, range)) return; if (!font) { #if PLATFORM(MAC) [attributedString removeAttribute:NSAccessibilityFontTextAttribute range:range]; #endif return; } auto fontAttributes = adoptNS([[NSMutableDictionary alloc] init]); auto familyName = adoptCF(CTFontCopyFamilyName(font)); NSNumber *size = [NSNumber numberWithFloat:CTFontGetSize(font)]; #if PLATFORM(IOS_FAMILY) auto fullName = adoptCF(CTFontCopyFullName(font)); if (fullName) [fontAttributes setValue:bridge_cast(fullName.get()) forKey:UIAccessibilityTokenFontName]; if (familyName) [fontAttributes setValue:bridge_cast(familyName.get()) forKey:UIAccessibilityTokenFontFamily]; if ([size boolValue]) [fontAttributes setValue:size forKey:UIAccessibilityTokenFontSize]; auto traits = CTFontGetSymbolicTraits(font); if (traits & kCTFontTraitBold) [fontAttributes setValue:@YES forKey:UIAccessibilityTokenBold]; if (traits & kCTFontTraitItalic) [fontAttributes setValue:@YES forKey:UIAccessibilityTokenItalic]; [attributedString addAttributes:fontAttributes.get() range:range]; #endif #if PLATFORM(MAC) [fontAttributes setValue:size forKey:NSAccessibilityFontSizeKey]; if (familyName) [fontAttributes setValue:bridge_cast(familyName.get()) forKey:NSAccessibilityFontFamilyKey]; auto postScriptName = adoptCF(CTFontCopyPostScriptName(font)); if (postScriptName) [fontAttributes setValue:bridge_cast(postScriptName.get()) forKey:NSAccessibilityFontNameKey]; auto displayName = adoptCF(CTFontCopyDisplayName(font)); if (displayName) [fontAttributes setValue:bridge_cast(displayName.get()) forKey:NSAccessibilityVisibleNameKey]; [attributedString addAttribute:NSAccessibilityFontTextAttribute value:fontAttributes.get() range:range]; #endif } static void AXAttributeStringSetNumber(NSMutableAttributedString* attrString, NSString* attribute, NSNumber* number, NSRange range) { if (number) [attrString addAttribute:attribute value:number range:range]; else [attrString removeAttribute:attribute range:range]; } static void AXAttributeStringSetStyle(NSMutableAttributedString* attrString, RenderObject* renderer, NSRange range) { auto& style = renderer->style(); // set basic font info AXAttributedStringSetFont(attrString, style.fontCascade().primaryFont().getCTFont(), range); auto decor = style.textDecorationsInEffect(); if (decor & TextDecorationLine::Underline) AXAttributeStringSetNumber(attrString, UIAccessibilityTokenUnderline, @YES, range); // Add code context if this node is within a block. AccessibilityObject* axObject = renderer->document().axObjectCache()->getOrCreate(renderer); auto matchFunc = [] (const AXCoreObject& object) { return object.node() && object.node()->hasTagName(codeTag); }; if (const AXCoreObject* parent = Accessibility::findAncestor(*axObject, true, WTFMove(matchFunc))) [attrString addAttribute:UIAccessibilityTextAttributeContext value:UIAccessibilityTextualContextSourceCode range:range]; } static void AXAttributedStringAppendText(NSMutableAttributedString* attrString, Node* node, StringView text) { // skip invisible text if (!node->renderer()) return; // easier to calculate the range before appending the string NSRange attrStringRange = NSMakeRange([attrString length], text.length()); // append the string from this node [[attrString mutableString] appendString:text.createNSStringWithoutCopying().get()]; // set new attributes AXAttributeStringSetStyle(attrString, node->renderer(), attrStringRange); AXAttributeStringSetHeadingLevel(attrString, node->renderer(), attrStringRange); AXAttributeStringSetBlockquoteLevel(attrString, node->renderer(), attrStringRange); AXAttributeStringSetLanguage(attrString, node->renderer(), attrStringRange); } NSRange makeNSRange(std::optional range) { if (!range) return NSMakeRange(NSNotFound, 0); auto& document = range->start.document(); auto* frame = document.frame(); if (!frame) return NSMakeRange(NSNotFound, 0); auto* rootEditableElement = frame->selection().selection().rootEditableElement(); auto* scope = rootEditableElement ? rootEditableElement : document.documentElement(); if (!scope) return NSMakeRange(NSNotFound, 0); // Mouse events may cause TSM to attempt to create an NSRange for a portion of the view // that is not inside the current editable region. These checks ensure we don't produce // potentially invalid data when responding to such requests. if (!scope->contains(range->start.container.ptr()) || !scope->contains(range->end.container.ptr())) return NSMakeRange(NSNotFound, 0); return NSMakeRange(characterCount({ { *scope, 0 }, range->start }), characterCount(*range)); } std::optional makeDOMRange(Document* document, NSRange range) { if (range.location == NSNotFound) return std::nullopt; // our critical assumption is that we are only called by input methods that // concentrate on a given area containing the selection // We have to do this because of text fields and textareas. The DOM for those is not // directly in the document DOM, so serialization is problematic. Our solution is // to use the root editable element of the selection start as the positional base. // That fits with AppKit's idea of an input context. auto selectionRoot = document->frame()->selection().selection().rootEditableElement(); auto scope = selectionRoot ? selectionRoot : document->documentElement(); if (!scope) return std::nullopt; return resolveCharacterRange(makeRangeSelectingNodeContents(*scope), range); } // Returns an array of strings and AXObject wrappers corresponding to the text // runs and replacement nodes included in the given range. - (NSArray *)contentForSimpleRange:(const SimpleRange&)range attributed:(BOOL)attributed { auto array = adoptNS([[NSMutableArray alloc] init]); // Iterate over the range to build the AX attributed string. TextIterator it(range); for (; !it.atEnd(); it.advance()) { Node& node = it.range().start.container; // Non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX). if (it.text().length()) { if (!attributed) { // First check if this is represented by a link. auto* linkObject = AccessibilityObject::anchorElementForNode(&node); if (addObjectWrapperToArray(linkObject, array.get())) continue; // Next check if this region is represented by a heading. auto* headingObject = AccessibilityObject::headingElementForNode(&node); if (addObjectWrapperToArray(headingObject, array.get())) continue; StringView listMarkerText = AccessibilityObject::listMarkerTextForNodeAndPosition(&node, makeContainerOffsetPosition(it.range().start)); if (!listMarkerText.isEmpty()) [array addObject:listMarkerText.createNSString().get()]; // There was not an element representation, so just return the text. [array addObject:it.text().createNSString().get()]; } else { StringView listMarkerText = AccessibilityObject::listMarkerTextForNodeAndPosition(&node, makeContainerOffsetPosition(it.range().start)); if (!listMarkerText.isEmpty()) { auto attrString = adoptNS([[NSMutableAttributedString alloc] init]); AXAttributedStringAppendText(attrString.get(), &node, listMarkerText); [array addObject:attrString.get()]; } auto attrString = adoptNS([[NSMutableAttributedString alloc] init]); AXAttributedStringAppendText(attrString.get(), &node, it.text()); [array addObject:attrString.get()]; } } else { if (Node* replacedNode = it.node()) { auto* object = self.axBackingObject->axObjectCache()->getOrCreate(replacedNode->renderer()); if (object) addObjectWrapperToArray(object, array.get()); } } } return array.autorelease(); } - (WebCore::AXCoreObject*)baseUpdateBackingStore { #if PLATFORM(MAC) auto* backingObject = self.updateObjectBackingStore; if (!backingObject) return nullptr; #else if (![self _prepareAccessibilityCall]) return nullptr; auto* backingObject = self.axBackingObject; #endif return backingObject; } - (NSArray *)lineRectsAndText { auto backingObject = self.baseUpdateBackingStore; if (!backingObject) return nil; auto range = backingObject->elementRange(); if (!range) return nil; Vector>> lines; auto start = VisiblePosition { makeContainerOffsetPosition(range->start) }; auto rangeEnd = VisiblePosition { makeContainerOffsetPosition(range->end) }; while (!start.isNull() && start <= rangeEnd) { auto end = backingObject->nextLineEndPosition(start); if (end <= start) break; auto rect = backingObject->boundsForVisiblePositionRange({start, end}); auto lineRange = makeSimpleRange(start, end); if (!lineRange) break; NSArray *content = [self contentForSimpleRange:*lineRange attributed:YES]; auto text = adoptNS([[NSMutableAttributedString alloc] init]); for (id item in content) { if ([item isKindOfClass:NSAttributedString.class]) [text appendAttributedString:item]; else if ([item isKindOfClass:WebAccessibilityObjectWrapper.class]) { #if PLATFORM(MAC) auto *wrapper = static_cast(item); auto* object = wrapper.axBackingObject; if (!object) continue; NSString *label; switch (object->roleValue()) { case AccessibilityRole::StaticText: label = object->stringValue(); break; case AccessibilityRole::Image: { String name = object->titleAttributeValue(); if (name.isEmpty()) name = object->descriptionAttributeValue(); label = name; break; } default: label = nil; break; } #else NSString *label = static_cast(item).accessibilityLabel; #endif if (!label) continue; auto attributedLabel = adoptNS([[NSAttributedString alloc] initWithString:label]); [text appendAttributedString:attributedLabel.get()]; } } lines.append({rect, text}); start = end; // If start is at a hard breakline "\n", move to the beginning of the next line. while (isEndOfLine(start)) { end = start.next(); auto endOfLineRange = makeSimpleRange(start, end); if (!endOfLineRange) break; TextIterator it(*endOfLineRange); if (it.atEnd() || it.text().length() != 1 || it.text()[0] != '\n') break; start = end; } } if (lines.isEmpty()) return nil; return createNSArray(lines, [self] (const auto& line) { return @{ @"rect": [NSValue valueWithRect:[self convertRectToSpace:FloatRect(line.first) space:AccessibilityConversionSpace::Screen]], @"text": line.second.get() }; }).autorelease(); } - (NSString *)ariaLandmarkRoleDescription { return self.axBackingObject->ariaLandmarkRoleDescription(); } - (NSString *)accessibilityPlatformMathSubscriptKey { ASSERT_NOT_REACHED(); return nil; } - (NSString *)accessibilityPlatformMathSuperscriptKey { ASSERT_NOT_REACHED(); return nil; } - (NSArray *)accessibilityMathPostscriptPairs { AccessibilityObject::AccessibilityMathMultiscriptPairs pairs; self.axBackingObject->mathPostscripts(pairs); return convertMathPairsToNSArray(pairs, [self accessibilityPlatformMathSubscriptKey], [self accessibilityPlatformMathSuperscriptKey]); } - (NSArray *)accessibilityMathPrescriptPairs { AccessibilityObject::AccessibilityMathMultiscriptPairs pairs; self.axBackingObject->mathPrescripts(pairs); return convertMathPairsToNSArray(pairs, [self accessibilityPlatformMathSubscriptKey], [self accessibilityPlatformMathSuperscriptKey]); } - (NSDictionary *)baseAccessibilityResolvedEditingStyles { NSMutableDictionary *results = [NSMutableDictionary dictionary]; auto editingStyles = self.axBackingObject->resolvedEditingStyles(); for (String& key : editingStyles.keys()) { auto value = editingStyles.get(key); id result = WTF::switchOn(value, [] (String& typedValue) -> id { return (NSString *)typedValue; }, [] (bool& typedValue) -> id { return @(typedValue); }, [] (int& typedValue) -> id { return @(typedValue); }, [] (auto&) { return nil; } ); results[(NSString *)key] = result; } return results; } // This is set by DRT when it wants to listen for notifications. static BOOL accessibilityShouldRepostNotifications; + (void)accessibilitySetShouldRepostNotifications:(BOOL)repost { accessibilityShouldRepostNotifications = repost; #if PLATFORM(MAC) AXObjectCache::setShouldRepostNotificationsForTests(repost); #endif } - (void)accessibilityPostedNotification:(NSString *)notificationName { if (accessibilityShouldRepostNotifications) [self accessibilityPostedNotification:notificationName userInfo:nil]; } static bool isValueTypeSupported(id value) { return [value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[WebAccessibilityObjectWrapperBase class]]; } static NSArray *arrayRemovingNonSupportedTypes(NSArray *array) { ASSERT([array isKindOfClass:[NSArray class]]); auto mutableArray = adoptNS([array mutableCopy]); for (NSUInteger i = 0; i < [mutableArray count];) { id value = [mutableArray objectAtIndex:i]; if ([value isKindOfClass:[NSDictionary class]]) [mutableArray replaceObjectAtIndex:i withObject:dictionaryRemovingNonSupportedTypes(value)]; else if ([value isKindOfClass:[NSArray class]]) [mutableArray replaceObjectAtIndex:i withObject:arrayRemovingNonSupportedTypes(value)]; else if (!isValueTypeSupported(value)) { [mutableArray removeObjectAtIndex:i]; continue; } i++; } return mutableArray.autorelease(); } static NSDictionary *dictionaryRemovingNonSupportedTypes(NSDictionary *dictionary) { if (!dictionary) return nil; ASSERT([dictionary isKindOfClass:[NSDictionary class]]); auto mutableDictionary = adoptNS([dictionary mutableCopy]); for (NSString *key in dictionary) { id value = [dictionary objectForKey:key]; if ([value isKindOfClass:[NSDictionary class]]) [mutableDictionary setObject:dictionaryRemovingNonSupportedTypes(value) forKey:key]; else if ([value isKindOfClass:[NSArray class]]) [mutableDictionary setObject:arrayRemovingNonSupportedTypes(value) forKey:key]; else if (!isValueTypeSupported(value)) [mutableDictionary removeObjectForKey:key]; } return mutableDictionary.autorelease(); } - (void)accessibilityPostedNotification:(NSString *)notificationName userInfo:(NSDictionary *)userInfo { if (accessibilityShouldRepostNotifications) { ASSERT(notificationName); userInfo = dictionaryRemovingNonSupportedTypes(userInfo); NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:notificationName, @"notificationName", userInfo, @"userInfo", nil]; [[NSNotificationCenter defaultCenter] postNotificationName:@"AXDRTNotification" object:self userInfo:info]; } } - (NSString *)innerHTML { if (auto* backingObject = self.axBackingObject) return backingObject->innerHTML(); return nil; } - (NSString *)outerHTML { if (auto* backingObject = self.axBackingObject) return backingObject->outerHTML(); return nil; } #pragma mark Search helpers typedef HashMap AccessibilitySearchKeyMap; struct SearchKeyEntry { String key; AccessibilitySearchKey value; }; static AccessibilitySearchKeyMap* createAccessibilitySearchKeyMap() { const SearchKeyEntry searchKeys[] = { { NSAccessibilityAnyTypeSearchKey, AccessibilitySearchKey::AnyType }, { NSAccessibilityArticleSearchKey, AccessibilitySearchKey::Article }, { NSAccessibilityBlockquoteSameLevelSearchKey, AccessibilitySearchKey::BlockquoteSameLevel }, { NSAccessibilityBlockquoteSearchKey, AccessibilitySearchKey::Blockquote }, { NSAccessibilityBoldFontSearchKey, AccessibilitySearchKey::BoldFont }, { NSAccessibilityButtonSearchKey, AccessibilitySearchKey::Button }, { NSAccessibilityCheckBoxSearchKey, AccessibilitySearchKey::CheckBox }, { NSAccessibilityControlSearchKey, AccessibilitySearchKey::Control }, { NSAccessibilityDifferentTypeSearchKey, AccessibilitySearchKey::DifferentType }, { NSAccessibilityFontChangeSearchKey, AccessibilitySearchKey::FontChange }, { NSAccessibilityFontColorChangeSearchKey, AccessibilitySearchKey::FontColorChange }, { NSAccessibilityFrameSearchKey, AccessibilitySearchKey::Frame }, { NSAccessibilityGraphicSearchKey, AccessibilitySearchKey::Graphic }, { NSAccessibilityHeadingLevel1SearchKey, AccessibilitySearchKey::HeadingLevel1 }, { NSAccessibilityHeadingLevel2SearchKey, AccessibilitySearchKey::HeadingLevel2 }, { NSAccessibilityHeadingLevel3SearchKey, AccessibilitySearchKey::HeadingLevel3 }, { NSAccessibilityHeadingLevel4SearchKey, AccessibilitySearchKey::HeadingLevel4 }, { NSAccessibilityHeadingLevel5SearchKey, AccessibilitySearchKey::HeadingLevel5 }, { NSAccessibilityHeadingLevel6SearchKey, AccessibilitySearchKey::HeadingLevel6 }, { NSAccessibilityHeadingSameLevelSearchKey, AccessibilitySearchKey::HeadingSameLevel }, { NSAccessibilityHeadingSearchKey, AccessibilitySearchKey::Heading }, { NSAccessibilityHighlightedSearchKey, AccessibilitySearchKey::Highlighted }, { NSAccessibilityKeyboardFocusableSearchKey, AccessibilitySearchKey::KeyboardFocusable }, { NSAccessibilityItalicFontSearchKey, AccessibilitySearchKey::ItalicFont }, { NSAccessibilityLandmarkSearchKey, AccessibilitySearchKey::Landmark }, { NSAccessibilityLinkSearchKey, AccessibilitySearchKey::Link }, { NSAccessibilityListSearchKey, AccessibilitySearchKey::List }, { NSAccessibilityLiveRegionSearchKey, AccessibilitySearchKey::LiveRegion }, { NSAccessibilityMisspelledWordSearchKey, AccessibilitySearchKey::MisspelledWord }, { NSAccessibilityOutlineSearchKey, AccessibilitySearchKey::Outline }, { NSAccessibilityPlainTextSearchKey, AccessibilitySearchKey::PlainText }, { NSAccessibilityRadioGroupSearchKey, AccessibilitySearchKey::RadioGroup }, { NSAccessibilitySameTypeSearchKey, AccessibilitySearchKey::SameType }, { NSAccessibilityStaticTextSearchKey, AccessibilitySearchKey::StaticText }, { NSAccessibilityStyleChangeSearchKey, AccessibilitySearchKey::StyleChange }, { NSAccessibilityTableSameLevelSearchKey, AccessibilitySearchKey::TableSameLevel }, { NSAccessibilityTableSearchKey, AccessibilitySearchKey::Table }, { NSAccessibilityTextFieldSearchKey, AccessibilitySearchKey::TextField }, { NSAccessibilityUnderlineSearchKey, AccessibilitySearchKey::Underline }, { NSAccessibilityUnvisitedLinkSearchKey, AccessibilitySearchKey::UnvisitedLink }, { NSAccessibilityVisitedLinkSearchKey, AccessibilitySearchKey::VisitedLink } }; AccessibilitySearchKeyMap* searchKeyMap = new AccessibilitySearchKeyMap; for (size_t i = 0; i < WTF_ARRAY_LENGTH(searchKeys); i++) searchKeyMap->set(searchKeys[i].key, searchKeys[i].value); return searchKeyMap; } static AccessibilitySearchKey accessibilitySearchKeyForString(const String& value) { if (value.isEmpty()) return AccessibilitySearchKey::AnyType; static const AccessibilitySearchKeyMap* searchKeyMap = createAccessibilitySearchKeyMap(); AccessibilitySearchKey searchKey = searchKeyMap->get(value); return static_cast(searchKey) ? searchKey : AccessibilitySearchKey::AnyType; } static std::optional makeVectorElement(const AccessibilitySearchKey*, id arrayElement) { if (![arrayElement isKindOfClass:NSString.class]) return std::nullopt; return { { accessibilitySearchKeyForString(arrayElement) } }; } AccessibilitySearchCriteria accessibilitySearchCriteriaForSearchPredicateParameterizedAttribute(const NSDictionary *parameterizedAttribute) { NSString *directionParameter = [parameterizedAttribute objectForKey:@"AXDirection"]; NSNumber *immediateDescendantsOnlyParameter = [parameterizedAttribute objectForKey:NSAccessibilityImmediateDescendantsOnly]; NSNumber *resultsLimitParameter = [parameterizedAttribute objectForKey:@"AXResultsLimit"]; NSString *searchTextParameter = [parameterizedAttribute objectForKey:@"AXSearchText"]; WebAccessibilityObjectWrapperBase *startElementParameter = [parameterizedAttribute objectForKey:@"AXStartElement"]; NSNumber *visibleOnlyParameter = [parameterizedAttribute objectForKey:@"AXVisibleOnly"]; id searchKeyParameter = [parameterizedAttribute objectForKey:@"AXSearchKey"]; AccessibilitySearchDirection direction = AccessibilitySearchDirection::Next; if ([directionParameter isKindOfClass:[NSString class]]) direction = [directionParameter isEqualToString:@"AXDirectionNext"] ? AccessibilitySearchDirection::Next : AccessibilitySearchDirection::Previous; bool immediateDescendantsOnly = false; if ([immediateDescendantsOnlyParameter isKindOfClass:[NSNumber class]]) immediateDescendantsOnly = [immediateDescendantsOnlyParameter boolValue]; unsigned resultsLimit = 0; if ([resultsLimitParameter isKindOfClass:[NSNumber class]]) resultsLimit = [resultsLimitParameter unsignedIntValue]; String searchText; if ([searchTextParameter isKindOfClass:[NSString class]]) searchText = searchTextParameter; AXCoreObject* startElement = nullptr; if ([startElementParameter isKindOfClass:[WebAccessibilityObjectWrapperBase class]]) startElement = [startElementParameter axBackingObject]; bool visibleOnly = false; if ([visibleOnlyParameter isKindOfClass:[NSNumber class]]) visibleOnly = [visibleOnlyParameter boolValue]; AccessibilitySearchCriteria criteria = AccessibilitySearchCriteria(startElement, direction, searchText, resultsLimit, visibleOnly, immediateDescendantsOnly); if ([searchKeyParameter isKindOfClass:[NSString class]]) criteria.searchKeys.append(accessibilitySearchKeyForString(searchKeyParameter)); else if ([searchKeyParameter isKindOfClass:[NSArray class]]) criteria.searchKeys = makeVector(searchKeyParameter); return criteria; } @end #endif // ENABLE(ACCESSIBILITY)