tor-browser

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

TextLeafRange.cpp (99446B)


      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
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "TextLeafRange.h"
      8 
      9 #include "HyperTextAccessible-inl.h"
     10 #include "mozilla/a11y/Accessible.h"
     11 #include "mozilla/a11y/CacheConstants.h"
     12 #include "mozilla/a11y/DocAccessible.h"
     13 #include "mozilla/a11y/DocAccessibleParent.h"
     14 #include "mozilla/a11y/LocalAccessible.h"
     15 #include "mozilla/BinarySearch.h"
     16 #include "mozilla/Casting.h"
     17 #include "mozilla/dom/AbstractRange.h"
     18 #include "mozilla/dom/CharacterData.h"
     19 #include "mozilla/dom/HTMLInputElement.h"
     20 #include "mozilla/PresShell.h"
     21 #include "mozilla/intl/Segmenter.h"
     22 #include "mozilla/intl/WordBreaker.h"
     23 #include "mozilla/StaticPrefs_layout.h"
     24 #include "mozilla/TextEditor.h"
     25 #include "nsAccUtils.h"
     26 #include "nsBlockFrame.h"
     27 #include "nsFocusManager.h"
     28 #include "nsFrameSelection.h"
     29 #include "nsIAccessiblePivot.h"
     30 #include "nsILineIterator.h"
     31 #include "nsINode.h"
     32 #include "nsStyleStructInlines.h"
     33 #include "nsTArray.h"
     34 #include "nsTextFrame.h"
     35 #include "nsUnicharUtils.h"
     36 #include "Pivot.h"
     37 #include "TextAttrs.h"
     38 #include "TextRange.h"
     39 
     40 using mozilla::intl::WordBreaker;
     41 using FindWordOptions = mozilla::intl::WordBreaker::FindWordOptions;
     42 
     43 namespace mozilla::a11y {
     44 
     45 /*** Helpers ***/
     46 
     47 /**
     48 * These two functions convert between rendered and content text offsets.
     49 * When text DOM nodes are rendered, the rendered text often does not contain
     50 * all the whitespace from the source. For example, by default, the text
     51 * "a   b" will be rendered as "a b"; i.e. multiple spaces are compressed to
     52 * one. TextLeafAccessibles contain rendered text, but when we query layout, we
     53 * need to provide offsets into the original content text. Similarly, layout
     54 * returns content offsets, but we need to convert them to rendered offsets to
     55 * map them to TextLeafAccessibles.
     56 */
     57 
     58 static int32_t RenderedToContentOffset(LocalAccessible* aAcc,
     59                                       uint32_t aRenderedOffset) {
     60  nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame());
     61  if (!frame) {
     62    MOZ_ASSERT(!aAcc->HasOwnContent() || aAcc->IsHTMLBr(),
     63               "No text frame because this is a XUL label[value] text leaf or "
     64               "a BR element.");
     65    return static_cast<int32_t>(aRenderedOffset);
     66  }
     67 
     68  if (frame->StyleText()->WhiteSpaceIsSignificant() &&
     69      frame->StyleText()->NewlineIsSignificant(frame)) {
     70    // Spaces and new lines aren't altered, so the content and rendered offsets
     71    // are the same. This happens in pre-formatted text and text fields.
     72    return static_cast<int32_t>(aRenderedOffset);
     73  }
     74 
     75  nsIFrame::RenderedText text =
     76      frame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
     77                             nsIFrame::TextOffsetType::OffsetsInRenderedText,
     78                             nsIFrame::TrailingWhitespace::DontTrim);
     79  return text.mOffsetWithinNodeText;
     80 }
     81 
     82 static uint32_t ContentToRenderedOffset(LocalAccessible* aAcc,
     83                                        int32_t aContentOffset) {
     84  nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame());
     85  if (!frame) {
     86    MOZ_ASSERT(!aAcc->HasOwnContent(),
     87               "No text frame because this is a XUL label[value] text leaf.");
     88    return aContentOffset;
     89  }
     90 
     91  if (frame->StyleText()->WhiteSpaceIsSignificant() &&
     92      frame->StyleText()->NewlineIsSignificant(frame)) {
     93    // Spaces and new lines aren't altered, so the content and rendered offsets
     94    // are the same. This happens in pre-formatted text and text fields.
     95    return aContentOffset;
     96  }
     97 
     98  nsIFrame::RenderedText text =
     99      frame->GetRenderedText(aContentOffset, aContentOffset + 1,
    100                             nsIFrame::TextOffsetType::OffsetsInContentText,
    101                             nsIFrame::TrailingWhitespace::DontTrim);
    102  return text.mOffsetWithinNodeRenderedText;
    103 }
    104 
    105 class LeafRule : public PivotRule {
    106 public:
    107  explicit LeafRule(bool aIgnoreListItemMarker)
    108      : mIgnoreListItemMarker(aIgnoreListItemMarker) {}
    109 
    110  virtual uint16_t Match(Accessible* aAcc) override {
    111    if (aAcc->IsOuterDoc()) {
    112      // Treat an embedded doc as a single character in this document, but do
    113      // not descend inside it.
    114      return nsIAccessibleTraversalRule::FILTER_MATCH |
    115             nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
    116    }
    117 
    118    if (mIgnoreListItemMarker && aAcc->Role() == roles::LISTITEM_MARKER) {
    119      // Ignore list item markers if configured to do so.
    120      return nsIAccessibleTraversalRule::FILTER_IGNORE;
    121    }
    122 
    123    // We deliberately include Accessibles such as empty input elements and
    124    // empty containers, as these can be at the start of a line.
    125    if (!aAcc->HasChildren()) {
    126      return nsIAccessibleTraversalRule::FILTER_MATCH;
    127    }
    128    return nsIAccessibleTraversalRule::FILTER_IGNORE;
    129  }
    130 
    131 private:
    132  bool mIgnoreListItemMarker;
    133 };
    134 
    135 static HyperTextAccessible* HyperTextFor(LocalAccessible* aAcc) {
    136  for (LocalAccessible* acc = aAcc; acc; acc = acc->LocalParent()) {
    137    if (HyperTextAccessible* ht = acc->AsHyperText()) {
    138      return ht;
    139    }
    140  }
    141  return nullptr;
    142 }
    143 
    144 static Accessible* NextLeaf(Accessible* aOrigin, bool aIsEditable = false,
    145                            bool aIgnoreListItemMarker = false) {
    146  MOZ_ASSERT(aOrigin);
    147  Accessible* doc = nsAccUtils::DocumentFor(aOrigin);
    148  Pivot pivot(doc);
    149  auto rule = LeafRule(aIgnoreListItemMarker);
    150  Accessible* leaf = pivot.Next(aOrigin, rule);
    151  if (aIsEditable && leaf) {
    152    return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE)
    153               ? leaf
    154               : nullptr;
    155  }
    156  return leaf;
    157 }
    158 
    159 static Accessible* PrevLeaf(Accessible* aOrigin, bool aIsEditable = false,
    160                            bool aIgnoreListItemMarker = false) {
    161  MOZ_ASSERT(aOrigin);
    162  Accessible* doc = nsAccUtils::DocumentFor(aOrigin);
    163  Pivot pivot(doc);
    164  auto rule = LeafRule(aIgnoreListItemMarker);
    165  Accessible* leaf = pivot.Prev(aOrigin, rule);
    166  if (aIsEditable && leaf) {
    167    return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE)
    168               ? leaf
    169               : nullptr;
    170  }
    171  return leaf;
    172 }
    173 
    174 static nsIFrame* GetFrameInBlock(const LocalAccessible* aAcc) {
    175  dom::HTMLInputElement* input =
    176      dom::HTMLInputElement::FromNodeOrNull(aAcc->GetContent());
    177  if (!input) {
    178    if (LocalAccessible* parent = aAcc->LocalParent()) {
    179      input = dom::HTMLInputElement::FromNodeOrNull(parent->GetContent());
    180    }
    181  }
    182 
    183  if (input) {
    184    // If this is a single line input (or a leaf of an input) we want to return
    185    // the top frame of the input element and not the text leaf's frame because
    186    // the leaf may be inside of an embedded block frame in the input's shadow
    187    // DOM that we aren't interested in.
    188    return input->GetPrimaryFrame();
    189  }
    190 
    191  return aAcc->GetFrame();
    192 }
    193 
    194 /**
    195 * Returns true if the given frames are on different lines.
    196 */
    197 static bool AreFramesOnDifferentLines(nsIFrame* aFrame1, nsIFrame* aFrame2) {
    198  MOZ_ASSERT(aFrame1 && aFrame2);
    199  if (aFrame1 == aFrame2) {
    200    // This can happen if two Accessibles share the same frame; e.g. image maps.
    201    return false;
    202  }
    203  auto [block1, lineFrame1] = aFrame1->GetContainingBlockForLine(
    204      /* aLockScroll */ false);
    205  if (!block1) {
    206    // Error; play it safe.
    207    return true;
    208  }
    209  auto [block2, lineFrame2] = aFrame2->GetContainingBlockForLine(
    210      /* aLockScroll */ false);
    211  if (lineFrame1 == lineFrame2) {
    212    return false;
    213  }
    214  if (block1 != block2) {
    215    // These frames are in different blocks, so they're on different lines.
    216    return true;
    217  }
    218  if (nsBlockFrame* block = do_QueryFrame(block1)) {
    219    // If we have a block frame, it's faster for us to use
    220    // BlockInFlowLineIterator because it uses the line cursor.
    221    bool found = false;
    222    block->SetupLineCursorForQuery();
    223    nsBlockInFlowLineIterator it1(block, lineFrame1, &found);
    224    if (!found) {
    225      // Error; play it safe.
    226      return true;
    227    }
    228    found = false;
    229    nsBlockInFlowLineIterator it2(block, lineFrame2, &found);
    230    return !found || it1.GetLineList() != it2.GetLineList() ||
    231           it1.GetLine() != it2.GetLine();
    232  }
    233  AutoAssertNoDomMutations guard;
    234  nsILineIterator* it = block1->GetLineIterator();
    235  MOZ_ASSERT(it, "GetLineIterator impl in line-container blocks is infallible");
    236  int32_t line1 = it->FindLineContaining(lineFrame1);
    237  if (line1 < 0) {
    238    // Error; play it safe.
    239    return true;
    240  }
    241  int32_t line2 = it->FindLineContaining(lineFrame2, line1);
    242  return line1 != line2;
    243 }
    244 
    245 static bool IsLocalAccAtLineStart(LocalAccessible* aAcc) {
    246  if (aAcc->NativeRole() == roles::LISTITEM_MARKER) {
    247    // A bullet always starts a line.
    248    return true;
    249  }
    250  // Splitting of content across lines is handled by layout.
    251  // nsIFrame::IsLogicallyAtLineEdge queries whether a frame is the first frame
    252  // on its line. However, we can't use that because the first frame on a line
    253  // might not be included in the a11y tree; e.g. an empty span, or space
    254  // in the DOM after a line break which is stripped when rendered. Instead, we
    255  // get the line number for this Accessible's frame and the line number for the
    256  // previous leaf Accessible's frame and compare them.
    257  Accessible* prev = PrevLeaf(aAcc);
    258  LocalAccessible* prevLocal = prev ? prev->AsLocal() : nullptr;
    259  if (!prevLocal) {
    260    // There's nothing before us, so this is the start of the first line.
    261    return true;
    262  }
    263  if (prevLocal->NativeRole() == roles::LISTITEM_MARKER) {
    264    // If there is  a bullet immediately before us and we're inside the same
    265    // list item, this is not the start of a line.
    266    LocalAccessible* listItem = prevLocal->LocalParent();
    267    MOZ_ASSERT(listItem);
    268    LocalAccessible* doc = listItem->Document();
    269    MOZ_ASSERT(doc);
    270    for (LocalAccessible* parent = aAcc->LocalParent(); parent && parent != doc;
    271         parent = parent->LocalParent()) {
    272      if (parent == listItem) {
    273        return false;
    274      }
    275    }
    276  }
    277 
    278  nsIFrame* thisFrame = GetFrameInBlock(aAcc);
    279  if (!thisFrame) {
    280    return false;
    281  }
    282 
    283  nsIFrame* prevFrame = GetFrameInBlock(prevLocal);
    284  if (!prevFrame) {
    285    return false;
    286  }
    287 
    288  // The previous leaf might cross lines. We want to compare against the last
    289  // line.
    290  prevFrame = prevFrame->LastContinuation();
    291  // if the lines are different, that means there's nothing before us on the
    292  // same line, so we're at the start.
    293  return AreFramesOnDifferentLines(thisFrame, prevFrame);
    294 }
    295 
    296 /**
    297 * There are many kinds of word break, but we only need to treat punctuation and
    298 * space specially.
    299 */
    300 enum WordBreakClass { eWbcSpace = 0, eWbcPunct, eWbcOther };
    301 
    302 static WordBreakClass GetWordBreakClass(char16_t aChar) {
    303  // Based on IsSelectionInlineWhitespace and IsSelectionNewline in
    304  // layout/generic/nsTextFrame.cpp.
    305  const char16_t kCharNbsp = 0xA0;
    306  switch (aChar) {
    307    case ' ':
    308    case kCharNbsp:
    309    case '\t':
    310    case '\f':
    311    case '\n':
    312    case '\r':
    313      return eWbcSpace;
    314    default:
    315      break;
    316  }
    317  return mozilla::IsPunctuationForWordSelect(aChar) ? eWbcPunct : eWbcOther;
    318 }
    319 
    320 /**
    321 * Words can cross Accessibles. To work out whether we're at the start of a
    322 * word, we might have to check the previous leaf. This class handles querying
    323 * the previous WordBreakClass, crossing Accessibles if necessary.
    324 */
    325 class PrevWordBreakClassWalker {
    326 public:
    327  PrevWordBreakClassWalker(Accessible* aAcc, const nsAString& aText,
    328                           int32_t aOffset)
    329      : mAcc(aAcc), mText(aText), mOffset(aOffset) {
    330    mClass = GetWordBreakClass(mText.CharAt(mOffset));
    331  }
    332 
    333  WordBreakClass CurClass() { return mClass; }
    334 
    335  Maybe<WordBreakClass> PrevClass() {
    336    for (;;) {
    337      if (!PrevChar()) {
    338        return Nothing();
    339      }
    340      WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
    341      if (curClass != mClass) {
    342        mClass = curClass;
    343        return Some(curClass);
    344      }
    345    }
    346    MOZ_ASSERT_UNREACHABLE();
    347    return Nothing();
    348  }
    349 
    350  bool IsStartOfGroup() {
    351    if (!PrevChar()) {
    352      // There are no characters before us.
    353      return true;
    354    }
    355    WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
    356    // We wanted to peek at the previous character, not really move to it.
    357    ++mOffset;
    358    return curClass != mClass;
    359  }
    360 
    361 private:
    362  bool PrevChar() {
    363    if (mOffset > 0) {
    364      --mOffset;
    365      return true;
    366    }
    367    if (!mAcc) {
    368      // PrevChar was called already and failed.
    369      return false;
    370    }
    371    mAcc = PrevLeaf(mAcc);
    372    if (!mAcc) {
    373      return false;
    374    }
    375    mText.Truncate();
    376    mAcc->AppendTextTo(mText);
    377    mOffset = static_cast<int32_t>(mText.Length()) - 1;
    378    return true;
    379  }
    380 
    381  Accessible* mAcc;
    382  nsAutoString mText;
    383  int32_t mOffset;
    384  WordBreakClass mClass;
    385 };
    386 
    387 /**
    388 * WordBreaker breaks at all space, punctuation, etc. We want to emulate
    389 * layout, so that's not what we want. This function determines whether this
    390 * is acceptable as the start of a word for our purposes.
    391 */
    392 static bool IsAcceptableWordStart(Accessible* aAcc, const nsAutoString& aText,
    393                                  int32_t aOffset) {
    394  PrevWordBreakClassWalker walker(aAcc, aText, aOffset);
    395  if (!walker.IsStartOfGroup()) {
    396    // If we're not at the start of a WordBreaker group, this can't be the
    397    // start of a word.
    398    return false;
    399  }
    400  WordBreakClass curClass = walker.CurClass();
    401  if (curClass == eWbcSpace) {
    402    // Space isn't the start of a word.
    403    return false;
    404  }
    405  Maybe<WordBreakClass> prevClass = walker.PrevClass();
    406  if (curClass == eWbcPunct && (!prevClass || prevClass.value() != eWbcSpace)) {
    407    // Punctuation isn't the start of a word (unless it is after space).
    408    return false;
    409  }
    410  if (!prevClass || prevClass.value() != eWbcPunct) {
    411    // If there's nothing before this or the group before this isn't
    412    // punctuation, this is the start of a word.
    413    return true;
    414  }
    415  // At this point, we know the group before this is punctuation.
    416  if (!StaticPrefs::layout_word_select_stop_at_punctuation()) {
    417    // When layout.word_select.stop_at_punctuation is false (defaults to true),
    418    // if there is punctuation before this, this is not the start of a word.
    419    return false;
    420  }
    421  Maybe<WordBreakClass> prevPrevClass = walker.PrevClass();
    422  if (!prevPrevClass || prevPrevClass.value() == eWbcSpace) {
    423    // If there is punctuation before this and space (or nothing) before the
    424    // punctuation, this is not the start of a word.
    425    return false;
    426  }
    427  return true;
    428 }
    429 
    430 class BlockRule : public PivotRule {
    431 public:
    432  virtual uint16_t Match(Accessible* aAcc) override {
    433    if (RefPtr<nsAtom>(aAcc->DisplayStyle()) == nsGkAtoms::block ||
    434        aAcc->IsHTMLListItem() || aAcc->IsTableRow() || aAcc->IsTableCell()) {
    435      return nsIAccessibleTraversalRule::FILTER_MATCH;
    436    }
    437    return nsIAccessibleTraversalRule::FILTER_IGNORE;
    438  }
    439 };
    440 
    441 /**
    442 * Find DOM ranges which map to text attributes overlapping the requested
    443 * LocalAccessible and offsets. This includes ranges that begin or end outside
    444 * of the given LocalAccessible. Note that the offset arguments are rendered
    445 * offsets, but because the returned ranges are DOM ranges, those offsets are
    446 * content offsets. See the documentation for
    447 * dom::Selection::GetRangesForIntervalArray for information about the
    448 * aAllowAdjacent argument.
    449 */
    450 static nsTArray<std::pair<nsTArray<dom::AbstractRange*>, nsStaticAtom*>>
    451 FindDOMTextOffsetAttributes(LocalAccessible* aAcc, int32_t aRenderedStart,
    452                            int32_t aRenderedEnd, bool aAllowAdjacent = false) {
    453  nsTArray<std::pair<nsTArray<dom::AbstractRange*>, nsStaticAtom*>> result;
    454  if (!aAcc->IsTextLeaf() || !aAcc->HasOwnContent() ||
    455      !aAcc->GetContent()->IsText()) {
    456    return result;
    457  }
    458  nsIFrame* frame = aAcc->GetFrame();
    459  RefPtr<nsFrameSelection> frameSel =
    460      frame ? frame->GetFrameSelection() : nullptr;
    461  if (!frameSel) {
    462    return result;
    463  }
    464  nsINode* node = aAcc->GetNode();
    465  uint32_t contentStart = RenderedToContentOffset(aAcc, aRenderedStart);
    466  uint32_t contentEnd =
    467      aRenderedEnd == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
    468          ? dom::CharacterData::FromNode(node)->TextLength()
    469          : RenderedToContentOffset(aAcc, aRenderedEnd);
    470  const std::pair<mozilla::SelectionType, nsStaticAtom*>
    471      kSelectionTypesToAttributes[] = {
    472          {SelectionType::eSpellCheck, nsGkAtoms::spelling},
    473          {SelectionType::eTargetText, nsGkAtoms::mark},
    474      };
    475  size_t highlightCount = frameSel->HighlightSelectionCount();
    476  result.SetCapacity(std::size(kSelectionTypesToAttributes) + highlightCount);
    477 
    478  auto appendRanges = [&](dom::Selection* aDomSel, nsStaticAtom* aAttr) {
    479    nsTArray<dom::AbstractRange*> domRanges;
    480    aDomSel->GetAbstractRangesForIntervalArray(
    481        node, contentStart, node, contentEnd, aAllowAdjacent, &domRanges);
    482    if (!domRanges.IsEmpty()) {
    483      result.AppendElement(std::make_pair(std::move(domRanges), aAttr));
    484    }
    485  };
    486 
    487  for (auto [selType, attr] : kSelectionTypesToAttributes) {
    488    dom::Selection* domSel = frameSel->GetSelection(selType);
    489    if (!domSel) {
    490      continue;
    491    }
    492    appendRanges(domSel, attr);
    493  }
    494 
    495  for (size_t h = 0; h < highlightCount; ++h) {
    496    RefPtr<dom::Selection> domSel = frameSel->HighlightSelection(h);
    497    MOZ_ASSERT(domSel);
    498    nsStaticAtom* attr = nullptr;
    499    MOZ_ASSERT(domSel->HighlightSelectionData().mHighlight);
    500    switch (domSel->HighlightSelectionData().mHighlight->Type()) {
    501      case dom::HighlightType::Highlight:
    502        attr = nsGkAtoms::mark;
    503        break;
    504      case dom::HighlightType::Spelling_error:
    505        attr = nsGkAtoms::spelling;
    506        break;
    507      case dom::HighlightType::Grammar_error:
    508        attr = nsGkAtoms::grammar;
    509        break;
    510    }
    511    MOZ_ASSERT(attr);
    512    appendRanges(domSel, attr);
    513  }
    514 
    515  return result;
    516 }
    517 
    518 /**
    519 * Given two DOM nodes get DOM Selection object that is common
    520 * to both of them.
    521 */
    522 static dom::Selection* GetDOMSelection(const nsIContent* aStartContent,
    523                                       const nsIContent* aEndContent) {
    524  nsIFrame* startFrame = aStartContent->GetPrimaryFrame();
    525  const nsFrameSelection* startFrameSel =
    526      startFrame ? startFrame->GetConstFrameSelection() : nullptr;
    527  nsIFrame* endFrame = aEndContent->GetPrimaryFrame();
    528  const nsFrameSelection* endFrameSel =
    529      endFrame ? endFrame->GetConstFrameSelection() : nullptr;
    530 
    531  if (startFrameSel != endFrameSel) {
    532    // Start and end point don't share the same selection state.
    533    // This could happen when both points aren't in the same editable.
    534    return nullptr;
    535  }
    536 
    537  return startFrameSel ? &startFrameSel->NormalSelection() : nullptr;
    538 }
    539 
    540 std::pair<nsIContent*, uint32_t> TextLeafPoint::ToDOMPoint(
    541    bool aIncludeGenerated) const {
    542  if (!(*this) || !mAcc->IsLocal()) {
    543    MOZ_ASSERT_UNREACHABLE("Invalid point");
    544    return {nullptr, 0};
    545  }
    546 
    547  nsIContent* content = mAcc->AsLocal()->GetContent();
    548  nsIFrame* frame = content ? content->GetPrimaryFrame() : nullptr;
    549  MOZ_ASSERT(frame);
    550 
    551  if (!aIncludeGenerated && frame && frame->IsGeneratedContentFrame()) {
    552    // List markers accessibles represent the generated content element,
    553    // before/after text accessibles represent the child text nodes.
    554    auto generatedElement = content->IsGeneratedContentContainerForMarker()
    555                                ? content
    556                                : content->GetParentElement();
    557    auto parent = generatedElement ? generatedElement->GetParent() : nullptr;
    558    MOZ_ASSERT(parent);
    559    if (parent) {
    560      if (generatedElement->IsGeneratedContentContainerForAfter()) {
    561        // Use the end offset of the parent element for trailing generated
    562        // content.
    563        return {parent, parent->GetChildCount()};
    564      }
    565 
    566      if (generatedElement->IsGeneratedContentContainerForBefore() ||
    567          generatedElement->IsGeneratedContentContainerForMarker()) {
    568        // Use the start offset of the parent element for leading generated
    569        // content.
    570        return {parent, 0};
    571      }
    572 
    573      MOZ_ASSERT_UNREACHABLE("Unknown generated content type!");
    574    }
    575  }
    576 
    577  if (mAcc->IsTextLeaf()) {
    578    // For text nodes, DOM uses a character offset within the node.
    579    return {content, RenderedToContentOffset(mAcc->AsLocal(), mOffset)};
    580  }
    581 
    582  if (!mAcc->IsHyperText()) {
    583    // For non-text nodes (e.g. images), DOM points use the child index within
    584    // the parent. mOffset could be 0 (for the start of the node) or 1 (for the
    585    // end of the node). mOffset could be 1 if this is the last Accessible in a
    586    // container and the point is at the end of the container.
    587    MOZ_ASSERT(mOffset == 0 || mOffset == 1);
    588    nsIContent* parent = content->GetParent();
    589    MOZ_ASSERT(parent);
    590    // ComputeIndexOf() could return Nothing if this is an anonymous child.
    591    if (auto childIndex = parent->ComputeIndexOf(content)) {
    592      return {parent, mOffset == 0 ? *childIndex : *childIndex + 1};
    593    }
    594  }
    595 
    596  // This could be an empty editable container, whitespace or an empty doc. In
    597  // any case, the offset inside should be 0.
    598  MOZ_ASSERT(mOffset == 0);
    599 
    600  if (RefPtr<TextControlElement> textControlElement =
    601          TextControlElement::FromNodeOrNull(content)) {
    602    // This is an empty input, use the shadow root's element.
    603    if (RefPtr<TextEditor> textEditor = textControlElement->GetTextEditor()) {
    604      if (textEditor->IsEmpty()) {
    605        MOZ_ASSERT(mOffset == 0);
    606        return {textEditor->GetRoot(), 0};
    607      }
    608    }
    609  }
    610 
    611  return {content, 0};
    612 }
    613 
    614 static bool IsLineBreakContinuation(nsTextFrame* aContinuation) {
    615  // A fluid continuation always means a new line.
    616  if (aContinuation->HasAnyStateBits(NS_FRAME_IS_FLUID_CONTINUATION)) {
    617    return true;
    618  }
    619  // If both this continuation and the previous continuation are bidi
    620  // continuations, this continuation might be both a bidi split and on a new
    621  // line.
    622  if (!aContinuation->HasAnyStateBits(NS_FRAME_IS_BIDI)) {
    623    return true;
    624  }
    625  nsTextFrame* prev = aContinuation->GetPrevContinuation();
    626  if (!prev) {
    627    // aContinuation is the primary frame. We can't be sure if this starts a new
    628    // line, as there might be other nodes before it. That is handled by
    629    // IsLocalAccAtLineStart.
    630    return false;
    631  }
    632  if (!prev->HasAnyStateBits(NS_FRAME_IS_BIDI)) {
    633    return true;
    634  }
    635  return AreFramesOnDifferentLines(aContinuation, prev);
    636 }
    637 
    638 static bool IsCaretValid(TextLeafPoint aPoint) {
    639  Accessible* acc = aPoint.mAcc;
    640  if (!acc->IsHyperText()) {
    641    acc = acc->Parent();
    642  }
    643  if (!(acc->State() & states::EDITABLE)) {
    644    return true;
    645  }
    646  // The caret is within editable content.
    647  Accessible* focus = FocusMgr() ? FocusMgr()->FocusedAccessible() : nullptr;
    648  if (!focus) {
    649    return false;
    650  }
    651  // If the focus isn't an editor, the caret can't be inside an editor. This
    652  // can happen, for example, when a text input is the last element in a
    653  // container and a user clicks in the empty area at the end of the container.
    654  // In this case, the caret is actually at the end of the container outside the
    655  // input. This can also happen if there is an empty area in a container before
    656  // an input and a user clicks there. TextLeafPoint can't represent either of
    657  // these cases and it's generally not useful. We must not normalize this to
    658  // the nearest leaf because this would put the caret inside an editor which
    659  // isn't focused. Instead, we pretend there is no caret. See bug 1950748 for
    660  // more details.
    661  return focus->State() & states::EDITABLE;
    662 }
    663 
    664 /*** TextLeafPoint ***/
    665 
    666 TextLeafPoint::TextLeafPoint(Accessible* aAcc, int32_t aOffset) {
    667  MOZ_ASSERT(aOffset >= 0 ||
    668             aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
    669  if (!aAcc) {
    670    // Construct an invalid point.
    671    mAcc = nullptr;
    672    mOffset = 0;
    673    return;
    674  }
    675 
    676  // Even though an OuterDoc contains a document, we treat it as a leaf because
    677  // we don't want to move into another document.
    678  if (!aAcc->IsOuterDoc() && aAcc->HasChildren()) {
    679    // Find a leaf. This might not necessarily be a TextLeafAccessible; it
    680    // could be an empty container.
    681    auto GetChild = [&aOffset](Accessible* acc) -> Accessible* {
    682      if (acc->IsOuterDoc()) {
    683        return nullptr;
    684      }
    685      return aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
    686                 ? acc->FirstChild()
    687                 : acc->LastChild();
    688    };
    689 
    690    for (Accessible* acc = GetChild(aAcc); acc; acc = GetChild(acc)) {
    691      mAcc = acc;
    692    }
    693    mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
    694                  ? 0
    695                  : nsAccUtils::TextLength(mAcc);
    696    return;
    697  }
    698  mAcc = aAcc;
    699  mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
    700                ? aOffset
    701                : nsAccUtils::TextLength(mAcc);
    702 }
    703 
    704 bool TextLeafPoint::operator<(const TextLeafPoint& aPoint) const {
    705  if (mAcc == aPoint.mAcc) {
    706    return mOffset < aPoint.mOffset;
    707  }
    708  return mAcc->IsBefore(aPoint.mAcc);
    709 }
    710 
    711 bool TextLeafPoint::operator<=(const TextLeafPoint& aPoint) const {
    712  return *this == aPoint || *this < aPoint;
    713 }
    714 
    715 bool TextLeafPoint::IsDocEdge(nsDirection aDirection) const {
    716  if (aDirection == eDirPrevious) {
    717    return mOffset == 0 && !PrevLeaf(mAcc);
    718  }
    719 
    720  return mOffset == static_cast<int32_t>(nsAccUtils::TextLength(mAcc)) &&
    721         !NextLeaf(mAcc);
    722 }
    723 
    724 bool TextLeafPoint::IsLeafAfterListItemMarker() const {
    725  Accessible* prev = PrevLeaf(mAcc);
    726  return prev && prev->Role() == roles::LISTITEM_MARKER &&
    727         prev->Parent()->IsAncestorOf(mAcc);
    728 }
    729 
    730 bool TextLeafPoint::IsEmptyLastLine() const {
    731  if (mAcc->IsHTMLBr() && mOffset == 1) {
    732    return true;
    733  }
    734  if (!mAcc->IsTextLeaf()) {
    735    return false;
    736  }
    737  if (mOffset < static_cast<int32_t>(nsAccUtils::TextLength(mAcc))) {
    738    return false;
    739  }
    740  nsAutoString text;
    741  mAcc->AppendTextTo(text, mOffset - 1, 1);
    742  return text.CharAt(0) == '\n';
    743 }
    744 
    745 char16_t TextLeafPoint::GetChar() const {
    746  nsAutoString text;
    747  mAcc->AppendTextTo(text, mOffset, 1);
    748  return text.CharAt(0);
    749 }
    750 
    751 TextLeafPoint TextLeafPoint::FindPrevLineStartSameLocalAcc(
    752    bool aIncludeOrigin) const {
    753  LocalAccessible* acc = mAcc->AsLocal();
    754  MOZ_ASSERT(acc);
    755  if (mOffset == 0) {
    756    if (aIncludeOrigin && IsLocalAccAtLineStart(acc)) {
    757      return *this;
    758    }
    759    return TextLeafPoint();
    760  }
    761  nsIFrame* frame = acc->GetFrame();
    762  if (!frame) {
    763    // This can happen if this is an empty element with display: contents. In
    764    // that case, this Accessible contains no lines.
    765    return TextLeafPoint();
    766  }
    767  if (!frame->IsTextFrame()) {
    768    if (IsLocalAccAtLineStart(acc)) {
    769      return TextLeafPoint(acc, 0);
    770    }
    771    return TextLeafPoint();
    772  }
    773  // Each line of a text node is rendered as a continuation frame. Get the
    774  // continuation containing the origin.
    775  int32_t origOffset = mOffset;
    776  origOffset = RenderedToContentOffset(acc, origOffset);
    777  nsTextFrame* continuation = nullptr;
    778  int32_t unusedOffsetInContinuation = 0;
    779  frame->GetChildFrameContainingOffset(
    780      origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation);
    781  MOZ_ASSERT(continuation);
    782  int32_t lineStart = continuation->GetContentOffset();
    783  if (lineStart > 0 && (
    784                           // A line starts at the origin, but the caller
    785                           // doesn't want this included.
    786                           (!aIncludeOrigin && lineStart == origOffset) ||
    787                           !IsLineBreakContinuation(continuation))) {
    788    // Go back one more, skipping continuations that aren't line breaks or the
    789    // primary frame.
    790    for (nsTextFrame* prev = continuation->GetPrevContinuation(); prev;
    791         prev = prev->GetPrevContinuation()) {
    792      continuation = prev;
    793      if (IsLineBreakContinuation(continuation)) {
    794        break;
    795      }
    796    }
    797    MOZ_ASSERT(continuation);
    798    lineStart = continuation->GetContentOffset();
    799  }
    800  MOZ_ASSERT(lineStart >= 0);
    801  MOZ_ASSERT(lineStart == 0 || IsLineBreakContinuation(continuation));
    802  if (lineStart == 0 && !IsLocalAccAtLineStart(acc)) {
    803    // This is the first line of this text node, but there is something else
    804    // on the same line before this text node, so don't return this as a line
    805    // start.
    806    return TextLeafPoint();
    807  }
    808  lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart));
    809  return TextLeafPoint(acc, lineStart);
    810 }
    811 
    812 TextLeafPoint TextLeafPoint::FindNextLineStartSameLocalAcc(
    813    bool aIncludeOrigin) const {
    814  LocalAccessible* acc = mAcc->AsLocal();
    815  MOZ_ASSERT(acc);
    816  if (aIncludeOrigin && mOffset == 0 && IsLocalAccAtLineStart(acc)) {
    817    return *this;
    818  }
    819  nsIFrame* frame = acc->GetFrame();
    820  if (!frame) {
    821    // This can happen if this is an empty element with display: contents. In
    822    // that case, this Accessible contains no lines.
    823    return TextLeafPoint();
    824  }
    825  if (!frame->IsTextFrame()) {
    826    // There can't be multiple lines in a non-text leaf.
    827    return TextLeafPoint();
    828  }
    829  // Each line of a text node is rendered as a continuation frame. Get the
    830  // continuation containing the origin.
    831  int32_t origOffset = mOffset;
    832  origOffset = RenderedToContentOffset(acc, origOffset);
    833  nsTextFrame* continuation = nullptr;
    834  int32_t unusedOffsetInContinuation = 0;
    835  frame->GetChildFrameContainingOffset(
    836      origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation);
    837  MOZ_ASSERT(continuation);
    838  if (
    839      // A line starts at the origin and the caller wants this included.
    840      aIncludeOrigin && continuation->GetContentOffset() == origOffset &&
    841      IsLineBreakContinuation(continuation) &&
    842      // If this is the first line of this text node (offset 0), don't treat it
    843      // as a line start if there's something else on the line before this text
    844      // node.
    845      !(origOffset == 0 && !IsLocalAccAtLineStart(acc))) {
    846    return *this;
    847  }
    848  // Get the next continuation, skipping continuations that aren't line breaks.
    849  while ((continuation = continuation->GetNextContinuation())) {
    850    if (IsLineBreakContinuation(continuation)) {
    851      break;
    852    }
    853  }
    854  if (!continuation) {
    855    return TextLeafPoint();
    856  }
    857  int32_t lineStart = continuation->GetContentOffset();
    858  lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart));
    859  return TextLeafPoint(acc, lineStart);
    860 }
    861 
    862 TextLeafPoint TextLeafPoint::FindLineStartSameRemoteAcc(
    863    nsDirection aDirection, bool aIncludeOrigin) const {
    864  RemoteAccessible* acc = mAcc->AsRemote();
    865  MOZ_ASSERT(acc);
    866  auto lines = acc->GetCachedTextLines();
    867  if (!lines) {
    868    return TextLeafPoint();
    869  }
    870  size_t index;
    871  // If BinarySearch returns true, mOffset is in the array and index points at
    872  // it. If BinarySearch returns false, mOffset is not in the array and index
    873  // points at the next line start after mOffset.
    874  if (BinarySearch(*lines, 0, lines->Length(), mOffset, &index)) {
    875    if (aIncludeOrigin) {
    876      return *this;
    877    }
    878    if (aDirection == eDirNext) {
    879      // We don't want to include the origin. Get the next line start.
    880      ++index;
    881    }
    882  }
    883  MOZ_ASSERT(index <= lines->Length());
    884  if ((aDirection == eDirNext && index == lines->Length()) ||
    885      (aDirection == eDirPrevious && index == 0)) {
    886    return TextLeafPoint();
    887  }
    888  // index points at the line start after mOffset.
    889  if (aDirection == eDirPrevious) {
    890    --index;
    891  }
    892  return TextLeafPoint(mAcc, lines->ElementAt(index));
    893 }
    894 
    895 TextLeafPoint TextLeafPoint::FindLineStartSameAcc(
    896    nsDirection aDirection, bool aIncludeOrigin,
    897    bool aIgnoreListItemMarker) const {
    898  TextLeafPoint boundary;
    899  if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 &&
    900      IsLeafAfterListItemMarker()) {
    901    // If:
    902    // (1) we are ignoring list markers
    903    // (2) we should include origin
    904    // (3) we are at the start of a leaf that follows a list item marker
    905    // ...then return this point.
    906    return *this;
    907  }
    908 
    909  if (mAcc->IsLocal()) {
    910    boundary = aDirection == eDirNext
    911                   ? FindNextLineStartSameLocalAcc(aIncludeOrigin)
    912                   : FindPrevLineStartSameLocalAcc(aIncludeOrigin);
    913  } else {
    914    boundary = FindLineStartSameRemoteAcc(aDirection, aIncludeOrigin);
    915  }
    916 
    917  if (aIgnoreListItemMarker && aDirection == eDirPrevious && !boundary &&
    918      mOffset != 0 && IsLeafAfterListItemMarker()) {
    919    // If:
    920    // (1) we are ignoring list markers
    921    // (2) we are searching backwards in accessible
    922    // (3) we did not find a line start before this point
    923    // (4) we are in a leaf that follows a list item marker
    924    // ...then return the first point in this accessible.
    925    boundary = TextLeafPoint(mAcc, 0);
    926  }
    927 
    928  return boundary;
    929 }
    930 
    931 TextLeafPoint TextLeafPoint::FindPrevWordStartSameAcc(
    932    bool aIncludeOrigin) const {
    933  if (mOffset == 0 && !aIncludeOrigin) {
    934    // We can't go back any further and the caller doesn't want the origin
    935    // included, so there's nothing more to do.
    936    return TextLeafPoint();
    937  }
    938  nsAutoString text;
    939  mAcc->AppendTextTo(text);
    940  TextLeafPoint lineStart = *this;
    941  if (!aIncludeOrigin || (lineStart.mOffset == 1 && text.Length() == 1 &&
    942                          text.CharAt(0) == '\n')) {
    943    // We're not interested in a line that starts here, either because
    944    // aIncludeOrigin is false or because we're at the end of a line break
    945    // node.
    946    --lineStart.mOffset;
    947  }
    948  // A word never starts with a line feed character. If there are multiple
    949  // consecutive line feed characters and we're after the first of them, the
    950  // previous line start will be a line feed character. Skip this and any prior
    951  // consecutive line feed first.
    952  for (; lineStart.mOffset >= 0 && text.CharAt(lineStart.mOffset) == '\n';
    953       --lineStart.mOffset) {
    954  }
    955  if (lineStart.mOffset < 0) {
    956    // There's no line start for our purposes.
    957    lineStart = TextLeafPoint();
    958  } else {
    959    lineStart =
    960        lineStart.FindLineStartSameAcc(eDirPrevious, /* aIncludeOrigin */ true);
    961  }
    962  // Keep walking backward until we find an acceptable word start.
    963  intl::WordRange word;
    964  if (mOffset == 0) {
    965    word.mBegin = 0;
    966  } else if (mOffset == static_cast<int32_t>(text.Length())) {
    967    word = WordBreaker::FindWord(
    968        text, mOffset - 1,
    969        StaticPrefs::layout_word_select_stop_at_punctuation()
    970            ? FindWordOptions::StopAtPunctuation
    971            : FindWordOptions::None);
    972  } else {
    973    word = WordBreaker::FindWord(
    974        text, mOffset,
    975        StaticPrefs::layout_word_select_stop_at_punctuation()
    976            ? FindWordOptions::StopAtPunctuation
    977            : FindWordOptions::None);
    978  }
    979  for (;; word = WordBreaker::FindWord(
    980              text, word.mBegin - 1,
    981              StaticPrefs::layout_word_select_stop_at_punctuation()
    982                  ? FindWordOptions::StopAtPunctuation
    983                  : FindWordOptions::None)) {
    984    if (!aIncludeOrigin && static_cast<int32_t>(word.mBegin) == mOffset) {
    985      // A word possibly starts at the origin, but the caller doesn't want this
    986      // included.
    987      MOZ_ASSERT(word.mBegin != 0);
    988      continue;
    989    }
    990    if (lineStart && static_cast<int32_t>(word.mBegin) < lineStart.mOffset) {
    991      // A line start always starts a new word.
    992      return lineStart;
    993    }
    994    if (IsAcceptableWordStart(mAcc, text, static_cast<int32_t>(word.mBegin))) {
    995      break;
    996    }
    997    if (word.mBegin == 0) {
    998      // We can't go back any further.
    999      if (lineStart) {
   1000        // A line start always starts a new word.
   1001        return lineStart;
   1002      }
   1003      return TextLeafPoint();
   1004    }
   1005  }
   1006  return TextLeafPoint(mAcc, static_cast<int32_t>(word.mBegin));
   1007 }
   1008 
   1009 TextLeafPoint TextLeafPoint::FindNextWordStartSameAcc(
   1010    bool aIncludeOrigin) const {
   1011  nsAutoString text;
   1012  mAcc->AppendTextTo(text);
   1013  int32_t wordStart = mOffset;
   1014  if (aIncludeOrigin) {
   1015    if (wordStart == 0) {
   1016      if (IsAcceptableWordStart(mAcc, text, 0)) {
   1017        return *this;
   1018      }
   1019    } else {
   1020      // The origin might start a word, so search from just before it.
   1021      --wordStart;
   1022    }
   1023  }
   1024  TextLeafPoint lineStart = FindLineStartSameAcc(eDirNext, aIncludeOrigin);
   1025  if (lineStart) {
   1026    // A word never starts with a line feed character. If there are multiple
   1027    // consecutive line feed characters, lineStart will point at the second of
   1028    // them. Skip this and any subsequent consecutive line feed.
   1029    for (; lineStart.mOffset < static_cast<int32_t>(text.Length()) &&
   1030           text.CharAt(lineStart.mOffset) == '\n';
   1031         ++lineStart.mOffset) {
   1032    }
   1033    if (lineStart.mOffset == static_cast<int32_t>(text.Length())) {
   1034      // There's no line start for our purposes.
   1035      lineStart = TextLeafPoint();
   1036    }
   1037  }
   1038  // Keep walking forward until we find an acceptable word start.
   1039  intl::WordBreakIteratorUtf16 wordBreakIter(text);
   1040  int32_t previousPos = wordStart;
   1041  Maybe<uint32_t> nextBreak = wordBreakIter.Seek(wordStart);
   1042  for (;;) {
   1043    if (!nextBreak || *nextBreak == text.Length()) {
   1044      if (lineStart) {
   1045        // A line start always starts a new word.
   1046        return lineStart;
   1047      }
   1048      if (StaticPrefs::layout_word_select_stop_at_punctuation()) {
   1049        // If layout.word_select.stop_at_punctuation is true, we have to look
   1050        // for punctuation class since it may not break state in UAX#29.
   1051        for (int32_t i = previousPos + 1;
   1052             i < static_cast<int32_t>(text.Length()); i++) {
   1053          if (IsAcceptableWordStart(mAcc, text, i)) {
   1054            return TextLeafPoint(mAcc, i);
   1055          }
   1056        }
   1057      }
   1058      return TextLeafPoint();
   1059    }
   1060    wordStart = AssertedCast<int32_t>(*nextBreak);
   1061    if (lineStart && wordStart > lineStart.mOffset) {
   1062      // A line start always starts a new word.
   1063      return lineStart;
   1064    }
   1065    if (IsAcceptableWordStart(mAcc, text, wordStart)) {
   1066      break;
   1067    }
   1068 
   1069    if (StaticPrefs::layout_word_select_stop_at_punctuation()) {
   1070      // If layout.word_select.stop_at_punctuation is true, we have to look
   1071      // for punctuation class since it may not break state in UAX#29.
   1072      for (int32_t i = previousPos + 1; i < wordStart; i++) {
   1073        if (IsAcceptableWordStart(mAcc, text, i)) {
   1074          return TextLeafPoint(mAcc, i);
   1075        }
   1076      }
   1077    }
   1078    previousPos = wordStart;
   1079    nextBreak = wordBreakIter.Next();
   1080  }
   1081  return TextLeafPoint(mAcc, wordStart);
   1082 }
   1083 
   1084 /* static */
   1085 TextLeafPoint TextLeafPoint::GetCaret(Accessible* aAcc) {
   1086  if (LocalAccessible* localAcc = aAcc->AsLocal()) {
   1087    // Use the HyperTextAccessible caret offset. Eventually, we'll want to move
   1088    // that code into TextLeafPoint, but existing code depends on it being based
   1089    // on HyperTextAccessible (including caret events).
   1090    int32_t htOffset = -1;
   1091    // Try the cached caret.
   1092    HyperTextAccessible* ht = SelectionMgr()->AccessibleWithCaret(&htOffset);
   1093    if (ht) {
   1094      MOZ_ASSERT(htOffset != -1);
   1095    } else {
   1096      // There is no cached caret, but there might still be a caret; see bug
   1097      // 1425112.
   1098      ht = HyperTextFor(localAcc);
   1099      if (!ht) {
   1100        return TextLeafPoint();
   1101      }
   1102      // An offset can only refer to a child, but the caret might be in a deeper
   1103      // descendant. Walk to the deepest HyperTextAccessible using CaretOffset.
   1104      bool gotCaret = false;
   1105      for (;;) {
   1106        htOffset = ht->CaretOffset();
   1107        if (htOffset == -1) {
   1108          break;
   1109        }
   1110        // A descendant might return -1 in some cases, but it's okay as long as
   1111        // the call on the outermost HyperTextAccessible succeeds.
   1112        gotCaret = true;
   1113        LocalAccessible* child = ht->GetChildAtOffset(htOffset);
   1114        if (!child) {
   1115          break;
   1116        }
   1117        if (HyperTextAccessible* childHt = child->AsHyperText()) {
   1118          ht = childHt;
   1119        } else {
   1120          break;
   1121        }
   1122      }
   1123      if (!gotCaret) {
   1124        return TextLeafPoint();
   1125      }
   1126    }
   1127    // As noted above, CaretOffset on a descendant might return -1. Use 0 in
   1128    // that case.
   1129    TextLeafPoint point = ht->ToTextLeafPoint(htOffset == -1 ? 0 : htOffset);
   1130    if (!point) {
   1131      // Bug 1905021: This happens in the wild, but we don't understand why.
   1132      // ToTextLeafPoint should only fail if the HyperText offset is invalid,
   1133      // but CaretOffset shouldn't return an invalid offset.
   1134      MOZ_ASSERT_UNREACHABLE(
   1135          "Got HyperText CaretOffset but ToTextLeafPoint failed");
   1136      return point;
   1137    }
   1138    if (!IsCaretValid(point)) {
   1139      return TextLeafPoint();
   1140    }
   1141    nsIFrame* frame = ht->GetFrame();
   1142    RefPtr<nsFrameSelection> sel = frame ? frame->GetFrameSelection() : nullptr;
   1143    if (sel && sel->GetHint() == CaretAssociationHint::Before) {
   1144      // CaretAssociationHint::Before can mean that the caret is at the end of
   1145      // a line. However, this can also occur in a few other situations:
   1146      // 1. The caret is before the start of a node in the middle of a line.
   1147      // This happens when moving the cursor forward to a new node.
   1148      // 2. The user clicks the mouse on a character other than the first in a
   1149      // node.
   1150      // 3. The caret is somewhere other than the start of a line and the user
   1151      // presses down or up arrow to move by line.
   1152      if (point.mOffset <
   1153          static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc))) {
   1154        // The caret is at the end of a line if the point is at the start of a
   1155        // line but not at the start of a paragraph.
   1156        point.mIsEndOfLineInsertionPoint =
   1157            point.FindPrevLineStartSameLocalAcc(/* aIncludeOrigin */ true) ==
   1158                point &&
   1159            !point.IsParagraphStart();
   1160      } else {
   1161        // This is the end of a node. CaretAssociationHint::Before is only used
   1162        // at the end of a node if the caret is at the end of a line.
   1163        point.mIsEndOfLineInsertionPoint = true;
   1164      }
   1165    }
   1166    return point;
   1167  }
   1168 
   1169  // Ideally, we'd cache the caret as a leaf, but our events are based on
   1170  // HyperText for now.
   1171  DocAccessibleParent* remoteDoc = aAcc->AsRemote()->Document();
   1172  auto [ht, htOffset] = remoteDoc->GetCaret();
   1173  if (!ht) {
   1174    return TextLeafPoint();
   1175  }
   1176  TextLeafPoint point = ht->ToTextLeafPoint(htOffset);
   1177  if (!point) {
   1178    // The caret offset should usually be in sync with the tree. However, tree
   1179    // and selection updates happen using separate IPDL calls, so it's possible
   1180    // for a client caret query to arrive between them. Thus, we can end up
   1181    // with an invalid caret here.
   1182    return point;
   1183  }
   1184  if (!IsCaretValid(point)) {
   1185    return TextLeafPoint();
   1186  }
   1187  point.mIsEndOfLineInsertionPoint = remoteDoc->IsCaretAtEndOfLine();
   1188  return point;
   1189 }
   1190 
   1191 TextLeafPoint TextLeafPoint::AdjustEndOfLine() const {
   1192  MOZ_ASSERT(mIsEndOfLineInsertionPoint);
   1193  // Use the last character on the line so that we search for word and line
   1194  // boundaries on the current line, not the next line.
   1195  return TextLeafPoint(mAcc, mOffset)
   1196      .FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
   1197 }
   1198 
   1199 TextLeafPoint TextLeafPoint::FindBoundary(AccessibleTextBoundary aBoundaryType,
   1200                                          nsDirection aDirection,
   1201                                          BoundaryFlags aFlags) const {
   1202  if (mIsEndOfLineInsertionPoint) {
   1203    // In this block, we deliberately don't propagate mIsEndOfLineInsertionPoint
   1204    // to derived points because otherwise, a call to FindBoundary on the
   1205    // returned point would also return the same point.
   1206    if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR ||
   1207        aBoundaryType == nsIAccessibleText::BOUNDARY_CLUSTER) {
   1208      if (aDirection == eDirNext || (aDirection == eDirPrevious &&
   1209                                     aFlags & BoundaryFlags::eIncludeOrigin)) {
   1210        // The caller wants the current or next character/cluster. Return no
   1211        // character, since otherwise, this would move past the first character
   1212        // on the next line.
   1213        return TextLeafPoint(mAcc, mOffset);
   1214      }
   1215      // The caller wants the previous character/cluster. Return that as normal.
   1216      return TextLeafPoint(mAcc, mOffset)
   1217          .FindBoundary(aBoundaryType, aDirection, aFlags);
   1218    }
   1219    // For any other boundary, we need to start on this line, not the next, even
   1220    // though mOffset refers to the next.
   1221    return AdjustEndOfLine().FindBoundary(aBoundaryType, aDirection, aFlags);
   1222  }
   1223 
   1224  bool inEditableAndStopInIt = (aFlags & BoundaryFlags::eStopInEditable) &&
   1225                               mAcc->Parent() &&
   1226                               (mAcc->Parent()->State() & states::EDITABLE);
   1227  if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) {
   1228    return FindLineEnd(aDirection,
   1229                       inEditableAndStopInIt
   1230                           ? aFlags
   1231                           : (aFlags & ~BoundaryFlags::eStopInEditable));
   1232  }
   1233  if (aBoundaryType == nsIAccessibleText::BOUNDARY_WORD_END) {
   1234    return FindWordEnd(aDirection,
   1235                       inEditableAndStopInIt
   1236                           ? aFlags
   1237                           : (aFlags & ~BoundaryFlags::eStopInEditable));
   1238  }
   1239  if ((aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_START ||
   1240       aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) &&
   1241      (aFlags & BoundaryFlags::eIncludeOrigin) && aDirection == eDirPrevious &&
   1242      IsEmptyLastLine()) {
   1243    // If we're at an empty line at the end of an Accessible,  we don't want to
   1244    // walk into the previous line. For example, this can happen if the caret
   1245    // is positioned on an empty line at the end of a textarea.
   1246    return *this;
   1247  }
   1248  bool includeOrigin = !!(aFlags & BoundaryFlags::eIncludeOrigin);
   1249  bool ignoreListItemMarker = !!(aFlags & BoundaryFlags::eIgnoreListItemMarker);
   1250  Accessible* lastAcc = nullptr;
   1251  for (TextLeafPoint searchFrom = *this; searchFrom;
   1252       searchFrom = searchFrom.NeighborLeafPoint(
   1253           aDirection, inEditableAndStopInIt, ignoreListItemMarker)) {
   1254    lastAcc = searchFrom.mAcc;
   1255    if (ignoreListItemMarker && searchFrom == *this &&
   1256        searchFrom.mAcc->Role() == roles::LISTITEM_MARKER) {
   1257      continue;
   1258    }
   1259    TextLeafPoint boundary;
   1260    // Search for the boundary within the current Accessible.
   1261    switch (aBoundaryType) {
   1262      case nsIAccessibleText::BOUNDARY_CHAR:
   1263        if (includeOrigin) {
   1264          boundary = searchFrom;
   1265        } else if (aDirection == eDirPrevious && searchFrom.mOffset > 0) {
   1266          boundary.mAcc = searchFrom.mAcc;
   1267          boundary.mOffset = searchFrom.mOffset - 1;
   1268        } else if (aDirection == eDirNext &&
   1269                   searchFrom.mOffset + 1 <
   1270                       static_cast<int32_t>(
   1271                           nsAccUtils::TextLength(searchFrom.mAcc))) {
   1272          boundary.mAcc = searchFrom.mAcc;
   1273          boundary.mOffset = searchFrom.mOffset + 1;
   1274        }
   1275        break;
   1276      case nsIAccessibleText::BOUNDARY_WORD_START:
   1277        if (aDirection == eDirPrevious) {
   1278          boundary = searchFrom.FindPrevWordStartSameAcc(includeOrigin);
   1279        } else {
   1280          boundary = searchFrom.FindNextWordStartSameAcc(includeOrigin);
   1281        }
   1282        break;
   1283      case nsIAccessibleText::BOUNDARY_LINE_START:
   1284        boundary = searchFrom.FindLineStartSameAcc(aDirection, includeOrigin,
   1285                                                   ignoreListItemMarker);
   1286        break;
   1287      case nsIAccessibleText::BOUNDARY_PARAGRAPH:
   1288        boundary = searchFrom.FindParagraphSameAcc(aDirection, includeOrigin,
   1289                                                   ignoreListItemMarker);
   1290        break;
   1291      case nsIAccessibleText::BOUNDARY_CLUSTER:
   1292        boundary = searchFrom.FindClusterSameAcc(aDirection, includeOrigin);
   1293        break;
   1294      default:
   1295        MOZ_ASSERT_UNREACHABLE();
   1296        break;
   1297    }
   1298    if (boundary) {
   1299      return boundary;
   1300    }
   1301 
   1302    // The start/end of the Accessible might be a boundary. If so, we must stop
   1303    // on it.
   1304    includeOrigin = true;
   1305  }
   1306 
   1307  MOZ_ASSERT(lastAcc);
   1308  // No further leaf was found. Use the start/end of the first/last leaf.
   1309  return TextLeafPoint(
   1310      lastAcc, aDirection == eDirPrevious
   1311                   ? 0
   1312                   : static_cast<int32_t>(nsAccUtils::TextLength(lastAcc)));
   1313 }
   1314 
   1315 TextLeafPoint TextLeafPoint::FindLineEnd(nsDirection aDirection,
   1316                                         BoundaryFlags aFlags) const {
   1317  if (aDirection == eDirPrevious && IsEmptyLastLine()) {
   1318    // If we're at an empty line at the end of an Accessible,  we don't want to
   1319    // walk into the previous line. For example, this can happen if the caret
   1320    // is positioned on an empty line at the end of a textarea.
   1321    // Because we want the line end, we must walk back to the line feed
   1322    // character.
   1323    return FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
   1324                        aFlags & ~BoundaryFlags::eIncludeOrigin);
   1325  }
   1326  if ((aFlags & BoundaryFlags::eIncludeOrigin) && IsLineFeedChar()) {
   1327    return *this;
   1328  }
   1329  if (aDirection == eDirPrevious && !(aFlags & BoundaryFlags::eIncludeOrigin)) {
   1330    // If there is a line feed immediately before us, return that.
   1331    TextLeafPoint prevChar =
   1332        FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
   1333                     aFlags & ~BoundaryFlags::eIncludeOrigin);
   1334    if (prevChar.IsLineFeedChar()) {
   1335      return prevChar;
   1336    }
   1337  }
   1338  TextLeafPoint searchFrom = *this;
   1339  if (aDirection == eDirNext && IsLineFeedChar()) {
   1340    // If we search for the next line start from a line feed, we'll get the
   1341    // character immediately following the line feed. We actually want the
   1342    // next line start after that. Skip the line feed.
   1343    searchFrom = FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
   1344                              aFlags & ~BoundaryFlags::eIncludeOrigin);
   1345  }
   1346  TextLeafPoint lineStart = searchFrom.FindBoundary(
   1347      nsIAccessibleText::BOUNDARY_LINE_START, aDirection, aFlags);
   1348  if (aDirection == eDirNext && IsEmptyLastLine()) {
   1349    // There is a line feed immediately before us, but that's actually the end
   1350    // of the previous line, not the end of our empty line. Don't walk back.
   1351    return lineStart;
   1352  }
   1353  // If there is a line feed before this line start (at the end of the previous
   1354  // line), we must return that.
   1355  TextLeafPoint prevChar =
   1356      lineStart.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
   1357                             aFlags & ~BoundaryFlags::eIncludeOrigin);
   1358  if (prevChar && prevChar.IsLineFeedChar()) {
   1359    return prevChar;
   1360  }
   1361  return lineStart;
   1362 }
   1363 
   1364 bool TextLeafPoint::IsSpace() const {
   1365  return GetWordBreakClass(GetChar()) == eWbcSpace;
   1366 }
   1367 
   1368 TextLeafPoint TextLeafPoint::FindWordEnd(nsDirection aDirection,
   1369                                         BoundaryFlags aFlags) const {
   1370  char16_t origChar = GetChar();
   1371  const bool origIsSpace = GetWordBreakClass(origChar) == eWbcSpace;
   1372  bool prevIsSpace = false;
   1373  if (aDirection == eDirPrevious ||
   1374      ((aFlags & BoundaryFlags::eIncludeOrigin) && origIsSpace) || !origChar) {
   1375    TextLeafPoint prev =
   1376        FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
   1377                     aFlags & ~BoundaryFlags::eIncludeOrigin);
   1378    if (aDirection == eDirPrevious && prev == *this) {
   1379      return *this;  // Can't go any further.
   1380    }
   1381    prevIsSpace = prev.IsSpace();
   1382    if ((aFlags & BoundaryFlags::eIncludeOrigin) &&
   1383        (origIsSpace || IsDocEdge(eDirNext)) && !prevIsSpace) {
   1384      // The origin is space or end of document, but the previous
   1385      // character is not. This means we're at the end of a word.
   1386      return *this;
   1387    }
   1388  }
   1389  TextLeafPoint boundary = *this;
   1390  if (aDirection == eDirPrevious && !prevIsSpace) {
   1391    // If there isn't space immediately before us, first find the start of the
   1392    // previous word.
   1393    boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START,
   1394                            eDirPrevious, aFlags);
   1395  } else if (aDirection == eDirNext &&
   1396             (origIsSpace || (!origChar && prevIsSpace))) {
   1397    // We're within the space at the end of the word. Skip over the space. We
   1398    // can do that by searching for the next word start.
   1399    boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirNext,
   1400                            aFlags & ~BoundaryFlags::eIncludeOrigin);
   1401    if (boundary.IsSpace()) {
   1402      // The next word starts with a space. This can happen if there is a space
   1403      // after or at the start of a block element.
   1404      return boundary;
   1405    }
   1406  }
   1407  if (aDirection == eDirNext) {
   1408    BoundaryFlags flags = aFlags;
   1409    if (IsDocEdge(eDirPrevious)) {
   1410      // If this is the start of the doc don't be inclusive in the word-start
   1411      // search because there is no preceding block where this could be a
   1412      // word-end for.
   1413      flags &= ~BoundaryFlags::eIncludeOrigin;
   1414    }
   1415    boundary = boundary.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START,
   1416                                     eDirNext, flags);
   1417  }
   1418  // At this point, boundary is either the start of a word or at a space. A
   1419  // word ends at the beginning of consecutive space. Therefore, skip back to
   1420  // the start of any space before us.
   1421  TextLeafPoint prev = boundary;
   1422  for (;;) {
   1423    prev = prev.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
   1424                             aFlags & ~BoundaryFlags::eIncludeOrigin);
   1425    if (prev == boundary) {
   1426      break;  // Can't go any further.
   1427    }
   1428    if (!prev.IsSpace()) {
   1429      break;
   1430    }
   1431    boundary = prev;
   1432  }
   1433  return boundary;
   1434 }
   1435 
   1436 TextLeafPoint TextLeafPoint::FindParagraphSameAcc(
   1437    nsDirection aDirection, bool aIncludeOrigin,
   1438    bool aIgnoreListItemMarker) const {
   1439  if (aIncludeOrigin && IsDocEdge(eDirPrevious)) {
   1440    // The top of the document is a paragraph boundary.
   1441    return *this;
   1442  }
   1443 
   1444  if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 &&
   1445      IsLeafAfterListItemMarker()) {
   1446    // If we are in a list item and the previous sibling is
   1447    // a bullet, the 0 offset in this leaf is a line start.
   1448    return *this;
   1449  }
   1450 
   1451  if (mAcc->IsTextLeaf() &&
   1452      // We don't want to copy strings unnecessarily. See below for the context
   1453      // of these individual conditions.
   1454      ((aIncludeOrigin && mOffset > 0) || aDirection == eDirNext ||
   1455       mOffset >= 2)) {
   1456    // If there is a line feed, a new paragraph begins after it.
   1457    nsAutoString text;
   1458    mAcc->AppendTextTo(text);
   1459    if (aIncludeOrigin && mOffset > 0 && text.CharAt(mOffset - 1) == '\n') {
   1460      return TextLeafPoint(mAcc, mOffset);
   1461    }
   1462    int32_t lfOffset = -1;
   1463    if (aDirection == eDirNext) {
   1464      lfOffset = text.FindChar('\n', mOffset);
   1465    } else if (mOffset >= 2) {
   1466      // A line feed at mOffset - 1 means the origin begins a new paragraph,
   1467      // but we already handled aIncludeOrigin above. Therefore, we search from
   1468      // mOffset - 2.
   1469      lfOffset = text.RFindChar('\n', mOffset - 2);
   1470    }
   1471    if (lfOffset != -1 && lfOffset + 1 < static_cast<int32_t>(text.Length())) {
   1472      return TextLeafPoint(mAcc, lfOffset + 1);
   1473    }
   1474  }
   1475 
   1476  if (aIgnoreListItemMarker && mOffset > 0 && aDirection == eDirPrevious &&
   1477      IsLeafAfterListItemMarker()) {
   1478    // No line breaks were found in the preceding text to this offset.
   1479    // If we are in a list item and the previous sibling is
   1480    // a bullet, the 0 offset in this leaf is a line start.
   1481    return TextLeafPoint(mAcc, 0);
   1482  }
   1483 
   1484  // Check whether this Accessible begins a paragraph.
   1485  if ((!aIncludeOrigin && mOffset == 0) ||
   1486      (aDirection == eDirNext && mOffset > 0)) {
   1487    // The caller isn't interested in whether this Accessible begins a
   1488    // paragraph.
   1489    return TextLeafPoint();
   1490  }
   1491  Accessible* prevLeaf = PrevLeaf(mAcc);
   1492  BlockRule blockRule;
   1493  Pivot pivot(nsAccUtils::DocumentFor(mAcc));
   1494  Accessible* prevBlock = pivot.Prev(mAcc, blockRule);
   1495  // Check if we're the first leaf after a block element.
   1496  if (prevBlock) {
   1497    if (
   1498        // If there's no previous leaf, we must be the first leaf after the
   1499        // block.
   1500        !prevLeaf ||
   1501        // A block can be a leaf; e.g. an empty div or paragraph.
   1502        prevBlock == prevLeaf) {
   1503      return TextLeafPoint(mAcc, 0);
   1504    }
   1505    if (prevBlock->IsAncestorOf(mAcc)) {
   1506      // We're inside the block.
   1507      if (!prevBlock->IsAncestorOf(prevLeaf)) {
   1508        // The previous leaf isn't inside the block. That means we're the first
   1509        // leaf in the block.
   1510        return TextLeafPoint(mAcc, 0);
   1511      }
   1512    } else {
   1513      // We aren't inside the block, so the block ends before us.
   1514      if (prevBlock->IsAncestorOf(prevLeaf)) {
   1515        // The previous leaf is inside the block. That means we're the first
   1516        // leaf after the block. This case is necessary because a block causes a
   1517        // paragraph break both before and after it.
   1518        return TextLeafPoint(mAcc, 0);
   1519      }
   1520    }
   1521  }
   1522  if (!prevLeaf || prevLeaf->IsHTMLBr()) {
   1523    // We're the first leaf after a line break or the start of the document.
   1524    return TextLeafPoint(mAcc, 0);
   1525  }
   1526  if (prevLeaf->IsTextLeaf()) {
   1527    // There's a text leaf before us. Check if it ends with a line feed.
   1528    nsAutoString text;
   1529    prevLeaf->AppendTextTo(text, nsAccUtils::TextLength(prevLeaf) - 1, 1);
   1530    if (text.CharAt(0) == '\n') {
   1531      return TextLeafPoint(mAcc, 0);
   1532    }
   1533  }
   1534  return TextLeafPoint();
   1535 }
   1536 
   1537 TextLeafPoint TextLeafPoint::FindClusterSameAcc(nsDirection aDirection,
   1538                                                bool aIncludeOrigin) const {
   1539  // We don't support clusters which cross nodes. We can live with that because
   1540  // editor doesn't seem to fully support this either.
   1541  if (aIncludeOrigin && mOffset == 0) {
   1542    // Since we don't cross nodes, offset 0 always begins a cluster.
   1543    return *this;
   1544  }
   1545  if (aDirection == eDirPrevious) {
   1546    if (mOffset == 0) {
   1547      // We can't go back any further.
   1548      return TextLeafPoint();
   1549    }
   1550    if (!aIncludeOrigin && mOffset == 1) {
   1551      // Since we don't cross nodes, offset 0 always begins a cluster. We can't
   1552      // take this fast path if aIncludeOrigin is true because offset 1 might
   1553      // start a cluster, but we don't know that yet.
   1554      return TextLeafPoint(mAcc, 0);
   1555    }
   1556  }
   1557  nsAutoString text;
   1558  mAcc->AppendTextTo(text);
   1559  if (text.IsEmpty()) {
   1560    return TextLeafPoint();
   1561  }
   1562  if (aDirection == eDirNext &&
   1563      mOffset == static_cast<int32_t>(text.Length())) {
   1564    return TextLeafPoint();
   1565  }
   1566  // There is GraphemeClusterBreakReverseIteratorUtf16, but it "doesn't
   1567  // handle conjoining Jamo and emoji". Therefore, we must use
   1568  // GraphemeClusterBreakIteratorUtf16 even when moving backward.
   1569  // GraphemeClusterBreakIteratorUtf16::Seek() always starts from the beginning
   1570  // and repeatedly calls Next(), regardless of the seek offset. The best we
   1571  // can do is call Next() until we find the offset we need.
   1572  intl::GraphemeClusterBreakIteratorUtf16 iter(text);
   1573  // Since we don't cross nodes, offset 0 always begins a cluster.
   1574  int32_t prevCluster = 0;
   1575  while (Maybe<uint32_t> next = iter.Next()) {
   1576    int32_t cluster = static_cast<int32_t>(*next);
   1577    if (aIncludeOrigin && cluster == mOffset) {
   1578      return *this;
   1579    }
   1580    if (aDirection == eDirPrevious) {
   1581      if (cluster >= mOffset) {
   1582        return TextLeafPoint(mAcc, prevCluster);
   1583      }
   1584      prevCluster = cluster;
   1585    } else if (cluster > mOffset) {
   1586      MOZ_ASSERT(aDirection == eDirNext);
   1587      return TextLeafPoint(mAcc, cluster);
   1588    }
   1589  }
   1590  return TextLeafPoint();
   1591 }
   1592 
   1593 void TextLeafPoint::AddTextOffsetAttributes(AccAttributes* aAttrs) const {
   1594  auto expose = [aAttrs](nsAtom* aAttr) {
   1595    if (aAttr == nsGkAtoms::spelling || aAttr == nsGkAtoms::grammar) {
   1596      // XXX We don't correctly handle exposure of overlapping spelling and
   1597      // grammar errors. See bug 1944217. For now, we expose the one we most
   1598      // recently encountered.
   1599      aAttrs->SetAttribute(nsGkAtoms::invalid, aAttr);
   1600    } else if (aAttr == nsGkAtoms::mark) {
   1601      aAttrs->SetAttribute(aAttr, true);
   1602    }
   1603  };
   1604 
   1605  if (LocalAccessible* acc = mAcc->AsLocal()) {
   1606    auto ranges = FindDOMTextOffsetAttributes(acc, mOffset, mOffset + 1);
   1607    for (auto& [domRanges, attr] : ranges) {
   1608      MOZ_ASSERT(domRanges.Length() >= 1);
   1609      expose(attr);
   1610    }
   1611    return;
   1612  }
   1613 
   1614  RemoteAccessible* acc = mAcc->AsRemote();
   1615  MOZ_ASSERT(acc);
   1616  if (RequestDomainsIfInactive(CacheDomain::TextOffsetAttributes)) {
   1617    return;
   1618  }
   1619  if (!acc->mCachedFields) {
   1620    return;
   1621  }
   1622  auto offsetAttrs =
   1623      acc->mCachedFields->GetAttribute<nsTArray<TextOffsetAttribute>>(
   1624          CacheKey::TextOffsetAttributes);
   1625  if (!offsetAttrs) {
   1626    return;
   1627  }
   1628  // offsetAttrs is sorted by mStartOffset, but ranges can overlap each other.
   1629  // Thus, we must check all ranges with an encompassing start offset.
   1630  for (const TextOffsetAttribute& range : *offsetAttrs) {
   1631    if (range.mStartOffset > mOffset) {
   1632      // offsetAttrs is sorted by mStartOffset. Therefor, there aren't any
   1633      // ranges of interest after this.
   1634      break;
   1635    }
   1636    if (range.mEndOffset != TextOffsetAttribute::kOutsideLeaf &&
   1637        range.mEndOffset <= mOffset) {
   1638      // range ends inside mAcc but before mOffset, so it doesn't encompass us.
   1639      continue;
   1640    }
   1641    // mOffset is within range.
   1642    expose(range.mAttribute);
   1643  }
   1644 }
   1645 
   1646 TextLeafPoint TextLeafPoint::FindTextOffsetAttributeSameAcc(
   1647    nsDirection aDirection, bool aIncludeOrigin) const {
   1648  if (!aIncludeOrigin && mOffset == 0 && aDirection == eDirPrevious) {
   1649    return TextLeafPoint();
   1650  }
   1651  if (LocalAccessible* acc = mAcc->AsLocal()) {
   1652    nsINode* node = acc->GetNode();
   1653    // There are multiple selection types. The ranges for each selection type
   1654    // are sorted, but the ranges aren't sorted between selection types.
   1655    // Therefore, we need to look for the closest matching offset in each
   1656    // selection type. We keep track of that in the dest variable as we check
   1657    // each selection type.
   1658    int32_t dest = -1;
   1659    if (aDirection == eDirNext) {
   1660      // We want to find both start and end points, so we pass true for
   1661      // aAllowAdjacent.
   1662      auto ranges = FindDOMTextOffsetAttributes(
   1663          acc, mOffset, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT,
   1664          /* aAllowAdjacent */ true);
   1665      for (auto& [domRanges, attr] : ranges) {
   1666        for (dom::AbstractRange* domRange : domRanges) {
   1667          if (domRange->GetStartContainer() == node) {
   1668            int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
   1669                acc, static_cast<int32_t>(domRange->StartOffset())));
   1670            if (aIncludeOrigin && matchOffset == mOffset) {
   1671              return *this;
   1672            }
   1673            if (matchOffset > mOffset) {
   1674              if (dest == -1 || matchOffset <= dest) {
   1675                dest = matchOffset;
   1676              }
   1677              // ranges is sorted by start, so there can't be a closer range
   1678              // offset after this. This is the only case where we can break
   1679              // out of the loop. In the cases below, we must keep iterating
   1680              // because the end offsets aren't sorted.
   1681              break;
   1682            }
   1683          }
   1684          if (domRange->GetEndContainer() == node) {
   1685            int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
   1686                acc, static_cast<int32_t>(domRange->EndOffset())));
   1687            if (aIncludeOrigin && matchOffset == mOffset) {
   1688              return *this;
   1689            }
   1690            if (matchOffset > mOffset && (dest == -1 || matchOffset <= dest)) {
   1691              dest = matchOffset;
   1692            }
   1693          }
   1694        }
   1695      }
   1696    } else {
   1697      auto ranges = FindDOMTextOffsetAttributes(acc, 0, mOffset,
   1698                                                /* aAllowAdjacent */ true);
   1699      for (auto& [domRanges, attr] : ranges) {
   1700        for (dom::AbstractRange* domRange : Reversed(domRanges)) {
   1701          if (domRange->GetEndContainer() == node) {
   1702            int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
   1703                acc, static_cast<int32_t>(domRange->EndOffset())));
   1704            if (aIncludeOrigin && matchOffset == mOffset) {
   1705              return *this;
   1706            }
   1707            if (matchOffset < mOffset && (dest == -1 || matchOffset >= dest)) {
   1708              dest = matchOffset;
   1709            }
   1710          }
   1711          if (domRange->GetStartContainer() == node) {
   1712            int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
   1713                acc, static_cast<int32_t>(domRange->StartOffset())));
   1714            if (aIncludeOrigin && matchOffset == mOffset) {
   1715              return *this;
   1716            }
   1717            if (matchOffset < mOffset && (dest == -1 || matchOffset >= dest)) {
   1718              dest = matchOffset;
   1719            }
   1720          }
   1721        }
   1722      }
   1723    }
   1724    if (dest == -1) {
   1725      return TextLeafPoint();
   1726    }
   1727    return TextLeafPoint(mAcc, dest);
   1728  }
   1729 
   1730  RemoteAccessible* acc = mAcc->AsRemote();
   1731  MOZ_ASSERT(acc);
   1732  if (RequestDomainsIfInactive(CacheDomain::TextOffsetAttributes)) {
   1733    return TextLeafPoint();
   1734  }
   1735  if (!acc->mCachedFields) {
   1736    return TextLeafPoint();
   1737  }
   1738  auto offsetAttrs =
   1739      acc->mCachedFields->GetAttribute<nsTArray<TextOffsetAttribute>>(
   1740          CacheKey::TextOffsetAttributes);
   1741  if (!offsetAttrs) {
   1742    return TextLeafPoint();
   1743  }
   1744  // offsetAttrs is sorted by mStartOffset, but ranges can overlap each other.
   1745  // Therefore, we must consider all ranges with an encompassing start offset.
   1746  // An earlier range might end after a later range, so we keep track of the
   1747  // closest offset in the dest variable and adjust that as we iterate.
   1748  int32_t dest = -1;
   1749  for (const TextOffsetAttribute& range : *offsetAttrs) {
   1750    // Although range end offsets are exclusive, we must still treat them as a
   1751    // boundary, since the end of a range still means a change in text
   1752    // attributes and text offset attribute ranges do not have to be adjacent.
   1753    if (aIncludeOrigin &&
   1754        (range.mStartOffset == mOffset || range.mEndOffset == mOffset)) {
   1755      return *this;
   1756    }
   1757    if (aDirection == eDirNext) {
   1758      if (range.mStartOffset > mOffset) {
   1759        if (dest == -1 || range.mStartOffset < dest) {
   1760          // range.mStartOffset is the closest offset we've seen thus far.
   1761          dest = range.mStartOffset;
   1762        }
   1763        // offsetAttrs is sorted by mStartOffset, so there can't be a closer
   1764        // range offset after this.
   1765        break;
   1766      }
   1767      if (range.mEndOffset > mOffset &&
   1768          (dest == -1 || range.mEndOffset < dest)) {
   1769        // range.mEndOffset is the closest offset we've seen thus far.
   1770        dest = range.mEndOffset;
   1771      }
   1772    } else {
   1773      if (range.mEndOffset != TextOffsetAttribute::kOutsideLeaf &&
   1774          range.mEndOffset < mOffset && range.mEndOffset > dest) {
   1775        // range.mEndOffset is the closest offset we've seen thus far.
   1776        dest = range.mEndOffset;
   1777      }
   1778      if (range.mStartOffset >= mOffset) {
   1779        // offsetAttrs is sorted by mStartOffset, so any range hereafter is in
   1780        // the wrong direction.
   1781        break;
   1782      }
   1783      if (range.mStartOffset != TextOffsetAttribute::kOutsideLeaf &&
   1784          range.mStartOffset > dest) {
   1785        // range.mStartOffset is the closest offset we've seen thus far.
   1786        dest = range.mStartOffset;
   1787      }
   1788    }
   1789  }
   1790  if (dest == -1) {
   1791    // There's no boundary in the requested direction.
   1792    return TextLeafPoint();
   1793  }
   1794  return TextLeafPoint(mAcc, dest);
   1795 }
   1796 
   1797 TextLeafPoint TextLeafPoint::NeighborLeafPoint(
   1798    nsDirection aDirection, bool aIsEditable,
   1799    bool aIgnoreListItemMarker) const {
   1800  Accessible* acc = aDirection == eDirPrevious
   1801                        ? PrevLeaf(mAcc, aIsEditable, aIgnoreListItemMarker)
   1802                        : NextLeaf(mAcc, aIsEditable, aIgnoreListItemMarker);
   1803  if (!acc) {
   1804    return TextLeafPoint();
   1805  }
   1806 
   1807  return TextLeafPoint(
   1808      acc, aDirection == eDirPrevious
   1809               ? static_cast<int32_t>(nsAccUtils::TextLength(acc)) - 1
   1810               : 0);
   1811 }
   1812 
   1813 LayoutDeviceIntRect TextLeafPoint::ComputeBoundsFromFrame() const {
   1814  LocalAccessible* local = mAcc->AsLocal();
   1815  MOZ_ASSERT(local, "Can't compute bounds in frame from non-local acc");
   1816  nsIFrame* frame = local->GetFrame();
   1817  MOZ_ASSERT(frame, "No frame found for acc!");
   1818 
   1819  if (!frame || !frame->IsTextFrame()) {
   1820    return local->Bounds();
   1821  }
   1822 
   1823  // Substring must be entirely within the same text node.
   1824  MOZ_ASSERT(frame->IsPrimaryFrame(),
   1825             "Cannot compute content offset on non-primary frame");
   1826  nsIFrame::RenderedText text = frame->GetRenderedText(
   1827      mOffset, mOffset + 1, nsIFrame::TextOffsetType::OffsetsInRenderedText,
   1828      nsIFrame::TrailingWhitespace::DontTrim);
   1829  int32_t contentOffset = text.mOffsetWithinNodeText;
   1830  int32_t contentOffsetInFrame;
   1831  // Get the right frame continuation -- not really a child, but a sibling of
   1832  // the primary frame passed in
   1833  nsresult rv = frame->GetChildFrameContainingOffset(
   1834      contentOffset, true, &contentOffsetInFrame, &frame);
   1835  NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
   1836 
   1837  // Start with this frame's screen rect, which we will shrink based on
   1838  // the char we care about within it.
   1839  nsRect frameScreenRect = frame->GetScreenRectInAppUnits();
   1840 
   1841  // Add the point where the char starts to the frameScreenRect
   1842  nsPoint frameTextStartPoint;
   1843  rv = frame->GetPointFromOffset(contentOffset, &frameTextStartPoint);
   1844  NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
   1845 
   1846  // Use the next offset to calculate the width
   1847  // XXX(morgan) does this work for vertical text?
   1848  nsPoint frameTextEndPoint;
   1849  rv = frame->GetPointFromOffset(contentOffset + 1, &frameTextEndPoint);
   1850  NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
   1851 
   1852  frameScreenRect.SetRectX(
   1853      frameScreenRect.X() +
   1854          std::min(frameTextStartPoint.x, frameTextEndPoint.x),
   1855      mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x));
   1856 
   1857  nsPresContext* presContext = local->Document()->PresContext();
   1858  return LayoutDeviceIntRect::FromAppUnitsToNearest(
   1859      frameScreenRect, presContext->AppUnitsPerDevPixel());
   1860 }
   1861 
   1862 LayoutDeviceIntRect TextLeafPoint::InsertionPointBounds() const {
   1863  if (TextLeafPoint::GetCaret(mAcc) == *this) {
   1864    for (Accessible* acc = mAcc; acc; acc = acc->Parent()) {
   1865      if (HyperTextAccessibleBase* ht = acc->AsHyperTextBase()) {
   1866        return ht->GetCaretRect().first;
   1867      }
   1868    }
   1869  }
   1870 
   1871  LayoutDeviceIntRect currentBounds = CharBounds();
   1872  if (currentBounds.IsEmpty()) {
   1873    return LayoutDeviceIntRect();
   1874  }
   1875 
   1876  // When 'reversed' is true we calculate the writing direction using a
   1877  // neighboring character that is past the current one (eg. in LTR the
   1878  // character to the right.)
   1879  bool reversed = false;
   1880  // We are conservative here and stay within the bounds of the editable so
   1881  // we don't get confused with other text-flows outside of this block.
   1882  TextLeafPoint neighborChar =
   1883      FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
   1884                   BoundaryFlags::eStopInEditable);
   1885  if (*this == neighborChar) {
   1886    // If the current char is the first, use the next char past the current
   1887    // to extrapolate the writing direction.
   1888    neighborChar = FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
   1889                                BoundaryFlags::eStopInEditable);
   1890    reversed = true;
   1891  } else {
   1892    if (*this ==
   1893        FindBoundary(
   1894            nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
   1895            BoundaryFlags::eStopInEditable | BoundaryFlags::eIncludeOrigin)) {
   1896      // If this char is a newline the previous char will be on the previous
   1897      // line either equal or past the current one (eg. in LTR the previous char
   1898      // will be to the right of the current instead of the left.)
   1899      reversed = true;
   1900    }
   1901  }
   1902 
   1903  LayoutDeviceIntRect neighborBounds = neighborChar.CharBounds();
   1904  if (neighborBounds.IsEmpty()) {
   1905    // Either the point is invalid or the accessible is not visible.
   1906    // We tried, didn't we..
   1907    return LayoutDeviceIntRect();
   1908  }
   1909 
   1910  // An axis-agnostic function that determines writing direction and aligns
   1911  // the caret where insertion point should be.
   1912  auto alignRectToInsertion = [](int32_t neighborStart, bool reversed,
   1913                                 int32_t& currentStart,
   1914                                 int32_t& currentLength) {
   1915    // The caret is always 1px wide (or tall, in vertical text).
   1916    const int32_t caretLength = 1;
   1917    int32_t delta = (neighborStart - currentStart);
   1918    if (reversed) {
   1919      delta *= -1;
   1920    }
   1921    if (delta > 0) {
   1922      // Previous character is to the end (eg. right in ltr) of the current one,
   1923      // or next character is to the start (eg. left in ltr) of the current one.
   1924      // Align the caret to the end of the current character.
   1925      currentStart += currentLength - caretLength;
   1926    }
   1927    currentLength = caretLength;
   1928  };
   1929 
   1930  WritingMode wm = mAcc->GetWritingMode();
   1931  if (wm == WritingMode() && mAcc->Parent()) {
   1932    // mAcc is probably a text leaf with no stored writing mode, use its parent.
   1933    wm = mAcc->Parent()->GetWritingMode();
   1934  }
   1935 
   1936  if (!wm.IsVertical()) {
   1937    // Horizontal text.
   1938    alignRectToInsertion(neighborBounds.x, reversed, currentBounds.x,
   1939                         currentBounds.width);
   1940  } else {
   1941    // Vertical text
   1942    alignRectToInsertion(neighborBounds.y, reversed, currentBounds.y,
   1943                         currentBounds.height);
   1944  }
   1945 
   1946  return currentBounds;
   1947 }
   1948 
   1949 /* static */
   1950 nsTArray<TextOffsetAttribute> TextLeafPoint::GetTextOffsetAttributes(
   1951    LocalAccessible* aAcc) {
   1952  nsINode* node = aAcc->GetNode();
   1953  auto ranges = FindDOMTextOffsetAttributes(
   1954      aAcc, 0, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
   1955  size_t capacity = 0;
   1956  for (auto& [domRanges, attr] : ranges) {
   1957    capacity += domRanges.Length();
   1958  }
   1959  nsTArray<TextOffsetAttribute> offsets(capacity);
   1960  for (auto& [domRanges, attr] : ranges) {
   1961    for (dom::AbstractRange* domRange : domRanges) {
   1962      TextOffsetAttribute& data = *offsets.AppendElement();
   1963      data.mAttribute = attr;
   1964      if (domRange->GetStartContainer() == node) {
   1965        data.mStartOffset = static_cast<int32_t>(ContentToRenderedOffset(
   1966            aAcc, static_cast<int32_t>(domRange->StartOffset())));
   1967      } else {
   1968        // This range overlaps aAcc, but starts before it.
   1969        // This can only happen for the first range.
   1970        MOZ_ASSERT(domRange == *domRanges.begin());
   1971        data.mStartOffset = TextOffsetAttribute::kOutsideLeaf;
   1972      }
   1973      if (domRange->GetEndContainer() == node) {
   1974        data.mEndOffset = static_cast<int32_t>(ContentToRenderedOffset(
   1975            aAcc, static_cast<int32_t>(domRange->EndOffset())));
   1976      } else {
   1977        // This range overlaps aAcc, but ends after it.
   1978        // This can only happen for the last range.
   1979        MOZ_ASSERT(domRange == *domRanges.rbegin());
   1980        data.mEndOffset = TextOffsetAttribute::kOutsideLeaf;
   1981      }
   1982    }
   1983  }
   1984  offsets.Sort();
   1985  return offsets;
   1986 }
   1987 
   1988 /* static */
   1989 void TextLeafPoint::UpdateCachedTextOffsetAttributes(
   1990    dom::Document* aDocument, const dom::AbstractRange& aRange) {
   1991  DocAccessible* docAcc = GetExistingDocAccessible(aDocument);
   1992  if (!docAcc) {
   1993    return;
   1994  }
   1995  LocalAccessible* startAcc = docAcc->GetAccessible(aRange.GetStartContainer());
   1996  LocalAccessible* endAcc = docAcc->GetAccessible(aRange.GetEndContainer());
   1997  if (!startAcc || !endAcc) {
   1998    return;
   1999  }
   2000  for (Accessible* acc = startAcc; acc; acc = NextLeaf(acc)) {
   2001    if (acc->IsTextLeaf()) {
   2002      docAcc->QueueCacheUpdate(acc->AsLocal(),
   2003                               CacheDomain::TextOffsetAttributes);
   2004    }
   2005    if (acc == endAcc) {
   2006      // Subtle: We check this here rather than in the loop condition because
   2007      // we want to include endAcc but stop once we reach it. Putting it in the
   2008      // loop condition would mean we stop at endAcc, but we would also exclude
   2009      // it; i.e. we wouldn't push the cache for it.
   2010      break;
   2011    }
   2012  }
   2013 }
   2014 
   2015 already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributesLocalAcc(
   2016    bool aIncludeDefaults) const {
   2017  LocalAccessible* acc = mAcc->AsLocal();
   2018  MOZ_ASSERT(acc);
   2019  MOZ_ASSERT(acc->IsText());
   2020  // TextAttrsMgr wants a HyperTextAccessible.
   2021  LocalAccessible* parent = acc->LocalParent();
   2022  if (!parent) {
   2023    // This should only happen if a client query occurs after a hide event is
   2024    // queued for acc and after acc is detached from the document, but before
   2025    // the event is fired and thus before acc is shut down.
   2026    MOZ_ASSERT(!acc->IsInDocument());
   2027    return nullptr;
   2028  }
   2029  HyperTextAccessible* hyperAcc = parent->AsHyperText();
   2030  MOZ_ASSERT(hyperAcc);
   2031  RefPtr<AccAttributes> attributes = new AccAttributes();
   2032  if (hyperAcc) {
   2033    TextAttrsMgr mgr(hyperAcc, aIncludeDefaults, acc);
   2034    mgr.GetAttributes(attributes);
   2035  }
   2036  return attributes.forget();
   2037 }
   2038 
   2039 already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributes(
   2040    bool aIncludeDefaults) const {
   2041  if (!mAcc->IsText()) {
   2042    return nullptr;
   2043  }
   2044  RefPtr<AccAttributes> attrs;
   2045  if (mAcc->IsLocal()) {
   2046    attrs = GetTextAttributesLocalAcc(aIncludeDefaults);
   2047  } else {
   2048    if (aIncludeDefaults) {
   2049      attrs = mAcc->AsRemote()->DefaultTextAttributes();
   2050    } else {
   2051      attrs = new AccAttributes();
   2052    }
   2053    if (auto thisAttrs = mAcc->AsRemote()->GetCachedTextAttributes()) {
   2054      thisAttrs->CopyTo(attrs);
   2055    }
   2056  }
   2057  AddTextOffsetAttributes(attrs);
   2058  return attrs.forget();
   2059 }
   2060 
   2061 TextLeafPoint TextLeafPoint::FindTextAttrsStart(nsDirection aDirection,
   2062                                                bool aIncludeOrigin) const {
   2063  if (mIsEndOfLineInsertionPoint) {
   2064    return AdjustEndOfLine().FindTextAttrsStart(aDirection, aIncludeOrigin);
   2065  }
   2066  RefPtr<const AccAttributes> lastAttrs;
   2067  if (mAcc->IsText()) {
   2068    lastAttrs = GetTextAttributes();
   2069  }
   2070  if (aIncludeOrigin && aDirection == eDirNext && mOffset == 0) {
   2071    if (!mAcc->IsText()) {
   2072      // Anything other than text breaks an attrs run.
   2073      return *this;
   2074    }
   2075    // Even when searching forward, the only way to know whether the origin is
   2076    // the start of a text attrs run is to compare with the previous sibling.
   2077    TextLeafPoint point;
   2078    point.mAcc = mAcc->PrevSibling();
   2079    if (!point.mAcc || !point.mAcc->IsText()) {
   2080      return *this;
   2081    }
   2082    RefPtr<const AccAttributes> attrs = point.GetTextAttributes();
   2083    if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) {
   2084      return *this;
   2085    }
   2086  }
   2087  TextLeafPoint lastPoint = *this;
   2088  // If we're at the start of the container and searching for a previous start,
   2089  // start the search from the previous leaf. Otherwise, we'll miss the previous
   2090  // start.
   2091  const bool shouldTraversePrevLeaf = [&]() {
   2092    const bool shouldTraverse =
   2093        !aIncludeOrigin && aDirection == eDirPrevious && mOffset == 0;
   2094    Accessible* prevSibling = mAcc->PrevSibling();
   2095    if (prevSibling) {
   2096      return shouldTraverse && !prevSibling->IsText();
   2097    }
   2098    return shouldTraverse;
   2099  }();
   2100  if (shouldTraversePrevLeaf) {
   2101    // Go to the previous leaf and start the search from there, if it exists.
   2102    Accessible* prevLeaf = PrevLeaf(mAcc);
   2103    if (!prevLeaf) {
   2104      return *this;
   2105    }
   2106    lastPoint = TextLeafPoint(
   2107        prevLeaf, static_cast<int32_t>(nsAccUtils::TextLength(prevLeaf)));
   2108  }
   2109  // This loop searches within a container (that is, it only looks at siblings).
   2110  // We might cross containers before or after this loop, but not within it.
   2111  for (;;) {
   2112    if (TextLeafPoint offsetAttr = lastPoint.FindTextOffsetAttributeSameAcc(
   2113            aDirection, aIncludeOrigin && lastPoint.mAcc == mAcc)) {
   2114      // An offset attribute starts or ends somewhere in the Accessible we're
   2115      // considering. This causes an attribute change, so return that point.
   2116      return offsetAttr;
   2117    }
   2118    TextLeafPoint point;
   2119    point.mAcc = aDirection == eDirNext ? lastPoint.mAcc->NextSibling()
   2120                                        : lastPoint.mAcc->PrevSibling();
   2121    if (!point.mAcc || !point.mAcc->IsText()) {
   2122      break;
   2123    }
   2124    RefPtr<const AccAttributes> attrs = point.GetTextAttributes();
   2125    if (((!lastAttrs || !attrs) && attrs != lastAttrs) ||
   2126        (attrs && !attrs->Equal(lastAttrs))) {
   2127      // The attributes change here. If we're moving forward, we want to return
   2128      // this point.
   2129      if (aDirection == eDirNext) {
   2130        return point;
   2131      }
   2132 
   2133      // Otherwise, we're moving backward and we've now moved before the start
   2134      // point of the current text attributes run.
   2135      const auto attrsStart = TextLeafPoint(lastPoint.mAcc, 0);
   2136 
   2137      // Return the current text attributes run start point if:
   2138      //   1. The caller wants this function to include the origin in the
   2139      //   search (aIncludeOrigin implies that we must return the first text
   2140      //   attributes run start point that we find, even if that point is the
   2141      //   origin)
   2142      //   2. Our search did not begin on the text attributes run start point
   2143      if (aIncludeOrigin || attrsStart != *this) {
   2144        return attrsStart;
   2145      }
   2146 
   2147      // Otherwise, the origin was the attributes run start point and the caller
   2148      // wants this function to ignore it in its search. Keep searching.
   2149    }
   2150    lastPoint = point;
   2151    if (aDirection == eDirPrevious) {
   2152      // On the next iteration, we want to search for offset attributes from the
   2153      // end of this Accessible.
   2154      lastPoint.mOffset =
   2155          static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc));
   2156    }
   2157    lastAttrs = attrs;
   2158  }
   2159 
   2160  // We couldn't move any further in this container.
   2161  if (aDirection == eDirPrevious) {
   2162    // Treat the start of a container as a format boundary.
   2163    return TextLeafPoint(lastPoint.mAcc, 0);
   2164  }
   2165  // If we're at the end of the container then we have to use the start of the
   2166  // next leaf.
   2167  Accessible* nextLeaf = NextLeaf(lastPoint.mAcc);
   2168  if (nextLeaf) {
   2169    return TextLeafPoint(nextLeaf, 0);
   2170  }
   2171  // If there's no next leaf, then fall back to the end of the last point.
   2172  return TextLeafPoint(
   2173      lastPoint.mAcc,
   2174      static_cast<int32_t>(nsAccUtils::TextLength(lastPoint.mAcc)));
   2175 }
   2176 
   2177 LayoutDeviceIntRect TextLeafPoint::CharBounds() const {
   2178  if (!mAcc) {
   2179    return LayoutDeviceIntRect();
   2180  }
   2181 
   2182  if (mAcc->IsHTMLBr()) {
   2183    // HTML <br> elements don't provide character bounds, but do provide text (a
   2184    // line feed). They also have 0 width and/or height, depending on the
   2185    // doctype and writing mode. Expose minimum 1 x 1 so clients treat it as an
   2186    // actual rectangle; e.g. when the caret is positioned on a <br>.
   2187    LayoutDeviceIntRect bounds = mAcc->Bounds();
   2188    if (bounds.width == 0) {
   2189      bounds.width = 1;
   2190    }
   2191    if (bounds.height == 0) {
   2192      bounds.height = 1;
   2193    }
   2194    return bounds;
   2195  }
   2196 
   2197  if (!mAcc->IsTextLeaf()) {
   2198    // This could be an empty container. Alternatively, it could be a list
   2199    // bullet,which does provide text but doesn't support character bounds. In
   2200    // either case, return the Accessible's bounds.
   2201    return mAcc->Bounds();
   2202  }
   2203 
   2204  auto maybeAdjustLineFeedBounds = [this](LayoutDeviceIntRect& aBounds) {
   2205    if (!IsLineFeedChar()) {
   2206      return;
   2207    }
   2208    // Line feeds have a 0 width or height, depending on the writing mode.
   2209    // Use 1 instead so that clients treat it as an actual rectangle; e.g. when
   2210    // displaying the caret when it is positioned on a line feed.
   2211    MOZ_ASSERT(aBounds.IsZeroArea());
   2212    if (aBounds.width == 0) {
   2213      aBounds.width = 1;
   2214    }
   2215    if (aBounds.height == 0) {
   2216      aBounds.height = 1;
   2217    }
   2218  };
   2219 
   2220  if (LocalAccessible* local = mAcc->AsLocal()) {
   2221    if (mOffset >= 0 &&
   2222        static_cast<uint32_t>(mOffset) >= nsAccUtils::TextLength(local)) {
   2223      // It's valid for a caller to query the length because the caret might be
   2224      // at the end of editable text. In that case, we should just silently
   2225      // return. However, we assert that the offset isn't greater than the
   2226      // length.
   2227      NS_ASSERTION(
   2228          static_cast<uint32_t>(mOffset) <= nsAccUtils::TextLength(local),
   2229          "Wrong in offset");
   2230      return LayoutDeviceIntRect();
   2231    }
   2232 
   2233    LayoutDeviceIntRect bounds = ComputeBoundsFromFrame();
   2234 
   2235    // This document may have a resolution set, we will need to multiply
   2236    // the document-relative coordinates by that value and re-apply the doc's
   2237    // screen coordinates.
   2238    nsPresContext* presContext = local->Document()->PresContext();
   2239    nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
   2240    LayoutDeviceIntRect orgRectPixels =
   2241        LayoutDeviceIntRect::FromAppUnitsToNearest(
   2242            rootFrame->GetScreenRectInAppUnits(),
   2243            presContext->AppUnitsPerDevPixel());
   2244    bounds.MoveBy(-orgRectPixels.X(), -orgRectPixels.Y());
   2245    bounds.ScaleRoundOut(presContext->PresShell()->GetResolution());
   2246    bounds.MoveBy(orgRectPixels.X(), orgRectPixels.Y());
   2247    maybeAdjustLineFeedBounds(bounds);
   2248    return bounds;
   2249  }
   2250 
   2251  if (RequestDomainsIfInactive(CacheDomain::TextBounds)) {
   2252    return LayoutDeviceIntRect();
   2253  }
   2254  RemoteAccessible* remote = mAcc->AsRemote();
   2255  if (!remote->mCachedFields) {
   2256    return LayoutDeviceIntRect();
   2257  }
   2258 
   2259  nsRect charBounds = remote->GetCachedCharRect(mOffset);
   2260  // A character can have 0 width, but we still want its other coordinates.
   2261  // Thus, we explicitly test for an all-0 rect here to determine whether this
   2262  // is a valid char rect, rather than using IsZeroArea or IsEmpty.
   2263  if (!charBounds.IsEqualRect(0, 0, 0, 0)) {
   2264    LayoutDeviceIntRect bounds = remote->BoundsWithOffset(Some(charBounds));
   2265    maybeAdjustLineFeedBounds(bounds);
   2266    return bounds;
   2267  }
   2268 
   2269  return LayoutDeviceIntRect();
   2270 }
   2271 
   2272 bool TextLeafPoint::ContainsPoint(int32_t aX, int32_t aY) {
   2273  if (mAcc && !mAcc->IsText()) {
   2274    // If we're dealing with an empty embedded object, use the
   2275    // accessible's non-text bounds.
   2276    return mAcc->Bounds().Contains(aX, aY);
   2277  }
   2278 
   2279  return CharBounds().Contains(aX, aY);
   2280 }
   2281 
   2282 /*** TextLeafRange ***/
   2283 
   2284 bool TextLeafRange::Crop(Accessible* aContainer) {
   2285  TextLeafPoint containerStart(aContainer, 0);
   2286  TextLeafPoint containerEnd(aContainer,
   2287                             nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
   2288 
   2289  if (mEnd < containerStart || containerEnd < mStart) {
   2290    // The range ends before the container, or starts after it.
   2291    return false;
   2292  }
   2293 
   2294  if (mStart < containerStart) {
   2295    // If range start is before container start, adjust range start to
   2296    // start of container.
   2297    mStart = containerStart;
   2298  }
   2299 
   2300  if (containerEnd < mEnd) {
   2301    // If range end is after container end, adjust range end to end of
   2302    // container.
   2303    mEnd = containerEnd;
   2304  }
   2305 
   2306  return true;
   2307 }
   2308 
   2309 LayoutDeviceIntRect TextLeafRange::Bounds() const {
   2310  // We can't simply query the first and last character, and union their bounds.
   2311  // They might reside on different lines, so a simple union may yield an
   2312  // incorrect width. We should use the length of the longest spanned line for
   2313  // our width. To achieve this, walk all the lines and union them into the
   2314  // result rectangle.
   2315  LayoutDeviceIntRect result;
   2316  const bool succeeded = WalkLineRects(
   2317      [&result](TextLeafRange aLine, LayoutDeviceIntRect aLineRect) {
   2318        result.UnionRect(result, aLineRect);
   2319      });
   2320 
   2321  if (!succeeded) {
   2322    return {};
   2323  }
   2324  return result;
   2325 }
   2326 
   2327 nsTArray<LayoutDeviceIntRect> TextLeafRange::LineRects() const {
   2328  // Get the bounds of the content so we can restrict our lines to just the
   2329  // text visible within the bounds of the document.
   2330  Maybe<LayoutDeviceIntRect> contentBounds;
   2331  if (Accessible* doc = nsAccUtils::DocumentFor(mStart.mAcc)) {
   2332    contentBounds.emplace(doc->Bounds());
   2333  }
   2334 
   2335  nsTArray<LayoutDeviceIntRect> lineRects;
   2336  WalkLineRects([&lineRects, &contentBounds](TextLeafRange aLine,
   2337                                             LayoutDeviceIntRect aLineRect) {
   2338    // Clip the bounds to the bounds of the content area.
   2339    bool boundsVisible = true;
   2340    if (contentBounds.isSome()) {
   2341      boundsVisible = aLineRect.IntersectRect(aLineRect, *contentBounds);
   2342    }
   2343    if (boundsVisible) {
   2344      lineRects.AppendElement(aLineRect);
   2345    }
   2346  });
   2347 
   2348  return lineRects;
   2349 }
   2350 
   2351 TextLeafPoint TextLeafRange::TextLeafPointAtScreenPoint(int32_t aX,
   2352                                                        int32_t aY) const {
   2353  // Step backwards one character to make the endPoint inclusive. This means we
   2354  // can use operator!= when comparing against endPoint below (which is very
   2355  // fast), rather than operator< (which might be significantly slower).
   2356  const TextLeafPoint endPoint =
   2357      mEnd.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
   2358 
   2359  // If there are no characters in this container, we might have moved endPoint
   2360  // before mStart. In that case, we shouldn't try to move farther forward, as
   2361  // that might result in an infinite loop.
   2362  TextLeafPoint point = mStart;
   2363  if (mStart <= endPoint) {
   2364    for (; !point.ContainsPoint(aX, aY) && point != endPoint;
   2365         point =
   2366             point.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext)) {
   2367    }
   2368  }
   2369 
   2370  return point;
   2371 }
   2372 
   2373 bool TextLeafRange::SetSelection(int32_t aSelectionNum, bool aSetFocus) const {
   2374  if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) {
   2375    return false;
   2376  }
   2377 
   2378  if (mStart.mAcc->IsRemote()) {
   2379    DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document();
   2380    if (doc != mEnd.mAcc->AsRemote()->Document()) {
   2381      return false;
   2382    }
   2383 
   2384    (void)doc->SendSetTextSelection(mStart.mAcc->ID(), mStart.mOffset,
   2385                                    mEnd.mAcc->ID(), mEnd.mOffset,
   2386                                    aSelectionNum, aSetFocus);
   2387    return true;
   2388  }
   2389 
   2390  bool reversed = mEnd < mStart;
   2391  auto [startContent, startContentOffset] =
   2392      !reversed ? mStart.ToDOMPoint(false) : mEnd.ToDOMPoint(false);
   2393  auto [endContent, endContentOffset] =
   2394      !reversed ? mEnd.ToDOMPoint(false) : mStart.ToDOMPoint(false);
   2395  if (!startContent || !endContent) {
   2396    return false;
   2397  }
   2398 
   2399  RefPtr<dom::Selection> domSel = GetDOMSelection(startContent, endContent);
   2400  if (!domSel) {
   2401    return false;
   2402  }
   2403 
   2404  HyperTextAccessible* hyp = nullptr;
   2405  if (mStart.mAcc->IsHyperText()) {
   2406    hyp = mStart.mAcc->AsLocal()->AsHyperText();
   2407  } else {
   2408    Accessible* parent = mStart.mAcc->Parent();
   2409    if (parent) {
   2410      hyp = parent->AsLocal()->AsHyperText();
   2411      // Note that hyp will still be null here if the parent is not a HyperText.
   2412      // That's okay.
   2413    }
   2414  }
   2415 
   2416  // Before setting the selection range, we need to ensure that the editor
   2417  // is initialized. (See bug 804927.)
   2418  // Otherwise, it's possible that lazy editor initialization will override
   2419  // the selection we set here and leave the caret at the end of the text.
   2420  // By calling GetEditor here, we ensure that editor initialization is
   2421  // completed before we set the selection.
   2422  RefPtr<EditorBase> editor;
   2423  if (hyp) {
   2424    editor = hyp->GetEditor();
   2425  }
   2426 
   2427  // XXX isFocusable will be false if mStart is not a direct child of the
   2428  // contentEditable. However, contentEditables generally don't mess with
   2429  // selection when they are focused. This has also been our behavior for a very
   2430  // long time.
   2431  const bool isFocusable = hyp && hyp->InteractiveState() & states::FOCUSABLE;
   2432  // If the Accessible is focusable, focus it before setting the selection to
   2433  // override the control's own selection changes on focus if any; e.g. inputs
   2434  // that do select all on focus. This also ensures that the user can interact
   2435  // with wherever they've moved the caret. See bug 524115.
   2436  if (aSetFocus && isFocusable) {
   2437    hyp->TakeFocus();
   2438  }
   2439 
   2440  uint32_t rangeCount = 0;
   2441  if (aSelectionNum == kRemoveAllExistingSelectedRanges) {
   2442    domSel->RemoveAllRanges(IgnoreErrors());
   2443  } else {
   2444    rangeCount = domSel->RangeCount();
   2445  }
   2446  RefPtr<nsRange> domRange = nullptr;
   2447  const bool newRange =
   2448      aSelectionNum == static_cast<int32_t>(rangeCount) || aSelectionNum < 0;
   2449  if (newRange) {
   2450    domRange = nsRange::Create(startContent);
   2451  } else {
   2452    domRange = domSel->GetRangeAt(AssertedCast<uint32_t>(aSelectionNum));
   2453  }
   2454  if (!domRange) {
   2455    return false;
   2456  }
   2457 
   2458  domRange->SetStart(startContent, startContentOffset);
   2459  domRange->SetEnd(endContent, endContentOffset);
   2460 
   2461  // If this is not a new range, notify selection listeners that the existing
   2462  // selection range has changed. Otherwise, just add the new range.
   2463  if (!newRange) {
   2464    domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*domRange,
   2465                                                           IgnoreErrors());
   2466  }
   2467 
   2468  IgnoredErrorResult err;
   2469  domSel->AddRangeAndSelectFramesAndNotifyListeners(*domRange, err);
   2470  if (err.Failed()) {
   2471    return false;
   2472  }
   2473 
   2474  // Changing the direction of the selection assures that the caret
   2475  // will be at the logical end of the selection.
   2476  domSel->SetDirection(reversed ? eDirPrevious : eDirNext);
   2477 
   2478  // Make sure the selection is visible. See bug 1170242.
   2479  domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
   2480                         ScrollAxis(), ScrollAxis(),
   2481                         ScrollFlags::ScrollOverflowHidden);
   2482 
   2483  if (aSetFocus && mStart == mEnd && !isFocusable) {
   2484    // We're moving the caret. Notify nsFocusManager so that the focus position
   2485    // is correct. See bug 546068.
   2486    if (nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager()) {
   2487      MOZ_ASSERT(mStart.mAcc->AsLocal()->Document());
   2488      dom::Document* domDoc =
   2489          mStart.mAcc->AsLocal()->Document()->DocumentNode();
   2490      MOZ_ASSERT(domDoc);
   2491      nsCOMPtr<nsPIDOMWindowOuter> window = domDoc->GetWindow();
   2492      RefPtr<dom::Element> result;
   2493      DOMFocusManager->MoveFocus(
   2494          window, nullptr, nsIFocusManager::MOVEFOCUS_CARET,
   2495          nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result));
   2496    }
   2497  }
   2498  return true;
   2499 }
   2500 
   2501 /* static */
   2502 void TextLeafRange::GetSelection(Accessible* aAcc,
   2503                                 nsTArray<TextLeafRange>& aRanges) {
   2504  // Use HyperTextAccessibleBase::SelectionRanges. Eventually, we'll want to
   2505  // move that code into TextLeafPoint, but events and caching are based on
   2506  // HyperText offsets for now.
   2507  HyperTextAccessibleBase* hyp = aAcc->AsHyperTextBase();
   2508  if (!hyp) {
   2509    return;
   2510  }
   2511  AutoTArray<TextRange, 1> hypRanges;
   2512  hyp->CroppedSelectionRanges(hypRanges);
   2513  aRanges.SetCapacity(hypRanges.Length());
   2514  for (TextRange& hypRange : hypRanges) {
   2515    TextLeafPoint start =
   2516        hypRange.StartContainer()->AsHyperTextBase()->ToTextLeafPoint(
   2517            hypRange.StartOffset());
   2518    TextLeafPoint end =
   2519        hypRange.EndContainer()->AsHyperTextBase()->ToTextLeafPoint(
   2520            hypRange.EndOffset());
   2521    aRanges.EmplaceBack(start, end);
   2522  }
   2523 }
   2524 
   2525 void TextLeafRange::ScrollIntoView(uint32_t aScrollType) const {
   2526  if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) {
   2527    return;
   2528  }
   2529 
   2530  if (mStart.mAcc->IsRemote()) {
   2531    DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document();
   2532    if (doc != mEnd.mAcc->AsRemote()->Document()) {
   2533      // Can't scroll range that spans docs.
   2534      return;
   2535    }
   2536 
   2537    (void)doc->SendScrollTextLeafRangeIntoView(mStart.mAcc->ID(),
   2538                                               mStart.mOffset, mEnd.mAcc->ID(),
   2539                                               mEnd.mOffset, aScrollType);
   2540    return;
   2541  }
   2542 
   2543  auto [startContent, startContentOffset] = mStart.ToDOMPoint();
   2544  auto [endContent, endContentOffset] = mEnd.ToDOMPoint();
   2545 
   2546  if (!startContent || !endContent) {
   2547    return;
   2548  }
   2549 
   2550  ErrorResult er;
   2551  RefPtr<nsRange> domRange = nsRange::Create(startContent, startContentOffset,
   2552                                             endContent, endContentOffset, er);
   2553  if (er.Failed()) {
   2554    return;
   2555  }
   2556 
   2557  nsCoreUtils::ScrollSubstringTo(mStart.mAcc->AsLocal()->GetFrame(), domRange,
   2558                                 aScrollType);
   2559 }
   2560 
   2561 nsTArray<TextLeafRange> TextLeafRange::VisibleLines(
   2562    Accessible* aContainer) const {
   2563  MOZ_ASSERT(aContainer);
   2564  nsTArray<TextLeafRange> lines;
   2565  if (mStart == mEnd) {
   2566    return lines;
   2567  }
   2568  // We want to restrict our lines to those visible within aContainer.
   2569  LayoutDeviceIntRect containerBounds = aContainer->Bounds();
   2570  WalkLineRects([&lines, &containerBounds](TextLeafRange aLine,
   2571                                           LayoutDeviceIntRect aLineRect) {
   2572    // XXX This doesn't correctly handle lines that are scrolled out where the
   2573    // scroll container is a descendant of aContainer. Such lines might
   2574    // intersect with containerBounds, but the scroll container could be a
   2575    // descendant of aContainer and should thus exclude this line. See bug
   2576    // 1945010 for more details.
   2577    if (aLineRect.Intersects(containerBounds)) {
   2578      lines.AppendElement(aLine);
   2579    }
   2580  });
   2581  return lines;
   2582 }
   2583 
   2584 bool TextLeafRange::WalkLineRects(LineRectCallback aCallback) const {
   2585  if (mEnd < mStart) {
   2586    return false;
   2587  }
   2588  if (mStart == mEnd) {
   2589    // Return the insertion point bounds for the offset if range is collapsed.
   2590    aCallback(*this, mStart.InsertionPointBounds());
   2591    return true;
   2592  }
   2593 
   2594  bool locatedFinalLine = false;
   2595  TextLeafPoint currPoint = mStart;
   2596 
   2597  // Union the first and last chars of each line to create a line rect.
   2598  while (!locatedFinalLine) {
   2599    TextLeafPoint nextLineStartPoint = currPoint.FindBoundary(
   2600        nsIAccessibleText::BOUNDARY_LINE_START, eDirNext);
   2601    // If currPoint is at the end of the document, nextLineStartPoint will be
   2602    // equal to currPoint. However, this can only happen if mEnd is also the end
   2603    // of the document.
   2604    MOZ_ASSERT(nextLineStartPoint != currPoint || nextLineStartPoint == mEnd);
   2605    if (mEnd <= nextLineStartPoint) {
   2606      // nextLineStart is past the end of the range. Constrain this last line to
   2607      // the end of the range.
   2608      nextLineStartPoint = mEnd;
   2609      locatedFinalLine = true;
   2610    }
   2611    // Fetch the last point in the current line by going back one char from the
   2612    // start of the next line.
   2613    TextLeafPoint lastPointInLine = nextLineStartPoint.FindBoundary(
   2614        nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
   2615    MOZ_ASSERT(currPoint <= lastPointInLine);
   2616 
   2617    LayoutDeviceIntRect currLineRect = currPoint.CharBounds();
   2618    currLineRect.UnionRect(currLineRect, lastPointInLine.CharBounds());
   2619    // The range we pass must include the last character and range ends are
   2620    // exclusive, hence the use of nextLineStartPoint.
   2621    TextLeafRange currLine = TextLeafRange(currPoint, nextLineStartPoint);
   2622    aCallback(currLine, currLineRect);
   2623 
   2624    currPoint = nextLineStartPoint;
   2625  }
   2626  return true;
   2627 }
   2628 
   2629 TextLeafRange::Iterator TextLeafRange::Iterator::BeginIterator(
   2630    const TextLeafRange& aRange) {
   2631  Iterator result(aRange);
   2632 
   2633  result.mSegmentStart = aRange.mStart;
   2634  if (aRange.mStart.mAcc == aRange.mEnd.mAcc) {
   2635    result.mSegmentEnd = aRange.mEnd;
   2636  } else {
   2637    result.mSegmentEnd = TextLeafPoint(
   2638        aRange.mStart.mAcc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
   2639  }
   2640 
   2641  return result;
   2642 }
   2643 
   2644 TextLeafRange::Iterator TextLeafRange::Iterator::EndIterator(
   2645    const TextLeafRange& aRange) {
   2646  Iterator result(aRange);
   2647 
   2648  result.mSegmentEnd = TextLeafPoint();
   2649  result.mSegmentStart = TextLeafPoint();
   2650 
   2651  return result;
   2652 }
   2653 
   2654 TextLeafRange::Iterator& TextLeafRange::Iterator::operator++() {
   2655  if (mSegmentEnd.mAcc == mRange.mEnd.mAcc) {
   2656    mSegmentEnd = TextLeafPoint();
   2657    mSegmentStart = TextLeafPoint();
   2658    return *this;
   2659  }
   2660 
   2661  if (Accessible* nextLeaf = NextLeaf(mSegmentEnd.mAcc)) {
   2662    mSegmentStart = TextLeafPoint(nextLeaf, 0);
   2663    if (nextLeaf == mRange.mEnd.mAcc) {
   2664      mSegmentEnd = TextLeafPoint(nextLeaf, mRange.mEnd.mOffset);
   2665    } else {
   2666      mSegmentEnd =
   2667          TextLeafPoint(nextLeaf, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
   2668    }
   2669  } else {
   2670    mSegmentEnd = TextLeafPoint();
   2671    mSegmentStart = TextLeafPoint();
   2672  }
   2673 
   2674  return *this;
   2675 }
   2676 
   2677 }  // namespace mozilla::a11y