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