nsTextEquivUtils.cpp (17626B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:expandtab:shiftwidth=2:tabstop=2: 3 */ 4 /* This Source Code Form is subject to the terms of the Mozilla Public 5 * License, v. 2.0. If a copy of the MPL was not distributed with this 6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 7 8 #include "nsTextEquivUtils.h" 9 10 #include "LocalAccessible-inl.h" 11 #include "AccIterator.h" 12 #include "CssAltContent.h" 13 #include "nsCoreUtils.h" 14 #include "Relation.h" 15 #include "mozilla/dom/ChildIterator.h" 16 #include "mozilla/dom/Text.h" 17 18 using namespace mozilla; 19 using namespace mozilla::a11y; 20 21 /** 22 * The accessible for which we are computing a text equivalent. It is useful 23 * for bailing out during recursive text computation, or for special cases 24 * like the "Embedded Control" section of the AccName spec. 25 */ 26 static const Accessible* sInitiatorAcc = nullptr; 27 28 /* 29 * Track whether we're in an aria-describedby or aria-labelledby traversal. The 30 * browser should only follow those IDREFs, per the "LabelledBy" section of the 31 * AccName spec, "if [...] the current node is not already part of an ongoing 32 * aria-labelledby or aria-describedby traversal [...]" 33 */ 34 static bool sInAriaRelationTraversal = false; 35 36 /* 37 * Track the accessibles that we've consulted so far while computing the text 38 * alternative for an accessible. Per the Name From Content section of the Acc 39 * Name spec, "[e]ach node in the subtree is consulted only once." 40 */ 41 static nsTHashSet<const Accessible*>& GetReferencedAccs() { 42 static nsTHashSet<const Accessible*> sReferencedAccs; 43 return sReferencedAccs; 44 } 45 46 //////////////////////////////////////////////////////////////////////////////// 47 // nsTextEquivUtils. Public. 48 49 bool nsTextEquivUtils::HasNameRule(const Accessible* aAccessible, 50 ETextEquivRule aRule) { 51 uint32_t rule = GetRoleRule(aAccessible->Role()); 52 if (aAccessible->IsHTMLTableRow() && !aAccessible->HasStrongARIARole()) { 53 // For table row accessibles, we only want to calculate the name from the 54 // sub tree if an ARIA role is present. 55 rule = eNameFromSubtreeIfReqRule; 56 } 57 58 return (rule & aRule) == aRule; 59 } 60 61 nsresult nsTextEquivUtils::GetNameFromSubtree(const Accessible* aAccessible, 62 nsAString& aName) { 63 aName.Truncate(); 64 65 if (GetReferencedAccs().Contains(aAccessible)) { 66 return NS_OK; 67 } 68 69 // Remember the initiating accessible so we know when we've returned to it. 70 if (GetReferencedAccs().IsEmpty()) { 71 sInitiatorAcc = aAccessible; 72 } 73 GetReferencedAccs().Insert(aAccessible); 74 75 Relation customActions(aAccessible->RelationByType(RelationType::ACTION)); 76 while (Accessible* target = customActions.Next()) { 77 // aria-action targets are excluded from name calculation, so consider any 78 // of these targets as "referenced" for our purposes. 79 GetReferencedAccs().Insert(target); 80 } 81 82 if (HasNameRule(aAccessible, eNameFromSubtreeRule)) { 83 nsAutoString name; 84 AppendFromAccessibleChildren(aAccessible, &name); 85 name.CompressWhitespace(); 86 if (!nsCoreUtils::IsWhitespaceString(name)) aName = name; 87 } 88 89 // Once the text alternative computation is complete (i.e., once we've 90 // returned to the initiator acc), clear out the referenced accessibles and 91 // reset the initiator acc. 92 if (aAccessible == sInitiatorAcc) { 93 GetReferencedAccs().Clear(); 94 sInitiatorAcc = nullptr; 95 } 96 97 return NS_OK; 98 } 99 100 void nsTextEquivUtils::GetTextEquivFromAccIterable( 101 const Accessible* aAccessible, AccIterable* aIter, nsAString& aTextEquiv) { 102 if (GetReferencedAccs().Contains(aAccessible)) { 103 // We got here when trying to resolve a dependant label or description, 104 // early out. 105 return; 106 } 107 // Remember the initiating accessible so we know when we've returned to it. 108 if (GetReferencedAccs().IsEmpty()) { 109 sInitiatorAcc = aAccessible; 110 } 111 // This method doesn't allow self-referencing accessibles, so add the 112 // initiator to the referenced accs hash. 113 GetReferencedAccs().Insert(aAccessible); 114 115 aTextEquiv.Truncate(); 116 117 while (Accessible* acc = aIter->Next()) { 118 if (!aTextEquiv.IsEmpty()) aTextEquiv += ' '; 119 120 if (GetReferencedAccs().Contains(acc)) { 121 continue; 122 } 123 GetReferencedAccs().Insert(acc); 124 125 AppendFromAccessible(acc, &aTextEquiv); 126 } 127 128 if (aAccessible == sInitiatorAcc) { 129 // This is the top-level initiator, clear the hash. 130 GetReferencedAccs().Clear(); 131 sInitiatorAcc = nullptr; 132 } else { 133 // This is not the top-level initiator, just remove the calling accessible. 134 GetReferencedAccs().Remove(aAccessible); 135 } 136 } 137 138 bool nsTextEquivUtils::GetTextEquivFromIDRefs( 139 const LocalAccessible* aAccessible, nsAtom* aIDRefsAttr, 140 nsAString& aTextEquiv) { 141 bool usedHiddenOrSelf = false; 142 // If this is an aria-labelledby or aria-describedby traversal and we're 143 // already in such a traversal, or if we've already consulted the given 144 // accessible, early out. 145 const bool isAriaTraversal = aIDRefsAttr == nsGkAtoms::aria_labelledby || 146 aIDRefsAttr == nsGkAtoms::aria_describedby; 147 if ((sInAriaRelationTraversal && isAriaTraversal) || 148 GetReferencedAccs().Contains(aAccessible)) { 149 return usedHiddenOrSelf; 150 } 151 152 aTextEquiv.Truncate(); 153 154 nsIContent* content = aAccessible->GetContent(); 155 if (!content) { 156 return usedHiddenOrSelf; 157 } 158 159 nsIContent* refContent = nullptr; 160 AssociatedElementsIterator iter(aAccessible->Document(), content, 161 aIDRefsAttr); 162 while ((refContent = iter.NextElem())) { 163 if (!aTextEquiv.IsEmpty()) aTextEquiv += ' '; 164 165 // Note that we're in an aria-labelledby or aria-describedby traversal. 166 if (isAriaTraversal) { 167 sInAriaRelationTraversal = true; 168 } 169 170 // Reset the aria-labelledby / aria-describedby traversal tracking when we 171 // exit. Reset on scope exit because NS_ENSURE_SUCCESS may return. 172 auto onExit = MakeScopeExit([isAriaTraversal]() { 173 if (isAriaTraversal) { 174 sInAriaRelationTraversal = false; 175 } 176 }); 177 usedHiddenOrSelf |= 178 AppendTextEquivFromContent(aAccessible, refContent, &aTextEquiv); 179 } 180 181 return usedHiddenOrSelf; 182 } 183 184 bool nsTextEquivUtils::AppendTextEquivFromContent( 185 const LocalAccessible* aInitiatorAcc, nsIContent* aContent, 186 nsAString* aString) { 187 // Prevent recursion which can cause infinite loops. 188 LocalAccessible* accessible = 189 aInitiatorAcc->Document()->GetAccessible(aContent); 190 bool usedHiddenOrSelf = aInitiatorAcc == accessible; 191 if (GetReferencedAccs().Contains(aInitiatorAcc) || 192 GetReferencedAccs().Contains(accessible)) { 193 return usedHiddenOrSelf; 194 } 195 196 // Remember the initiating accessible so we know when we've returned to it. 197 if (GetReferencedAccs().IsEmpty()) { 198 sInitiatorAcc = aInitiatorAcc; 199 } 200 GetReferencedAccs().Insert(aInitiatorAcc); 201 202 if (accessible) { 203 AppendFromAccessible(accessible, aString); 204 GetReferencedAccs().Insert(accessible); 205 } else { 206 // The given content is invisible or otherwise inaccessible, so use the DOM 207 // subtree. 208 AppendFromDOMNode(aContent, aString); 209 usedHiddenOrSelf = true; 210 } 211 212 // Once the text alternative computation is complete (i.e., once we've 213 // returned to the initiator acc), clear out the referenced accessibles and 214 // reset the initiator acc. 215 if (aInitiatorAcc == sInitiatorAcc) { 216 GetReferencedAccs().Clear(); 217 sInitiatorAcc = nullptr; 218 } 219 220 return usedHiddenOrSelf; 221 } 222 223 nsresult nsTextEquivUtils::AppendTextEquivFromTextContent(nsIContent* aContent, 224 nsAString* aString) { 225 if (auto cssAlt = CssAltContent(aContent)) { 226 AccType type = aContent->GetPrimaryFrame() 227 ? aContent->GetPrimaryFrame()->AccessibleType() 228 : AccType::eNoType; 229 if (type == AccType::eNoType || type == AccType::eTextLeafType) { 230 // If this is a text leaf, or an empty content, append its alt text here. 231 // In the case of image alt contents, we will get to those with the 232 // accessible based subtree name calculation. 233 cssAlt.AppendToString(*aString); 234 return NS_OK; 235 } 236 } else if (aContent->IsText()) { 237 if (aContent->TextLength() > 0) { 238 nsIFrame* frame = aContent->GetPrimaryFrame(); 239 if (frame) { 240 nsIFrame::RenderedText text = frame->GetRenderedText( 241 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText, 242 nsIFrame::TrailingWhitespace::DontTrim); 243 aString->Append(text.mString); 244 } else { 245 // If aContent is an object that is display: none, we have no a frame. 246 aContent->GetAsText()->AppendTextTo(*aString); 247 } 248 } 249 250 return NS_OK; 251 } 252 253 if (aContent->IsHTMLElement() && 254 aContent->NodeInfo()->Equals(nsGkAtoms::br)) { 255 aString->AppendLiteral("\r\n"); 256 return NS_OK; 257 } 258 259 return NS_OK_NO_NAME_CLAUSE_HANDLED; 260 } 261 262 nsresult nsTextEquivUtils::AppendFromDOMChildren(nsIContent* aContent, 263 nsAString* aString) { 264 auto iter = 265 dom::AllChildrenIterator(aContent, nsIContent::eAllChildren, true); 266 while (nsIContent* childContent = iter.GetNextChild()) { 267 nsresult rv = AppendFromDOMNode(childContent, aString); 268 NS_ENSURE_SUCCESS(rv, rv); 269 } 270 271 return NS_OK; 272 } 273 274 //////////////////////////////////////////////////////////////////////////////// 275 // nsTextEquivUtils. Private. 276 277 nsresult nsTextEquivUtils::AppendFromAccessibleChildren( 278 const Accessible* aAccessible, nsAString* aString) { 279 nsresult rv = NS_OK_NO_NAME_CLAUSE_HANDLED; 280 281 uint32_t childCount = aAccessible->ChildCount(); 282 for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { 283 Accessible* child = aAccessible->ChildAt(childIdx); 284 // If we've already consulted this child, don't consult it again. 285 if (GetReferencedAccs().Contains(child)) { 286 continue; 287 } 288 rv = AppendFromAccessible(child, aString); 289 NS_ENSURE_SUCCESS(rv, rv); 290 } 291 292 return rv; 293 } 294 295 nsresult nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible, 296 nsAString* aString) { 297 // XXX: is it necessary to care the accessible is not a document? 298 bool isHTMLBlock = false; 299 if (aAccessible->IsLocal() && aAccessible->AsLocal()->IsContent()) { 300 nsIContent* content = aAccessible->AsLocal()->GetContent(); 301 nsresult rv = AppendTextEquivFromTextContent(content, aString); 302 if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) return rv; 303 if (!content->IsText()) { 304 nsIFrame* frame = content->GetPrimaryFrame(); 305 if (frame) { 306 // If this is a block level frame (as opposed to span level), we need to 307 // add spaces around that block's text, so we don't get words jammed 308 // together in final name. 309 const nsStyleDisplay* display = frame->StyleDisplay(); 310 if (display->IsBlockOutsideStyle() || 311 display->mDisplay == StyleDisplay::InlineBlock || 312 display->mDisplay == StyleDisplay::TableCell) { 313 isHTMLBlock = true; 314 if (!aString->IsEmpty()) { 315 aString->Append(char16_t(' ')); 316 } 317 } 318 } 319 } 320 } else if (aAccessible->IsRemote()) { 321 if (aAccessible->IsText()) { 322 // Leafs should have their text appended with no spacing. 323 nsAutoString name; 324 aAccessible->Name(name); 325 aString->Append(name); 326 return NS_OK; 327 } 328 if (RefPtr<nsAtom>(aAccessible->DisplayStyle()) == nsGkAtoms::block || 329 aAccessible->IsHTMLListItem() || aAccessible->IsTableRow() || 330 aAccessible->IsTableCell()) { 331 // Similar to local case above, we need to add spaces around block level 332 // accessibles. 333 isHTMLBlock = true; 334 if (!aString->IsEmpty()) { 335 aString->Append(char16_t(' ')); 336 } 337 } 338 } 339 340 bool isEmptyTextEquiv = true; 341 342 // Attempt to find the value. If it's non-empty, append and return it. See the 343 // "Embedded Control" section of the name spec. 344 nsAutoString val; 345 nsresult rv = AppendFromValue(aAccessible, &val); 346 NS_ENSURE_SUCCESS(rv, rv); 347 if (rv == NS_OK) { 348 AppendString(aString, val); 349 return NS_OK; 350 } 351 352 // If the name is from tooltip, we retrieve it now but only append it to the 353 // result string later as a last resort. Otherwise, we append the name now. 354 nsAutoString text; 355 if (aAccessible->Name(text) != eNameFromTooltip) { 356 isEmptyTextEquiv = !AppendString(aString, text); 357 } 358 359 // Implementation of the "Name From Content" step of the text alternative 360 // computation guide. Traverse the accessible's subtree if allowed. 361 if (isEmptyTextEquiv) { 362 if (ShouldIncludeInSubtreeCalculation(aAccessible)) { 363 rv = AppendFromAccessibleChildren(aAccessible, aString); 364 NS_ENSURE_SUCCESS(rv, rv); 365 366 if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) isEmptyTextEquiv = false; 367 } 368 } 369 370 // Implementation of the "Tooltip" step 371 if (isEmptyTextEquiv && !text.IsEmpty()) { 372 AppendString(aString, text); 373 if (isHTMLBlock) { 374 aString->Append(char16_t(' ')); 375 } 376 return NS_OK; 377 } 378 379 if (!isEmptyTextEquiv && isHTMLBlock) { 380 aString->Append(char16_t(' ')); 381 } 382 return rv; 383 } 384 385 nsresult nsTextEquivUtils::AppendFromValue(Accessible* aAccessible, 386 nsAString* aString) { 387 if (!HasNameRule(aAccessible, eNameFromValueRule)) { 388 return NS_OK_NO_NAME_CLAUSE_HANDLED; 389 } 390 391 // Implementation of the "Embedded Control" step of the text alternative 392 // computation. If the given accessible is not the root accessible (the 393 // accessible the text alternative is computed for in the end) then append the 394 // accessible value. 395 if (aAccessible == sInitiatorAcc) { 396 return NS_OK_NO_NAME_CLAUSE_HANDLED; 397 } 398 399 // For listboxes in non-initiator computations, we need to get the selected 400 // item and append its text alternative. 401 nsAutoString text; 402 if (aAccessible->IsListControl()) { 403 Accessible* selected = aAccessible->GetSelectedItem(0); 404 if (selected) { 405 nsresult rv = AppendFromAccessible(selected, &text); 406 NS_ENSURE_SUCCESS(rv, rv); 407 return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED; 408 } 409 return NS_ERROR_FAILURE; 410 } 411 412 // For other accessibles, get the value directly. 413 aAccessible->Value(text); 414 415 return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED; 416 } 417 418 nsresult nsTextEquivUtils::AppendFromDOMNode(nsIContent* aContent, 419 nsAString* aString) { 420 nsresult rv = AppendTextEquivFromTextContent(aContent, aString); 421 NS_ENSURE_SUCCESS(rv, rv); 422 423 if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) return NS_OK; 424 425 if (aContent->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::style)) { 426 // The text within these elements is never meant for users. 427 return NS_OK; 428 } 429 430 if (aContent->IsXULElement()) { 431 nsAutoString textEquivalent; 432 if (aContent->NodeInfo()->Equals(nsGkAtoms::label, kNameSpaceID_XUL)) { 433 aContent->AsElement()->GetAttr(nsGkAtoms::value, textEquivalent); 434 } else { 435 aContent->AsElement()->GetAttr(nsGkAtoms::label, textEquivalent); 436 } 437 438 if (textEquivalent.IsEmpty()) { 439 aContent->AsElement()->GetAttr(nsGkAtoms::tooltiptext, textEquivalent); 440 } 441 442 AppendString(aString, textEquivalent); 443 } 444 445 return AppendFromDOMChildren(aContent, aString); 446 } 447 448 bool nsTextEquivUtils::AppendString(nsAString* aString, 449 const nsAString& aTextEquivalent) { 450 if (aTextEquivalent.IsEmpty()) return false; 451 452 // Insert spaces to insure that words from controls aren't jammed together. 453 if (!aString->IsEmpty() && !nsCoreUtils::IsWhitespace(aString->Last())) { 454 aString->Append(char16_t(' ')); 455 } 456 457 aString->Append(aTextEquivalent); 458 459 if (!nsCoreUtils::IsWhitespace(aString->Last())) { 460 aString->Append(char16_t(' ')); 461 } 462 463 return true; 464 } 465 466 uint32_t nsTextEquivUtils::GetRoleRule(role aRole) { 467 #define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ 468 msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \ 469 nameRule) \ 470 case roles::geckoRole: \ 471 return nameRule; 472 473 switch (aRole) { 474 #include "RoleMap.h" 475 default: 476 MOZ_CRASH("Unknown role."); 477 } 478 479 #undef ROLE 480 } 481 482 bool nsTextEquivUtils::ShouldIncludeInSubtreeCalculation( 483 Accessible* aAccessible) { 484 if (HasNameRule(aAccessible, eNameFromSubtreeRule)) { 485 return true; 486 } 487 488 if (!HasNameRule(aAccessible, eNameFromSubtreeIfReqRule)) { 489 return false; 490 } 491 492 if (aAccessible == sInitiatorAcc) { 493 // We're calculating the text equivalent for this accessible, but this 494 // accessible should only be included when calculating the text equivalent 495 // for something else. 496 return false; 497 } 498 499 // sInitiatorAcc can be null when, for example, LocalAccessible::Value calls 500 // GetTextEquivFromSubtree. 501 role initiatorRole = sInitiatorAcc ? sInitiatorAcc->Role() : roles::NOTHING; 502 if (initiatorRole == roles::OUTLINEITEM && 503 aAccessible->Role() == roles::GROUPING) { 504 // Child treeitems are contained in a group. We don't want to include those 505 // in the parent treeitem's text equivalent. 506 return false; 507 } 508 509 return true; 510 } 511 512 bool nsTextEquivUtils::IsWhitespaceLeaf(Accessible* aAccessible) { 513 if (!aAccessible || !aAccessible->IsTextLeaf()) { 514 return false; 515 } 516 517 nsAutoString name; 518 aAccessible->Name(name); 519 return nsCoreUtils::IsWhitespaceString(name); 520 }