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