tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }