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