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/DocAccessible.h"
12 #include "mozilla/a11y/DocAccessibleParent.h"
13 #include "mozilla/a11y/LocalAccessible.h"
14 #include "mozilla/BinarySearch.h"
15 #include "mozilla/Casting.h"
16 #include "mozilla/intl/Segmenter.h"
17 #include "mozilla/intl/WordBreaker.h"
18 #include "mozilla/StaticPrefs_layout.h"
19 #include "nsAccUtils.h"
20 #include "nsContentUtils.h"
21 #include "nsIAccessiblePivot.h"
22 #include "nsILineIterator.h"
23 #include "nsTArray.h"
24 #include "nsTextFrame.h"
25 #include "nsUnicodeProperties.h"
26 #include "Pivot.h"
27 #include "TextAttrs.h"
28 
29 using mozilla::intl::WordBreaker;
30 
31 namespace mozilla::a11y {
32 
33 /*** Helpers ***/
34 
35 /**
36  * These two functions convert between rendered and content text offsets.
37  * When text DOM nodes are rendered, the rendered text often does not contain
38  * all the whitespace from the source. For example, by default, the text
39  * "a   b" will be rendered as "a b"; i.e. multiple spaces are compressed to
40  * one. TextLeafAccessibles contain rendered text, but when we query layout, we
41  * need to provide offsets into the original content text. Similarly, layout
42  * returns content offsets, but we need to convert them to rendered offsets to
43  * map them to TextLeafAccessibles.
44  */
45 
RenderedToContentOffset(LocalAccessible * aAcc,uint32_t aRenderedOffset)46 static int32_t RenderedToContentOffset(LocalAccessible* aAcc,
47                                        uint32_t aRenderedOffset) {
48   if (aAcc->LocalParent() && aAcc->LocalParent()->IsTextField()) {
49     return static_cast<int32_t>(aRenderedOffset);
50   }
51 
52   nsIFrame* frame = aAcc->GetFrame();
53   MOZ_ASSERT(frame && frame->IsTextFrame());
54 
55   nsIFrame::RenderedText text =
56       frame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
57                              nsIFrame::TextOffsetType::OffsetsInRenderedText,
58                              nsIFrame::TrailingWhitespace::DontTrim);
59   return text.mOffsetWithinNodeText;
60 }
61 
ContentToRenderedOffset(LocalAccessible * aAcc,int32_t aContentOffset)62 static uint32_t ContentToRenderedOffset(LocalAccessible* aAcc,
63                                         int32_t aContentOffset) {
64   if (aAcc->LocalParent() && aAcc->LocalParent()->IsTextField()) {
65     return aContentOffset;
66   }
67 
68   nsIFrame* frame = aAcc->GetFrame();
69   MOZ_ASSERT(frame && frame->IsTextFrame());
70 
71   nsIFrame::RenderedText text =
72       frame->GetRenderedText(aContentOffset, aContentOffset + 1,
73                              nsIFrame::TextOffsetType::OffsetsInContentText,
74                              nsIFrame::TrailingWhitespace::DontTrim);
75   return text.mOffsetWithinNodeRenderedText;
76 }
77 
78 class LeafRule : public PivotRule {
79  public:
Match(Accessible * aAcc)80   virtual uint16_t Match(Accessible* aAcc) override {
81     if (aAcc->IsOuterDoc()) {
82       return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
83     }
84     // We deliberately include Accessibles such as empty input elements and
85     // empty containers, as these can be at the start of a line.
86     if (!aAcc->HasChildren()) {
87       return nsIAccessibleTraversalRule::FILTER_MATCH;
88     }
89     return nsIAccessibleTraversalRule::FILTER_IGNORE;
90   }
91 };
92 
93 /**
94  * Get the document Accessible which owns a given Accessible.
95  * This function is needed because there is no unified base class for local and
96  * remote documents and thus there is no unified way to retrieve the document
97  * from an Accessible.
98  */
DocumentFor(Accessible * aAcc)99 static Accessible* DocumentFor(Accessible* aAcc) {
100   if (LocalAccessible* localAcc = aAcc->AsLocal()) {
101     return localAcc->Document();
102   }
103   return aAcc->AsRemote()->Document();
104 }
105 
HyperTextFor(LocalAccessible * aAcc)106 static HyperTextAccessible* HyperTextFor(LocalAccessible* aAcc) {
107   for (LocalAccessible* acc = aAcc; acc; acc = acc->LocalParent()) {
108     if (HyperTextAccessible* ht = acc->AsHyperText()) {
109       return ht;
110     }
111   }
112   return nullptr;
113 }
114 
NextLeaf(Accessible * aOrigin)115 static Accessible* NextLeaf(Accessible* aOrigin) {
116   Accessible* doc = DocumentFor(aOrigin);
117   Pivot pivot(doc);
118   auto rule = LeafRule();
119   return pivot.Next(aOrigin, rule);
120 }
121 
PrevLeaf(Accessible * aOrigin)122 static Accessible* PrevLeaf(Accessible* aOrigin) {
123   Accessible* doc = DocumentFor(aOrigin);
124   Pivot pivot(doc);
125   auto rule = LeafRule();
126   return pivot.Prev(aOrigin, rule);
127 }
128 
IsLocalAccAtLineStart(LocalAccessible * aAcc)129 static bool IsLocalAccAtLineStart(LocalAccessible* aAcc) {
130   if (aAcc->NativeRole() == roles::LISTITEM_MARKER) {
131     // A bullet always starts a line.
132     return true;
133   }
134   // Splitting of content across lines is handled by layout.
135   // nsIFrame::IsLogicallyAtLineEdge queries whether a frame is the first frame
136   // on its line. However, we can't use that because the first frame on a line
137   // might not be included in the a11y tree; e.g. an empty span, or space
138   // in the DOM after a line break which is stripped when rendered. Instead, we
139   // get the line number for this Accessible's frame and the line number for the
140   // previous leaf Accessible's frame and compare them.
141   Accessible* prev = PrevLeaf(aAcc);
142   LocalAccessible* prevLocal = prev ? prev->AsLocal() : nullptr;
143   if (!prevLocal) {
144     // There's nothing before us, so this is the start of the first line.
145     return true;
146   }
147   if (prevLocal->NativeRole() == roles::LISTITEM_MARKER) {
148     // If there is  a bullet immediately before us and we're inside the same
149     // list item, this is not the start of a line.
150     LocalAccessible* listItem = prevLocal->LocalParent();
151     MOZ_ASSERT(listItem);
152     LocalAccessible* doc = listItem->Document();
153     MOZ_ASSERT(doc);
154     for (LocalAccessible* parent = aAcc->LocalParent(); parent && parent != doc;
155          parent = parent->LocalParent()) {
156       if (parent == listItem) {
157         return false;
158       }
159     }
160   }
161   nsIFrame* thisFrame = aAcc->GetFrame();
162   if (!thisFrame) {
163     return false;
164   }
165   // Even though we have a leaf Accessible, there might be a child frame; e.g.
166   // an empty input element is a leaf Accessible but has an empty text frame
167   // child. To get a line number, we need a leaf frame.
168   nsIFrame::GetFirstLeaf(&thisFrame);
169   nsIFrame* prevFrame = prevLocal->GetFrame();
170   if (!prevFrame) {
171     return false;
172   }
173   nsIFrame::GetLastLeaf(&prevFrame);
174   auto [thisBlock, thisLineFrame] = thisFrame->GetContainingBlockForLine(
175       /* aLockScroll */ false);
176   if (!thisBlock) {
177     // We couldn't get the containing block for this frame. In that case, we
178     // play it safe and assume this is the beginning of a new line.
179     return true;
180   }
181   auto [prevBlock, prevLineFrame] = prevFrame->GetContainingBlockForLine(
182       /* aLockScroll */ false);
183   if (thisBlock != prevBlock) {
184     // If the blocks are different, that means there's nothing before us on the
185     // same line, so we're at the start.
186     return true;
187   }
188   nsAutoLineIterator it = prevBlock->GetLineIterator();
189   MOZ_ASSERT(it, "GetLineIterator impl in line-container blocks is infallible");
190   int32_t prevLineNum = it->FindLineContaining(prevLineFrame);
191   if (prevLineNum < 0) {
192     // Error; play it safe.
193     return true;
194   }
195   int32_t thisLineNum = it->FindLineContaining(thisLineFrame, prevLineNum);
196   // if the blocks and line numbers are different, that means there's nothing
197   // before us on the same line, so we're at the start.
198   return thisLineNum != prevLineNum;
199 }
200 
201 /**
202  * There are many kinds of word break, but we only need to treat punctuation and
203  * space specially.
204  */
205 enum WordBreakClass { eWbcSpace = 0, eWbcPunct, eWbcOther };
206 
GetWordBreakClass(char16_t aChar)207 static WordBreakClass GetWordBreakClass(char16_t aChar) {
208   // Based on IsSelectionInlineWhitespace and IsSelectionNewline in
209   // layout/generic/nsTextFrame.cpp.
210   const char16_t kCharNbsp = 0xA0;
211   switch (aChar) {
212     case ' ':
213     case kCharNbsp:
214     case '\t':
215     case '\f':
216     case '\n':
217     case '\r':
218       return eWbcSpace;
219     default:
220       break;
221   }
222   // Based on ClusterIterator::IsPunctuation in
223   // layout/generic/nsTextFrame.cpp.
224   uint8_t cat = unicode::GetGeneralCategory(aChar);
225   switch (cat) {
226     case HB_UNICODE_GENERAL_CATEGORY_CONNECT_PUNCTUATION: /* Pc */
227       if (aChar == '_' &&
228           !StaticPrefs::layout_word_select_stop_at_underscore()) {
229         return eWbcOther;
230       }
231       [[fallthrough]];
232     case HB_UNICODE_GENERAL_CATEGORY_DASH_PUNCTUATION:    /* Pd */
233     case HB_UNICODE_GENERAL_CATEGORY_CLOSE_PUNCTUATION:   /* Pe */
234     case HB_UNICODE_GENERAL_CATEGORY_FINAL_PUNCTUATION:   /* Pf */
235     case HB_UNICODE_GENERAL_CATEGORY_INITIAL_PUNCTUATION: /* Pi */
236     case HB_UNICODE_GENERAL_CATEGORY_OTHER_PUNCTUATION:   /* Po */
237     case HB_UNICODE_GENERAL_CATEGORY_OPEN_PUNCTUATION:    /* Ps */
238     case HB_UNICODE_GENERAL_CATEGORY_CURRENCY_SYMBOL:     /* Sc */
239     case HB_UNICODE_GENERAL_CATEGORY_MATH_SYMBOL:         /* Sm */
240     case HB_UNICODE_GENERAL_CATEGORY_OTHER_SYMBOL:        /* So */
241       return eWbcPunct;
242     default:
243       break;
244   }
245   return eWbcOther;
246 }
247 
248 /**
249  * Words can cross Accessibles. To work out whether we're at the start of a
250  * word, we might have to check the previous leaf. This class handles querying
251  * the previous WordBreakClass, crossing Accessibles if necessary.
252  */
253 class PrevWordBreakClassWalker {
254  public:
PrevWordBreakClassWalker(Accessible * aAcc,const nsAString & aText,int32_t aOffset)255   PrevWordBreakClassWalker(Accessible* aAcc, const nsAString& aText,
256                            int32_t aOffset)
257       : mAcc(aAcc), mText(aText), mOffset(aOffset) {
258     mClass = GetWordBreakClass(mText.CharAt(mOffset));
259   }
260 
CurClass()261   WordBreakClass CurClass() { return mClass; }
262 
PrevClass()263   Maybe<WordBreakClass> PrevClass() {
264     for (;;) {
265       if (!PrevChar()) {
266         return Nothing();
267       }
268       WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
269       if (curClass != mClass) {
270         mClass = curClass;
271         return Some(curClass);
272       }
273     }
274     MOZ_ASSERT_UNREACHABLE();
275     return Nothing();
276   }
277 
IsStartOfGroup()278   bool IsStartOfGroup() {
279     if (!PrevChar()) {
280       // There are no characters before us.
281       return true;
282     }
283     WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
284     // We wanted to peek at the previous character, not really move to it.
285     ++mOffset;
286     return curClass != mClass;
287   }
288 
289  private:
PrevChar()290   bool PrevChar() {
291     if (mOffset > 0) {
292       --mOffset;
293       return true;
294     }
295     if (!mAcc) {
296       // PrevChar was called already and failed.
297       return false;
298     }
299     mAcc = PrevLeaf(mAcc);
300     if (!mAcc) {
301       return false;
302     }
303     mText.Truncate();
304     mAcc->AppendTextTo(mText);
305     mOffset = static_cast<int32_t>(mText.Length()) - 1;
306     return true;
307   }
308 
309   Accessible* mAcc;
310   nsAutoString mText;
311   int32_t mOffset;
312   WordBreakClass mClass;
313 };
314 
315 /**
316  * WordBreaker breaks at all space, punctuation, etc. We want to emulate
317  * layout, so that's not what we want. This function determines whether this
318  * is acceptable as the start of a word for our purposes.
319  */
IsAcceptableWordStart(Accessible * aAcc,const nsAutoString & aText,int32_t aOffset)320 static bool IsAcceptableWordStart(Accessible* aAcc, const nsAutoString& aText,
321                                   int32_t aOffset) {
322   PrevWordBreakClassWalker walker(aAcc, aText, aOffset);
323   if (!walker.IsStartOfGroup()) {
324     // If we're not at the start of a WordBreaker group, this can't be the
325     // start of a word.
326     return false;
327   }
328   WordBreakClass curClass = walker.CurClass();
329   if (curClass == eWbcSpace) {
330     // Space isn't the start of a word.
331     return false;
332   }
333   Maybe<WordBreakClass> prevClass = walker.PrevClass();
334   if (curClass == eWbcPunct && (!prevClass || prevClass.value() != eWbcSpace)) {
335     // Punctuation isn't the start of a word (unless it is after space).
336     return false;
337   }
338   if (!prevClass || prevClass.value() != eWbcPunct) {
339     // If there's nothing before this or the group before this isn't
340     // punctuation, this is the start of a word.
341     return true;
342   }
343   // At this point, we know the group before this is punctuation.
344   if (!StaticPrefs::layout_word_select_stop_at_punctuation()) {
345     // When layout.word_select.stop_at_punctuation is false (defaults to true),
346     // if there is punctuation before this, this is not the start of a word.
347     return false;
348   }
349   Maybe<WordBreakClass> prevPrevClass = walker.PrevClass();
350   if (!prevPrevClass || prevPrevClass.value() == eWbcSpace) {
351     // If there is punctuation before this and space (or nothing) before the
352     // punctuation, this is not the start of a word.
353     return false;
354   }
355   return true;
356 }
357 
358 class BlockRule : public PivotRule {
359  public:
Match(Accessible * aAcc)360   virtual uint16_t Match(Accessible* aAcc) override {
361     if (RefPtr<nsAtom>(aAcc->DisplayStyle()) == nsGkAtoms::block ||
362         aAcc->IsHTMLListItem() ||
363         // XXX Bullets are inline-block, but the old local implementation treats
364         // them as block because IsBlockFrame() returns true. Semantically,
365         // they shouldn't be treated as blocks, so this should be removed once
366         // we only have a single implementation to deal with.
367         (aAcc->IsText() && aAcc->Role() == roles::LISTITEM_MARKER)) {
368       return nsIAccessibleTraversalRule::FILTER_MATCH;
369     }
370     return nsIAccessibleTraversalRule::FILTER_IGNORE;
371   }
372 };
373 
374 /*** TextLeafPoint ***/
375 
TextLeafPoint(Accessible * aAcc,int32_t aOffset)376 TextLeafPoint::TextLeafPoint(Accessible* aAcc, int32_t aOffset) {
377   if (aOffset != nsIAccessibleText::TEXT_OFFSET_CARET && aAcc->HasChildren()) {
378     // Find a leaf. This might not necessarily be a TextLeafAccessible; it
379     // could be an empty container.
380     for (Accessible* acc = aAcc->FirstChild(); acc; acc = acc->FirstChild()) {
381       mAcc = acc;
382     }
383     mOffset = 0;
384     return;
385   }
386   mAcc = aAcc;
387   mOffset = aOffset;
388 }
389 
operator <(const TextLeafPoint & aPoint) const390 bool TextLeafPoint::operator<(const TextLeafPoint& aPoint) const {
391   if (mAcc == aPoint.mAcc) {
392     return mOffset < aPoint.mOffset;
393   }
394   return mAcc->IsBefore(aPoint.mAcc);
395 }
396 
IsEmptyLastLine() const397 bool TextLeafPoint::IsEmptyLastLine() const {
398   if (mAcc->IsHTMLBr() && mOffset == 1) {
399     return true;
400   }
401   if (!mAcc->IsTextLeaf()) {
402     return false;
403   }
404   if (mOffset < static_cast<int32_t>(nsAccUtils::TextLength(mAcc))) {
405     return false;
406   }
407   nsAutoString text;
408   mAcc->AppendTextTo(text, mOffset - 1, 1);
409   return text.CharAt(0) == '\n';
410 }
411 
GetChar() const412 char16_t TextLeafPoint::GetChar() const {
413   nsAutoString text;
414   mAcc->AppendTextTo(text, mOffset, 1);
415   return text.CharAt(0);
416 }
417 
FindPrevLineStartSameLocalAcc(bool aIncludeOrigin) const418 TextLeafPoint TextLeafPoint::FindPrevLineStartSameLocalAcc(
419     bool aIncludeOrigin) const {
420   LocalAccessible* acc = mAcc->AsLocal();
421   MOZ_ASSERT(acc);
422   if (mOffset == 0) {
423     if (aIncludeOrigin && IsLocalAccAtLineStart(acc)) {
424       return *this;
425     }
426     return TextLeafPoint();
427   }
428   nsIFrame* frame = acc->GetFrame();
429   if (!frame) {
430     // This can happen if this is an empty element with display: contents. In
431     // that case, this Accessible contains no lines.
432     return TextLeafPoint();
433   }
434   if (!frame->IsTextFrame()) {
435     if (IsLocalAccAtLineStart(acc)) {
436       return TextLeafPoint(acc, 0);
437     }
438     return TextLeafPoint();
439   }
440   // Each line of a text node is rendered as a continuation frame. Get the
441   // continuation containing the origin.
442   int32_t origOffset = mOffset;
443   origOffset = RenderedToContentOffset(acc, origOffset);
444   nsTextFrame* continuation = nullptr;
445   int32_t unusedOffsetInContinuation = 0;
446   frame->GetChildFrameContainingOffset(
447       origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation);
448   MOZ_ASSERT(continuation);
449   int32_t lineStart = continuation->GetContentOffset();
450   if (!aIncludeOrigin && lineStart > 0 && lineStart == origOffset) {
451     // A line starts at the origin, but the caller doesn't want this included.
452     // Go back one more.
453     continuation = continuation->GetPrevContinuation();
454     MOZ_ASSERT(continuation);
455     lineStart = continuation->GetContentOffset();
456   }
457   MOZ_ASSERT(lineStart >= 0);
458   if (lineStart == 0 && !IsLocalAccAtLineStart(acc)) {
459     // This is the first line of this text node, but there is something else
460     // on the same line before this text node, so don't return this as a line
461     // start.
462     return TextLeafPoint();
463   }
464   lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart));
465   return TextLeafPoint(acc, lineStart);
466 }
467 
FindNextLineStartSameLocalAcc(bool aIncludeOrigin) const468 TextLeafPoint TextLeafPoint::FindNextLineStartSameLocalAcc(
469     bool aIncludeOrigin) const {
470   LocalAccessible* acc = mAcc->AsLocal();
471   MOZ_ASSERT(acc);
472   if (aIncludeOrigin && mOffset == 0 && IsLocalAccAtLineStart(acc)) {
473     return *this;
474   }
475   nsIFrame* frame = acc->GetFrame();
476   if (!frame) {
477     // This can happen if this is an empty element with display: contents. In
478     // that case, this Accessible contains no lines.
479     return TextLeafPoint();
480   }
481   if (!frame->IsTextFrame()) {
482     // There can't be multiple lines in a non-text leaf.
483     return TextLeafPoint();
484   }
485   // Each line of a text node is rendered as a continuation frame. Get the
486   // continuation containing the origin.
487   int32_t origOffset = mOffset;
488   origOffset = RenderedToContentOffset(acc, origOffset);
489   nsTextFrame* continuation = nullptr;
490   int32_t unusedOffsetInContinuation = 0;
491   frame->GetChildFrameContainingOffset(
492       origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation);
493   MOZ_ASSERT(continuation);
494   if (
495       // A line starts at the origin and the caller wants this included.
496       aIncludeOrigin && continuation->GetContentOffset() == origOffset &&
497       // If this is the first line of this text node (offset 0), don't treat it
498       // as a line start if there's something else on the line before this text
499       // node.
500       !(origOffset == 0 && !IsLocalAccAtLineStart(acc))) {
501     return *this;
502   }
503   continuation = continuation->GetNextContinuation();
504   if (!continuation) {
505     return TextLeafPoint();
506   }
507   int32_t lineStart = continuation->GetContentOffset();
508   lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart));
509   return TextLeafPoint(acc, lineStart);
510 }
511 
FindLineStartSameRemoteAcc(nsDirection aDirection,bool aIncludeOrigin) const512 TextLeafPoint TextLeafPoint::FindLineStartSameRemoteAcc(
513     nsDirection aDirection, bool aIncludeOrigin) const {
514   RemoteAccessible* acc = mAcc->AsRemote();
515   MOZ_ASSERT(acc);
516   auto lines = acc->GetCachedTextLines();
517   if (!lines) {
518     return TextLeafPoint();
519   }
520   size_t index;
521   // If BinarySearch returns true, mOffset is in the array and index points at
522   // it. If BinarySearch returns false, mOffset is not in the array and index
523   // points at the next line start after mOffset.
524   if (BinarySearch(*lines, 0, lines->Length(), mOffset, &index)) {
525     if (aIncludeOrigin) {
526       return *this;
527     }
528     if (aDirection == eDirNext) {
529       // We don't want to include the origin. Get the next line start.
530       ++index;
531     }
532   }
533   MOZ_ASSERT(index <= lines->Length());
534   if ((aDirection == eDirNext && index == lines->Length()) || index == 0) {
535     return TextLeafPoint();
536   }
537   // index points at the line start after mOffset.
538   if (aDirection == eDirPrevious) {
539     --index;
540   }
541   return TextLeafPoint(mAcc, lines->ElementAt(index));
542 }
543 
FindLineStartSameAcc(nsDirection aDirection,bool aIncludeOrigin) const544 TextLeafPoint TextLeafPoint::FindLineStartSameAcc(nsDirection aDirection,
545                                                   bool aIncludeOrigin) const {
546   if (mAcc->IsLocal()) {
547     return aDirection == eDirNext
548                ? FindNextLineStartSameLocalAcc(aIncludeOrigin)
549                : FindPrevLineStartSameLocalAcc(aIncludeOrigin);
550   }
551   return FindLineStartSameRemoteAcc(aDirection, aIncludeOrigin);
552 }
553 
FindPrevWordStartSameAcc(bool aIncludeOrigin) const554 TextLeafPoint TextLeafPoint::FindPrevWordStartSameAcc(
555     bool aIncludeOrigin) const {
556   if (mOffset == 0 && !aIncludeOrigin) {
557     // We can't go back any further and the caller doesn't want the origin
558     // included, so there's nothing more to do.
559     return TextLeafPoint();
560   }
561   nsAutoString text;
562   mAcc->AppendTextTo(text);
563   TextLeafPoint lineStart = *this;
564   if (!aIncludeOrigin || (lineStart.mOffset == 1 && text.Length() == 1 &&
565                           text.CharAt(0) == '\n')) {
566     // We're not interested in a line that starts here, either because
567     // aIncludeOrigin is false or because we're at the end of a line break
568     // node.
569     --lineStart.mOffset;
570   }
571   // A word never starts with a line feed character. If there are multiple
572   // consecutive line feed characters and we're after the first of them, the
573   // previous line start will be a line feed character. Skip this and any prior
574   // consecutive line feed first.
575   for (; lineStart.mOffset >= 0 && text.CharAt(lineStart.mOffset) == '\n';
576        --lineStart.mOffset) {
577   }
578   if (lineStart.mOffset < 0) {
579     // There's no line start for our purposes.
580     lineStart = TextLeafPoint();
581   } else {
582     lineStart =
583         lineStart.FindLineStartSameAcc(eDirPrevious, /* aIncludeOrigin */ true);
584   }
585   // Keep walking backward until we find an acceptable word start.
586   intl::WordRange word;
587   if (mOffset == 0) {
588     word.mBegin = 0;
589   } else if (mOffset == static_cast<int32_t>(text.Length())) {
590     word = WordBreaker::FindWord(text.get(), text.Length(), mOffset - 1);
591   } else {
592     word = WordBreaker::FindWord(text.get(), text.Length(), mOffset);
593   }
594   for (;; word = WordBreaker::FindWord(text.get(), text.Length(),
595                                        word.mBegin - 1)) {
596     if (!aIncludeOrigin && static_cast<int32_t>(word.mBegin) == mOffset) {
597       // A word possibly starts at the origin, but the caller doesn't want this
598       // included.
599       MOZ_ASSERT(word.mBegin != 0);
600       continue;
601     }
602     if (lineStart && static_cast<int32_t>(word.mBegin) < lineStart.mOffset) {
603       // A line start always starts a new word.
604       return lineStart;
605     }
606     if (IsAcceptableWordStart(mAcc, text, static_cast<int32_t>(word.mBegin))) {
607       break;
608     }
609     if (word.mBegin == 0) {
610       // We can't go back any further.
611       if (lineStart) {
612         // A line start always starts a new word.
613         return lineStart;
614       }
615       return TextLeafPoint();
616     }
617   }
618   return TextLeafPoint(mAcc, static_cast<int32_t>(word.mBegin));
619 }
620 
FindNextWordStartSameAcc(bool aIncludeOrigin) const621 TextLeafPoint TextLeafPoint::FindNextWordStartSameAcc(
622     bool aIncludeOrigin) const {
623   nsAutoString text;
624   mAcc->AppendTextTo(text);
625   int32_t wordStart = mOffset;
626   if (aIncludeOrigin) {
627     if (wordStart == 0) {
628       if (IsAcceptableWordStart(mAcc, text, 0)) {
629         return *this;
630       }
631     } else {
632       // The origin might start a word, so search from just before it.
633       --wordStart;
634     }
635   }
636   TextLeafPoint lineStart = FindLineStartSameAcc(eDirNext, aIncludeOrigin);
637   if (lineStart) {
638     // A word never starts with a line feed character. If there are multiple
639     // consecutive line feed characters, lineStart will point at the second of
640     // them. Skip this and any subsequent consecutive line feed.
641     for (; lineStart.mOffset < static_cast<int32_t>(text.Length()) &&
642            text.CharAt(lineStart.mOffset) == '\n';
643          ++lineStart.mOffset) {
644     }
645     if (lineStart.mOffset == static_cast<int32_t>(text.Length())) {
646       // There's no line start for our purposes.
647       lineStart = TextLeafPoint();
648     }
649   }
650   // Keep walking forward until we find an acceptable word start.
651   intl::WordBreakIteratorUtf16 wordBreakIter(text);
652   Maybe<uint32_t> nextBreak = wordBreakIter.Seek(wordStart);
653   for (;;) {
654     if (!nextBreak || *nextBreak == text.Length()) {
655       if (lineStart) {
656         // A line start always starts a new word.
657         return lineStart;
658       }
659       return TextLeafPoint();
660     }
661     wordStart = AssertedCast<int32_t>(*nextBreak);
662     if (lineStart && wordStart > lineStart.mOffset) {
663       // A line start always starts a new word.
664       return lineStart;
665     }
666     if (IsAcceptableWordStart(mAcc, text, wordStart)) {
667       break;
668     }
669     nextBreak = wordBreakIter.Next();
670   }
671   return TextLeafPoint(mAcc, wordStart);
672 }
673 
IsCaretAtEndOfLine() const674 bool TextLeafPoint::IsCaretAtEndOfLine() const {
675   MOZ_ASSERT(IsCaret());
676   if (LocalAccessible* acc = mAcc->AsLocal()) {
677     HyperTextAccessible* ht = HyperTextFor(acc);
678     if (!ht) {
679       return false;
680     }
681     // Use HyperTextAccessible::IsCaretAtEndOfLine. Eventually, we'll want to
682     // move that code into TextLeafPoint, but existing code depends on it living
683     // in HyperTextAccessible (including caret events).
684     return ht->IsCaretAtEndOfLine();
685   }
686   return mAcc->AsRemote()->Document()->IsCaretAtEndOfLine();
687 }
688 
ActualizeCaret(bool aAdjustAtEndOfLine) const689 TextLeafPoint TextLeafPoint::ActualizeCaret(bool aAdjustAtEndOfLine) const {
690   MOZ_ASSERT(IsCaret());
691   HyperTextAccessibleBase* ht;
692   int32_t htOffset;
693   if (LocalAccessible* acc = mAcc->AsLocal()) {
694     // Use HyperTextAccessible::CaretOffset. Eventually, we'll want to move
695     // that code into TextLeafPoint, but existing code depends on it living in
696     // HyperTextAccessible (including caret events).
697     ht = HyperTextFor(acc);
698     if (!ht) {
699       return TextLeafPoint();
700     }
701     htOffset = ht->CaretOffset();
702     if (htOffset == -1) {
703       return TextLeafPoint();
704     }
705   } else {
706     // Ideally, we'd cache the caret as a leaf, but our events are based on
707     // HyperText for now.
708     std::tie(ht, htOffset) = mAcc->AsRemote()->Document()->GetCaret();
709     if (!ht) {
710       return TextLeafPoint();
711     }
712   }
713   if (aAdjustAtEndOfLine && htOffset > 0 && IsCaretAtEndOfLine()) {
714     // It is the same character offset when the caret is visually at the very
715     // end of a line or the start of a new line (soft line break). Getting text
716     // at the line should provide the line with the visual caret. Otherwise,
717     // screen readers will announce the wrong line as the user presses up or
718     // down arrow and land at the end of a line.
719     --htOffset;
720   }
721   return ht->ToTextLeafPoint(htOffset);
722 }
723 
FindBoundary(AccessibleTextBoundary aBoundaryType,nsDirection aDirection,bool aIncludeOrigin) const724 TextLeafPoint TextLeafPoint::FindBoundary(AccessibleTextBoundary aBoundaryType,
725                                           nsDirection aDirection,
726                                           bool aIncludeOrigin) const {
727   if (IsCaret()) {
728     if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
729       if (IsCaretAtEndOfLine()) {
730         // The caret is at the end of the line. Return no character.
731         return ActualizeCaret(/* aAdjustAtEndOfLine */ false);
732       }
733     }
734     return ActualizeCaret().FindBoundary(aBoundaryType, aDirection,
735                                          aIncludeOrigin);
736   }
737   if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) {
738     return FindLineEnd(aDirection, aIncludeOrigin);
739   }
740   if (aBoundaryType == nsIAccessibleText::BOUNDARY_WORD_END) {
741     return FindWordEnd(aDirection, aIncludeOrigin);
742   }
743   if ((aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_START ||
744        aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) &&
745       aIncludeOrigin && aDirection == eDirPrevious && IsEmptyLastLine()) {
746     // If we're at an empty line at the end of an Accessible,  we don't want to
747     // walk into the previous line. For example, this can happen if the caret
748     // is positioned on an empty line at the end of a textarea.
749     return *this;
750   }
751   if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR && aIncludeOrigin) {
752     return *this;
753   }
754   TextLeafPoint searchFrom = *this;
755   bool includeOrigin = aIncludeOrigin;
756   for (;;) {
757     TextLeafPoint boundary;
758     // Search for the boundary within the current Accessible.
759     switch (aBoundaryType) {
760       case nsIAccessibleText::BOUNDARY_CHAR:
761         if (aDirection == eDirPrevious && searchFrom.mOffset > 0) {
762           boundary.mAcc = searchFrom.mAcc;
763           boundary.mOffset = searchFrom.mOffset - 1;
764         } else if (aDirection == eDirNext) {
765           if (includeOrigin) {
766             // We've moved to the next leaf. That means we've set the offset
767             // to 0, so we're already at the next character.
768             boundary = searchFrom;
769           } else if (searchFrom.mOffset + 1 <
770                      static_cast<int32_t>(
771                          nsAccUtils::TextLength(searchFrom.mAcc))) {
772             boundary.mAcc = searchFrom.mAcc;
773             boundary.mOffset = searchFrom.mOffset + 1;
774           }
775         }
776         break;
777       case nsIAccessibleText::BOUNDARY_WORD_START:
778         if (aDirection == eDirPrevious) {
779           boundary = searchFrom.FindPrevWordStartSameAcc(includeOrigin);
780         } else {
781           boundary = searchFrom.FindNextWordStartSameAcc(includeOrigin);
782         }
783         break;
784       case nsIAccessibleText::BOUNDARY_LINE_START:
785         boundary = searchFrom.FindLineStartSameAcc(aDirection, includeOrigin);
786         break;
787       case nsIAccessibleText::BOUNDARY_PARAGRAPH:
788         boundary = searchFrom.FindParagraphSameAcc(aDirection, includeOrigin);
789         break;
790       default:
791         MOZ_ASSERT_UNREACHABLE();
792         break;
793     }
794     if (boundary) {
795       return boundary;
796     }
797     // We didn't find it in this Accessible, so try the previous/next leaf.
798     Accessible* acc = aDirection == eDirPrevious ? PrevLeaf(searchFrom.mAcc)
799                                                  : NextLeaf(searchFrom.mAcc);
800     if (!acc) {
801       // No further leaf was found. Use the start/end of the first/last leaf.
802       return TextLeafPoint(
803           searchFrom.mAcc,
804           aDirection == eDirPrevious
805               ? 0
806               : static_cast<int32_t>(nsAccUtils::TextLength(searchFrom.mAcc)));
807     }
808     searchFrom.mAcc = acc;
809     // When searching backward, search from the end of the text in the
810     // Accessible. When searching forward, search from the start of the text.
811     searchFrom.mOffset = aDirection == eDirPrevious
812                              ? static_cast<int32_t>(nsAccUtils::TextLength(acc))
813                              : 0;
814     // The start/end of the Accessible might be a boundary. If so, we must stop
815     // on it.
816     includeOrigin = true;
817   }
818   MOZ_ASSERT_UNREACHABLE();
819   return TextLeafPoint();
820 }
821 
FindLineEnd(nsDirection aDirection,bool aIncludeOrigin) const822 TextLeafPoint TextLeafPoint::FindLineEnd(nsDirection aDirection,
823                                          bool aIncludeOrigin) const {
824   if (aDirection == eDirPrevious && IsEmptyLastLine()) {
825     // If we're at an empty line at the end of an Accessible,  we don't want to
826     // walk into the previous line. For example, this can happen if the caret
827     // is positioned on an empty line at the end of a textarea.
828     // Because we want the line end, we must walk back to the line feed
829     // character.
830     return FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
831   }
832   if (aIncludeOrigin && IsLineFeedChar()) {
833     return *this;
834   }
835   if (aDirection == eDirPrevious && !aIncludeOrigin) {
836     // If there is a line feed immediately before us, return that.
837     TextLeafPoint prevChar =
838         FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
839     if (prevChar.IsLineFeedChar()) {
840       return prevChar;
841     }
842   }
843   TextLeafPoint searchFrom = *this;
844   if (aDirection == eDirNext && (IsLineFeedChar() || IsEmptyLastLine())) {
845     // If we search for the next line start from a line feed, we'll get the
846     // character immediately following the line feed. We actually want the
847     // next line start after that. Skip the line feed.
848     searchFrom = FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext);
849   }
850   TextLeafPoint lineStart = searchFrom.FindBoundary(
851       nsIAccessibleText::BOUNDARY_LINE_START, aDirection, aIncludeOrigin);
852   // If there is a line feed before this line start (at the end of the previous
853   // line), we must return that.
854   TextLeafPoint prevChar = lineStart.FindBoundary(
855       nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, false);
856   if (prevChar && prevChar.IsLineFeedChar()) {
857     return prevChar;
858   }
859   return lineStart;
860 }
861 
IsSpace() const862 bool TextLeafPoint::IsSpace() const {
863   return GetWordBreakClass(GetChar()) == eWbcSpace;
864 }
865 
FindWordEnd(nsDirection aDirection,bool aIncludeOrigin) const866 TextLeafPoint TextLeafPoint::FindWordEnd(nsDirection aDirection,
867                                          bool aIncludeOrigin) const {
868   char16_t origChar = GetChar();
869   const bool origIsSpace = GetWordBreakClass(origChar) == eWbcSpace;
870   bool prevIsSpace = false;
871   if (aDirection == eDirPrevious || (aIncludeOrigin && origIsSpace) ||
872       !origChar) {
873     TextLeafPoint prev =
874         FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious, false);
875     if (aDirection == eDirPrevious && prev == *this) {
876       return *this;  // Can't go any further.
877     }
878     prevIsSpace = prev.IsSpace();
879     if (aIncludeOrigin && origIsSpace && !prevIsSpace) {
880       // The origin is space, but the previous character is not. This means
881       // we're at the end of a word.
882       return *this;
883     }
884   }
885   TextLeafPoint boundary = *this;
886   if (aDirection == eDirPrevious && !prevIsSpace) {
887     // If there isn't space immediately before us, first find the start of the
888     // previous word.
889     boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START,
890                             eDirPrevious, aIncludeOrigin);
891   } else if (aDirection == eDirNext &&
892              (origIsSpace || (!origChar && prevIsSpace))) {
893     // We're within the space at the end of the word. Skip over the space. We
894     // can do that by searching for the next word start.
895     boundary =
896         FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirNext, false);
897     if (boundary.IsSpace()) {
898       // The next word starts with a space. This can happen if there is a space
899       // after or at the start of a block element.
900       return boundary;
901     }
902   }
903   if (aDirection == eDirNext) {
904     boundary = boundary.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START,
905                                      eDirNext, aIncludeOrigin);
906   }
907   // At this point, boundary is either the start of a word or at a space. A
908   // word ends at the beginning of consecutive space. Therefore, skip back to
909   // the start of any space before us.
910   TextLeafPoint prev = boundary;
911   for (;;) {
912     prev = prev.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
913     if (prev == boundary) {
914       break;  // Can't go any further.
915     }
916     if (!prev.IsSpace()) {
917       break;
918     }
919     boundary = prev;
920   }
921   return boundary;
922 }
923 
FindParagraphSameAcc(nsDirection aDirection,bool aIncludeOrigin) const924 TextLeafPoint TextLeafPoint::FindParagraphSameAcc(nsDirection aDirection,
925                                                   bool aIncludeOrigin) const {
926   if (mAcc->IsTextLeaf() &&
927       // We don't want to copy strings unnecessarily. See below for the context
928       // of these individual conditions.
929       ((aIncludeOrigin && mOffset > 0) || aDirection == eDirNext ||
930        mOffset >= 2)) {
931     // If there is a line feed, a new paragraph begins after it.
932     nsAutoString text;
933     mAcc->AppendTextTo(text);
934     if (aIncludeOrigin && mOffset > 0 && text.CharAt(mOffset - 1) == '\n') {
935       return TextLeafPoint(mAcc, mOffset);
936     }
937     int32_t lfOffset = -1;
938     if (aDirection == eDirNext) {
939       lfOffset = text.FindChar('\n', mOffset);
940     } else if (mOffset >= 2) {
941       // A line feed at mOffset - 1 means the origin begins a new paragraph,
942       // but we already handled aIncludeOrigin above. Therefore, we search from
943       // mOffset - 2.
944       lfOffset = text.RFindChar('\n', mOffset - 2);
945     }
946     if (lfOffset != -1 && lfOffset + 1 < static_cast<int32_t>(text.Length())) {
947       return TextLeafPoint(mAcc, lfOffset + 1);
948     }
949   }
950 
951   // Check whether this Accessible begins a paragraph.
952   if ((!aIncludeOrigin && mOffset == 0) ||
953       (aDirection == eDirNext && mOffset > 0)) {
954     // The caller isn't interested in whether this Accessible begins a
955     // paragraph.
956     return TextLeafPoint();
957   }
958   Accessible* prevLeaf = PrevLeaf(mAcc);
959   BlockRule blockRule;
960   Pivot pivot(DocumentFor(mAcc));
961   Accessible* prevBlock = pivot.Prev(mAcc, blockRule);
962   // Check if we're the first leaf after a block element.
963   if (prevBlock &&
964       // If there's no previous leaf, we must be the first leaf after the block.
965       (!prevLeaf ||
966        // A block can be a leaf; e.g. an empty div or paragraph.
967        prevBlock == prevLeaf ||
968        // If we aren't inside the block, the block must be before us. This
969        // check is important because a block causes a paragraph break both
970        // before and after it.
971        !prevBlock->IsAncestorOf(mAcc) ||
972        // If we are inside the block and the previous leaf isn't, we must be
973        // the first leaf in the block.
974        !prevBlock->IsAncestorOf(prevLeaf))) {
975     return TextLeafPoint(mAcc, 0);
976   }
977   if (!prevLeaf || prevLeaf->IsHTMLBr()) {
978     // We're the first leaf after a line break or the start of the document.
979     return TextLeafPoint(mAcc, 0);
980   }
981   if (prevLeaf->IsTextLeaf()) {
982     // There's a text leaf before us. Check if it ends with a line feed.
983     nsAutoString text;
984     prevLeaf->AppendTextTo(text, nsAccUtils::TextLength(prevLeaf) - 1, 1);
985     if (text.CharAt(0) == '\n') {
986       return TextLeafPoint(mAcc, 0);
987     }
988   }
989   return TextLeafPoint();
990 }
991 
GetTextAttributesLocalAcc(bool aIncludeDefaults) const992 already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributesLocalAcc(
993     bool aIncludeDefaults) const {
994   LocalAccessible* acc = mAcc->AsLocal();
995   MOZ_ASSERT(acc);
996   MOZ_ASSERT(acc->IsText());
997   // TextAttrsMgr wants a HyperTextAccessible.
998   LocalAccessible* parent = acc->LocalParent();
999   HyperTextAccessible* hyperAcc = parent->AsHyperText();
1000   MOZ_ASSERT(hyperAcc);
1001   RefPtr<AccAttributes> attributes = new AccAttributes();
1002   TextAttrsMgr mgr(hyperAcc, aIncludeDefaults, acc,
1003                    acc ? acc->IndexInParent() : -1);
1004   mgr.GetAttributes(attributes, nullptr, nullptr);
1005   return attributes.forget();
1006 }
1007 
GetTextAttributes(bool aIncludeDefaults) const1008 already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributes(
1009     bool aIncludeDefaults) const {
1010   if (!mAcc->IsText()) {
1011     return nullptr;
1012   }
1013   if (mAcc->IsLocal()) {
1014     return GetTextAttributesLocalAcc(aIncludeDefaults);
1015   }
1016   RefPtr<AccAttributes> attrs = new AccAttributes();
1017   if (aIncludeDefaults) {
1018     Accessible* parent = mAcc->Parent();
1019     if (parent && parent->IsRemote() && parent->IsHyperText()) {
1020       if (auto defAttrs = parent->AsRemote()->GetCachedTextAttributes()) {
1021         defAttrs->CopyTo(attrs);
1022       }
1023     }
1024   }
1025   if (auto thisAttrs = mAcc->AsRemote()->GetCachedTextAttributes()) {
1026     thisAttrs->CopyTo(attrs);
1027   }
1028   return attrs.forget();
1029 }
1030 
FindTextAttrsStart(nsDirection aDirection,bool aIncludeOrigin,const AccAttributes * aOriginAttrs,bool aIncludeDefaults) const1031 TextLeafPoint TextLeafPoint::FindTextAttrsStart(
1032     nsDirection aDirection, bool aIncludeOrigin,
1033     const AccAttributes* aOriginAttrs, bool aIncludeDefaults) const {
1034   if (IsCaret()) {
1035     return ActualizeCaret().FindTextAttrsStart(aDirection, aIncludeOrigin,
1036                                                aOriginAttrs, aIncludeDefaults);
1037   }
1038   // XXX Add support for spelling errors.
1039   RefPtr<const AccAttributes> lastAttrs;
1040   const bool isRemote = mAcc->IsRemote();
1041   if (isRemote) {
1042     // For RemoteAccessible, leaf attrs and default attrs are cached
1043     // separately. To combine them, we have to copy. Since we're not walking
1044     // outside the container, we don't care about defaults. Therefore, we
1045     // always just fetch the leaf attrs.
1046     // We ignore aOriginAttrs because it might include defaults. Fetching leaf
1047     // attrs is very cheap anyway.
1048     lastAttrs = mAcc->AsRemote()->GetCachedTextAttributes();
1049   } else {
1050     // For LocalAccessible, we want to avoid calculating attrs more than
1051     // necessary, so we want to use aOriginAttrs if provided.
1052     if (aOriginAttrs) {
1053       lastAttrs = aOriginAttrs;
1054       // Whether we include defaults henceforth must match aOriginAttrs, which
1055       // depends on aIncludeDefaults. Defaults are always calculated even if
1056       // they aren't returned, so calculation cost isn't a concern.
1057     } else {
1058       lastAttrs = GetTextAttributesLocalAcc(aIncludeDefaults);
1059     }
1060   }
1061   if (aIncludeOrigin && aDirection == eDirNext && mOffset == 0) {
1062     // Even when searching forward, the only way to know whether the origin is
1063     // the start of a text attrs run is to compare with the previous sibling.
1064     // Anything other than text breaks an attrs run.
1065     TextLeafPoint point;
1066     point.mAcc = mAcc->PrevSibling();
1067     if (!point.mAcc || !point.mAcc->IsText()) {
1068       return *this;
1069     }
1070     // For RemoteAccessible, we can get attributes from the cache without any
1071     // calculation or copying.
1072     RefPtr<const AccAttributes> attrs =
1073         isRemote ? point.mAcc->AsRemote()->GetCachedTextAttributes()
1074                  : point.GetTextAttributesLocalAcc(aIncludeDefaults);
1075     if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) {
1076       return *this;
1077     }
1078   }
1079   TextLeafPoint lastPoint(mAcc, 0);
1080   for (;;) {
1081     TextLeafPoint point;
1082     point.mAcc = aDirection == eDirNext ? lastPoint.mAcc->NextSibling()
1083                                         : lastPoint.mAcc->PrevSibling();
1084     if (!point.mAcc || !point.mAcc->IsText()) {
1085       break;
1086     }
1087     RefPtr<const AccAttributes> attrs =
1088         isRemote ? point.mAcc->AsRemote()->GetCachedTextAttributes()
1089                  : point.GetTextAttributesLocalAcc(aIncludeDefaults);
1090     if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) {
1091       // The attributes change here. If we're moving forward, we want to
1092       // return this point. If we're moving backward, we've now moved before
1093       // the start of the attrs run containing the origin, so return the last
1094       // point we hit.
1095       if (aDirection == eDirPrevious) {
1096         point = lastPoint;
1097       }
1098       if (!aIncludeOrigin && point == *this) {
1099         MOZ_ASSERT(aDirection == eDirPrevious);
1100         // The origin is the start of an attrs run, but the caller doesn't want
1101         // the origin included.
1102         continue;
1103       }
1104       return point;
1105     }
1106     lastPoint = point;
1107     lastAttrs = attrs;
1108   }
1109   // We couldn't move any further. Use the start/end.
1110   return TextLeafPoint(
1111       lastPoint.mAcc,
1112       aDirection == eDirPrevious
1113           ? 0
1114           : static_cast<int32_t>(nsAccUtils::TextLength(lastPoint.mAcc)));
1115 }
1116 
1117 }  // namespace mozilla::a11y
1118