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