tor-browser

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

HyperTextAccessibleBase.cpp (31712B)


      1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 #include "HyperTextAccessibleBase.h"
      7 
      8 #include "mozilla/a11y/Accessible.h"
      9 #include "nsAccUtils.h"
     10 #include "TextLeafRange.h"
     11 #include "TextRange.h"
     12 
     13 namespace mozilla::a11y {
     14 
     15 int32_t HyperTextAccessibleBase::GetChildIndexAtOffset(uint32_t aOffset) const {
     16  auto& offsets =
     17      const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets();
     18  int32_t lastOffset = 0;
     19  const uint32_t offsetCount = offsets.Length();
     20 
     21  if (offsetCount > 0) {
     22    lastOffset = offsets[offsetCount - 1];
     23    if (static_cast<int32_t>(aOffset) < lastOffset) {
     24      // We've cached up to aOffset.
     25      size_t index;
     26      if (BinarySearch(offsets, 0, offsetCount, static_cast<int32_t>(aOffset),
     27                       &index)) {
     28        // aOffset is the exclusive end of a child, so return the child before
     29        // it.
     30        return static_cast<int32_t>((index < offsetCount - 1) ? index + 1
     31                                                              : index);
     32      }
     33      if (index == offsetCount) {
     34        // aOffset is past the end of the text.
     35        return -1;
     36      }
     37      // index points at the exclusive end after aOffset.
     38      return static_cast<int32_t>(index);
     39    }
     40  }
     41 
     42  // We haven't yet cached up to aOffset. Find it, caching as we go.
     43  const Accessible* thisAcc = Acc();
     44  uint32_t childCount = thisAcc->ChildCount();
     45  // Even though we're only caching up to aOffset, it's likely that we'll
     46  // eventually cache offsets for all children. Pre-allocate thus to minimize
     47  // re-allocations.
     48  offsets.SetCapacity(childCount);
     49  while (offsets.Length() < childCount) {
     50    Accessible* child = thisAcc->ChildAt(offsets.Length());
     51    lastOffset += static_cast<int32_t>(nsAccUtils::TextLength(child));
     52    offsets.AppendElement(lastOffset);
     53    if (static_cast<int32_t>(aOffset) < lastOffset) {
     54      return static_cast<int32_t>(offsets.Length() - 1);
     55    }
     56  }
     57 
     58  if (static_cast<int32_t>(aOffset) == lastOffset) {
     59    return static_cast<int32_t>(offsets.Length() - 1);
     60  }
     61 
     62  return -1;
     63 }
     64 
     65 Accessible* HyperTextAccessibleBase::GetChildAtOffset(uint32_t aOffset) const {
     66  const Accessible* thisAcc = Acc();
     67  return thisAcc->ChildAt(GetChildIndexAtOffset(aOffset));
     68 }
     69 
     70 int32_t HyperTextAccessibleBase::GetChildOffset(const Accessible* aChild,
     71                                                bool aInvalidateAfter) const {
     72  const Accessible* thisAcc = Acc();
     73  if (aChild->Parent() != thisAcc) {
     74    return -1;
     75  }
     76  int32_t index = aChild->IndexInParent();
     77  if (index == -1) {
     78    return -1;
     79  }
     80  return GetChildOffset(index, aInvalidateAfter);
     81 }
     82 
     83 int32_t HyperTextAccessibleBase::GetChildOffset(uint32_t aChildIndex,
     84                                                bool aInvalidateAfter) const {
     85  auto& offsets =
     86      const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets();
     87  if (aChildIndex == 0) {
     88    if (aInvalidateAfter) {
     89      offsets.Clear();
     90    }
     91    return 0;
     92  }
     93 
     94  int32_t countCachedAfterChild = static_cast<int32_t>(offsets.Length()) -
     95                                  static_cast<int32_t>(aChildIndex);
     96  if (countCachedAfterChild > 0) {
     97    // We've cached up to aChildIndex.
     98    if (aInvalidateAfter) {
     99      offsets.RemoveElementsAt(aChildIndex, countCachedAfterChild);
    100    }
    101    return offsets[aChildIndex - 1];
    102  }
    103 
    104  // We haven't yet cached up to aChildIndex. Find it, caching as we go.
    105  const Accessible* thisAcc = Acc();
    106  // Even though we're only caching up to aChildIndex, it's likely that we'll
    107  // eventually cache offsets for all children. Pre-allocate thus to minimize
    108  // re-allocations.
    109  offsets.SetCapacity(thisAcc->ChildCount());
    110  uint32_t lastOffset = offsets.IsEmpty() ? 0 : offsets[offsets.Length() - 1];
    111  while (offsets.Length() < aChildIndex) {
    112    Accessible* child = thisAcc->ChildAt(offsets.Length());
    113    lastOffset += nsAccUtils::TextLength(child);
    114    offsets.AppendElement(lastOffset);
    115  }
    116 
    117  return offsets[aChildIndex - 1];
    118 }
    119 
    120 uint32_t HyperTextAccessibleBase::CharacterCount() const {
    121  return GetChildOffset(Acc()->ChildCount());
    122 }
    123 
    124 index_t HyperTextAccessibleBase::ConvertMagicOffset(int32_t aOffset) const {
    125  if (aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT) {
    126    return CharacterCount();
    127  }
    128 
    129  if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
    130    return CaretOffset();
    131  }
    132 
    133  return aOffset;
    134 }
    135 
    136 void HyperTextAccessibleBase::TextSubstring(int32_t aStartOffset,
    137                                            int32_t aEndOffset,
    138                                            nsAString& aText) const {
    139  aText.Truncate();
    140 
    141  index_t startOffset = ConvertMagicOffset(aStartOffset);
    142  index_t endOffset = ConvertMagicOffset(aEndOffset);
    143  if (!startOffset.IsValid() || !endOffset.IsValid() ||
    144      startOffset > endOffset || endOffset > CharacterCount()) {
    145    NS_ERROR("Wrong in offset");
    146    return;
    147  }
    148 
    149  int32_t startChildIdx = GetChildIndexAtOffset(startOffset);
    150  if (startChildIdx == -1) {
    151    return;
    152  }
    153 
    154  int32_t endChildIdx = GetChildIndexAtOffset(endOffset);
    155  if (endChildIdx == -1) {
    156    return;
    157  }
    158 
    159  const Accessible* thisAcc = Acc();
    160  if (startChildIdx == endChildIdx) {
    161    int32_t childOffset = GetChildOffset(startChildIdx);
    162    if (childOffset == -1) {
    163      return;
    164    }
    165 
    166    Accessible* child = thisAcc->ChildAt(startChildIdx);
    167    child->AppendTextTo(aText, startOffset - childOffset,
    168                        endOffset - startOffset);
    169    return;
    170  }
    171 
    172  int32_t startChildOffset = GetChildOffset(startChildIdx);
    173  if (startChildOffset == -1) {
    174    return;
    175  }
    176 
    177  Accessible* startChild = thisAcc->ChildAt(startChildIdx);
    178  startChild->AppendTextTo(aText, startOffset - startChildOffset);
    179 
    180  for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx;
    181       childIdx++) {
    182    Accessible* child = thisAcc->ChildAt(childIdx);
    183    child->AppendTextTo(aText);
    184  }
    185 
    186  int32_t endChildOffset = GetChildOffset(endChildIdx);
    187  if (endChildOffset == -1) {
    188    return;
    189  }
    190 
    191  Accessible* endChild = thisAcc->ChildAt(endChildIdx);
    192  endChild->AppendTextTo(aText, 0, endOffset - endChildOffset);
    193 }
    194 
    195 bool HyperTextAccessibleBase::CharAt(int32_t aOffset, nsAString& aChar,
    196                                     int32_t* aStartOffset,
    197                                     int32_t* aEndOffset) {
    198  MOZ_ASSERT(!aStartOffset == !aEndOffset,
    199             "Offsets should be both defined or both undefined!");
    200 
    201  int32_t childIdx = GetChildIndexAtOffset(aOffset);
    202  if (childIdx == -1) {
    203    return false;
    204  }
    205 
    206  Accessible* child = Acc()->ChildAt(childIdx);
    207  child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1);
    208 
    209  if (aStartOffset && aEndOffset) {
    210    *aStartOffset = aOffset;
    211    *aEndOffset = aOffset + aChar.Length();
    212  }
    213  return true;
    214 }
    215 
    216 LayoutDeviceIntRect HyperTextAccessibleBase::CharBounds(int32_t aOffset,
    217                                                        uint32_t aCoordType) {
    218  index_t offset = ConvertMagicOffset(aOffset);
    219  if (!offset.IsValid() || offset > CharacterCount()) {
    220    return LayoutDeviceIntRect();
    221  }
    222  TextLeafPoint point = ToTextLeafPoint(static_cast<int32_t>(offset), false);
    223  if (!point.mAcc) {
    224    return LayoutDeviceIntRect();
    225  }
    226 
    227  LayoutDeviceIntRect bounds = point.CharBounds();
    228  if (!bounds.x && !bounds.y && bounds.IsZeroArea()) {
    229    return bounds;
    230  }
    231  nsAccUtils::ConvertScreenCoordsTo(&bounds.x, &bounds.y, aCoordType, Acc());
    232  return bounds;
    233 }
    234 
    235 LayoutDeviceIntRect HyperTextAccessibleBase::TextBounds(int32_t aStartOffset,
    236                                                        int32_t aEndOffset,
    237                                                        uint32_t aCoordType) {
    238  LayoutDeviceIntRect result;
    239  if (CharacterCount() == 0) {
    240    result = Acc()->Bounds();
    241    nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc());
    242    return result;
    243  }
    244 
    245  index_t startOffset = ConvertMagicOffset(aStartOffset);
    246  index_t endOffset = ConvertMagicOffset(aEndOffset);
    247  if (!startOffset.IsValid() || startOffset > endOffset) {
    248    return LayoutDeviceIntRect();
    249  }
    250 
    251  TextLeafPoint startPoint =
    252      ToTextLeafPoint(static_cast<int32_t>(startOffset), false);
    253  TextLeafPoint endPoint =
    254      ToTextLeafPoint(static_cast<int32_t>(endOffset), true);
    255  if (!endPoint) {
    256    // The caller provided an invalid offset.
    257    return LayoutDeviceIntRect();
    258  }
    259 
    260  TextLeafRange range(startPoint, endPoint);
    261  result = range.Bounds();
    262 
    263  // Calls to TextLeafRange::Bounds() will construct screen coordinates.
    264  // Perform any additional conversions here.
    265  nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc());
    266  return result;
    267 }
    268 
    269 int32_t HyperTextAccessibleBase::OffsetAtPoint(int32_t aX, int32_t aY,
    270                                               uint32_t aCoordType) {
    271  Accessible* thisAcc = Acc();
    272  LayoutDeviceIntPoint coords =
    273      nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc);
    274  if (!thisAcc->Bounds().Contains(coords.x, coords.y)) {
    275    // The requested point does not exist in this accessible.
    276    // Check if we used fuzzy hittesting to get here and, if
    277    // so, return 0 to indicate this text leaf is a valid match.
    278    LayoutDeviceIntPoint p(aX, aY);
    279    if (aCoordType != nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) {
    280      p = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc);
    281    }
    282    if (Accessible* doc = nsAccUtils::DocumentFor(thisAcc)) {
    283      Accessible* hittestMatch = doc->ChildAtPoint(
    284          p.x, p.y, Accessible::EWhichChildAtPoint::DeepestChild);
    285      if (hittestMatch && thisAcc == hittestMatch->Parent()) {
    286        return 0;
    287      }
    288    }
    289    return -1;
    290  }
    291 
    292  TextLeafPoint startPoint = ToTextLeafPoint(0, false);
    293  // Walk to the very end of the text contained in this hypertext in order to
    294  // hit test it in its entirety.
    295  TextLeafPoint endPoint =
    296      ToTextLeafPoint(static_cast<int32_t>(CharacterCount()), true);
    297  TextLeafRange range{startPoint, endPoint};
    298  TextLeafPoint point = range.TextLeafPointAtScreenPoint(coords.x, coords.y);
    299  if (!point.ContainsPoint(coords.x, coords.y)) {
    300    LayoutDeviceIntRect startRect = startPoint.CharBounds();
    301    if (coords.x < startRect.x || coords.y < startRect.y) {
    302      // Bug 1816601: The point is within the container but above or to the left
    303      // of the rectangle at offset 0. We should really return -1, but we've
    304      // returned 0 for many years due to a bug. Some users have unfortunately
    305      // come to rely on this, so perpetuate this here.
    306      return 0;
    307    }
    308    return -1;
    309  }
    310  DebugOnly<bool> ok = false;
    311  int32_t htOffset;
    312  std::tie(ok, htOffset) =
    313      TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false);
    314  MOZ_ASSERT(ok, "point should be a descendant of this");
    315  return htOffset;
    316 }
    317 
    318 TextLeafPoint HyperTextAccessibleBase::ToTextLeafPoint(int32_t aOffset,
    319                                                       bool aDescendToEnd) {
    320  Accessible* thisAcc = Acc();
    321  if (!thisAcc->HasChildren()) {
    322    return TextLeafPoint(thisAcc, 0);
    323  }
    324  Accessible* child = GetChildAtOffset(aOffset);
    325  if (!child) {
    326    return TextLeafPoint();
    327  }
    328  int32_t offset = aOffset - GetChildOffset(child);
    329  if (HyperTextAccessibleBase* childHt = child->AsHyperTextBase()) {
    330    // This child is an embedded object, so the offset can only be 0 or 1.
    331    MOZ_ASSERT(offset == 0 || offset == 1);
    332    // Offset 1 refers to the end of this container, so descend to its end.
    333    const bool end = aDescendToEnd || offset == 1;
    334    return childHt->ToTextLeafPoint(
    335        end ? static_cast<int32_t>(childHt->CharacterCount()) : 0, end);
    336  }
    337  return TextLeafPoint(child, offset);
    338 }
    339 
    340 std::pair<bool, int32_t> HyperTextAccessibleBase::TransformOffset(
    341    Accessible* aDescendant, int32_t aOffset, bool aIsEndOffset) const {
    342  const Accessible* thisAcc = Acc();
    343  // From the descendant, go up and get the immediate child of this hypertext.
    344  int32_t offset = aOffset;
    345  Accessible* descendant = aDescendant;
    346  while (descendant) {
    347    Accessible* parent = descendant->Parent();
    348    if (parent == thisAcc) {
    349      return {true, GetChildOffset(descendant) + offset};
    350    }
    351 
    352    // This offset no longer applies because the passed-in text object is not
    353    // a child of the hypertext. This happens when there are nested hypertexts,
    354    // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
    355    // to make it relative the hypertext.
    356    // If the end offset is not supposed to be inclusive and the original point
    357    // is not at 0 offset then the returned offset should be after an embedded
    358    // character the original point belongs to.
    359    if (aIsEndOffset) {
    360      offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
    361    } else {
    362      offset = 0;
    363    }
    364 
    365    descendant = parent;
    366  }
    367 
    368  // The given a11y point cannot be mapped to an offset relative to this
    369  // hypertext accessible. Return the start or the end depending on whether this
    370  // is a start ofset or an end offset, thus clipping to the relevant endpoint.
    371  return {false, aIsEndOffset ? static_cast<int32_t>(CharacterCount()) : 0};
    372 }
    373 
    374 void HyperTextAccessibleBase::AdjustOriginIfEndBoundary(
    375    TextLeafPoint& aOrigin, AccessibleTextBoundary aBoundaryType,
    376    bool aAtOffset) const {
    377  if (aBoundaryType != nsIAccessibleText::BOUNDARY_LINE_END &&
    378      aBoundaryType != nsIAccessibleText::BOUNDARY_WORD_END) {
    379    return;
    380  }
    381  TextLeafPoint actualOrig = aOrigin;
    382  // We explicitly care about the character at this offset. We don't want
    383  // FindBoundary to behave differently even if this is the insertion point at
    384  // the end of a line.
    385  actualOrig.mIsEndOfLineInsertionPoint = false;
    386  if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) {
    387    if (!actualOrig.IsLineFeedChar()) {
    388      return;
    389    }
    390    aOrigin =
    391        actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
    392  } else {  // BOUNDARY_WORD_END
    393    if (aAtOffset) {
    394      // For TextAtOffset with BOUNDARY_WORD_END, we follow WebKitGtk here and
    395      // return the word which ends after the origin if the origin is a word end
    396      // boundary. Also, if the caret is at the end of a line, our tests expect
    397      // the word after the caret, not the word before. The reason for that
    398      // is a mystery lost to history. We can do that by explicitly using the
    399      // caret without adjusting for end of line.
    400      aOrigin = actualOrig;
    401      return;
    402    }
    403    if (!actualOrig.IsSpace()) {
    404      return;
    405    }
    406    TextLeafPoint prevChar =
    407        actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
    408    if (prevChar != actualOrig && !prevChar.IsSpace()) {
    409      // aOrigin is a word end boundary.
    410      aOrigin = prevChar;
    411    }
    412  }
    413 }
    414 
    415 void HyperTextAccessibleBase::TextBeforeOffset(
    416    int32_t aOffset, AccessibleTextBoundary aBoundaryType,
    417    int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) {
    418  *aStartOffset = *aEndOffset = 0;
    419  aText.Truncate();
    420 
    421  if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
    422      aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
    423    return;  // Not implemented.
    424  }
    425 
    426  uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
    427  if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
    428    NS_ERROR("Wrong given offset!");
    429    return;
    430  }
    431 
    432  if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
    433    if (adjustedOffset > 0) {
    434      CharAt(static_cast<int32_t>(adjustedOffset) - 1, aText, aStartOffset,
    435             aEndOffset);
    436    }
    437    return;
    438  }
    439 
    440  TextLeafPoint orig;
    441  if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
    442    orig = TextLeafPoint::GetCaret(Acc());
    443  } else {
    444    orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset));
    445  }
    446  if (!orig) {
    447    // This can happen if aOffset is invalid.
    448    return;
    449  }
    450  AdjustOriginIfEndBoundary(orig, aBoundaryType);
    451  TextLeafPoint end =
    452      orig.FindBoundary(aBoundaryType, eDirPrevious,
    453                        TextLeafPoint::BoundaryFlags::eIncludeOrigin);
    454  bool ok;
    455  std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
    456                                              /* aIsEndOffset */ true);
    457  if (!ok) {
    458    // There is no previous boundary inside this HyperText.
    459    *aStartOffset = *aEndOffset = 0;
    460    return;
    461  }
    462  TextLeafPoint start = end.FindBoundary(aBoundaryType, eDirPrevious);
    463  // If TransformOffset fails because start is outside this HyperText,
    464  // *aStartOffset will be 0, which is what we want.
    465  std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
    466                                                /* aIsEndOffset */ false);
    467  TextSubstring(*aStartOffset, *aEndOffset, aText);
    468 }
    469 
    470 void HyperTextAccessibleBase::TextAtOffset(int32_t aOffset,
    471                                           AccessibleTextBoundary aBoundaryType,
    472                                           int32_t* aStartOffset,
    473                                           int32_t* aEndOffset,
    474                                           nsAString& aText) {
    475  *aStartOffset = *aEndOffset = 0;
    476  aText.Truncate();
    477 
    478  if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
    479      aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
    480    return;  // Not implemented.
    481  }
    482 
    483  uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
    484  if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
    485    NS_ERROR("Wrong given offset!");
    486    return;
    487  }
    488 
    489  if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
    490    if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
    491      TextLeafPoint caret = TextLeafPoint::GetCaret(Acc());
    492      if (caret.mIsEndOfLineInsertionPoint) {
    493        // The caret is at the end of the line. Return no character.
    494        *aStartOffset = *aEndOffset = static_cast<int32_t>(adjustedOffset);
    495        return;
    496      }
    497    }
    498    CharAt(adjustedOffset, aText, aStartOffset, aEndOffset);
    499    return;
    500  }
    501 
    502  TextLeafPoint start, end;
    503  if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
    504    start = TextLeafPoint::GetCaret(Acc());
    505    AdjustOriginIfEndBoundary(start, aBoundaryType, /* aAtOffset */ true);
    506    end = start;
    507  } else {
    508    start = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset));
    509    Accessible* childAcc = GetChildAtOffset(adjustedOffset);
    510    if (childAcc && childAcc->IsHyperText()) {
    511      // We're searching for boundaries enclosing an embedded object.
    512      // An embedded object might contain several boundaries itself.
    513      // Thus, we must ensure we search for the end boundary from the last
    514      // text in the subtree, not just the first.
    515      // For example, if the embedded object is a link and it contains two
    516      // words, but the second word expands beyond the link, we want to
    517      // include the part of the second word which is outside of the link.
    518      end = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset),
    519                            /* aDescendToEnd */ true);
    520    } else {
    521      AdjustOriginIfEndBoundary(start, aBoundaryType,
    522                                /* aAtOffset */ true);
    523      end = start;
    524    }
    525  }
    526  if (!start) {
    527    // This can happen if aOffset is invalid.
    528    return;
    529  }
    530  start = start.FindBoundary(aBoundaryType, eDirPrevious,
    531                             TextLeafPoint::BoundaryFlags::eIncludeOrigin);
    532  bool ok;
    533  std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
    534                                                /* aIsEndOffset */ false);
    535  end = end.FindBoundary(aBoundaryType, eDirNext);
    536  std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
    537                                              /* aIsEndOffset */ true);
    538  TextSubstring(*aStartOffset, *aEndOffset, aText);
    539 }
    540 
    541 void HyperTextAccessibleBase::TextAfterOffset(
    542    int32_t aOffset, AccessibleTextBoundary aBoundaryType,
    543    int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) {
    544  *aStartOffset = *aEndOffset = 0;
    545  aText.Truncate();
    546 
    547  if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
    548      aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
    549    return;  // Not implemented.
    550  }
    551 
    552  uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
    553  if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
    554    NS_ERROR("Wrong given offset!");
    555    return;
    556  }
    557 
    558  if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
    559    if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET && adjustedOffset > 0 &&
    560        TextLeafPoint::GetCaret(Acc()).mIsEndOfLineInsertionPoint) {
    561      --adjustedOffset;
    562    }
    563    uint32_t count = CharacterCount();
    564    if (adjustedOffset >= count) {
    565      *aStartOffset = *aEndOffset = static_cast<int32_t>(count);
    566    } else {
    567      CharAt(static_cast<int32_t>(adjustedOffset) + 1, aText, aStartOffset,
    568             aEndOffset);
    569    }
    570    return;
    571  }
    572 
    573  TextLeafPoint orig;
    574  if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
    575    orig = TextLeafPoint::GetCaret(Acc());
    576  } else {
    577    orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset),
    578                           /* aDescendToEnd */ true);
    579  }
    580  if (!orig) {
    581    // This can happen if aOffset is invalid.
    582    return;
    583  }
    584  AdjustOriginIfEndBoundary(orig, aBoundaryType);
    585  TextLeafPoint start = orig.FindBoundary(aBoundaryType, eDirNext);
    586  bool ok;
    587  std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
    588                                                /* aIsEndOffset */ false);
    589  if (!ok) {
    590    // There is no next boundary inside this HyperText.
    591    *aStartOffset = *aEndOffset = static_cast<int32_t>(CharacterCount());
    592    return;
    593  }
    594  TextLeafPoint end = start.FindBoundary(aBoundaryType, eDirNext);
    595  // If TransformOffset fails because end is outside this HyperText,
    596  // *aEndOffset will be CharacterCount(), which is what we want.
    597  std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
    598                                              /* aIsEndOffset */ true);
    599  TextSubstring(*aStartOffset, *aEndOffset, aText);
    600 }
    601 
    602 int32_t HyperTextAccessibleBase::CaretOffset() const {
    603  TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc()));
    604  if (point.mOffset == 0 && point.mAcc == Acc()) {
    605    // If a text box is empty, there will be no children, so point.mAcc will be
    606    // this HyperText.
    607    return 0;
    608  }
    609  auto [ok, htOffset] =
    610      TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false);
    611  if (!ok) {
    612    // The caret is not within this HyperText.
    613    return -1;
    614  }
    615  return htOffset;
    616 }
    617 
    618 void HyperTextAccessibleBase::SetCaretOffset(int32_t aOffset) {
    619  TextLeafPoint point = ToTextLeafPoint(aOffset);
    620  TextLeafRange range(point, point);
    621  if (!range) {
    622    NS_ERROR("Wrong in offset");
    623    return;
    624  }
    625  range.SetSelection(TextLeafRange::kRemoveAllExistingSelectedRanges);
    626 }
    627 
    628 int32_t HyperTextAccessibleBase::CaretLineNumber() {
    629  TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc()));
    630  if (point.mOffset == 0 && point.mAcc == Acc()) {
    631    MOZ_ASSERT(CharacterCount() == 0);
    632    // If a text box is empty, there will be no children, so point.mAcc will be
    633    // this HyperText.
    634    return 1;
    635  }
    636 
    637  if (!point.mAcc ||
    638      (point.mAcc != Acc() && !Acc()->IsAncestorOf(point.mAcc))) {
    639    // The caret is not within this HyperText.
    640    return -1;
    641  }
    642 
    643  // Walk forward by line from the start of the container.
    644  TextLeafPoint line = TextLeafPoint(Acc(), 0);
    645  int32_t lineNumber = 0;
    646  for (; line && line < point;
    647       line = line.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START,
    648                                eDirNext)) {
    649    ++lineNumber;
    650  }
    651  // The caret might be right at the start of a line, in which case we should
    652  // increment the line number. We shouldn't do that if the caret is at the end
    653  // of a line or container, though.
    654  if (line == point && !point.mIsEndOfLineInsertionPoint &&
    655      point.mOffset <
    656          static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc))) {
    657    ++lineNumber;
    658  }
    659 
    660  return lineNumber;
    661 }
    662 
    663 bool HyperTextAccessibleBase::IsValidOffset(int32_t aOffset) {
    664  index_t offset = ConvertMagicOffset(aOffset);
    665  return offset.IsValid() && offset <= CharacterCount();
    666 }
    667 
    668 bool HyperTextAccessibleBase::IsValidRange(int32_t aStartOffset,
    669                                           int32_t aEndOffset) {
    670  index_t startOffset = ConvertMagicOffset(aStartOffset);
    671  index_t endOffset = ConvertMagicOffset(aEndOffset);
    672  return startOffset.IsValid() && endOffset.IsValid() &&
    673         startOffset <= endOffset && endOffset <= CharacterCount();
    674 }
    675 
    676 uint32_t HyperTextAccessibleBase::LinkCount() {
    677  return Acc()->EmbeddedChildCount();
    678 }
    679 
    680 Accessible* HyperTextAccessibleBase::LinkAt(uint32_t aIndex) {
    681  return Acc()->EmbeddedChildAt(aIndex);
    682 }
    683 
    684 int32_t HyperTextAccessibleBase::LinkIndexOf(Accessible* aLink) {
    685  return Acc()->IndexOfEmbeddedChild(aLink);
    686 }
    687 
    688 already_AddRefed<AccAttributes> HyperTextAccessibleBase::TextAttributes(
    689    bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
    690    int32_t* aEndOffset) {
    691  *aStartOffset = *aEndOffset = 0;
    692  index_t offset = ConvertMagicOffset(aOffset);
    693  if (!offset.IsValid() || offset > CharacterCount()) {
    694    NS_ERROR("Wrong in offset!");
    695    return RefPtr{new AccAttributes()}.forget();
    696  }
    697 
    698  Accessible* originAcc = GetChildAtOffset(offset);
    699  if (!originAcc) {
    700    // Offset 0 is correct offset when accessible has empty text. Include
    701    // default attributes if they were requested, otherwise return empty set.
    702    if (offset == 0) {
    703      if (aIncludeDefAttrs) {
    704        return DefaultTextAttributes();
    705      }
    706    }
    707    return RefPtr{new AccAttributes()}.forget();
    708  }
    709 
    710  if (!originAcc->IsText()) {
    711    // This is an embedded object. One or more consecutive embedded objects
    712    // form a single attrs run with no attributes.
    713    *aStartOffset = aOffset;
    714    *aEndOffset = aOffset + 1;
    715    Accessible* parent = originAcc->Parent();
    716    if (!parent) {
    717      return RefPtr{new AccAttributes()}.forget();
    718    }
    719    int32_t originIdx = originAcc->IndexInParent();
    720    if (originIdx > 0) {
    721      // Check for embedded objects before the origin.
    722      for (uint32_t idx = originIdx - 1;; --idx) {
    723        Accessible* sibling = parent->ChildAt(idx);
    724        if (sibling->IsText()) {
    725          break;
    726        }
    727        --*aStartOffset;
    728        if (idx == 0) {
    729          break;
    730        }
    731      }
    732    }
    733    // Check for embedded objects after the origin.
    734    for (uint32_t idx = originIdx + 1;; ++idx) {
    735      Accessible* sibling = parent->ChildAt(idx);
    736      if (!sibling || sibling->IsText()) {
    737        break;
    738      }
    739      ++*aEndOffset;
    740    }
    741    return RefPtr{new AccAttributes()}.forget();
    742  }
    743 
    744  TextLeafPoint origin = ToTextLeafPoint(static_cast<int32_t>(offset));
    745  TextLeafPoint start =
    746      origin.FindTextAttrsStart(eDirPrevious, /* aIncludeOrigin */ true);
    747  bool ok;
    748  std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
    749                                                /* aIsEndOffset */ false);
    750  TextLeafPoint end =
    751      origin.FindTextAttrsStart(eDirNext, /* aIncludeOrigin */ false);
    752  std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
    753                                              /* aIsEndOffset */ true);
    754  return origin.GetTextAttributes(aIncludeDefAttrs);
    755 }
    756 
    757 void HyperTextAccessibleBase::CroppedSelectionRanges(
    758    nsTArray<TextRange>& aRanges) const {
    759  SelectionRanges(&aRanges);
    760  Accessible* acc = const_cast<Accessible*>(Acc());
    761 
    762  size_t startIndex = 0;
    763  size_t endIndex = aRanges.Length();
    764  // If this is the document, it contains all ranges, so there's no need to
    765  // search for overlapping ranges.
    766  if (!acc->IsDoc()) {
    767    // Find overlapping ranges. We use binary searches here, which is far more
    768    // efficient than cropping every range in the list.
    769    // Find the first range that ends after acc starts.
    770    TextPoint thisPoint = TextPoint(acc, 0);
    771    startIndex =
    772        UpperBound(aRanges, 0, aRanges.Length(), [&](const TextRange& range) {
    773          return thisPoint.Compare(range.EndPoint());
    774        });
    775    // Find the first range that starts after acc ends. This will be the first
    776    // non-overlapping range after startIndex; i.e. the exclusive end of our
    777    // desired span.
    778    thisPoint = TextPoint(acc, CharacterCount());
    779    endIndex = UpperBound(aRanges, startIndex, aRanges.Length(),
    780                          [&](const TextRange& range) {
    781                            return thisPoint.Compare(range.StartPoint());
    782                          });
    783  }
    784 
    785  // Exclude ranges that don't overlap acc.
    786  size_t nextIndex = 0;
    787  aRanges.RemoveElementsBy([&](TextRange& range) {
    788    // Set index to the index of range. Increment nextIndex ready for the next
    789    // iteration.
    790    size_t index = nextIndex++;
    791    if (range.StartPoint() == range.EndPoint()) {
    792      return true;  // Collapsed, so remove this range.
    793    }
    794    // Remove (return true for) ranges that aren't between startIndex
    795    // (inclusive) and endIndex (exclusive).
    796    if (index < startIndex || index >= endIndex) {
    797      return true;
    798    }
    799    // The first and last ranges might extend beyond acc, so crop them.
    800    if (index == startIndex || index == endIndex - 1) {
    801      DebugOnly<bool> cropped = range.Crop(const_cast<Accessible*>(acc));
    802      MOZ_ASSERT(cropped, "range should overlap and thus crop successfully");
    803    }
    804    return false;
    805  });
    806 }
    807 
    808 int32_t HyperTextAccessibleBase::SelectionCount() {
    809  nsTArray<TextRange> ranges;
    810  CroppedSelectionRanges(ranges);
    811  return static_cast<int32_t>(ranges.Length());
    812 }
    813 
    814 bool HyperTextAccessibleBase::SelectionBoundsAt(int32_t aSelectionNum,
    815                                                int32_t* aStartOffset,
    816                                                int32_t* aEndOffset) {
    817  nsTArray<TextRange> ranges;
    818  CroppedSelectionRanges(ranges);
    819  if (aSelectionNum >= static_cast<int32_t>(ranges.Length())) {
    820    return false;
    821  }
    822  TextRange& range = ranges[aSelectionNum];
    823  Accessible* thisAcc = Acc();
    824  if (range.StartContainer() == thisAcc) {
    825    *aStartOffset = range.StartOffset();
    826  } else {
    827    bool ok;
    828    // range.StartContainer() isn't a text leaf, so don't use its offset.
    829    std::tie(ok, *aStartOffset) =
    830        TransformOffset(range.StartContainer(), 0, /* aDescendToEnd */ false);
    831  }
    832  if (range.EndContainer() == thisAcc) {
    833    *aEndOffset = range.EndOffset();
    834  } else {
    835    bool ok;
    836    // range.EndContainer() isn't a text leaf, so don't use its offset. If
    837    // range.EndOffset() is > 0, we want to include this container, so pas
    838    // offset 1.
    839    std::tie(ok, *aEndOffset) =
    840        TransformOffset(range.EndContainer(), range.EndOffset() == 0 ? 0 : 1,
    841                        /* aDescendToEnd */ true);
    842  }
    843  return true;
    844 }
    845 
    846 bool HyperTextAccessibleBase::SetSelectionBoundsAt(int32_t aSelectionNum,
    847                                                   int32_t aStartOffset,
    848                                                   int32_t aEndOffset) {
    849  TextLeafRange range(ToTextLeafPoint(aStartOffset),
    850                      ToTextLeafPoint(aEndOffset, true));
    851  if (!range) {
    852    NS_ERROR("Wrong in offset");
    853    return false;
    854  }
    855 
    856  return range.SetSelection(aSelectionNum);
    857 }
    858 
    859 void HyperTextAccessibleBase::ScrollSubstringTo(int32_t aStartOffset,
    860                                                int32_t aEndOffset,
    861                                                uint32_t aScrollType) {
    862  TextLeafRange range(ToTextLeafPoint(aStartOffset),
    863                      ToTextLeafPoint(aEndOffset, true));
    864  range.ScrollIntoView(aScrollType);
    865 }
    866 
    867 }  // namespace mozilla::a11y