tor-browser

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

UiaTextRange.cpp (48148B)


      1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "UiaTextRange.h"
      8 
      9 #include "mozilla/a11y/HyperTextAccessibleBase.h"
     10 #include "nsAccUtils.h"
     11 #include "nsIAccessibleTypes.h"
     12 #include "TextLeafRange.h"
     13 #include <comdef.h>
     14 #include <propvarutil.h>
     15 #include <unordered_set>
     16 
     17 namespace mozilla::a11y {
     18 
     19 template <typename T>
     20 HRESULT GetAttribute(TEXTATTRIBUTEID aAttributeId, T const& aRangeOrPoint,
     21                     VARIANT& aRetVal);
     22 
     23 static int CompareVariants(const VARIANT& aFirst, const VARIANT& aSecond) {
     24  // MinGW lacks support for VariantCompare, but does support converting to
     25  // PROPVARIANT and PropVariantCompareEx. Use this as a workaround for MinGW
     26  // builds, but avoid the extra work otherwise. See Bug 1944732.
     27 #if defined(__MINGW32__) || defined(__MINGW64__) || defined(__MINGW__)
     28  PROPVARIANT firstPropVar;
     29  PROPVARIANT secondPropVar;
     30  VariantToPropVariant(&aFirst, &firstPropVar);
     31  VariantToPropVariant(&aSecond, &secondPropVar);
     32  return PropVariantCompareEx(firstPropVar, secondPropVar, PVCU_DEFAULT,
     33                              PVCHF_DEFAULT);
     34 #else
     35  return VariantCompare(aFirst, aSecond);
     36 #endif
     37 }
     38 
     39 // Used internally to safely get a UiaTextRange from a COM pointer provided
     40 // to us by a client.
     41 // {74B8E664-4578-4B52-9CBC-30A7A8271AE8}
     42 static const GUID IID_UiaTextRange = {
     43    0x74b8e664,
     44    0x4578,
     45    0x4b52,
     46    {0x9c, 0xbc, 0x30, 0xa7, 0xa8, 0x27, 0x1a, 0xe8}};
     47 
     48 // Helpers
     49 
     50 static TextLeafPoint GetEndpoint(TextLeafRange& aRange,
     51                                 enum TextPatternRangeEndpoint aEndpoint) {
     52  if (aEndpoint == TextPatternRangeEndpoint_Start) {
     53    return aRange.Start();
     54  }
     55  return aRange.End();
     56 }
     57 
     58 static void RemoveExcludedAccessiblesFromRange(TextLeafRange& aRange) {
     59  MOZ_ASSERT(aRange);
     60  TextLeafPoint start = aRange.Start();
     61  TextLeafPoint end = aRange.End();
     62  if (start == end) {
     63    // The range is collapsed. It doesn't include anything.
     64    return;
     65  }
     66  if (end.mOffset != 0) {
     67    // It is theoretically possible for start to be at the exclusive end of a
     68    // previous Accessible (i.e. mOffset is its length), so the range doesn't
     69    // really encompass that Accessible's text and we should thus exclude that
     70    // Accessible. However, that hasn't been seen in practice yet. If it does
     71    // occur and cause problems, we should adjust the start point here.
     72    return;
     73  }
     74  // end is at the start of its Accessible. This can happen because we always
     75  // search for the start of a character, word, etc. Since the end of a range
     76  // is exclusive, the range doesn't include anything in this Accessible.
     77  // Move the end back so that it doesn't touch this Accessible at all. This
     78  // is important when determining what Accessibles lie within this range
     79  // because otherwise, we'd incorrectly consider an Accessible which the range
     80  // doesn't actually cover.
     81  // Move to the previous character.
     82  end = end.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
     83  // We want the position immediately after this character in the same
     84  // Accessible.
     85  ++end.mOffset;
     86  if (start <= end) {
     87    aRange.SetEnd(end);
     88  }
     89 }
     90 
     91 static bool IsUiaEmbeddedObject(const Accessible* aAcc) {
     92  // "For UI Automation, an embedded object is any element that has non-textual
     93  // boundaries such as an image, hyperlink, table, or document type"
     94  // https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-textpattern-and-embedded-objects-overview
     95  if (aAcc->IsText()) {
     96    return false;
     97  }
     98  switch (aAcc->Role()) {
     99    case roles::CONTENT_DELETION:
    100    case roles::CONTENT_INSERTION:
    101    case roles::EMPHASIS:
    102    case roles::LANDMARK:
    103    case roles::MARK:
    104    case roles::NAVIGATION:
    105    case roles::NOTE:
    106    case roles::PARAGRAPH:
    107    case roles::REGION:
    108    case roles::SECTION:
    109    case roles::STRONG:
    110    case roles::SUBSCRIPT:
    111    case roles::SUPERSCRIPT:
    112    case roles::TEXT:
    113    case roles::TEXT_CONTAINER:
    114      return false;
    115    default:
    116      break;
    117  }
    118  return true;
    119 }
    120 
    121 static NotNull<Accessible*> GetSelectionContainer(TextLeafRange& aRange) {
    122  Accessible* acc = aRange.Start().mAcc;
    123  MOZ_ASSERT(acc);
    124  if (acc->IsTextLeaf()) {
    125    if (Accessible* parent = acc->Parent()) {
    126      acc = parent;
    127    }
    128  }
    129  if (acc->IsTextField()) {
    130    // Gecko uses an independent selection for <input> and <textarea>.
    131    return WrapNotNull(acc);
    132  }
    133  // For everything else (including contentEditable), Gecko uses the document
    134  // selection.
    135  return WrapNotNull(nsAccUtils::DocumentFor(acc));
    136 }
    137 
    138 static TextLeafPoint NormalizePoint(Accessible* aAcc, int32_t aOffset) {
    139  if (!aAcc) {
    140    return TextLeafPoint(aAcc, aOffset);
    141  }
    142  int32_t length = static_cast<int32_t>(nsAccUtils::TextLength(aAcc));
    143  if (aOffset > length) {
    144    // This range was created when this leaf contained more characters, but some
    145    // characters were since removed. Restrict to the new length.
    146    aOffset = length;
    147  }
    148  return TextLeafPoint(aAcc, aOffset);
    149 }
    150 
    151 // UiaTextRange
    152 
    153 UiaTextRange::UiaTextRange(const TextLeafRange& aRange) {
    154  MOZ_ASSERT(aRange);
    155  SetRange(aRange);
    156 }
    157 
    158 void UiaTextRange::SetRange(const TextLeafRange& aRange) {
    159  TextLeafPoint start = aRange.Start();
    160  mStartAcc = MsaaAccessible::GetFrom(start.mAcc);
    161  MOZ_ASSERT(mStartAcc);
    162  mStartOffset = start.mOffset;
    163  TextLeafPoint end = aRange.End();
    164  mEndAcc = MsaaAccessible::GetFrom(end.mAcc);
    165  MOZ_ASSERT(mEndAcc);
    166  mEndOffset = end.mOffset;
    167  // Special handling of the insertion point at the end of a line only makes
    168  // sense when dealing with the caret, which is a collapsed range.
    169  mIsEndOfLineInsertionPoint = start == end && start.mIsEndOfLineInsertionPoint;
    170 }
    171 
    172 TextLeafRange UiaTextRange::GetRange() const {
    173  // Either Accessible might have been shut down because it was removed from the
    174  // tree. In that case, Acc() will return null, resulting in an invalid
    175  // TextLeafPoint and thus an invalid TextLeafRange. Any caller is expected to
    176  // handle this case.
    177  if (mIsEndOfLineInsertionPoint) {
    178    MOZ_ASSERT(mStartAcc == mEndAcc && mStartOffset == mEndOffset);
    179    TextLeafPoint point = NormalizePoint(mStartAcc->Acc(), mStartOffset);
    180    point.mIsEndOfLineInsertionPoint = true;
    181    return TextLeafRange(point, point);
    182  }
    183  return TextLeafRange(NormalizePoint(mStartAcc->Acc(), mStartOffset),
    184                       NormalizePoint(mEndAcc->Acc(), mEndOffset));
    185 }
    186 
    187 /* static */
    188 TextLeafRange UiaTextRange::GetRangeFrom(ITextRangeProvider* aProvider) {
    189  if (aProvider) {
    190    RefPtr<UiaTextRange> uiaRange;
    191    aProvider->QueryInterface(IID_UiaTextRange, getter_AddRefs(uiaRange));
    192    if (uiaRange) {
    193      return uiaRange->GetRange();
    194    }
    195  }
    196  return TextLeafRange();
    197 }
    198 
    199 /* static */
    200 TextLeafPoint UiaTextRange::FindBoundary(const TextLeafPoint& aOrigin,
    201                                         enum TextUnit aUnit,
    202                                         nsDirection aDirection,
    203                                         bool aIncludeOrigin) {
    204  if (aUnit == TextUnit_Page || aUnit == TextUnit_Document) {
    205    // The UIA documentation is a little inconsistent regarding the Document
    206    // unit:
    207    // https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-textpattern-and-embedded-objects-overview
    208    // First, it says:
    209    // "Objects backed by the same text store as their container are referred to
    210    // as "compatible" embedded objects. These objects can be TextPattern
    211    // objects themselves and, in this case, their text ranges are comparable to
    212    // text ranges obtained from their container. This enables the providers to
    213    // expose client information about the individual TextPattern objects as if
    214    // they were one, large text provider."
    215    // But later, it says:
    216    // "For embedded TextPattern objects, the Document unit only recognizes the
    217    // content contained within that element."
    218    // If ranges are equivalent regardless of what object they were created
    219    // from, this doesn't make sense because this would mean that the Document
    220    // unit would change depending on where the range was positioned at the
    221    // time. Instead, Gecko restricts the range to an editable text control for
    222    // ITextProvider::get_DocumentRange, but returns the full document for
    223    // TextUnit_Document. This is consistent with Microsoft Word and Chromium.
    224    Accessible* doc = nsAccUtils::DocumentFor(aOrigin.mAcc);
    225    if (aDirection == eDirPrevious) {
    226      return TextLeafPoint(doc, 0);
    227    }
    228    return TextLeafPoint(doc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
    229  }
    230  if (aUnit == TextUnit_Format) {
    231    // The UIA documentation says that TextUnit_Format aims to define ranges
    232    // that "include all text that shares all the same attributes."
    233    // FindTextAttrsStart considers container boundaries to be format boundaries
    234    // even if UIA may not. UIA's documentation may consider the next container
    235    // to be part of the same format run, since it may have the same attributes.
    236    // UIA considers embedded objects to be format boundaries, which is a more
    237    // restrictive understanding of boundaries than what Gecko implements here.
    238    return aOrigin.FindTextAttrsStart(aDirection, aIncludeOrigin);
    239  }
    240  AccessibleTextBoundary boundary;
    241  switch (aUnit) {
    242    case TextUnit_Character:
    243      boundary = nsIAccessibleText::BOUNDARY_CLUSTER;
    244      break;
    245    case TextUnit_Word:
    246      boundary = nsIAccessibleText::BOUNDARY_WORD_START;
    247      break;
    248    case TextUnit_Line:
    249      boundary = nsIAccessibleText::BOUNDARY_LINE_START;
    250      break;
    251    case TextUnit_Paragraph:
    252      boundary = nsIAccessibleText::BOUNDARY_PARAGRAPH;
    253      break;
    254    default:
    255      return TextLeafPoint();
    256  }
    257  return aOrigin.FindBoundary(
    258      boundary, aDirection,
    259      aIncludeOrigin ? TextLeafPoint::BoundaryFlags::eIncludeOrigin
    260                     : TextLeafPoint::BoundaryFlags::eDefaultBoundaryFlags);
    261 }
    262 
    263 bool UiaTextRange::MovePoint(TextLeafPoint& aPoint, enum TextUnit aUnit,
    264                             const int aRequestedCount, int& aActualCount) {
    265  aActualCount = 0;
    266  const nsDirection direction = aRequestedCount < 0 ? eDirPrevious : eDirNext;
    267  while (aActualCount != aRequestedCount) {
    268    TextLeafPoint oldPoint = aPoint;
    269    aPoint = FindBoundary(aPoint, aUnit, direction);
    270    if (!aPoint) {
    271      return false;  // Unit not supported.
    272    }
    273    if (aPoint == oldPoint) {
    274      break;  // Can't go any further.
    275    }
    276    direction == eDirPrevious ? --aActualCount : ++aActualCount;
    277  }
    278  return true;
    279 }
    280 
    281 void UiaTextRange::SetEndpoint(enum TextPatternRangeEndpoint aEndpoint,
    282                               const TextLeafPoint& aDest) {
    283  // Per the UIA documentation:
    284  // https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-moveendpointbyrange#remarks
    285  // https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-moveendpointbyunit#remarks
    286  // "If the endpoint being moved crosses the other endpoint of the same text
    287  // range, that other endpoint is moved also, resulting in a degenerate (empty)
    288  // range and ensuring the correct ordering of the endpoints (that is, the
    289  // start is always less than or equal to the end)."
    290  TextLeafRange origRange = GetRange();
    291  MOZ_ASSERT(origRange);
    292  if (aEndpoint == TextPatternRangeEndpoint_Start) {
    293    TextLeafPoint end = origRange.End();
    294    if (end < aDest) {
    295      end = aDest;
    296    }
    297    SetRange({aDest, end});
    298  } else {
    299    TextLeafPoint start = origRange.Start();
    300    if (aDest < start) {
    301      start = aDest;
    302    }
    303    SetRange({start, aDest});
    304  }
    305 }
    306 
    307 // IUnknown
    308 IMPL_IUNKNOWN2(UiaTextRange, ITextRangeProvider, UiaTextRange)
    309 
    310 // ITextRangeProvider methods
    311 
    312 STDMETHODIMP
    313 UiaTextRange::Clone(__RPC__deref_out_opt ITextRangeProvider** aRetVal) {
    314  if (!aRetVal) {
    315    return E_INVALIDARG;
    316  }
    317  TextLeafRange range = GetRange();
    318  if (!range) {
    319    return CO_E_OBJNOTCONNECTED;
    320  }
    321  RefPtr uiaRange = new UiaTextRange(range);
    322  uiaRange.forget(aRetVal);
    323  return S_OK;
    324 }
    325 
    326 STDMETHODIMP
    327 UiaTextRange::Compare(__RPC__in_opt ITextRangeProvider* aRange,
    328                      __RPC__out BOOL* aRetVal) {
    329  if (!aRange || !aRetVal) {
    330    return E_INVALIDARG;
    331  }
    332  *aRetVal = GetRange() == GetRangeFrom(aRange);
    333  return S_OK;
    334 }
    335 
    336 STDMETHODIMP
    337 UiaTextRange::CompareEndpoints(enum TextPatternRangeEndpoint aEndpoint,
    338                               __RPC__in_opt ITextRangeProvider* aTargetRange,
    339                               enum TextPatternRangeEndpoint aTargetEndpoint,
    340                               __RPC__out int* aRetVal) {
    341  if (!aTargetRange || !aRetVal) {
    342    return E_INVALIDARG;
    343  }
    344  TextLeafRange origRange = GetRange();
    345  if (!origRange) {
    346    return CO_E_OBJNOTCONNECTED;
    347  }
    348  TextLeafPoint origPoint = GetEndpoint(origRange, aEndpoint);
    349  TextLeafRange targetRange = GetRangeFrom(aTargetRange);
    350  if (!targetRange) {
    351    return E_INVALIDARG;
    352  }
    353  TextLeafPoint targetPoint = GetEndpoint(targetRange, aTargetEndpoint);
    354  if (origPoint == targetPoint) {
    355    *aRetVal = 0;
    356  } else if (origPoint < targetPoint) {
    357    *aRetVal = -1;
    358  } else {
    359    *aRetVal = 1;
    360  }
    361  return S_OK;
    362 }
    363 
    364 STDMETHODIMP
    365 UiaTextRange::ExpandToEnclosingUnit(enum TextUnit aUnit) {
    366  TextLeafRange range = GetRange();
    367  if (!range) {
    368    return CO_E_OBJNOTCONNECTED;
    369  }
    370  TextLeafPoint origin = range.Start();
    371  TextLeafPoint start = FindBoundary(origin, aUnit, eDirPrevious,
    372                                     /* aIncludeOrigin */ true);
    373  if (!start) {
    374    return E_FAIL;  // Unit not supported.
    375  }
    376  TextLeafPoint end = FindBoundary(origin, aUnit, eDirNext);
    377  SetRange({start, end});
    378  return S_OK;
    379 }
    380 
    381 // Search within the text range for the first subrange that has the given
    382 // attribute value. The resulting range might span multiple text attribute runs.
    383 // If aBackward, start the search from the end of the range.
    384 STDMETHODIMP
    385 UiaTextRange::FindAttribute(TEXTATTRIBUTEID aAttributeId, VARIANT aVal,
    386                            BOOL aBackward,
    387                            __RPC__deref_out_opt ITextRangeProvider** aRetVal) {
    388  if (!aRetVal) {
    389    return E_INVALIDARG;
    390  }
    391  *aRetVal = nullptr;
    392  TextLeafRange range = GetRange();
    393  if (!range) {
    394    return CO_E_OBJNOTCONNECTED;
    395  }
    396  MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed.");
    397 
    398  VARIANT value{};
    399 
    400  if (!aBackward) {
    401    Maybe<TextLeafPoint> matchingRangeStart{};
    402    // Begin with a range starting at the start of our original range and ending
    403    // at the next attribute run start point.
    404    TextLeafPoint startPoint = range.Start();
    405    TextLeafPoint endPoint = startPoint;
    406    endPoint = endPoint.FindTextAttrsStart(eDirNext);
    407    do {
    408      // Get the attribute value at the start point. Since we're moving through
    409      // text attribute runs, we don't need to check the entire range; this
    410      // point's attributes are those of the entire range.
    411      GetAttribute(aAttributeId, startPoint, value);
    412      //  CompareVariants is not valid if types are different. Verify the type
    413      //  first so the result is well-defined.
    414      if (aVal.vt == value.vt && CompareVariants(aVal, value) == 0) {
    415        if (!matchingRangeStart) {
    416          matchingRangeStart = Some(startPoint);
    417        }
    418      } else if (matchingRangeStart) {
    419        // We fell out of a matching range. We're moving forward, so the
    420        // matching range is [matchingRangeStart, startPoint).
    421        RefPtr uiaRange = new UiaTextRange(
    422            TextLeafRange{matchingRangeStart.value(), startPoint});
    423        uiaRange.forget(aRetVal);
    424        return S_OK;
    425      }
    426      startPoint = endPoint;
    427      // Advance only if startPoint != endPoint to avoid infinite loops if
    428      // FindTextAttrsStart returns the TextLeafPoint unchanged. This covers
    429      // cases like hitting the end of the document.
    430    } while ((endPoint = endPoint.FindTextAttrsStart(eDirNext)) &&
    431             endPoint <= range.End() && startPoint != endPoint);
    432    if (matchingRangeStart) {
    433      // We found a start point and reached the end of the range. The result is
    434      // [matchingRangeStart, stopPoint].
    435      RefPtr uiaRange = new UiaTextRange(
    436          TextLeafRange{matchingRangeStart.value(), range.End()});
    437      uiaRange.forget(aRetVal);
    438      return S_OK;
    439    }
    440  } else {
    441    Maybe<TextLeafPoint> matchingRangeEnd{};
    442    TextLeafPoint endPoint = range.End();
    443    TextLeafPoint startPoint = endPoint;
    444    startPoint = startPoint.FindTextAttrsStart(eDirPrevious);
    445    do {
    446      GetAttribute(aAttributeId, startPoint, value);
    447      if (aVal.vt == value.vt && CompareVariants(aVal, value) == 0) {
    448        if (!matchingRangeEnd) {
    449          matchingRangeEnd = Some(endPoint);
    450        }
    451      } else if (matchingRangeEnd) {
    452        // We fell out of a matching range. We're moving backward, so the
    453        // matching range is [endPoint, matchingRangeEnd).
    454        RefPtr uiaRange =
    455            new UiaTextRange(TextLeafRange{endPoint, matchingRangeEnd.value()});
    456        uiaRange.forget(aRetVal);
    457        return S_OK;
    458      }
    459      endPoint = startPoint;
    460      // Advance only if startPoint != endPoint to avoid infinite loops if
    461      // FindTextAttrsStart returns the TextLeafPoint unchanged. This covers
    462      // cases like hitting the start of the document.
    463    } while ((startPoint = startPoint.FindTextAttrsStart(eDirPrevious)) &&
    464             range.Start() <= startPoint && startPoint != endPoint);
    465    if (matchingRangeEnd) {
    466      // We found an end point and reached the start of the range. The result is
    467      // [range.Start(), matchingRangeEnd).
    468      RefPtr uiaRange = new UiaTextRange(
    469          TextLeafRange{range.Start(), matchingRangeEnd.value()});
    470      uiaRange.forget(aRetVal);
    471      return S_OK;
    472    }
    473  }
    474  return S_OK;
    475 }
    476 
    477 STDMETHODIMP
    478 UiaTextRange::FindText(__RPC__in BSTR aText, BOOL aBackward, BOOL aIgnoreCase,
    479                       __RPC__deref_out_opt ITextRangeProvider** aRetVal) {
    480  if (!aRetVal) {
    481    return E_INVALIDARG;
    482  }
    483  *aRetVal = nullptr;
    484  TextLeafRange range = GetRange();
    485  if (!range) {
    486    return CO_E_OBJNOTCONNECTED;
    487  }
    488  const TextLeafPoint origStart = range.Start();
    489  MOZ_ASSERT(origStart <= range.End(), "Range must be valid to proceed.");
    490 
    491  // We can't find anything in an empty range.
    492  if (origStart == range.End()) {
    493    return S_OK;
    494  }
    495 
    496  // Iterate over the range's leaf segments and append each leaf's text. Keep
    497  // track of the indices in the built string, associating them with the
    498  // Accessible pointer whose text begins at that index.
    499  nsTArray<std::pair<int32_t, Accessible*>> indexToAcc;
    500  nsAutoString rangeText;
    501  for (const TextLeafRange leafSegment : range) {
    502    const TextLeafPoint segmentStart = leafSegment.Start();
    503    Accessible* startAcc = segmentStart.mAcc;
    504    MOZ_ASSERT(startAcc, "Start acc of leaf segment was unexpectedly null.");
    505    indexToAcc.EmplaceBack(rangeText.Length(), startAcc);
    506    startAcc->AppendTextTo(rangeText, segmentStart.mOffset,
    507                           leafSegment.End().mOffset - segmentStart.mOffset);
    508  }
    509 
    510  // Find the search string's start position in the text of the range, ignoring
    511  // case if requested.
    512  const nsDependentString searchStr{aText};
    513  const int32_t startIndex = [&]() {
    514    if (aIgnoreCase) {
    515      ToLowerCase(rangeText);
    516      nsAutoString searchStrLower;
    517      ToLowerCase(searchStr, searchStrLower);
    518      return aBackward ? rangeText.RFind(searchStrLower)
    519                       : rangeText.Find(searchStrLower);
    520    } else {
    521      return aBackward ? rangeText.RFind(searchStr) : rangeText.Find(searchStr);
    522    }
    523  }();
    524  if (startIndex == kNotFound) {
    525    return S_OK;
    526  }
    527  const int32_t endIndex = startIndex + searchStr.Length();
    528 
    529  // Binary search for the (index, Accessible*) pair where the index is as large
    530  // as possible without exceeding the size of the search index. The associated
    531  // Accessible* is the Accessible for the resulting TextLeafPoint.
    532  auto GetNearestAccLessThanIndex = [&indexToAcc](int32_t aIndex) {
    533    MOZ_ASSERT(aIndex >= 0, "Search index is less than 0.");
    534    auto itr =
    535        std::lower_bound(indexToAcc.begin(), indexToAcc.end(), aIndex,
    536                         [](const std::pair<int32_t, Accessible*>& aPair,
    537                            int32_t aIndex) { return aPair.first <= aIndex; });
    538    MOZ_ASSERT(itr != indexToAcc.begin(),
    539               "Iterator is unexpectedly at the beginning.");
    540    --itr;
    541    return itr;
    542  };
    543 
    544  // Get the start offset to use for a given Accessible containing our match.
    545  auto getStartOffsetForAcc = [origStart](Accessible* aAcc) {
    546    if (aAcc == origStart.mAcc) {
    547      // aAcc is in the same leaf in which the origin range starts. The origin
    548      // range might start in the middle of the leaf, in which case our gathered
    549      // text starts there too.
    550      return origStart.mOffset;
    551    }
    552    return 0;
    553  };
    554 
    555  // Calculate the TextLeafPoint for the start and end of the found text.
    556  auto itr = GetNearestAccLessThanIndex(startIndex);
    557  Accessible* foundTextStart = itr->second;
    558  const int32_t offsetFromStart =
    559      startIndex - itr->first + getStartOffsetForAcc(foundTextStart);
    560  const TextLeafPoint rangeStart{foundTextStart, offsetFromStart};
    561 
    562  itr = GetNearestAccLessThanIndex(endIndex);
    563  Accessible* foundTextEnd = itr->second;
    564  const int32_t offsetFromEndAccStart =
    565      endIndex - itr->first + getStartOffsetForAcc(foundTextEnd);
    566  const TextLeafPoint rangeEnd{foundTextEnd, offsetFromEndAccStart};
    567 
    568  TextLeafRange resultRange{rangeStart, rangeEnd};
    569  RefPtr uiaRange = new UiaTextRange(resultRange);
    570  uiaRange.forget(aRetVal);
    571  return S_OK;
    572 }
    573 
    574 template <TEXTATTRIBUTEID Attr>
    575 struct AttributeTraits {
    576  /*
    577   * To define a trait of this type, define the following members on a
    578   * specialization of this template struct:
    579   *   // Define the (Gecko) representation of the attribute type.
    580   *   using AttrType = <the type associated with the TEXTATTRIBUTEID>;
    581   *
    582   *   // Returns the attribute value at the TextLeafPoint, or Nothing{} if none
    583   *   // can be calculated.
    584   *   static Maybe<AttrType> GetValue(TextLeafPoint aPoint);
    585   *
    586   *   // Return the default value specified by the UIA documentation.
    587   *   static AttrType DefaultValue();
    588   *
    589   *   // Write the given value to the VARIANT output parameter. This may
    590   *   // require a non-trivial transformation from Gecko's idea of the value
    591   *   // into VARIANT form.
    592   *   static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue);
    593   */
    594 };
    595 
    596 template <TEXTATTRIBUTEID Attr>
    597 HRESULT GetAttribute(const TextLeafRange& aRange, VARIANT& aVariant) {
    598  // Select the traits of the given TEXTATTRIBUTEID. This helps us choose the
    599  // correct functions to call to handle each attribute.
    600  using Traits = AttributeTraits<Attr>;
    601  using AttrType = typename Traits::AttrType;
    602 
    603  // Get the value at the start point. All other runs in the range must match
    604  // this value, otherwise the result is "mixed."
    605  const TextLeafPoint end = aRange.End();
    606  TextLeafPoint current = aRange.Start();
    607  Maybe<AttrType> val = Traits::GetValue(current);
    608  if (!val) {
    609    // Fall back to the UIA-specified default when we don't have an answer.
    610    val = Some(Traits::DefaultValue());
    611  }
    612 
    613  // Walk through the range one text attribute run start at a time, poking the
    614  // start points to check for the requested attribute. Stop before we hit the
    615  // end since the end point is either:
    616  //   1. At the start of the one-past-last text attribute run and hence
    617  //      excluded from the range
    618  //   2. After the start of the last text attribute run in the range and hence
    619  //      tested by that last run's start point
    620  while ((current = current.FindTextAttrsStart(eDirNext)) && current < end) {
    621    Maybe<AttrType> currentVal = Traits::GetValue(current);
    622    if (!currentVal) {
    623      // Fall back to the UIA-specified default when we don't have an answer.
    624      currentVal = Some(Traits::DefaultValue());
    625    }
    626    if (*currentVal != *val) {
    627      // If the attribute ever changes, then we need to return "[t]he address
    628      // of the value retrieved by the UiaGetReservedMixedAttributeValue
    629      // function."
    630      aVariant.vt = VT_UNKNOWN;
    631      return UiaGetReservedMixedAttributeValue(&aVariant.punkVal);
    632    }
    633  }
    634 
    635  // Write the value to the VARIANT output parameter.
    636  return Traits::WriteToVariant(aVariant, *val);
    637 }
    638 
    639 template <TEXTATTRIBUTEID Attr>
    640 HRESULT GetAttribute(TextLeafPoint const& aPoint, VARIANT& aVariant) {
    641  // Select the traits of the given TEXTATTRIBUTEID. This helps us choose the
    642  // correct functions to call to handle each attribute.
    643  using Traits = AttributeTraits<Attr>;
    644  using AttrType = typename Traits::AttrType;
    645 
    646  // Get the value at the given point.
    647  Maybe<AttrType> val = Traits::GetValue(aPoint);
    648  if (!val) {
    649    // Fall back to the UIA-specified default when we don't have an answer.
    650    val = Some(Traits::DefaultValue());
    651  }
    652  // Write the value to the VARIANT output parameter.
    653  return Traits::WriteToVariant(aVariant, *val);
    654 }
    655 
    656 // Dispatch to the proper GetAttribute template specialization for the given
    657 // TEXTATTRIBUTEID. T may be a TextLeafPoint or TextLeafRange; this function
    658 // will call the appropriate specialization and overload.
    659 template <typename T>
    660 HRESULT GetAttribute(TEXTATTRIBUTEID aAttributeId, T const& aRangeOrPoint,
    661                     VARIANT& aRetVal) {
    662  switch (aAttributeId) {
    663    case UIA_AnnotationTypesAttributeId:
    664      return GetAttribute<UIA_AnnotationTypesAttributeId>(aRangeOrPoint,
    665                                                          aRetVal);
    666    case UIA_FontNameAttributeId:
    667      return GetAttribute<UIA_FontNameAttributeId>(aRangeOrPoint, aRetVal);
    668    case UIA_FontSizeAttributeId:
    669      return GetAttribute<UIA_FontSizeAttributeId>(aRangeOrPoint, aRetVal);
    670    case UIA_FontWeightAttributeId:
    671      return GetAttribute<UIA_FontWeightAttributeId>(aRangeOrPoint, aRetVal);
    672    case UIA_IsHiddenAttributeId:
    673      return GetAttribute<UIA_IsHiddenAttributeId>(aRangeOrPoint, aRetVal);
    674    case UIA_IsItalicAttributeId:
    675      return GetAttribute<UIA_IsItalicAttributeId>(aRangeOrPoint, aRetVal);
    676    case UIA_IsReadOnlyAttributeId:
    677      return GetAttribute<UIA_IsReadOnlyAttributeId>(aRangeOrPoint, aRetVal);
    678    case UIA_StyleIdAttributeId:
    679      return GetAttribute<UIA_StyleIdAttributeId>(aRangeOrPoint, aRetVal);
    680    case UIA_IsSubscriptAttributeId:
    681      return GetAttribute<UIA_IsSubscriptAttributeId>(aRangeOrPoint, aRetVal);
    682    case UIA_IsSuperscriptAttributeId:
    683      return GetAttribute<UIA_IsSuperscriptAttributeId>(aRangeOrPoint, aRetVal);
    684    default:
    685      // If the attribute isn't supported, return "[t]he address of the value
    686      // retrieved by the UiaGetReservedNotSupportedValue function."
    687      aRetVal.vt = VT_UNKNOWN;
    688      return UiaGetReservedNotSupportedValue(&aRetVal.punkVal);
    689      break;
    690  }
    691  MOZ_ASSERT_UNREACHABLE("Unhandled UIA Attribute ID");
    692  return S_OK;
    693 }
    694 
    695 STDMETHODIMP
    696 UiaTextRange::GetAttributeValue(TEXTATTRIBUTEID aAttributeId,
    697                                __RPC__out VARIANT* aRetVal) {
    698  if (!aRetVal) {
    699    return E_INVALIDARG;
    700  }
    701  VariantInit(aRetVal);
    702  TextLeafRange range = GetRange();
    703  if (!range) {
    704    return CO_E_OBJNOTCONNECTED;
    705  }
    706  MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed.");
    707  return GetAttribute(aAttributeId, range, *aRetVal);
    708 }
    709 
    710 STDMETHODIMP
    711 UiaTextRange::GetBoundingRectangles(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
    712  if (!aRetVal) {
    713    return E_INVALIDARG;
    714  }
    715  *aRetVal = nullptr;
    716  TextLeafRange range = GetRange();
    717  if (!range) {
    718    return CO_E_OBJNOTCONNECTED;
    719  }
    720 
    721  // Get the rectangles for each line.
    722  nsTArray<LayoutDeviceIntRect> lineRects = range.LineRects();
    723  // For UIA's purposes, the rectangles of this array are four doubles arranged
    724  // in order {left, top, width, height}.
    725  SAFEARRAY* rectsVec = SafeArrayCreateVector(VT_R8, 0, lineRects.Length() * 4);
    726  if (!rectsVec) {
    727    return E_OUTOFMEMORY;
    728  }
    729 
    730  // Empty range, return an empty array.
    731  if (lineRects.IsEmpty()) {
    732    *aRetVal = rectsVec;
    733    return S_OK;
    734  }
    735 
    736  // Get the double array out of the SAFEARRAY so we can write to it directly.
    737  double* safeArrayData = nullptr;
    738  HRESULT hr =
    739      SafeArrayAccessData(rectsVec, reinterpret_cast<void**>(&safeArrayData));
    740  if (FAILED(hr) || !safeArrayData) {
    741    SafeArrayDestroy(rectsVec);
    742    return E_FAIL;
    743  }
    744 
    745  // Convert the int array to a double array.
    746  for (size_t index = 0; index < lineRects.Length(); ++index) {
    747    const LayoutDeviceIntRect& lineRect = lineRects[index];
    748    safeArrayData[index * 4 + 0] = static_cast<double>(lineRect.x);
    749    safeArrayData[index * 4 + 1] = static_cast<double>(lineRect.y);
    750    safeArrayData[index * 4 + 2] = static_cast<double>(lineRect.width);
    751    safeArrayData[index * 4 + 3] = static_cast<double>(lineRect.height);
    752  }
    753 
    754  // Release the lock on the data. If that fails, bail out.
    755  hr = SafeArrayUnaccessData(rectsVec);
    756  if (FAILED(hr)) {
    757    SafeArrayDestroy(rectsVec);
    758    return E_FAIL;
    759  }
    760 
    761  *aRetVal = rectsVec;
    762  return S_OK;
    763 }
    764 
    765 STDMETHODIMP
    766 UiaTextRange::GetEnclosingElement(
    767    __RPC__deref_out_opt IRawElementProviderSimple** aRetVal) {
    768  if (!aRetVal) {
    769    return E_INVALIDARG;
    770  }
    771  *aRetVal = nullptr;
    772  TextLeafRange range = GetRange();
    773  if (!range) {
    774    return CO_E_OBJNOTCONNECTED;
    775  }
    776  RemoveExcludedAccessiblesFromRange(range);
    777  Accessible* enclosing =
    778      range.Start().mAcc->GetClosestCommonInclusiveAncestor(range.End().mAcc);
    779  if (!enclosing) {
    780    return S_OK;
    781  }
    782  for (Accessible* acc = enclosing; acc && !acc->IsDoc(); acc = acc->Parent()) {
    783    if (nsAccUtils::MustPrune(acc) ||
    784        // Bug 1950535: Narrator won't report a link correctly when navigating
    785        // by character or word if we return a child text leaf. However, if
    786        // there is more than a single text leaf, we need to return the child
    787        // because it might have semantic significance; e.g. an embedded image.
    788        (acc->Role() == roles::LINK && acc->ChildCount() == 1 &&
    789         acc->FirstChild()->IsText())) {
    790      enclosing = acc;
    791      break;
    792    }
    793  }
    794  RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(enclosing);
    795  uia.forget(aRetVal);
    796  return S_OK;
    797 }
    798 
    799 STDMETHODIMP
    800 UiaTextRange::GetText(int aMaxLength, __RPC__deref_out_opt BSTR* aRetVal) {
    801  if (!aRetVal || aMaxLength < -1) {
    802    return E_INVALIDARG;
    803  }
    804  TextLeafRange range = GetRange();
    805  if (!range) {
    806    return CO_E_OBJNOTCONNECTED;
    807  }
    808  nsAutoString text;
    809  for (TextLeafRange segment : range) {
    810    TextLeafPoint start = segment.Start();
    811    int segmentLength = segment.End().mOffset - start.mOffset;
    812    // aMaxLength can be -1 to indicate no maximum.
    813    if (aMaxLength >= 0) {
    814      const int remaining = aMaxLength - text.Length();
    815      if (segmentLength > remaining) {
    816        segmentLength = remaining;
    817      }
    818    }
    819    start.mAcc->AppendTextTo(text, start.mOffset, segmentLength);
    820    if (aMaxLength >= 0 && static_cast<int32_t>(text.Length()) >= aMaxLength) {
    821      break;
    822    }
    823  }
    824  *aRetVal = ::SysAllocString(text.get());
    825  return S_OK;
    826 }
    827 
    828 STDMETHODIMP
    829 UiaTextRange::Move(enum TextUnit aUnit, int aCount, __RPC__out int* aRetVal) {
    830  if (!aRetVal) {
    831    return E_INVALIDARG;
    832  }
    833  TextLeafRange range = GetRange();
    834  if (!range) {
    835    return CO_E_OBJNOTCONNECTED;
    836  }
    837  TextLeafPoint start = range.Start();
    838  const bool wasCollapsed = start == range.End();
    839  if (!wasCollapsed) {
    840    // Per the UIA documentation:
    841    // https://learn.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-move#remarks
    842    // "For a non-degenerate (non-empty) text range, ITextRangeProvider::Move
    843    // should normalize and move the text range by performing the following
    844    // steps. ...
    845    // 2. If necessary, move the resulting text range backward in the document
    846    // to the beginning of the requested unit boundary."
    847    start = FindBoundary(start, aUnit, eDirPrevious, /* aIncludeOrigin */ true);
    848  }
    849  if (!MovePoint(start, aUnit, aCount, *aRetVal)) {
    850    return E_FAIL;
    851  }
    852  if (wasCollapsed) {
    853    // "For a degenerate text range, ITextRangeProvider::Move should simply move
    854    // the text insertion point by the specified number of text units."
    855    SetRange({start, start});
    856  } else {
    857    // "4. Expand the text range from the degenerate state by moving the ending
    858    // endpoint forward by one requested text unit boundary."
    859    TextLeafPoint end = FindBoundary(start, aUnit, eDirNext);
    860    if (end == start) {
    861      // start was already at the last boundary. Move start back to the previous
    862      // boundary.
    863      start = FindBoundary(start, aUnit, eDirPrevious);
    864      // In doing that, we ended up moving 1 less unit.
    865      --*aRetVal;
    866    }
    867    SetRange({start, end});
    868  }
    869  return S_OK;
    870 }
    871 
    872 STDMETHODIMP
    873 UiaTextRange::MoveEndpointByUnit(enum TextPatternRangeEndpoint aEndpoint,
    874                                 enum TextUnit aUnit, int aCount,
    875                                 __RPC__out int* aRetVal) {
    876  if (!aRetVal) {
    877    return E_INVALIDARG;
    878  }
    879  TextLeafRange range = GetRange();
    880  if (!range) {
    881    return CO_E_OBJNOTCONNECTED;
    882  }
    883  TextLeafPoint point = GetEndpoint(range, aEndpoint);
    884  if (!MovePoint(point, aUnit, aCount, *aRetVal)) {
    885    return E_FAIL;
    886  }
    887  SetEndpoint(aEndpoint, point);
    888  return S_OK;
    889 }
    890 
    891 STDMETHODIMP
    892 UiaTextRange::MoveEndpointByRange(
    893    enum TextPatternRangeEndpoint aEndpoint,
    894    __RPC__in_opt ITextRangeProvider* aTargetRange,
    895    enum TextPatternRangeEndpoint aTargetEndpoint) {
    896  if (!aTargetRange) {
    897    return E_INVALIDARG;
    898  }
    899  TextLeafRange origRange = GetRange();
    900  if (!origRange) {
    901    return CO_E_OBJNOTCONNECTED;
    902  }
    903  TextLeafRange targetRange = GetRangeFrom(aTargetRange);
    904  if (!targetRange) {
    905    return E_INVALIDARG;
    906  }
    907  TextLeafPoint dest = GetEndpoint(targetRange, aTargetEndpoint);
    908  SetEndpoint(aEndpoint, dest);
    909  return S_OK;
    910 }
    911 
    912 // XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
    913 MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP UiaTextRange::Select() {
    914  TextLeafRange range = GetRange();
    915  if (!range) {
    916    return CO_E_OBJNOTCONNECTED;
    917  }
    918  if (!range.SetSelection(TextLeafRange::kRemoveAllExistingSelectedRanges,
    919                          /* aSetFocus */ false)) {
    920    return UIA_E_INVALIDOPERATION;
    921  }
    922  return S_OK;
    923 }
    924 
    925 // XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
    926 MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP UiaTextRange::AddToSelection() {
    927  TextLeafRange range = GetRange();
    928  if (!range) {
    929    return CO_E_OBJNOTCONNECTED;
    930  }
    931  if (!range.SetSelection(-1, /* aSetFocus */ false)) {
    932    return UIA_E_INVALIDOPERATION;
    933  }
    934  return S_OK;
    935 }
    936 
    937 // XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
    938 MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP UiaTextRange::RemoveFromSelection() {
    939  TextLeafRange range = GetRange();
    940  if (!range) {
    941    return CO_E_OBJNOTCONNECTED;
    942  }
    943  NotNull<Accessible*> container = GetSelectionContainer(range);
    944  nsTArray<TextLeafRange> ranges;
    945  TextLeafRange::GetSelection(container, ranges);
    946  auto index = ranges.IndexOf(range);
    947  if (index != ranges.NoIndex) {
    948    HyperTextAccessibleBase* conHyp = container->AsHyperTextBase();
    949    MOZ_ASSERT(conHyp);
    950    conHyp->RemoveFromSelection(index);
    951    return S_OK;
    952  }
    953  // This range isn't in the collection of selected ranges.
    954  return UIA_E_INVALIDOPERATION;
    955 }
    956 
    957 // XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
    958 MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP
    959 UiaTextRange::ScrollIntoView(BOOL aAlignToTop) {
    960  TextLeafRange range = GetRange();
    961  if (!range) {
    962    return CO_E_OBJNOTCONNECTED;
    963  }
    964  range.ScrollIntoView(aAlignToTop
    965                           ? nsIAccessibleScrollType::SCROLL_TYPE_TOP_LEFT
    966                           : nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_RIGHT);
    967  return S_OK;
    968 }
    969 
    970 STDMETHODIMP
    971 UiaTextRange::GetChildren(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
    972  if (!aRetVal) {
    973    return E_INVALIDARG;
    974  }
    975  *aRetVal = nullptr;
    976  TextLeafRange range = GetRange();
    977  if (!range) {
    978    return CO_E_OBJNOTCONNECTED;
    979  }
    980  RemoveExcludedAccessiblesFromRange(range);
    981  Accessible* startAcc = range.Start().mAcc;
    982  Accessible* endAcc = range.End().mAcc;
    983  Accessible* common = startAcc->GetClosestCommonInclusiveAncestor(endAcc);
    984  if (!common) {
    985    return S_OK;
    986  }
    987  // Get all the direct children of `common` from `startAcc` through `endAcc`.
    988  // Find the index of the direct child containing startAcc.
    989  int32_t startIndex = -1;
    990  if (startAcc == common) {
    991    startIndex = 0;
    992  } else {
    993    Accessible* child = startAcc;
    994    for (;;) {
    995      Accessible* parent = child->Parent();
    996      if (parent == common) {
    997        startIndex = child->IndexInParent();
    998        break;
    999      }
   1000      child = parent;
   1001    }
   1002    MOZ_ASSERT(startIndex >= 0);
   1003  }
   1004  // Find the index of the direct child containing endAcc.
   1005  int32_t endIndex = -1;
   1006  if (endAcc == common) {
   1007    endIndex = static_cast<int32_t>(common->ChildCount()) - 1;
   1008  } else {
   1009    Accessible* child = endAcc;
   1010    for (;;) {
   1011      Accessible* parent = child->Parent();
   1012      if (parent == common) {
   1013        endIndex = child->IndexInParent();
   1014        break;
   1015      }
   1016      child = parent;
   1017    }
   1018    MOZ_ASSERT(endIndex >= 0);
   1019  }
   1020  // Now get the children between startIndex and endIndex.
   1021  // We guess 30 children because:
   1022  // 1. It's unlikely that a client would call GetChildren on a very large range
   1023  // because GetChildren is normally only called when reporting content and
   1024  // reporting the entire content of a massive range in one hit isn't ideal for
   1025  // performance.
   1026  // 2. A client is more likely to query the content of a line, paragraph, etc.
   1027  // 3. It seems unlikely that there would be more than 30 children in a line or
   1028  // paragraph, especially because we're only including children that are
   1029  // considered embedded objects by UIA.
   1030  AutoTArray<Accessible*, 30> children;
   1031  for (int32_t i = startIndex; i <= endIndex; ++i) {
   1032    Accessible* child = common->ChildAt(static_cast<uint32_t>(i));
   1033    if (IsUiaEmbeddedObject(child)) {
   1034      children.AppendElement(child);
   1035    }
   1036  }
   1037  *aRetVal = AccessibleArrayToUiaArray(children);
   1038  return S_OK;
   1039 }
   1040 
   1041 /*
   1042 * AttributeTraits template specializations
   1043 */
   1044 
   1045 template <>
   1046 struct AttributeTraits<UIA_AnnotationTypesAttributeId> {
   1047  // Avoiding nsTHashSet here because it has no operator==.
   1048  using AttrType = std::unordered_set<int32_t>;
   1049  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1050    // Check all of the given annotations. Build a set of the annotations that
   1051    // are present at the given TextLeafPoint.
   1052    RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
   1053    if (!attrs) {
   1054      return {};
   1055    }
   1056    AttrType annotationsAtPoint{};
   1057 
   1058    // The "invalid" atom as a key in text attributes could have value
   1059    // "spelling", "grammar", or "true". Spelling and grammar map directly to
   1060    // UIA. A non-specific "invalid" indicates a generic data validation error,
   1061    // and is mapped as such.
   1062    if (auto invalid =
   1063            attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::invalid)) {
   1064      const nsAtom* invalidAtom = invalid->get();
   1065      if (invalidAtom == nsGkAtoms::spelling) {
   1066        annotationsAtPoint.insert(AnnotationType_SpellingError);
   1067      } else if (invalidAtom == nsGkAtoms::grammar) {
   1068        annotationsAtPoint.insert(AnnotationType_GrammarError);
   1069      } else if (invalidAtom == nsGkAtoms::_true) {
   1070        annotationsAtPoint.insert(AnnotationType_DataValidationError);
   1071      }
   1072    }
   1073 
   1074    // The presence of the "mark" atom as a key in text attributes indicates a
   1075    // highlight at this point.
   1076    if (attrs->GetAttribute<bool>(nsGkAtoms::mark)) {
   1077      annotationsAtPoint.insert(AnnotationType_Highlighted);
   1078    }
   1079 
   1080    return Some(annotationsAtPoint);
   1081  }
   1082 
   1083  static AttrType DefaultValue() {
   1084    // Per UIA documentation, the default is an empty collection.
   1085    return {};
   1086  }
   1087 
   1088  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1089    SAFEARRAY* outputArr =
   1090        SafeArrayCreateVector(VT_I4, 0, static_cast<ULONG>(aValue.size()));
   1091    if (!outputArr) {
   1092      return E_OUTOFMEMORY;
   1093    }
   1094 
   1095    // Copy the elements from the unordered_set to the SAFEARRAY.
   1096    LONG index = 0;
   1097    for (auto value : aValue) {
   1098      const HRESULT hr = SafeArrayPutElement(outputArr, &index, &value);
   1099      if (FAILED(hr)) {
   1100        SafeArrayDestroy(outputArr);
   1101        return hr;
   1102      }
   1103      ++index;
   1104    }
   1105 
   1106    aVariant.vt = VT_ARRAY | VT_I4;
   1107    aVariant.parray = outputArr;
   1108    return S_OK;
   1109  }
   1110 };
   1111 
   1112 template <>
   1113 struct AttributeTraits<UIA_FontWeightAttributeId> {
   1114  using AttrType = int32_t;  // LONG, but AccAttributes only accepts int32_t
   1115  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1116    RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
   1117    if (!attrs) {
   1118      return {};
   1119    }
   1120    return attrs->GetAttribute<AttrType>(nsGkAtoms::font_weight);
   1121  }
   1122 
   1123  static AttrType DefaultValue() {
   1124    // See GDI LOGFONT structure and related standards.
   1125    return FW_DONTCARE;
   1126  }
   1127 
   1128  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1129    aVariant.vt = VT_I4;
   1130    aVariant.lVal = aValue;
   1131    return S_OK;
   1132  }
   1133 };
   1134 
   1135 template <>
   1136 struct AttributeTraits<UIA_FontSizeAttributeId> {
   1137  using AttrType = FontSize;
   1138  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1139    RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
   1140    if (!attrs) {
   1141      return {};
   1142    }
   1143    return attrs->GetAttribute<AttrType>(nsGkAtoms::font_size);
   1144  }
   1145 
   1146  static AttrType DefaultValue() { return FontSize{0}; }
   1147 
   1148  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1149    aVariant.vt = VT_I4;
   1150    aVariant.lVal = aValue.mValue;
   1151    return S_OK;
   1152  }
   1153 };
   1154 
   1155 template <>
   1156 struct AttributeTraits<UIA_FontNameAttributeId> {
   1157  using AttrType = RefPtr<nsAtom>;
   1158  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1159    RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
   1160    if (!attrs) {
   1161      return {};
   1162    }
   1163    return attrs->GetAttribute<AttrType>(nsGkAtoms::font_family);
   1164  }
   1165 
   1166  static AttrType DefaultValue() {
   1167    // Default to the empty string (not null).
   1168    return RefPtr<nsAtom>(nsGkAtoms::_empty);
   1169  }
   1170 
   1171  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1172    if (!aValue) {
   1173      return E_INVALIDARG;
   1174    }
   1175    BSTR valueBStr = ::SysAllocString(aValue->GetUTF16String());
   1176    if (!valueBStr) {
   1177      return E_OUTOFMEMORY;
   1178    }
   1179    aVariant.vt = VT_BSTR;
   1180    aVariant.bstrVal = valueBStr;
   1181    return S_OK;
   1182  }
   1183 };
   1184 
   1185 template <>
   1186 struct AttributeTraits<UIA_IsItalicAttributeId> {
   1187  using AttrType = bool;
   1188  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1189    RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
   1190    if (!attrs) {
   1191      return {};
   1192    }
   1193 
   1194    // If the value in the attributes is a RefPtr<nsAtom>, it may be "italic" or
   1195    // "normal"; check whether it is "italic".
   1196    auto atomResult =
   1197        attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::font_style);
   1198    if (atomResult) {
   1199      MOZ_ASSERT(*atomResult, "Atom must be non-null");
   1200      return Some((*atomResult)->Equals(u"italic"_ns));
   1201    }
   1202    // If the FontSlantStyle is not italic, the value is not stored as an nsAtom
   1203    // in AccAttributes, so there's no need to check further.
   1204    return {};
   1205  }
   1206 
   1207  static AttrType DefaultValue() { return false; }
   1208 
   1209  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1210    aVariant = _variant_t(aValue);
   1211    return S_OK;
   1212  }
   1213 };
   1214 
   1215 template <>
   1216 struct AttributeTraits<UIA_StyleIdAttributeId> {
   1217  using AttrType = int32_t;
   1218  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1219    Accessible* acc = aPoint.mAcc;
   1220    if (!acc || !acc->Parent()) {
   1221      return {};
   1222    }
   1223    acc = acc->Parent();
   1224    const role role = acc->Role();
   1225    if (role == roles::HEADING) {
   1226      switch (acc->GetLevel(true)) {
   1227        case 1:
   1228          return Some(StyleId_Heading1);
   1229        case 2:
   1230          return Some(StyleId_Heading2);
   1231        case 3:
   1232          return Some(StyleId_Heading3);
   1233        case 4:
   1234          return Some(StyleId_Heading4);
   1235        case 5:
   1236          return Some(StyleId_Heading5);
   1237        case 6:
   1238          return Some(StyleId_Heading6);
   1239        default:
   1240          return {};
   1241      }
   1242    }
   1243    if (role == roles::BLOCKQUOTE) {
   1244      return Some(StyleId_Quote);
   1245    }
   1246    if (role == roles::EMPHASIS) {
   1247      return Some(StyleId_Emphasis);
   1248    }
   1249    return {};
   1250  }
   1251 
   1252  static AttrType DefaultValue() { return 0; }
   1253 
   1254  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1255    aVariant.vt = VT_I4;
   1256    aVariant.lVal = aValue;
   1257    return S_OK;
   1258  }
   1259 };
   1260 
   1261 template <>
   1262 struct AttributeTraits<UIA_IsSubscriptAttributeId> {
   1263  using AttrType = bool;
   1264  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1265    RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
   1266    if (!attrs) {
   1267      return {};
   1268    }
   1269    auto atomResult =
   1270        attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::textPosition);
   1271    if (atomResult) {
   1272      MOZ_ASSERT(*atomResult, "Atom must be non-null");
   1273      return Some(*atomResult == nsGkAtoms::sub);
   1274    }
   1275    return {};
   1276  }
   1277 
   1278  static AttrType DefaultValue() { return false; }
   1279 
   1280  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1281    aVariant = _variant_t(aValue);
   1282    return S_OK;
   1283  }
   1284 };
   1285 
   1286 template <>
   1287 struct AttributeTraits<UIA_IsSuperscriptAttributeId> {
   1288  using AttrType = bool;
   1289  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1290    RefPtr<AccAttributes> attrs = aPoint.GetTextAttributes();
   1291    if (!attrs) {
   1292      return {};
   1293    }
   1294    auto atomResult =
   1295        attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::textPosition);
   1296    if (atomResult) {
   1297      MOZ_ASSERT(*atomResult, "Atom must be non-null");
   1298      return Some((*atomResult)->Equals(NS_ConvertASCIItoUTF16("super")));
   1299    }
   1300    return {};
   1301  }
   1302 
   1303  static AttrType DefaultValue() { return false; }
   1304 
   1305  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1306    aVariant = _variant_t(aValue);
   1307    return S_OK;
   1308  }
   1309 };
   1310 
   1311 template <>
   1312 struct AttributeTraits<UIA_IsHiddenAttributeId> {
   1313  using AttrType = bool;
   1314  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1315    if (!aPoint.mAcc) {
   1316      return {};
   1317    }
   1318    const uint64_t state = aPoint.mAcc->State();
   1319    return Some(!!(state & states::INVISIBLE));
   1320  }
   1321 
   1322  static AttrType DefaultValue() { return false; }
   1323 
   1324  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1325    aVariant = _variant_t(aValue);
   1326    return S_OK;
   1327  }
   1328 };
   1329 
   1330 template <>
   1331 struct AttributeTraits<UIA_IsReadOnlyAttributeId> {
   1332  using AttrType = bool;
   1333  static Maybe<AttrType> GetValue(TextLeafPoint aPoint) {
   1334    if (!aPoint.mAcc) {
   1335      return {};
   1336    }
   1337    Accessible* acc = aPoint.mAcc;
   1338    // If the TextLeafPoint we're dealing with is itself a hypertext, don't
   1339    // bother checking its parent since this is the Accessible we care about.
   1340    if (!acc->IsHyperText()) {
   1341      // Check the parent of the leaf, since the leaf itself will never be
   1342      // editable, but the parent may. Check for both text fields and
   1343      // hypertexts, since we might have something like <input> or a
   1344      // contenteditable <span>.
   1345      Accessible* parent = acc->Parent();
   1346      if (parent && parent->IsHyperText()) {
   1347        acc = parent;
   1348      } else {
   1349        return Some(true);
   1350      }
   1351    }
   1352    const uint64_t state = acc->State();
   1353    if (state & states::READONLY) {
   1354      return Some(true);
   1355    }
   1356    if (state & states::EDITABLE) {
   1357      return Some(false);
   1358    }
   1359    // Fall back to true if not editable or explicitly marked READONLY.
   1360    return Some(true);
   1361  }
   1362 
   1363  static AttrType DefaultValue() {
   1364    // UIA says the default is false, but we fall back to true in GetValue since
   1365    // most things on the web are read-only.
   1366    return false;
   1367  }
   1368 
   1369  static HRESULT WriteToVariant(VARIANT& aVariant, const AttrType& aValue) {
   1370    aVariant = _variant_t(aValue);
   1371    return S_OK;
   1372  }
   1373 };
   1374 
   1375 }  // namespace mozilla::a11y