Bug 1700051: part 46) Const-qualify `mozInlineSpellStatus::mAnchorRange`. r=smaug
[gecko.git] / accessible / generic / HyperTextAccessible.cpp
blobde48e5aa022c0a69ba020b7cd109141c2431ccc9
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 sw=2 et tw=78: */
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/. */
7 #include "HyperTextAccessible-inl.h"
9 #include "LocalAccessible-inl.h"
10 #include "nsAccessibilityService.h"
11 #include "nsAccessiblePivot.h"
12 #include "nsIAccessibleTypes.h"
13 #include "DocAccessible.h"
14 #include "HTMLListAccessible.h"
15 #include "Pivot.h"
16 #include "Relation.h"
17 #include "Role.h"
18 #include "States.h"
19 #include "TextAttrs.h"
20 #include "TextRange.h"
21 #include "TreeWalker.h"
23 #include "nsCaret.h"
24 #include "nsContentUtils.h"
25 #include "nsDebug.h"
26 #include "nsFocusManager.h"
27 #include "nsIEditingSession.h"
28 #include "nsContainerFrame.h"
29 #include "nsFrameSelection.h"
30 #include "nsILineIterator.h"
31 #include "nsIInterfaceRequestorUtils.h"
32 #include "nsPersistentProperties.h"
33 #include "nsIScrollableFrame.h"
34 #include "nsIMathMLFrame.h"
35 #include "nsRange.h"
36 #include "nsTextFragment.h"
37 #include "mozilla/Assertions.h"
38 #include "mozilla/BinarySearch.h"
39 #include "mozilla/EventStates.h"
40 #include "mozilla/HTMLEditor.h"
41 #include "mozilla/MathAlgorithms.h"
42 #include "mozilla/PresShell.h"
43 #include "mozilla/StaticPrefs_layout.h"
44 #include "mozilla/TextEditor.h"
45 #include "mozilla/dom/Element.h"
46 #include "mozilla/dom/HTMLBRElement.h"
47 #include "mozilla/dom/HTMLHeadingElement.h"
48 #include "mozilla/dom/Selection.h"
49 #include "gfxSkipChars.h"
50 #include <algorithm>
52 using namespace mozilla;
53 using namespace mozilla::a11y;
55 /**
56 * This class is used in HyperTextAccessible to search for paragraph
57 * boundaries.
59 class ParagraphBoundaryRule : public PivotRule {
60 public:
61 explicit ParagraphBoundaryRule(LocalAccessible* aAnchor,
62 uint32_t aAnchorTextoffset,
63 nsDirection aDirection,
64 bool aSkipAnchorSubtree = false)
65 : mAnchor(aAnchor),
66 mAnchorTextOffset(aAnchorTextoffset),
67 mDirection(aDirection),
68 mSkipAnchorSubtree(aSkipAnchorSubtree),
69 mLastMatchTextOffset(0) {}
71 virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override {
72 MOZ_ASSERT(aAccOrProxy.IsAccessible());
73 LocalAccessible* acc = aAccOrProxy.AsAccessible();
74 if (acc->IsOuterDoc()) {
75 // The child document might be remote and we can't (and don't want to)
76 // handle remote documents. Also, iframes are inline anyway and thus
77 // can't be paragraph boundaries. Therefore, skip this unconditionally.
78 return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
81 uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
82 if (mSkipAnchorSubtree && acc == mAnchor) {
83 result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
86 // First, deal with the case that we encountered a line break, for example,
87 // a br in a paragraph.
88 if (acc->Role() == roles::WHITESPACE) {
89 result |= nsIAccessibleTraversalRule::FILTER_MATCH;
90 return result;
93 // Now, deal with the case that we encounter a new block level accessible.
94 // This also means a new paragraph boundary start.
95 nsIFrame* frame = acc->GetFrame();
96 if (frame && frame->IsBlockFrame()) {
97 result |= nsIAccessibleTraversalRule::FILTER_MATCH;
98 return result;
101 // A text leaf can contain a line break if it's pre-formatted text.
102 if (acc->IsTextLeaf()) {
103 nsAutoString name;
104 acc->Name(name);
105 int32_t offset;
106 if (mDirection == eDirPrevious) {
107 if (acc == mAnchor && mAnchorTextOffset == 0) {
108 // We're already at the start of this node, so there can be no line
109 // break before.
110 return result;
112 // If we began on a line break, we don't want to match it, so search
113 // from 1 before our anchor offset.
114 offset =
115 name.RFindChar('\n', acc == mAnchor ? mAnchorTextOffset - 1 : -1);
116 } else {
117 offset = name.FindChar('\n', acc == mAnchor ? mAnchorTextOffset : 0);
119 if (offset != -1) {
120 // Line ebreak!
121 mLastMatchTextOffset = offset;
122 result |= nsIAccessibleTraversalRule::FILTER_MATCH;
126 return result;
129 // This is only valid if the last match was a text leaf. It returns the
130 // offset of the line break character in that text leaf.
131 uint32_t GetLastMatchTextOffset() { return mLastMatchTextOffset; }
133 private:
134 LocalAccessible* mAnchor;
135 uint32_t mAnchorTextOffset;
136 nsDirection mDirection;
137 bool mSkipAnchorSubtree;
138 uint32_t mLastMatchTextOffset;
142 * This class is used in HyperTextAccessible::FindParagraphStartOffset to
143 * search forward exactly one step from a match found by the above.
144 * It should only be initialized with a boundary, and it will skip that
145 * boundary's sub tree if it is a block element boundary.
147 class SkipParagraphBoundaryRule : public PivotRule {
148 public:
149 explicit SkipParagraphBoundaryRule(AccessibleOrProxy& aBoundary)
150 : mBoundary(aBoundary) {}
152 virtual uint16_t Match(const AccessibleOrProxy& aAccOrProxy) override {
153 MOZ_ASSERT(aAccOrProxy.IsAccessible());
154 // If matching the boundary, skip its sub tree.
155 if (aAccOrProxy == mBoundary) {
156 return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
158 return nsIAccessibleTraversalRule::FILTER_MATCH;
161 private:
162 AccessibleOrProxy& mBoundary;
165 ////////////////////////////////////////////////////////////////////////////////
166 // HyperTextAccessible
167 ////////////////////////////////////////////////////////////////////////////////
169 HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc)
170 : AccessibleWrap(aNode, aDoc) {
171 mType = eHyperTextType;
172 mGenericTypes |= eHyperText;
175 role HyperTextAccessible::NativeRole() const {
176 a11y::role r = GetAccService()->MarkupRole(mContent);
177 if (r != roles::NOTHING) return r;
179 nsIFrame* frame = GetFrame();
180 if (frame && frame->IsInlineFrame()) return roles::TEXT;
182 return roles::TEXT_CONTAINER;
185 uint64_t HyperTextAccessible::NativeState() const {
186 uint64_t states = AccessibleWrap::NativeState();
188 if (mContent->AsElement()->State().HasState(NS_EVENT_STATE_READWRITE)) {
189 states |= states::EDITABLE;
191 } else if (mContent->IsHTMLElement(nsGkAtoms::article)) {
192 // We want <article> to behave like a document in terms of readonly state.
193 states |= states::READONLY;
196 nsIFrame* frame = GetFrame();
197 if ((states & states::EDITABLE) || (frame && frame->IsSelectable(nullptr))) {
198 // If the accessible is editable the layout selectable state only disables
199 // mouse selection, but keyboard (shift+arrow) selection is still possible.
200 states |= states::SELECTABLE_TEXT;
203 return states;
206 nsIntRect HyperTextAccessible::GetBoundsInFrame(nsIFrame* aFrame,
207 uint32_t aStartRenderedOffset,
208 uint32_t aEndRenderedOffset) {
209 nsPresContext* presContext = mDoc->PresContext();
210 if (!aFrame->IsTextFrame()) {
211 return aFrame->GetScreenRectInAppUnits().ToNearestPixels(
212 presContext->AppUnitsPerDevPixel());
215 // Substring must be entirely within the same text node.
216 int32_t startContentOffset, endContentOffset;
217 nsresult rv = RenderedToContentOffset(aFrame, aStartRenderedOffset,
218 &startContentOffset);
219 NS_ENSURE_SUCCESS(rv, nsIntRect());
220 rv = RenderedToContentOffset(aFrame, aEndRenderedOffset, &endContentOffset);
221 NS_ENSURE_SUCCESS(rv, nsIntRect());
223 nsIFrame* frame;
224 int32_t startContentOffsetInFrame;
225 // Get the right frame continuation -- not really a child, but a sibling of
226 // the primary frame passed in
227 rv = aFrame->GetChildFrameContainingOffset(
228 startContentOffset, false, &startContentOffsetInFrame, &frame);
229 NS_ENSURE_SUCCESS(rv, nsIntRect());
231 nsRect screenRect;
232 while (frame && startContentOffset < endContentOffset) {
233 // Start with this frame's screen rect, which we will shrink based on
234 // the substring we care about within it. We will then add that frame to
235 // the total screenRect we are returning.
236 nsRect frameScreenRect = frame->GetScreenRectInAppUnits();
238 // Get the length of the substring in this frame that we want the bounds for
239 int32_t startFrameTextOffset, endFrameTextOffset;
240 frame->GetOffsets(startFrameTextOffset, endFrameTextOffset);
241 int32_t frameTotalTextLength = endFrameTextOffset - startFrameTextOffset;
242 int32_t seekLength = endContentOffset - startContentOffset;
243 int32_t frameSubStringLength =
244 std::min(frameTotalTextLength - startContentOffsetInFrame, seekLength);
246 // Add the point where the string starts to the frameScreenRect
247 nsPoint frameTextStartPoint;
248 rv = frame->GetPointFromOffset(startContentOffset, &frameTextStartPoint);
249 NS_ENSURE_SUCCESS(rv, nsIntRect());
251 // Use the point for the end offset to calculate the width
252 nsPoint frameTextEndPoint;
253 rv = frame->GetPointFromOffset(startContentOffset + frameSubStringLength,
254 &frameTextEndPoint);
255 NS_ENSURE_SUCCESS(rv, nsIntRect());
257 frameScreenRect.SetRectX(
258 frameScreenRect.X() +
259 std::min(frameTextStartPoint.x, frameTextEndPoint.x),
260 mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x));
262 screenRect.UnionRect(frameScreenRect, screenRect);
264 // Get ready to loop back for next frame continuation
265 startContentOffset += frameSubStringLength;
266 startContentOffsetInFrame = 0;
267 frame = frame->GetNextContinuation();
270 return screenRect.ToNearestPixels(presContext->AppUnitsPerDevPixel());
273 void HyperTextAccessible::TextSubstring(int32_t aStartOffset,
274 int32_t aEndOffset, nsAString& aText) {
275 aText.Truncate();
277 index_t startOffset = ConvertMagicOffset(aStartOffset);
278 index_t endOffset = ConvertMagicOffset(aEndOffset);
279 if (!startOffset.IsValid() || !endOffset.IsValid() ||
280 startOffset > endOffset || endOffset > CharacterCount()) {
281 NS_ERROR("Wrong in offset");
282 return;
285 int32_t startChildIdx = GetChildIndexAtOffset(startOffset);
286 if (startChildIdx == -1) return;
288 int32_t endChildIdx = GetChildIndexAtOffset(endOffset);
289 if (endChildIdx == -1) return;
291 if (startChildIdx == endChildIdx) {
292 int32_t childOffset = GetChildOffset(startChildIdx);
293 if (childOffset == -1) return;
295 LocalAccessible* child = LocalChildAt(startChildIdx);
296 child->AppendTextTo(aText, startOffset - childOffset,
297 endOffset - startOffset);
298 return;
301 int32_t startChildOffset = GetChildOffset(startChildIdx);
302 if (startChildOffset == -1) return;
304 LocalAccessible* startChild = LocalChildAt(startChildIdx);
305 startChild->AppendTextTo(aText, startOffset - startChildOffset);
307 for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx;
308 childIdx++) {
309 LocalAccessible* child = LocalChildAt(childIdx);
310 child->AppendTextTo(aText);
313 int32_t endChildOffset = GetChildOffset(endChildIdx);
314 if (endChildOffset == -1) return;
316 LocalAccessible* endChild = LocalChildAt(endChildIdx);
317 endChild->AppendTextTo(aText, 0, endOffset - endChildOffset);
320 uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode,
321 int32_t aNodeOffset,
322 bool aIsEndOffset) const {
323 if (!aNode) return 0;
325 uint32_t offset = 0;
326 nsINode* findNode = nullptr;
328 if (aNodeOffset == -1) {
329 findNode = aNode;
331 } else if (aNode->IsText()) {
332 // For text nodes, aNodeOffset comes in as a character offset
333 // Text offset will be added at the end, if we find the offset in this
334 // hypertext We want the "skipped" offset into the text (rendered text
335 // without the extra whitespace)
336 nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
337 NS_ENSURE_TRUE(frame, 0);
339 nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset);
340 NS_ENSURE_SUCCESS(rv, 0);
342 findNode = aNode;
344 } else {
345 // findNode could be null if aNodeOffset == # of child nodes, which means
346 // one of two things:
347 // 1) there are no children, and the passed-in node is not mContent -- use
348 // parentContent for the node to find
349 // 2) there are no children and the passed-in node is mContent, which means
350 // we're an empty nsIAccessibleText
351 // 3) there are children and we're at the end of the children
353 findNode = aNode->GetChildAt_Deprecated(aNodeOffset);
354 if (!findNode) {
355 if (aNodeOffset == 0) {
356 if (aNode == GetNode()) {
357 // Case #1: this accessible has no children and thus has empty text,
358 // we can only be at hypertext offset 0.
359 return 0;
362 // Case #2: there are no children, we're at this node.
363 findNode = aNode;
364 } else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) {
365 // Case #3: we're after the last child, get next node to this one.
366 for (nsINode* tmpNode = aNode;
367 !findNode && tmpNode && tmpNode != mContent;
368 tmpNode = tmpNode->GetParent()) {
369 findNode = tmpNode->GetNextSibling();
375 // Get accessible for this findNode, or if that node isn't accessible, use the
376 // accessible for the next DOM node which has one (based on forward depth
377 // first search)
378 LocalAccessible* descendant = nullptr;
379 if (findNode) {
380 dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode);
381 if (brElement && brElement->IsPaddingForEmptyEditor()) {
382 // This <br> is the hacky "padding <br> element" used when there is no
383 // text in the editor.
384 return 0;
387 descendant = mDoc->GetAccessible(findNode);
388 if (!descendant && findNode->IsContent()) {
389 LocalAccessible* container = mDoc->GetContainerAccessible(findNode);
390 if (container) {
391 TreeWalker walker(container, findNode->AsContent(),
392 TreeWalker::eWalkContextTree);
393 descendant = walker.Next();
394 if (!descendant) descendant = container;
399 return TransformOffset(descendant, offset, aIsEndOffset);
402 uint32_t HyperTextAccessible::TransformOffset(LocalAccessible* aDescendant,
403 uint32_t aOffset,
404 bool aIsEndOffset) const {
405 // From the descendant, go up and get the immediate child of this hypertext.
406 uint32_t offset = aOffset;
407 LocalAccessible* descendant = aDescendant;
408 while (descendant) {
409 LocalAccessible* parent = descendant->LocalParent();
410 if (parent == this) return GetChildOffset(descendant) + offset;
412 // This offset no longer applies because the passed-in text object is not
413 // a child of the hypertext. This happens when there are nested hypertexts,
414 // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
415 // to make it relative the hypertext.
416 // If the end offset is not supposed to be inclusive and the original point
417 // is not at 0 offset then the returned offset should be after an embedded
418 // character the original point belongs to.
419 if (aIsEndOffset) {
420 // Similar to our special casing in FindOffset, we add handling for
421 // bulleted lists here because PeekOffset returns the inner text node
422 // for a list when it should return the list bullet.
423 // We manually set the offset so the error doesn't propagate up.
424 if (offset == 0 && parent && parent->IsHTMLListItem() &&
425 descendant->LocalPrevSibling() &&
426 descendant->LocalPrevSibling()->GetFrame() &&
427 descendant->LocalPrevSibling()->GetFrame()->IsBulletFrame()) {
428 offset = 0;
429 } else {
430 offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
432 } else {
433 offset = 0;
436 descendant = parent;
439 // If the given a11y point cannot be mapped into offset relative this
440 // hypertext offset then return length as fallback value.
441 return CharacterCount();
444 DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const {
445 // 0 offset is valid even if no children. In this case the associated editor
446 // is empty so return a DOM point for editor root element.
447 if (aOffset == 0) {
448 RefPtr<TextEditor> textEditor = GetEditor();
449 if (textEditor) {
450 if (textEditor->IsEmpty()) {
451 return DOMPoint(textEditor->GetRoot(), 0);
456 int32_t childIdx = GetChildIndexAtOffset(aOffset);
457 if (childIdx == -1) return DOMPoint();
459 LocalAccessible* child = LocalChildAt(childIdx);
460 int32_t innerOffset = aOffset - GetChildOffset(childIdx);
462 // A text leaf case.
463 if (child->IsTextLeaf()) {
464 // The point is inside the text node. This is always true for any text leaf
465 // except a last child one. See assertion below.
466 if (aOffset < GetChildOffset(childIdx + 1)) {
467 nsIContent* content = child->GetContent();
468 int32_t idx = 0;
469 if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(),
470 innerOffset, &idx))) {
471 return DOMPoint();
474 return DOMPoint(content, idx);
477 // Set the DOM point right after the text node.
478 MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount());
479 innerOffset = 1;
482 // Case of embedded object. The point is either before or after the element.
483 NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!");
484 nsINode* node = child->GetNode();
485 nsINode* parentNode = node->GetParentNode();
486 return parentNode ? DOMPoint(parentNode,
487 parentNode->ComputeIndexOf(node) + innerOffset)
488 : DOMPoint();
491 uint32_t HyperTextAccessible::FindOffset(uint32_t aOffset,
492 nsDirection aDirection,
493 nsSelectionAmount aAmount,
494 EWordMovementType aWordMovementType) {
495 NS_ASSERTION(aDirection == eDirPrevious || aAmount != eSelectBeginLine,
496 "eSelectBeginLine should only be used with eDirPrevious");
498 // Find a leaf accessible frame to start with. PeekOffset wants this.
499 HyperTextAccessible* text = this;
500 LocalAccessible* child = nullptr;
501 int32_t innerOffset = aOffset;
503 do {
504 int32_t childIdx = text->GetChildIndexAtOffset(innerOffset);
506 // We can have an empty text leaf as our only child. Since empty text
507 // leaves are not accessible we then have no children, but 0 is a valid
508 // innerOffset.
509 if (childIdx == -1) {
510 NS_ASSERTION(innerOffset == 0 && !text->ChildCount(), "No childIdx?");
511 return DOMPointToOffset(text->GetNode(), 0, aDirection == eDirNext);
514 child = text->LocalChildAt(childIdx);
516 // HTML list items may need special processing because PeekOffset doesn't
517 // work with list bullets.
518 if (text->IsHTMLListItem()) {
519 HTMLLIAccessible* li = text->AsHTMLListItem();
520 if (child == li->Bullet()) {
521 // XXX: the logic is broken for multichar bullets in moving by
522 // char/cluster/word cases.
523 if (text != this) {
524 return aDirection == eDirPrevious ? TransformOffset(text, 0, false)
525 : TransformOffset(text, 1, true);
527 if (aDirection == eDirPrevious) return 0;
529 uint32_t nextOffset = GetChildOffset(1);
530 if (nextOffset == 0) return 0;
532 switch (aAmount) {
533 case eSelectLine:
534 case eSelectEndLine:
535 // Ask a text leaf next (if not empty) to the bullet for an offset
536 // since list item may be multiline.
537 return nextOffset < CharacterCount()
538 ? FindOffset(nextOffset, aDirection, aAmount,
539 aWordMovementType)
540 : nextOffset;
542 default:
543 return nextOffset;
548 innerOffset -= text->GetChildOffset(childIdx);
550 text = child->AsHyperText();
551 } while (text);
553 nsIFrame* childFrame = child->GetFrame();
554 if (!childFrame) {
555 NS_ERROR("No child frame");
556 return 0;
559 int32_t innerContentOffset = innerOffset;
560 if (child->IsTextLeaf()) {
561 NS_ASSERTION(childFrame->IsTextFrame(), "Wrong frame!");
562 RenderedToContentOffset(childFrame, innerOffset, &innerContentOffset);
565 nsIFrame* frameAtOffset = childFrame;
566 int32_t unusedOffsetInFrame = 0;
567 childFrame->GetChildFrameContainingOffset(
568 innerContentOffset, true, &unusedOffsetInFrame, &frameAtOffset);
570 const bool kIsJumpLinesOk = true; // okay to jump lines
571 const bool kIsScrollViewAStop = false; // do not stop at scroll views
572 const bool kIsKeyboardSelect = true; // is keyboard selection
573 const bool kIsVisualBidi = false; // use visual order for bidi text
574 nsPeekOffsetStruct pos(
575 aAmount, aDirection, innerContentOffset, nsPoint(0, 0), kIsJumpLinesOk,
576 kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi, false,
577 nsPeekOffsetStruct::ForceEditableRegion::No, aWordMovementType, false);
578 nsresult rv = frameAtOffset->PeekOffset(&pos);
580 // PeekOffset fails on last/first lines of the text in certain cases.
581 bool fallBackToSelectEndLine = false;
582 if (NS_FAILED(rv) && aAmount == eSelectLine) {
583 fallBackToSelectEndLine = aDirection == eDirNext;
584 pos.mAmount = fallBackToSelectEndLine ? eSelectEndLine : eSelectBeginLine;
585 frameAtOffset->PeekOffset(&pos);
587 if (!pos.mResultContent) {
588 NS_ERROR("No result content!");
589 return 0;
592 // Turn the resulting DOM point into an offset.
593 uint32_t hyperTextOffset = DOMPointToOffset(
594 pos.mResultContent, pos.mContentOffset, aDirection == eDirNext);
596 if (fallBackToSelectEndLine && IsLineEndCharAt(hyperTextOffset)) {
597 // We used eSelectEndLine, but the caller requested eSelectLine.
598 // If there's a '\n' at the end of the line, eSelectEndLine will stop on
599 // it rather than after it. This is not what we want, since the caller
600 // wants the next line, not the same line.
601 ++hyperTextOffset;
604 if (aDirection == eDirPrevious) {
605 // If we reached the end during search, this means we didn't find the DOM
606 // point and we're actually at the start of the paragraph
607 if (hyperTextOffset == CharacterCount()) return 0;
609 // PeekOffset stops right before bullet so return 0 to workaround it.
610 if (IsHTMLListItem() && aAmount == eSelectBeginLine &&
611 hyperTextOffset > 0) {
612 LocalAccessible* prevOffsetChild = GetChildAtOffset(hyperTextOffset - 1);
613 if (prevOffsetChild == AsHTMLListItem()->Bullet()) return 0;
617 return hyperTextOffset;
620 uint32_t HyperTextAccessible::FindWordBoundary(
621 uint32_t aOffset, nsDirection aDirection,
622 EWordMovementType aWordMovementType) {
623 uint32_t orig =
624 FindOffset(aOffset, aDirection, eSelectWord, aWordMovementType);
625 if (aWordMovementType != eStartWord) {
626 return orig;
628 if (aDirection == eDirPrevious) {
629 // When layout.word_select.stop_at_punctuation is true (the default),
630 // for a word beginning with punctuation, layout treats the punctuation
631 // as the start of the word when moving next. However, when moving
632 // previous, layout stops *after* the punctuation. We want to be
633 // consistent regardless of movement direction and always treat punctuation
634 // as the start of a word.
635 if (!StaticPrefs::layout_word_select_stop_at_punctuation()) {
636 return orig;
638 // Case 1: Example: "a @"
639 // If aOffset is 2 or 3, orig will be 0, but it should be 2. That is,
640 // previous word moved back too far.
641 LocalAccessible* child = GetChildAtOffset(orig);
642 if (child && child->IsHyperText()) {
643 // For a multi-word embedded object, previous word correctly goes back
644 // to the start of the word (the embedded object). Next word (below)
645 // incorrectly stops after the embedded object in this case, so return
646 // the already correct result.
647 // Example: "a x y b", where "x y" is an embedded link
648 // If aOffset is 4, orig will be 2, which is correct.
649 // If we get the next word (below), we'll end up returning 3 instead.
650 return orig;
652 uint32_t next = FindOffset(orig, eDirNext, eSelectWord, eStartWord);
653 if (next < aOffset) {
654 // Next word stopped on punctuation.
655 return next;
657 // case 2: example: "a @@b"
658 // If aOffset is 2, 3 or 4, orig will be 4, but it should be 2. That is,
659 // previous word didn't go back far enough.
660 if (orig == 0) {
661 return orig;
663 // Walk backwards by offset, getting the next word.
664 // In the loop, o is unsigned, so o >= 0 will always be true and won't
665 // prevent us from decrementing at 0. Instead, we check that o doesn't
666 // wrap around.
667 for (uint32_t o = orig - 1; o < orig; --o) {
668 next = FindOffset(o, eDirNext, eSelectWord, eStartWord);
669 if (next == orig) {
670 // Next word and previous word were consistent. This
671 // punctuation problem isn't applicable here.
672 break;
674 if (next < orig) {
675 // Next word stopped on punctuation.
676 return next;
679 } else {
680 // When layout.word_select.stop_at_punctuation is true (the default),
681 // when positioned on punctuation in the middle of a word, next word skips
682 // the rest of the word. However, when positioned before the punctuation,
683 // next word moves just after the punctuation. We want to be consistent
684 // regardless of starting position and always stop just after the
685 // punctuation.
686 // Next word can move too far when positioned on white space too.
687 // Example: "a b@c"
688 // If aOffset is 3, orig will be 5, but it should be 4. That is, next word
689 // moved too far.
690 if (aOffset == 0) {
691 return orig;
693 uint32_t prev = FindOffset(orig, eDirPrevious, eSelectWord, eStartWord);
694 if (prev <= aOffset) {
695 // orig definitely isn't too far forward.
696 return orig;
698 // Walk backwards by offset, getting the next word.
699 // In the loop, o is unsigned, so o >= 0 will always be true and won't
700 // prevent us from decrementing at 0. Instead, we check that o doesn't
701 // wrap around.
702 for (uint32_t o = aOffset - 1; o < aOffset; --o) {
703 uint32_t next = FindOffset(o, eDirNext, eSelectWord, eStartWord);
704 if (next > aOffset && next < orig) {
705 return next;
707 if (next <= aOffset) {
708 break;
712 return orig;
715 uint32_t HyperTextAccessible::FindLineBoundary(
716 uint32_t aOffset, EWhichLineBoundary aWhichLineBoundary) {
717 // Note: empty last line doesn't have own frame (a previous line contains '\n'
718 // character instead) thus when it makes a difference we need to process this
719 // case separately (otherwise operations are performed on previous line).
720 switch (aWhichLineBoundary) {
721 case ePrevLineBegin: {
722 // Fetch a previous line and move to its start (as arrow up and home keys
723 // were pressed).
724 if (IsEmptyLastLineOffset(aOffset)) {
725 return FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
728 uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
729 return FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine);
732 case ePrevLineEnd: {
733 if (IsEmptyLastLineOffset(aOffset)) return aOffset - 1;
735 // If offset is at first line then return 0 (first line start).
736 uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
737 if (tmpOffset == 0) return 0;
739 // Otherwise move to end of previous line (as arrow up and end keys were
740 // pressed).
741 tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
742 return FindOffset(tmpOffset, eDirNext, eSelectEndLine);
745 case eThisLineBegin: {
746 if (IsEmptyLastLineOffset(aOffset)) return aOffset;
748 // Move to begin of the current line (as home key was pressed).
749 uint32_t thisLineBeginOffset =
750 FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
751 if (IsCharAt(thisLineBeginOffset, kEmbeddedObjectChar)) {
752 // We landed on an embedded character, don't mess with possible embedded
753 // line breaks, and assume the offset is correct.
754 return thisLineBeginOffset;
757 // Sometimes, there is the possibility layout returned an
758 // offset smaller than it should. Sanity-check by moving to the end of the
759 // previous line and see if that has a greater offset.
760 uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
761 tmpOffset = FindOffset(tmpOffset, eDirNext, eSelectEndLine);
762 if (tmpOffset > thisLineBeginOffset && tmpOffset < aOffset) {
763 // We found a previous line offset. Return the next character after it
764 // as our start offset if it points to a line end char.
765 return IsLineEndCharAt(tmpOffset) ? tmpOffset + 1 : tmpOffset;
767 return thisLineBeginOffset;
770 case eThisLineEnd:
771 if (IsEmptyLastLineOffset(aOffset)) return aOffset;
773 // Move to end of the current line (as end key was pressed).
774 return FindOffset(aOffset, eDirNext, eSelectEndLine);
776 case eNextLineBegin: {
777 if (IsEmptyLastLineOffset(aOffset)) return aOffset;
779 // Move to begin of the next line if any (arrow down and home keys),
780 // otherwise end of the current line (arrow down only).
781 uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine);
782 uint32_t characterCount = CharacterCount();
783 if (tmpOffset == characterCount) {
784 return tmpOffset;
787 // Now, simulate the Home key on the next line to get its real offset.
788 uint32_t nextLineBeginOffset =
789 FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine);
790 // Sometimes, there are line breaks inside embedded characters. If this
791 // is the case, the cursor is after the line break, but the offset will
792 // be that of the embedded character, which points to before the line
793 // break. We definitely want the line break included.
794 if (IsCharAt(nextLineBeginOffset, kEmbeddedObjectChar)) {
795 // We can determine if there is a line break by pressing End from
796 // the queried offset. If there is a line break, the offset will be 1
797 // greater, since this line ends with the embed. If there is not, the
798 // value will be different even if a line break follows right after the
799 // embed.
800 uint32_t thisLineEndOffset =
801 FindOffset(aOffset, eDirNext, eSelectEndLine);
802 if (thisLineEndOffset == nextLineBeginOffset + 1) {
803 // If we're querying the offset of the embedded character, we want
804 // the end offset of the parent line instead. Press End
805 // once more from the current position, which is after the embed.
806 if (nextLineBeginOffset == aOffset) {
807 uint32_t thisLineEndOffset2 =
808 FindOffset(thisLineEndOffset, eDirNext, eSelectEndLine);
809 // The above returns an offset exclusive the final line break, so we
810 // need to add 1 to it to return an inclusive end offset. Make sure
811 // we don't overshoot if we've started from another embedded
812 // character that has a line break, or landed on another embedded
813 // character, or if the result is the very end.
814 return (thisLineEndOffset2 == characterCount ||
815 (IsCharAt(thisLineEndOffset, kEmbeddedObjectChar) &&
816 thisLineEndOffset2 == thisLineEndOffset + 1) ||
817 IsCharAt(thisLineEndOffset2, kEmbeddedObjectChar))
818 ? thisLineEndOffset2
819 : thisLineEndOffset2 + 1;
822 return thisLineEndOffset;
824 return nextLineBeginOffset;
827 // If the resulting offset is not greater than the offset we started from,
828 // layout could not find the offset for us. This can happen with certain
829 // inline-block elements.
830 if (nextLineBeginOffset <= aOffset) {
831 // Walk forward from the offset we started from up to tmpOffset,
832 // stopping after a line end character.
833 nextLineBeginOffset = aOffset;
834 while (nextLineBeginOffset < tmpOffset) {
835 if (IsLineEndCharAt(nextLineBeginOffset)) {
836 return nextLineBeginOffset + 1;
838 nextLineBeginOffset++;
842 return nextLineBeginOffset;
845 case eNextLineEnd: {
846 if (IsEmptyLastLineOffset(aOffset)) return aOffset;
848 // Move to next line end (as down arrow and end key were pressed).
849 uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine);
850 if (tmpOffset == CharacterCount()) return tmpOffset;
852 return FindOffset(tmpOffset, eDirNext, eSelectEndLine);
856 return 0;
859 int32_t HyperTextAccessible::FindParagraphStartOffset(uint32_t aOffset) {
860 // Because layout often gives us offsets that are incompatible with
861 // accessibility API requirements, for example when a paragraph contains
862 // presentational line breaks as found in Google Docs, use the accessibility
863 // tree to find the start offset instead.
864 LocalAccessible* child = GetChildAtOffset(aOffset);
865 if (!child) {
866 return -1; // Invalid offset
869 // Use the pivot class to search for the start offset.
870 Pivot p = Pivot(this);
871 ParagraphBoundaryRule boundaryRule = ParagraphBoundaryRule(
872 child, child->IsTextLeaf() ? aOffset - GetChildOffset(child) : 0,
873 eDirPrevious);
874 AccessibleOrProxy wrappedChild = AccessibleOrProxy(child);
875 AccessibleOrProxy match = p.Prev(wrappedChild, boundaryRule, true);
876 if (match.IsNull() || match.AsAccessible() == this) {
877 // Found nothing, or pivot found the root of the search, startOffset is 0.
878 // This will include all relevant text nodes.
879 return 0;
882 if (match == wrappedChild) {
883 // We started out on a boundary.
884 if (match.Role() == roles::WHITESPACE) {
885 // We are on a line break boundary, so force pivot to find the previous
886 // boundary. What we want is any text before this, if any.
887 match = p.Prev(match, boundaryRule);
888 if (match.IsNull() || match.AsAccessible() == this) {
889 // Same as before, we landed on the root, so offset is definitely 0.
890 return 0;
892 } else if (!match.AsAccessible()->IsTextLeaf()) {
893 // The match is a block element, which is always a starting point, so
894 // just return its offset.
895 return TransformOffset(match.AsAccessible(), 0, false);
899 if (match.AsAccessible()->IsTextLeaf()) {
900 // ParagraphBoundaryRule only returns a text leaf if it contains a line
901 // break. We want to stop after that.
902 return TransformOffset(match.AsAccessible(),
903 boundaryRule.GetLastMatchTextOffset() + 1, false);
906 // This is a previous boundary, we don't want to include it itself.
907 // So, walk forward one accessible, excluding the descendants of this
908 // boundary if it is a block element. The below call to Next should always be
909 // initialized with a boundary.
910 SkipParagraphBoundaryRule goForwardOneRule = SkipParagraphBoundaryRule(match);
911 match = p.Next(match, goForwardOneRule);
912 // We already know that the search skipped over at least one accessible,
913 // so match can't be null. Get its transformed offset.
914 MOZ_ASSERT(!match.IsNull());
915 return TransformOffset(match.AsAccessible(), 0, false);
918 int32_t HyperTextAccessible::FindParagraphEndOffset(uint32_t aOffset) {
919 // Because layout often gives us offsets that are incompatible with
920 // accessibility API requirements, for example when a paragraph contains
921 // presentational line breaks as found in Google Docs, use the accessibility
922 // tree to find the end offset instead.
923 LocalAccessible* child = GetChildAtOffset(aOffset);
924 if (!child) {
925 return -1; // invalid offset
928 // Use the pivot class to search for the end offset.
929 Pivot p = Pivot(this);
930 AccessibleOrProxy wrappedChild = AccessibleOrProxy(child);
931 ParagraphBoundaryRule boundaryRule = ParagraphBoundaryRule(
932 child, child->IsTextLeaf() ? aOffset - GetChildOffset(child) : 0,
933 eDirNext,
934 // In order to encompass all paragraphs inside embedded objects, not just
935 // the first, we want to skip the anchor's subtree.
936 /* aSkipAnchorSubtree */ true);
937 // Search forward for the end offset, including wrappedChild. We don't want
938 // to go beyond this point if this offset indicates a paragraph boundary.
939 AccessibleOrProxy match = p.Next(wrappedChild, boundaryRule, true);
940 if (!match.IsNull()) {
941 // Found something of relevance, adjust end offset.
942 LocalAccessible* matchAcc = match.AsAccessible();
943 uint32_t matchOffset;
944 if (matchAcc->IsTextLeaf()) {
945 // ParagraphBoundaryRule only returns a text leaf if it contains a line
946 // break.
947 matchOffset = boundaryRule.GetLastMatchTextOffset() + 1;
948 } else if (matchAcc->Role() != roles::WHITESPACE && matchAcc != child) {
949 // We found a block boundary that wasn't our origin. We want to stop
950 // right on it, not after it, since we don't want to include the content
951 // of the block.
952 matchOffset = 0;
953 } else {
954 matchOffset = nsAccUtils::TextLength(matchAcc);
956 return TransformOffset(matchAcc, matchOffset, true);
959 // Didn't find anything, end offset is character count.
960 return CharacterCount();
963 void HyperTextAccessible::TextBeforeOffset(int32_t aOffset,
964 AccessibleTextBoundary aBoundaryType,
965 int32_t* aStartOffset,
966 int32_t* aEndOffset,
967 nsAString& aText) {
968 *aStartOffset = *aEndOffset = 0;
969 aText.Truncate();
971 if (aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) {
972 // Not supported, bail out with empty text.
973 return;
976 index_t convertedOffset = ConvertMagicOffset(aOffset);
977 if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) {
978 NS_ERROR("Wrong in offset!");
979 return;
982 uint32_t adjustedOffset = convertedOffset;
983 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
984 adjustedOffset = AdjustCaretOffset(adjustedOffset);
987 switch (aBoundaryType) {
988 case nsIAccessibleText::BOUNDARY_CHAR:
989 if (convertedOffset != 0) {
990 CharAt(convertedOffset - 1, aText, aStartOffset, aEndOffset);
992 break;
994 case nsIAccessibleText::BOUNDARY_WORD_START: {
995 // If the offset is a word start (except text length offset) then move
996 // backward to find a start offset (end offset is the given offset).
997 // Otherwise move backward twice to find both start and end offsets.
998 if (adjustedOffset == CharacterCount()) {
999 *aEndOffset =
1000 FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord);
1001 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
1002 } else {
1003 *aStartOffset =
1004 FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord);
1005 *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord);
1006 if (*aEndOffset != static_cast<int32_t>(adjustedOffset)) {
1007 *aEndOffset = *aStartOffset;
1008 *aStartOffset =
1009 FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
1012 TextSubstring(*aStartOffset, *aEndOffset, aText);
1013 break;
1016 case nsIAccessibleText::BOUNDARY_WORD_END: {
1017 // Move word backward twice to find start and end offsets.
1018 *aEndOffset = FindWordBoundary(convertedOffset, eDirPrevious, eEndWord);
1019 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
1020 TextSubstring(*aStartOffset, *aEndOffset, aText);
1021 break;
1024 case nsIAccessibleText::BOUNDARY_LINE_START:
1025 *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineBegin);
1026 *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineBegin);
1027 TextSubstring(*aStartOffset, *aEndOffset, aText);
1028 break;
1030 case nsIAccessibleText::BOUNDARY_LINE_END: {
1031 *aEndOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd);
1032 int32_t tmpOffset = *aEndOffset;
1033 // Adjust offset if line is wrapped.
1034 if (*aEndOffset != 0 && !IsLineEndCharAt(*aEndOffset)) tmpOffset--;
1036 *aStartOffset = FindLineBoundary(tmpOffset, ePrevLineEnd);
1037 TextSubstring(*aStartOffset, *aEndOffset, aText);
1038 break;
1043 void HyperTextAccessible::TextAtOffset(int32_t aOffset,
1044 AccessibleTextBoundary aBoundaryType,
1045 int32_t* aStartOffset,
1046 int32_t* aEndOffset, nsAString& aText) {
1047 *aStartOffset = *aEndOffset = 0;
1048 aText.Truncate();
1050 uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
1051 if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
1052 NS_ERROR("Wrong given offset!");
1053 return;
1056 switch (aBoundaryType) {
1057 case nsIAccessibleText::BOUNDARY_CHAR:
1058 // Return no char if caret is at the end of wrapped line (case of no line
1059 // end character). Returning a next line char is confusing for AT.
1060 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET &&
1061 IsCaretAtEndOfLine()) {
1062 *aStartOffset = *aEndOffset = adjustedOffset;
1063 } else {
1064 CharAt(adjustedOffset, aText, aStartOffset, aEndOffset);
1066 break;
1068 case nsIAccessibleText::BOUNDARY_WORD_START:
1069 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
1070 adjustedOffset = AdjustCaretOffset(adjustedOffset);
1073 *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord);
1074 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
1075 TextSubstring(*aStartOffset, *aEndOffset, aText);
1076 break;
1078 case nsIAccessibleText::BOUNDARY_WORD_END:
1079 // Ignore the spec and follow what WebKitGtk does because Orca expects it,
1080 // i.e. return a next word at word end offset of the current word
1081 // (WebKitGtk behavior) instead the current word (AKT spec).
1082 *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eEndWord);
1083 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
1084 TextSubstring(*aStartOffset, *aEndOffset, aText);
1085 break;
1087 case nsIAccessibleText::BOUNDARY_LINE_START:
1088 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
1089 adjustedOffset = AdjustCaretOffset(adjustedOffset);
1092 *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineBegin);
1093 *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineBegin);
1094 TextSubstring(*aStartOffset, *aEndOffset, aText);
1095 break;
1097 case nsIAccessibleText::BOUNDARY_LINE_END:
1098 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
1099 adjustedOffset = AdjustCaretOffset(adjustedOffset);
1102 // In contrast to word end boundary we follow the spec here.
1103 *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd);
1104 *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineEnd);
1105 TextSubstring(*aStartOffset, *aEndOffset, aText);
1106 break;
1108 case nsIAccessibleText::BOUNDARY_PARAGRAPH: {
1109 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
1110 adjustedOffset = AdjustCaretOffset(adjustedOffset);
1113 if (IsEmptyLastLineOffset(adjustedOffset)) {
1114 // We are on the last line of a paragraph where there is no text.
1115 // For example, in a textarea where a new line has just been inserted.
1116 // In this case, return offsets for an empty line without text content.
1117 *aStartOffset = *aEndOffset = adjustedOffset;
1118 break;
1121 *aStartOffset = FindParagraphStartOffset(adjustedOffset);
1122 *aEndOffset = FindParagraphEndOffset(adjustedOffset);
1123 TextSubstring(*aStartOffset, *aEndOffset, aText);
1124 break;
1129 void HyperTextAccessible::TextAfterOffset(int32_t aOffset,
1130 AccessibleTextBoundary aBoundaryType,
1131 int32_t* aStartOffset,
1132 int32_t* aEndOffset,
1133 nsAString& aText) {
1134 *aStartOffset = *aEndOffset = 0;
1135 aText.Truncate();
1137 if (aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) {
1138 // Not supported, bail out with empty text.
1139 return;
1142 index_t convertedOffset = ConvertMagicOffset(aOffset);
1143 if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) {
1144 NS_ERROR("Wrong in offset!");
1145 return;
1148 uint32_t adjustedOffset = convertedOffset;
1149 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
1150 adjustedOffset = AdjustCaretOffset(adjustedOffset);
1153 switch (aBoundaryType) {
1154 case nsIAccessibleText::BOUNDARY_CHAR:
1155 // If caret is at the end of wrapped line (case of no line end character)
1156 // then char after the offset is a first char at next line.
1157 if (adjustedOffset >= CharacterCount()) {
1158 *aStartOffset = *aEndOffset = CharacterCount();
1159 } else {
1160 CharAt(adjustedOffset + 1, aText, aStartOffset, aEndOffset);
1162 break;
1164 case nsIAccessibleText::BOUNDARY_WORD_START:
1165 // Move word forward twice to find start and end offsets.
1166 *aStartOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord);
1167 *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord);
1168 TextSubstring(*aStartOffset, *aEndOffset, aText);
1169 break;
1171 case nsIAccessibleText::BOUNDARY_WORD_END:
1172 // If the offset is a word end (except 0 offset) then move forward to find
1173 // end offset (start offset is the given offset). Otherwise move forward
1174 // twice to find both start and end offsets.
1175 if (convertedOffset == 0) {
1176 *aStartOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord);
1177 *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord);
1178 } else {
1179 *aEndOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord);
1180 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
1181 if (*aStartOffset != static_cast<int32_t>(convertedOffset)) {
1182 *aStartOffset = *aEndOffset;
1183 *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord);
1186 TextSubstring(*aStartOffset, *aEndOffset, aText);
1187 break;
1189 case nsIAccessibleText::BOUNDARY_LINE_START:
1190 *aStartOffset = FindLineBoundary(adjustedOffset, eNextLineBegin);
1191 *aEndOffset = FindLineBoundary(*aStartOffset, eNextLineBegin);
1192 TextSubstring(*aStartOffset, *aEndOffset, aText);
1193 break;
1195 case nsIAccessibleText::BOUNDARY_LINE_END:
1196 *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineEnd);
1197 *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineEnd);
1198 TextSubstring(*aStartOffset, *aEndOffset, aText);
1199 break;
1203 already_AddRefed<nsIPersistentProperties> HyperTextAccessible::TextAttributes(
1204 bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
1205 int32_t* aEndOffset) {
1206 // 1. Get each attribute and its ranges one after another.
1207 // 2. As we get each new attribute, we pass the current start and end offsets
1208 // as in/out parameters. In other words, as attributes are collected,
1209 // the attribute range itself can only stay the same or get smaller.
1211 *aStartOffset = *aEndOffset = 0;
1212 index_t offset = ConvertMagicOffset(aOffset);
1213 if (!offset.IsValid() || offset > CharacterCount()) {
1214 NS_ERROR("Wrong in offset!");
1215 return nullptr;
1218 RefPtr<nsPersistentProperties> attributes = new nsPersistentProperties();
1220 LocalAccessible* accAtOffset = GetChildAtOffset(offset);
1221 if (!accAtOffset) {
1222 // Offset 0 is correct offset when accessible has empty text. Include
1223 // default attributes if they were requested, otherwise return empty set.
1224 if (offset == 0) {
1225 if (aIncludeDefAttrs) {
1226 TextAttrsMgr textAttrsMgr(this);
1227 textAttrsMgr.GetAttributes(attributes);
1229 return attributes.forget();
1231 return nullptr;
1234 int32_t accAtOffsetIdx = accAtOffset->IndexInParent();
1235 uint32_t startOffset = GetChildOffset(accAtOffsetIdx);
1236 uint32_t endOffset = GetChildOffset(accAtOffsetIdx + 1);
1237 int32_t offsetInAcc = offset - startOffset;
1239 TextAttrsMgr textAttrsMgr(this, aIncludeDefAttrs, accAtOffset,
1240 accAtOffsetIdx);
1241 textAttrsMgr.GetAttributes(attributes, &startOffset, &endOffset);
1243 // Compute spelling attributes on text accessible only.
1244 nsIFrame* offsetFrame = accAtOffset->GetFrame();
1245 if (offsetFrame && offsetFrame->IsTextFrame()) {
1246 int32_t nodeOffset = 0;
1247 RenderedToContentOffset(offsetFrame, offsetInAcc, &nodeOffset);
1249 // Set 'misspelled' text attribute.
1250 GetSpellTextAttr(accAtOffset->GetNode(), nodeOffset, &startOffset,
1251 &endOffset, attributes);
1254 *aStartOffset = startOffset;
1255 *aEndOffset = endOffset;
1256 return attributes.forget();
1259 already_AddRefed<nsIPersistentProperties>
1260 HyperTextAccessible::DefaultTextAttributes() {
1261 RefPtr<nsPersistentProperties> attributes = new nsPersistentProperties();
1263 TextAttrsMgr textAttrsMgr(this);
1264 textAttrsMgr.GetAttributes(attributes);
1265 return attributes.forget();
1268 int32_t HyperTextAccessible::GetLevelInternal() {
1269 if (auto* heading = dom::HTMLHeadingElement::FromNode(mContent)) {
1270 return heading->AccessibilityLevel();
1272 return AccessibleWrap::GetLevelInternal();
1275 void HyperTextAccessible::SetMathMLXMLRoles(
1276 nsIPersistentProperties* aAttributes) {
1277 // Add MathML xmlroles based on the position inside the parent.
1278 LocalAccessible* parent = LocalParent();
1279 if (parent) {
1280 switch (parent->Role()) {
1281 case roles::MATHML_CELL:
1282 case roles::MATHML_ENCLOSED:
1283 case roles::MATHML_ERROR:
1284 case roles::MATHML_MATH:
1285 case roles::MATHML_ROW:
1286 case roles::MATHML_SQUARE_ROOT:
1287 case roles::MATHML_STYLE:
1288 if (Role() == roles::MATHML_OPERATOR) {
1289 // This is an operator inside an <mrow> (or an inferred <mrow>).
1290 // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow
1291 // XXX We should probably do something similar for MATHML_FENCED, but
1292 // operators do not appear in the accessible tree. See bug 1175747.
1293 nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame());
1294 if (mathMLFrame) {
1295 nsEmbellishData embellishData;
1296 mathMLFrame->GetEmbellishData(embellishData);
1297 if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData.flags)) {
1298 if (!LocalPrevSibling()) {
1299 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1300 nsGkAtoms::open_fence);
1301 } else if (!LocalNextSibling()) {
1302 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1303 nsGkAtoms::close_fence);
1306 if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) {
1307 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1308 nsGkAtoms::separator_);
1312 break;
1313 case roles::MATHML_FRACTION:
1314 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1315 IndexInParent() == 0 ? nsGkAtoms::numerator
1316 : nsGkAtoms::denominator);
1317 break;
1318 case roles::MATHML_ROOT:
1319 nsAccUtils::SetAccAttr(
1320 aAttributes, nsGkAtoms::xmlroles,
1321 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index);
1322 break;
1323 case roles::MATHML_SUB:
1324 nsAccUtils::SetAccAttr(
1325 aAttributes, nsGkAtoms::xmlroles,
1326 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript);
1327 break;
1328 case roles::MATHML_SUP:
1329 nsAccUtils::SetAccAttr(
1330 aAttributes, nsGkAtoms::xmlroles,
1331 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript);
1332 break;
1333 case roles::MATHML_SUB_SUP: {
1334 int32_t index = IndexInParent();
1335 nsAccUtils::SetAccAttr(
1336 aAttributes, nsGkAtoms::xmlroles,
1337 index == 0
1338 ? nsGkAtoms::base
1339 : (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript));
1340 } break;
1341 case roles::MATHML_UNDER:
1342 nsAccUtils::SetAccAttr(
1343 aAttributes, nsGkAtoms::xmlroles,
1344 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript);
1345 break;
1346 case roles::MATHML_OVER:
1347 nsAccUtils::SetAccAttr(
1348 aAttributes, nsGkAtoms::xmlroles,
1349 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript);
1350 break;
1351 case roles::MATHML_UNDER_OVER: {
1352 int32_t index = IndexInParent();
1353 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1354 index == 0
1355 ? nsGkAtoms::base
1356 : (index == 1 ? nsGkAtoms::underscript
1357 : nsGkAtoms::overscript));
1358 } break;
1359 case roles::MATHML_MULTISCRIPTS: {
1360 // Get the <multiscripts> base.
1361 nsIContent* child;
1362 bool baseFound = false;
1363 for (child = parent->GetContent()->GetFirstChild(); child;
1364 child = child->GetNextSibling()) {
1365 if (child->IsMathMLElement()) {
1366 baseFound = true;
1367 break;
1370 if (baseFound) {
1371 nsIContent* content = GetContent();
1372 if (child == content) {
1373 // We are the base.
1374 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1375 nsGkAtoms::base);
1376 } else {
1377 // Browse the list of scripts to find us and determine our type.
1378 bool postscript = true;
1379 bool subscript = true;
1380 for (child = child->GetNextSibling(); child;
1381 child = child->GetNextSibling()) {
1382 if (!child->IsMathMLElement()) continue;
1383 if (child->IsMathMLElement(nsGkAtoms::mprescripts_)) {
1384 postscript = false;
1385 subscript = true;
1386 continue;
1388 if (child == content) {
1389 if (postscript) {
1390 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1391 subscript ? nsGkAtoms::subscript
1392 : nsGkAtoms::superscript);
1393 } else {
1394 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1395 subscript ? nsGkAtoms::presubscript
1396 : nsGkAtoms::presuperscript);
1398 break;
1400 subscript = !subscript;
1404 } break;
1405 default:
1406 break;
1411 already_AddRefed<nsIPersistentProperties>
1412 HyperTextAccessible::NativeAttributes() {
1413 nsCOMPtr<nsIPersistentProperties> attributes =
1414 AccessibleWrap::NativeAttributes();
1416 // 'formatting' attribute is deprecated, 'display' attribute should be
1417 // instead.
1418 nsIFrame* frame = GetFrame();
1419 if (frame && frame->IsBlockFrame()) {
1420 nsAutoString unused;
1421 attributes->SetStringProperty("formatting"_ns, u"block"_ns, unused);
1424 if (FocusMgr()->IsFocused(this)) {
1425 int32_t lineNumber = CaretLineNumber();
1426 if (lineNumber >= 1) {
1427 nsAutoString strLineNumber;
1428 strLineNumber.AppendInt(lineNumber);
1429 nsAccUtils::SetAccAttr(attributes, nsGkAtoms::lineNumber, strLineNumber);
1433 if (HasOwnContent()) {
1434 GetAccService()->MarkupAttributes(mContent, attributes);
1435 if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes);
1438 return attributes.forget();
1441 nsAtom* HyperTextAccessible::LandmarkRole() const {
1442 if (!HasOwnContent()) return nullptr;
1444 // For the html landmark elements we expose them like we do ARIA landmarks to
1445 // make AT navigation schemes "just work".
1446 if (mContent->IsHTMLElement(nsGkAtoms::nav)) {
1447 return nsGkAtoms::navigation;
1450 if (mContent->IsHTMLElement(nsGkAtoms::aside)) {
1451 return nsGkAtoms::complementary;
1454 if (mContent->IsHTMLElement(nsGkAtoms::main)) {
1455 return nsGkAtoms::main;
1458 return AccessibleWrap::LandmarkRole();
1461 int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY,
1462 uint32_t aCoordType) {
1463 nsIFrame* hyperFrame = GetFrame();
1464 if (!hyperFrame) return -1;
1466 nsIntPoint coords =
1467 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this);
1469 nsPresContext* presContext = mDoc->PresContext();
1470 nsPoint coordsInAppUnits =
1471 ToAppUnits(coords, presContext->AppUnitsPerDevPixel());
1473 nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits();
1474 if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y)) {
1475 return -1; // Not found
1478 nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(),
1479 coordsInAppUnits.y - frameScreenRect.Y());
1481 // Go through the frames to check if each one has the point.
1482 // When one does, add up the character offsets until we have a match
1484 // We have an point in an accessible child of this, now we need to add up the
1485 // offsets before it to what we already have
1486 int32_t offset = 0;
1487 uint32_t childCount = ChildCount();
1488 for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
1489 LocalAccessible* childAcc = mChildren[childIdx];
1491 nsIFrame* primaryFrame = childAcc->GetFrame();
1492 NS_ENSURE_TRUE(primaryFrame, -1);
1494 nsIFrame* frame = primaryFrame;
1495 while (frame) {
1496 nsIContent* content = frame->GetContent();
1497 NS_ENSURE_TRUE(content, -1);
1498 nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame);
1499 nsSize frameSize = frame->GetSize();
1500 if (pointInFrame.x < frameSize.width &&
1501 pointInFrame.y < frameSize.height) {
1502 // Finished
1503 if (frame->IsTextFrame()) {
1504 nsIFrame::ContentOffsets contentOffsets =
1505 frame->GetContentOffsetsFromPointExternal(
1506 pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE);
1507 if (contentOffsets.IsNull() || contentOffsets.content != content) {
1508 return -1; // Not found
1510 uint32_t addToOffset;
1511 nsresult rv = ContentToRenderedOffset(
1512 primaryFrame, contentOffsets.offset, &addToOffset);
1513 NS_ENSURE_SUCCESS(rv, -1);
1514 offset += addToOffset;
1516 return offset;
1518 frame = frame->GetNextContinuation();
1521 offset += nsAccUtils::TextLength(childAcc);
1524 return -1; // Not found
1527 nsIntRect HyperTextAccessible::TextBounds(int32_t aStartOffset,
1528 int32_t aEndOffset,
1529 uint32_t aCoordType) {
1530 index_t startOffset = ConvertMagicOffset(aStartOffset);
1531 index_t endOffset = ConvertMagicOffset(aEndOffset);
1532 if (!startOffset.IsValid() || !endOffset.IsValid() ||
1533 startOffset > endOffset || endOffset > CharacterCount()) {
1534 NS_ERROR("Wrong in offset");
1535 return nsIntRect();
1538 if (CharacterCount() == 0) {
1539 nsPresContext* presContext = mDoc->PresContext();
1540 // Empty content, use our own bound to at least get x,y coordinates
1541 nsIFrame* frame = GetFrame();
1542 if (!frame) {
1543 return nsIntRect();
1545 return frame->GetScreenRectInAppUnits().ToNearestPixels(
1546 presContext->AppUnitsPerDevPixel());
1549 int32_t childIdx = GetChildIndexAtOffset(startOffset);
1550 if (childIdx == -1) return nsIntRect();
1552 nsIntRect bounds;
1553 int32_t prevOffset = GetChildOffset(childIdx);
1554 int32_t offset1 = startOffset - prevOffset;
1556 while (childIdx < static_cast<int32_t>(ChildCount())) {
1557 nsIFrame* frame = LocalChildAt(childIdx++)->GetFrame();
1558 if (!frame) {
1559 MOZ_ASSERT_UNREACHABLE("No frame for a child!");
1560 continue;
1563 int32_t nextOffset = GetChildOffset(childIdx);
1564 if (nextOffset >= static_cast<int32_t>(endOffset)) {
1565 bounds.UnionRect(
1566 bounds, GetBoundsInFrame(frame, offset1, endOffset - prevOffset));
1567 break;
1570 bounds.UnionRect(bounds,
1571 GetBoundsInFrame(frame, offset1, nextOffset - prevOffset));
1573 prevOffset = nextOffset;
1574 offset1 = 0;
1577 // This document may have a resolution set, we will need to multiply
1578 // the document-relative coordinates by that value and re-apply the doc's
1579 // screen coordinates.
1580 nsPresContext* presContext = mDoc->PresContext();
1581 nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
1582 nsIntRect orgRectPixels =
1583 rootFrame->GetScreenRectInAppUnits().ToNearestPixels(
1584 presContext->AppUnitsPerDevPixel());
1585 bounds.MoveBy(-orgRectPixels.X(), -orgRectPixels.Y());
1586 bounds.ScaleRoundOut(presContext->PresShell()->GetResolution());
1587 bounds.MoveBy(orgRectPixels.X(), orgRectPixels.Y());
1589 auto boundsX = bounds.X();
1590 auto boundsY = bounds.Y();
1591 nsAccUtils::ConvertScreenCoordsTo(&boundsX, &boundsY, aCoordType, this);
1592 bounds.MoveTo(boundsX, boundsY);
1593 return bounds;
1596 already_AddRefed<TextEditor> HyperTextAccessible::GetEditor() const {
1597 if (!mContent->HasFlag(NODE_IS_EDITABLE)) {
1598 // If we're inside an editable container, then return that container's
1599 // editor
1600 LocalAccessible* ancestor = LocalParent();
1601 while (ancestor) {
1602 HyperTextAccessible* hyperText = ancestor->AsHyperText();
1603 if (hyperText) {
1604 // Recursion will stop at container doc because it has its own impl
1605 // of GetEditor()
1606 return hyperText->GetEditor();
1609 ancestor = ancestor->LocalParent();
1612 return nullptr;
1615 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent);
1616 nsCOMPtr<nsIEditingSession> editingSession;
1617 docShell->GetEditingSession(getter_AddRefs(editingSession));
1618 if (!editingSession) return nullptr; // No editing session interface
1620 dom::Document* docNode = mDoc->DocumentNode();
1621 RefPtr<HTMLEditor> htmlEditor =
1622 editingSession->GetHTMLEditorForWindow(docNode->GetWindow());
1623 return htmlEditor.forget();
1627 * =================== Caret & Selection ======================
1630 nsresult HyperTextAccessible::SetSelectionRange(int32_t aStartPos,
1631 int32_t aEndPos) {
1632 // Before setting the selection range, we need to ensure that the editor
1633 // is initialized. (See bug 804927.)
1634 // Otherwise, it's possible that lazy editor initialization will override
1635 // the selection we set here and leave the caret at the end of the text.
1636 // By calling GetEditor here, we ensure that editor initialization is
1637 // completed before we set the selection.
1638 RefPtr<TextEditor> textEditor = GetEditor();
1640 bool isFocusable = InteractiveState() & states::FOCUSABLE;
1642 // If accessible is focusable then focus it before setting the selection to
1643 // neglect control's selection changes on focus if any (for example, inputs
1644 // that do select all on focus).
1645 // some input controls
1646 if (isFocusable) TakeFocus();
1648 RefPtr<dom::Selection> domSel = DOMSelection();
1649 NS_ENSURE_STATE(domSel);
1651 // Set up the selection.
1652 for (int32_t idx = domSel->RangeCount() - 1; idx > 0; idx--) {
1653 RefPtr<nsRange> range{domSel->GetRangeAt(idx)};
1654 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
1655 IgnoreErrors());
1657 SetSelectionBoundsAt(0, aStartPos, aEndPos);
1659 // Make sure it is visible
1660 domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
1661 ScrollAxis(), ScrollAxis(),
1662 dom::Selection::SCROLL_FOR_CARET_MOVE |
1663 dom::Selection::SCROLL_OVERFLOW_HIDDEN);
1665 // When selection is done, move the focus to the selection if accessible is
1666 // not focusable. That happens when selection is set within hypertext
1667 // accessible.
1668 if (isFocusable) return NS_OK;
1670 nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
1671 if (DOMFocusManager) {
1672 NS_ENSURE_TRUE(mDoc, NS_ERROR_FAILURE);
1673 dom::Document* docNode = mDoc->DocumentNode();
1674 NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE);
1675 nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow();
1676 RefPtr<dom::Element> result;
1677 DOMFocusManager->MoveFocus(
1678 window, nullptr, nsIFocusManager::MOVEFOCUS_CARET,
1679 nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result));
1682 return NS_OK;
1685 int32_t HyperTextAccessible::CaretOffset() const {
1686 // Not focused focusable accessible except document accessible doesn't have
1687 // a caret.
1688 if (!IsDoc() && !FocusMgr()->IsFocused(this) &&
1689 (InteractiveState() & states::FOCUSABLE)) {
1690 return -1;
1693 // Check cached value.
1694 int32_t caretOffset = -1;
1695 HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset);
1697 // Use cached value if it corresponds to this accessible.
1698 if (caretOffset != -1) {
1699 if (text == this) return caretOffset;
1701 nsINode* textNode = text->GetNode();
1702 // Ignore offset if cached accessible isn't a text leaf.
1703 if (nsCoreUtils::IsAncestorOf(GetNode(), textNode)) {
1704 return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false);
1708 // No caret if the focused node is not inside this DOM node and this DOM node
1709 // is not inside of focused node.
1710 FocusManager::FocusDisposition focusDisp =
1711 FocusMgr()->IsInOrContainsFocus(this);
1712 if (focusDisp == FocusManager::eNone) return -1;
1714 // Turn the focus node and offset of the selection into caret hypretext
1715 // offset.
1716 dom::Selection* domSel = DOMSelection();
1717 NS_ENSURE_TRUE(domSel, -1);
1719 nsINode* focusNode = domSel->GetFocusNode();
1720 uint32_t focusOffset = domSel->FocusOffset();
1722 // No caret if this DOM node is inside of focused node but the selection's
1723 // focus point is not inside of this DOM node.
1724 if (focusDisp == FocusManager::eContainedByFocus) {
1725 nsINode* resultNode =
1726 nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset);
1728 nsINode* thisNode = GetNode();
1729 if (resultNode != thisNode &&
1730 !nsCoreUtils::IsAncestorOf(thisNode, resultNode)) {
1731 return -1;
1735 return DOMPointToOffset(focusNode, focusOffset);
1738 int32_t HyperTextAccessible::CaretLineNumber() {
1739 // Provide the line number for the caret, relative to the
1740 // currently focused node. Use a 1-based index
1741 RefPtr<nsFrameSelection> frameSelection = FrameSelection();
1742 if (!frameSelection) return -1;
1744 dom::Selection* domSel = frameSelection->GetSelection(SelectionType::eNormal);
1745 if (!domSel) return -1;
1747 nsINode* caretNode = domSel->GetFocusNode();
1748 if (!caretNode || !caretNode->IsContent()) return -1;
1750 nsIContent* caretContent = caretNode->AsContent();
1751 if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1;
1753 int32_t returnOffsetUnused;
1754 uint32_t caretOffset = domSel->FocusOffset();
1755 CaretAssociationHint hint = frameSelection->GetHint();
1756 nsIFrame* caretFrame = frameSelection->GetFrameForNodeOffset(
1757 caretContent, caretOffset, hint, &returnOffsetUnused);
1758 NS_ENSURE_TRUE(caretFrame, -1);
1760 int32_t lineNumber = 1;
1761 nsAutoLineIterator lineIterForCaret;
1762 nsIContent* hyperTextContent = IsContent() ? mContent.get() : nullptr;
1763 while (caretFrame) {
1764 if (hyperTextContent == caretFrame->GetContent()) {
1765 return lineNumber; // Must be in a single line hyper text, there is no
1766 // line iterator
1768 nsContainerFrame* parentFrame = caretFrame->GetParent();
1769 if (!parentFrame) break;
1771 // Add lines for the sibling frames before the caret
1772 nsIFrame* sibling = parentFrame->PrincipalChildList().FirstChild();
1773 while (sibling && sibling != caretFrame) {
1774 nsAutoLineIterator lineIterForSibling = sibling->GetLineIterator();
1775 if (lineIterForSibling) {
1776 // For the frames before that grab all the lines
1777 int32_t addLines = lineIterForSibling->GetNumLines();
1778 lineNumber += addLines;
1780 sibling = sibling->GetNextSibling();
1783 // Get the line number relative to the container with lines
1784 if (!lineIterForCaret) { // Add the caret line just once
1785 lineIterForCaret = parentFrame->GetLineIterator();
1786 if (lineIterForCaret) {
1787 // Ancestor of caret
1788 int32_t addLines = lineIterForCaret->FindLineContaining(caretFrame);
1789 lineNumber += addLines;
1793 caretFrame = parentFrame;
1796 MOZ_ASSERT_UNREACHABLE(
1797 "DOM ancestry had this hypertext but frame ancestry didn't");
1798 return lineNumber;
1801 LayoutDeviceIntRect HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) {
1802 *aWidget = nullptr;
1804 RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret();
1805 NS_ENSURE_TRUE(caret, LayoutDeviceIntRect());
1807 bool isVisible = caret->IsVisible();
1808 if (!isVisible) return LayoutDeviceIntRect();
1810 nsRect rect;
1811 nsIFrame* frame = caret->GetGeometry(&rect);
1812 if (!frame || rect.IsEmpty()) return LayoutDeviceIntRect();
1814 nsPoint offset;
1815 // Offset from widget origin to the frame origin, which includes chrome
1816 // on the widget.
1817 *aWidget = frame->GetNearestWidget(offset);
1818 NS_ENSURE_TRUE(*aWidget, LayoutDeviceIntRect());
1819 rect.MoveBy(offset);
1821 LayoutDeviceIntRect caretRect = LayoutDeviceIntRect::FromUnknownRect(
1822 rect.ToOutsidePixels(frame->PresContext()->AppUnitsPerDevPixel()));
1823 // clang-format off
1824 // ((content screen origin) - (content offset in the widget)) = widget origin on the screen
1825 // clang-format on
1826 caretRect.MoveBy((*aWidget)->WidgetToScreenOffset() -
1827 (*aWidget)->GetClientOffset());
1829 // Correct for character size, so that caret always matches the size of
1830 // the character. This is important for font size transitions, and is
1831 // necessary because the Gecko caret uses the previous character's size as
1832 // the user moves forward in the text by character.
1833 nsIntRect charRect = CharBounds(
1834 CaretOffset(), nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE);
1835 if (!charRect.IsEmpty()) {
1836 caretRect.SetTopEdge(charRect.Y());
1838 return caretRect;
1841 void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType,
1842 nsTArray<nsRange*>* aRanges) {
1843 // Ignore selection if it is not visible.
1844 RefPtr<nsFrameSelection> frameSelection = FrameSelection();
1845 if (!frameSelection || frameSelection->GetDisplaySelection() <=
1846 nsISelectionController::SELECTION_HIDDEN) {
1847 return;
1850 dom::Selection* domSel = frameSelection->GetSelection(aSelectionType);
1851 if (!domSel) return;
1853 nsINode* startNode = GetNode();
1855 RefPtr<TextEditor> textEditor = GetEditor();
1856 if (textEditor) {
1857 startNode = textEditor->GetRoot();
1860 if (!startNode) return;
1862 uint32_t childCount = startNode->GetChildCount();
1863 nsresult rv = domSel->GetRangesForIntervalArray(startNode, 0, startNode,
1864 childCount, true, aRanges);
1865 NS_ENSURE_SUCCESS_VOID(rv);
1867 // Remove collapsed ranges
1868 aRanges->RemoveElementsBy(
1869 [](const auto& range) { return range->Collapsed(); });
1872 int32_t HyperTextAccessible::SelectionCount() {
1873 nsTArray<nsRange*> ranges;
1874 GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
1875 return ranges.Length();
1878 bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum,
1879 int32_t* aStartOffset,
1880 int32_t* aEndOffset) {
1881 *aStartOffset = *aEndOffset = 0;
1883 nsTArray<nsRange*> ranges;
1884 GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
1886 uint32_t rangeCount = ranges.Length();
1887 if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(rangeCount)) {
1888 return false;
1891 nsRange* range = ranges[aSelectionNum];
1893 // Get start and end points.
1894 nsINode* startNode = range->GetStartContainer();
1895 nsINode* endNode = range->GetEndContainer();
1896 int32_t startOffset = range->StartOffset(), endOffset = range->EndOffset();
1898 // Make sure start is before end, by swapping DOM points. This occurs when
1899 // the user selects backwards in the text.
1900 const Maybe<int32_t> order =
1901 nsContentUtils::ComparePoints(endNode, endOffset, startNode, startOffset);
1903 if (!order) {
1904 MOZ_ASSERT_UNREACHABLE();
1905 return false;
1908 if (*order < 0) {
1909 nsINode* tempNode = startNode;
1910 startNode = endNode;
1911 endNode = tempNode;
1912 int32_t tempOffset = startOffset;
1913 startOffset = endOffset;
1914 endOffset = tempOffset;
1917 if (!startNode->IsInclusiveDescendantOf(mContent)) {
1918 *aStartOffset = 0;
1919 } else {
1920 *aStartOffset = DOMPointToOffset(startNode, startOffset);
1923 if (!endNode->IsInclusiveDescendantOf(mContent)) {
1924 *aEndOffset = CharacterCount();
1925 } else {
1926 *aEndOffset = DOMPointToOffset(endNode, endOffset, true);
1928 return true;
1931 bool HyperTextAccessible::SetSelectionBoundsAt(int32_t aSelectionNum,
1932 int32_t aStartOffset,
1933 int32_t aEndOffset) {
1934 index_t startOffset = ConvertMagicOffset(aStartOffset);
1935 index_t endOffset = ConvertMagicOffset(aEndOffset);
1936 if (!startOffset.IsValid() || !endOffset.IsValid() ||
1937 std::max(startOffset, endOffset) > CharacterCount()) {
1938 NS_ERROR("Wrong in offset");
1939 return false;
1942 TextRange range(this, this, startOffset, this, endOffset);
1943 return range.SetSelectionAt(aSelectionNum);
1946 bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
1947 RefPtr<dom::Selection> domSel = DOMSelection();
1948 if (!domSel) return false;
1950 if (aSelectionNum < 0 ||
1951 aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) {
1952 return false;
1955 const RefPtr<nsRange> range{domSel->GetRangeAt(aSelectionNum)};
1956 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
1957 IgnoreErrors());
1958 return true;
1961 void HyperTextAccessible::ScrollSubstringTo(int32_t aStartOffset,
1962 int32_t aEndOffset,
1963 uint32_t aScrollType) {
1964 TextRange range(this, this, aStartOffset, this, aEndOffset);
1965 range.ScrollIntoView(aScrollType);
1968 void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset,
1969 int32_t aEndOffset,
1970 uint32_t aCoordinateType,
1971 int32_t aX, int32_t aY) {
1972 nsIFrame* frame = GetFrame();
1973 if (!frame) return;
1975 nsIntPoint coords =
1976 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
1978 RefPtr<nsRange> domRange = nsRange::Create(mContent);
1979 TextRange range(this, this, aStartOffset, this, aEndOffset);
1980 if (!range.AssignDOMRange(domRange)) {
1981 return;
1984 nsPresContext* presContext = frame->PresContext();
1985 nsPoint coordsInAppUnits =
1986 ToAppUnits(coords, presContext->AppUnitsPerDevPixel());
1988 bool initialScrolled = false;
1989 nsIFrame* parentFrame = frame;
1990 while ((parentFrame = parentFrame->GetParent())) {
1991 nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
1992 if (scrollableFrame) {
1993 if (!initialScrolled) {
1994 // Scroll substring to the given point. Turn the point into percents
1995 // relative scrollable area to use nsCoreUtils::ScrollSubstringTo.
1996 nsRect frameRect = parentFrame->GetScreenRectInAppUnits();
1997 nscoord offsetPointX = coordsInAppUnits.x - frameRect.X();
1998 nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y();
2000 nsSize size(parentFrame->GetSize());
2002 // avoid divide by zero
2003 size.width = size.width ? size.width : 1;
2004 size.height = size.height ? size.height : 1;
2006 int16_t hPercent = offsetPointX * 100 / size.width;
2007 int16_t vPercent = offsetPointY * 100 / size.height;
2009 nsresult rv = nsCoreUtils::ScrollSubstringTo(
2010 frame, domRange, ScrollAxis(vPercent, WhenToScroll::Always),
2011 ScrollAxis(hPercent, WhenToScroll::Always));
2012 if (NS_FAILED(rv)) return;
2014 initialScrolled = true;
2015 } else {
2016 // Substring was scrolled to the given point already inside its closest
2017 // scrollable area. If there are nested scrollable areas then make
2018 // sure we scroll lower areas to the given point inside currently
2019 // traversed scrollable area.
2020 nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
2023 frame = parentFrame;
2027 void HyperTextAccessible::EnclosingRange(a11y::TextRange& aRange) const {
2028 if (IsTextField()) {
2029 aRange.Set(mDoc, const_cast<HyperTextAccessible*>(this), 0,
2030 const_cast<HyperTextAccessible*>(this), CharacterCount());
2031 } else {
2032 aRange.Set(mDoc, mDoc, 0, mDoc, mDoc->CharacterCount());
2036 void HyperTextAccessible::SelectionRanges(
2037 nsTArray<a11y::TextRange>* aRanges) const {
2038 dom::Selection* sel = DOMSelection();
2039 if (!sel) {
2040 return;
2043 TextRange::TextRangesFromSelection(sel, aRanges);
2046 void HyperTextAccessible::VisibleRanges(
2047 nsTArray<a11y::TextRange>* aRanges) const {}
2049 void HyperTextAccessible::RangeByChild(LocalAccessible* aChild,
2050 a11y::TextRange& aRange) const {
2051 HyperTextAccessible* ht = aChild->AsHyperText();
2052 if (ht) {
2053 aRange.Set(mDoc, ht, 0, ht, ht->CharacterCount());
2054 return;
2057 LocalAccessible* child = aChild;
2058 LocalAccessible* parent = nullptr;
2059 while ((parent = child->LocalParent()) && !(ht = parent->AsHyperText())) {
2060 child = parent;
2063 // If no text then return collapsed text range, otherwise return a range
2064 // containing the text enclosed by the given child.
2065 if (ht) {
2066 int32_t childIdx = child->IndexInParent();
2067 int32_t startOffset = ht->GetChildOffset(childIdx);
2068 int32_t endOffset =
2069 child->IsTextLeaf() ? ht->GetChildOffset(childIdx + 1) : startOffset;
2070 aRange.Set(mDoc, ht, startOffset, ht, endOffset);
2074 void HyperTextAccessible::RangeAtPoint(int32_t aX, int32_t aY,
2075 a11y::TextRange& aRange) const {
2076 LocalAccessible* child =
2077 mDoc->LocalChildAtPoint(aX, aY, EWhichChildAtPoint::DeepestChild);
2078 if (!child) return;
2080 LocalAccessible* parent = nullptr;
2081 while ((parent = child->LocalParent()) && !parent->IsHyperText()) {
2082 child = parent;
2085 // Return collapsed text range for the point.
2086 if (parent) {
2087 HyperTextAccessible* ht = parent->AsHyperText();
2088 int32_t offset = ht->GetChildOffset(child);
2089 aRange.Set(mDoc, ht, offset, ht, offset);
2093 ////////////////////////////////////////////////////////////////////////////////
2094 // LocalAccessible public
2096 // LocalAccessible protected
2097 ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const {
2098 // Check @alt attribute for invalid img elements.
2099 bool hasImgAlt = false;
2100 if (mContent->IsHTMLElement(nsGkAtoms::img)) {
2101 hasImgAlt = mContent->AsElement()->GetAttr(kNameSpaceID_None,
2102 nsGkAtoms::alt, aName);
2103 if (!aName.IsEmpty()) return eNameOK;
2106 ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName);
2107 if (!aName.IsEmpty()) return nameFlag;
2109 // Get name from title attribute for HTML abbr and acronym elements making it
2110 // a valid name from markup. Otherwise their name isn't picked up by recursive
2111 // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
2112 if (IsAbbreviation() && mContent->AsElement()->GetAttr(
2113 kNameSpaceID_None, nsGkAtoms::title, aName)) {
2114 aName.CompressWhitespace();
2117 return hasImgAlt ? eNoNameOnPurpose : eNameOK;
2120 void HyperTextAccessible::Shutdown() {
2121 mOffsets.Clear();
2122 AccessibleWrap::Shutdown();
2125 bool HyperTextAccessible::RemoveChild(LocalAccessible* aAccessible) {
2126 const int32_t childIndex = aAccessible->IndexInParent();
2127 if (childIndex < static_cast<int64_t>(mOffsets.Length())) {
2128 mOffsets.RemoveLastElements(mOffsets.Length() -
2129 aAccessible->IndexInParent());
2132 return AccessibleWrap::RemoveChild(aAccessible);
2135 bool HyperTextAccessible::InsertChildAt(uint32_t aIndex,
2136 LocalAccessible* aChild) {
2137 if (aIndex < mOffsets.Length()) {
2138 mOffsets.RemoveLastElements(mOffsets.Length() - aIndex);
2141 return AccessibleWrap::InsertChildAt(aIndex, aChild);
2144 Relation HyperTextAccessible::RelationByType(RelationType aType) const {
2145 Relation rel = LocalAccessible::RelationByType(aType);
2147 switch (aType) {
2148 case RelationType::NODE_CHILD_OF:
2149 if (HasOwnContent() && mContent->IsMathMLElement()) {
2150 LocalAccessible* parent = LocalParent();
2151 if (parent) {
2152 nsIContent* parentContent = parent->GetContent();
2153 if (parentContent &&
2154 parentContent->IsMathMLElement(nsGkAtoms::mroot_)) {
2155 // Add a relation pointing to the parent <mroot>.
2156 rel.AppendTarget(parent);
2160 break;
2161 case RelationType::NODE_PARENT_OF:
2162 if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot_)) {
2163 LocalAccessible* base = LocalChildAt(0);
2164 LocalAccessible* index = LocalChildAt(1);
2165 if (base && index) {
2166 // Append the <mroot> children in the order index, base.
2167 rel.AppendTarget(index);
2168 rel.AppendTarget(base);
2171 break;
2172 default:
2173 break;
2176 return rel;
2179 ////////////////////////////////////////////////////////////////////////////////
2180 // HyperTextAccessible public static
2182 nsresult HyperTextAccessible::ContentToRenderedOffset(
2183 nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const {
2184 if (!aFrame) {
2185 // Current frame not rendered -- this can happen if text is set on
2186 // something with display: none
2187 *aRenderedOffset = 0;
2188 return NS_OK;
2191 if (IsTextField()) {
2192 *aRenderedOffset = aContentOffset;
2193 return NS_OK;
2196 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
2197 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
2198 "Call on primary frame only");
2200 nsIFrame::RenderedText text =
2201 aFrame->GetRenderedText(aContentOffset, aContentOffset + 1,
2202 nsIFrame::TextOffsetType::OffsetsInContentText,
2203 nsIFrame::TrailingWhitespace::DontTrim);
2204 *aRenderedOffset = text.mOffsetWithinNodeRenderedText;
2206 return NS_OK;
2209 nsresult HyperTextAccessible::RenderedToContentOffset(
2210 nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const {
2211 if (IsTextField()) {
2212 *aContentOffset = aRenderedOffset;
2213 return NS_OK;
2216 *aContentOffset = 0;
2217 NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE);
2219 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
2220 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
2221 "Call on primary frame only");
2223 nsIFrame::RenderedText text =
2224 aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
2225 nsIFrame::TextOffsetType::OffsetsInRenderedText,
2226 nsIFrame::TrailingWhitespace::DontTrim);
2227 *aContentOffset = text.mOffsetWithinNodeText;
2229 return NS_OK;
2232 ////////////////////////////////////////////////////////////////////////////////
2233 // HyperTextAccessible public
2235 int32_t HyperTextAccessible::GetChildOffset(uint32_t aChildIndex,
2236 bool aInvalidateAfter) const {
2237 if (aChildIndex == 0) {
2238 if (aInvalidateAfter) mOffsets.Clear();
2240 return aChildIndex;
2243 int32_t count = mOffsets.Length() - aChildIndex;
2244 if (count > 0) {
2245 if (aInvalidateAfter) mOffsets.RemoveElementsAt(aChildIndex, count);
2247 return mOffsets[aChildIndex - 1];
2250 uint32_t lastOffset =
2251 mOffsets.IsEmpty() ? 0 : mOffsets[mOffsets.Length() - 1];
2253 while (mOffsets.Length() < aChildIndex) {
2254 LocalAccessible* child = mChildren[mOffsets.Length()];
2255 lastOffset += nsAccUtils::TextLength(child);
2256 mOffsets.AppendElement(lastOffset);
2259 return mOffsets[aChildIndex - 1];
2262 int32_t HyperTextAccessible::GetChildIndexAtOffset(uint32_t aOffset) const {
2263 uint32_t lastOffset = 0;
2264 const uint32_t offsetCount = mOffsets.Length();
2266 if (offsetCount > 0) {
2267 lastOffset = mOffsets[offsetCount - 1];
2268 if (aOffset < lastOffset) {
2269 size_t index;
2270 if (BinarySearch(mOffsets, 0, offsetCount, aOffset, &index)) {
2271 return (index < (offsetCount - 1)) ? index + 1 : index;
2274 return (index == offsetCount) ? -1 : index;
2278 uint32_t childCount = ChildCount();
2279 while (mOffsets.Length() < childCount) {
2280 LocalAccessible* child = LocalChildAt(mOffsets.Length());
2281 lastOffset += nsAccUtils::TextLength(child);
2282 mOffsets.AppendElement(lastOffset);
2283 if (aOffset < lastOffset) return mOffsets.Length() - 1;
2286 if (aOffset == lastOffset) return mOffsets.Length() - 1;
2288 return -1;
2291 ////////////////////////////////////////////////////////////////////////////////
2292 // HyperTextAccessible protected
2294 nsresult HyperTextAccessible::GetDOMPointByFrameOffset(
2295 nsIFrame* aFrame, int32_t aOffset, LocalAccessible* aAccessible,
2296 DOMPoint* aPoint) {
2297 NS_ENSURE_ARG(aAccessible);
2299 if (!aFrame) {
2300 // If the given frame is null then set offset after the DOM node of the
2301 // given accessible.
2302 NS_ASSERTION(!aAccessible->IsDoc(),
2303 "Shouldn't be called on document accessible!");
2305 nsIContent* content = aAccessible->GetContent();
2306 NS_ASSERTION(content, "Shouldn't operate on defunct accessible!");
2308 nsIContent* parent = content->GetParent();
2310 aPoint->idx = parent->ComputeIndexOf(content) + 1;
2311 aPoint->node = parent;
2313 } else if (aFrame->IsTextFrame()) {
2314 nsIContent* content = aFrame->GetContent();
2315 NS_ENSURE_STATE(content);
2317 nsIFrame* primaryFrame = content->GetPrimaryFrame();
2318 nsresult rv =
2319 RenderedToContentOffset(primaryFrame, aOffset, &(aPoint->idx));
2320 NS_ENSURE_SUCCESS(rv, rv);
2322 aPoint->node = content;
2324 } else {
2325 nsIContent* content = aFrame->GetContent();
2326 NS_ENSURE_STATE(content);
2328 nsIContent* parent = content->GetParent();
2329 NS_ENSURE_STATE(parent);
2331 aPoint->idx = parent->ComputeIndexOf(content);
2332 aPoint->node = parent;
2335 return NS_OK;
2338 // HyperTextAccessible
2339 void HyperTextAccessible::GetSpellTextAttr(
2340 nsINode* aNode, int32_t aNodeOffset, uint32_t* aStartOffset,
2341 uint32_t* aEndOffset, nsIPersistentProperties* aAttributes) {
2342 RefPtr<nsFrameSelection> fs = FrameSelection();
2343 if (!fs) return;
2345 dom::Selection* domSel = fs->GetSelection(SelectionType::eSpellCheck);
2346 if (!domSel) return;
2348 int32_t rangeCount = domSel->RangeCount();
2349 if (rangeCount <= 0) return;
2351 uint32_t startOffset = 0, endOffset = 0;
2352 for (int32_t idx = 0; idx < rangeCount; idx++) {
2353 const nsRange* range = domSel->GetRangeAt(idx);
2354 if (range->Collapsed()) continue;
2356 // See if the point comes after the range in which case we must continue in
2357 // case there is another range after this one.
2358 nsINode* endNode = range->GetEndContainer();
2359 int32_t endNodeOffset = range->EndOffset();
2360 Maybe<int32_t> order = nsContentUtils::ComparePoints(
2361 aNode, aNodeOffset, endNode, endNodeOffset);
2362 if (NS_WARN_IF(!order)) {
2363 continue;
2366 if (*order >= 0) {
2367 continue;
2370 // At this point our point is either in this range or before it but after
2371 // the previous range. So we check to see if the range starts before the
2372 // point in which case the point is in the missspelled range, otherwise it
2373 // must be before the range and after the previous one if any.
2374 nsINode* startNode = range->GetStartContainer();
2375 int32_t startNodeOffset = range->StartOffset();
2376 order = nsContentUtils::ComparePoints(startNode, startNodeOffset, aNode,
2377 aNodeOffset);
2378 if (!order) {
2379 // As (`aNode`, `aNodeOffset`) is comparable to the end of the range, it
2380 // should also be comparable to the range's start. Returning here
2381 // prevents crashes in release builds.
2382 MOZ_ASSERT_UNREACHABLE();
2383 return;
2386 if (*order <= 0) {
2387 startOffset = DOMPointToOffset(startNode, startNodeOffset);
2389 endOffset = DOMPointToOffset(endNode, endNodeOffset);
2391 if (startOffset > *aStartOffset) *aStartOffset = startOffset;
2393 if (endOffset < *aEndOffset) *aEndOffset = endOffset;
2395 if (aAttributes) {
2396 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid, u"spelling"_ns);
2399 return;
2402 // This range came after the point.
2403 endOffset = DOMPointToOffset(startNode, startNodeOffset);
2405 if (idx > 0) {
2406 const nsRange* prevRange = domSel->GetRangeAt(idx - 1);
2407 startOffset = DOMPointToOffset(prevRange->GetEndContainer(),
2408 prevRange->EndOffset());
2411 // The previous range might not be within this accessible. In that case,
2412 // DOMPointToOffset returns length as a fallback. We don't want to use
2413 // that offset if so, hence the startOffset < *aEndOffset check.
2414 if (startOffset > *aStartOffset && startOffset < *aEndOffset) {
2415 *aStartOffset = startOffset;
2418 if (endOffset < *aEndOffset) *aEndOffset = endOffset;
2420 return;
2423 // We never found a range that ended after the point, therefore we know that
2424 // the point is not in a range, that we do not need to compute an end offset,
2425 // and that we should use the end offset of the last range to compute the
2426 // start offset of the text attribute range.
2427 const nsRange* prevRange = domSel->GetRangeAt(rangeCount - 1);
2428 startOffset =
2429 DOMPointToOffset(prevRange->GetEndContainer(), prevRange->EndOffset());
2431 // The previous range might not be within this accessible. In that case,
2432 // DOMPointToOffset returns length as a fallback. We don't want to use
2433 // that offset if so, hence the startOffset < *aEndOffset check.
2434 if (startOffset > *aStartOffset && startOffset < *aEndOffset) {
2435 *aStartOffset = startOffset;