summaryrefslogtreecommitdiffstats
path: root/accessible/generic/HyperTextAccessible.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /accessible/generic/HyperTextAccessible.cpp
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'accessible/generic/HyperTextAccessible.cpp')
-rw-r--r--accessible/generic/HyperTextAccessible.cpp2357
1 files changed, 2357 insertions, 0 deletions
diff --git a/accessible/generic/HyperTextAccessible.cpp b/accessible/generic/HyperTextAccessible.cpp
new file mode 100644
index 0000000000..fec1b31b28
--- /dev/null
+++ b/accessible/generic/HyperTextAccessible.cpp
@@ -0,0 +1,2357 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HyperTextAccessible-inl.h"
+
+#include "nsAccessibilityService.h"
+#include "nsAccessiblePivot.h"
+#include "nsIAccessibleTypes.h"
+#include "AccAttributes.h"
+#include "DocAccessible.h"
+#include "HTMLListAccessible.h"
+#include "LocalAccessible-inl.h"
+#include "Pivot.h"
+#include "Relation.h"
+#include "Role.h"
+#include "States.h"
+#include "TextAttrs.h"
+#include "TextLeafRange.h"
+#include "TextRange.h"
+#include "TreeWalker.h"
+
+#include "nsCaret.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsFocusManager.h"
+#include "nsIEditingSession.h"
+#include "nsContainerFrame.h"
+#include "nsFrameSelection.h"
+#include "nsILineIterator.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIScrollableFrame.h"
+#include "nsIMathMLFrame.h"
+#include "nsRange.h"
+#include "nsTextFragment.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/BinarySearch.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/HTMLHeadingElement.h"
+#include "mozilla/dom/Selection.h"
+#include "gfxSkipChars.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+/**
+ * This class is used in HyperTextAccessible to search for paragraph
+ * boundaries.
+ */
+class ParagraphBoundaryRule : public PivotRule {
+ public:
+ explicit ParagraphBoundaryRule(LocalAccessible* aAnchor,
+ uint32_t aAnchorTextoffset,
+ nsDirection aDirection,
+ bool aSkipAnchorSubtree = false)
+ : mAnchor(aAnchor),
+ mAnchorTextOffset(aAnchorTextoffset),
+ mDirection(aDirection),
+ mSkipAnchorSubtree(aSkipAnchorSubtree),
+ mLastMatchTextOffset(0) {}
+
+ virtual uint16_t Match(Accessible* aAcc) override {
+ MOZ_ASSERT(aAcc && aAcc->IsLocal());
+ LocalAccessible* acc = aAcc->AsLocal();
+ if (acc->IsOuterDoc()) {
+ // The child document might be remote and we can't (and don't want to)
+ // handle remote documents. Also, iframes are inline anyway and thus
+ // can't be paragraph boundaries. Therefore, skip this unconditionally.
+ return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
+ if (mSkipAnchorSubtree && acc == mAnchor) {
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ // First, deal with the case that we encountered a line break, for example,
+ // a br in a paragraph.
+ if (acc->Role() == roles::WHITESPACE) {
+ result |= nsIAccessibleTraversalRule::FILTER_MATCH;
+ return result;
+ }
+
+ // Now, deal with the case that we encounter a new block level accessible.
+ // This also means a new paragraph boundary start.
+ nsIFrame* frame = acc->GetFrame();
+ if (frame && frame->IsBlockFrame() &&
+ acc->Role() != roles::LISTITEM_MARKER) {
+ result |= nsIAccessibleTraversalRule::FILTER_MATCH;
+ return result;
+ }
+
+ // A text leaf can contain a line break if it's pre-formatted text.
+ if (acc->IsTextLeaf()) {
+ nsAutoString name;
+ acc->Name(name);
+ int32_t offset;
+ if (mDirection == eDirPrevious) {
+ if (acc == mAnchor && mAnchorTextOffset == 0) {
+ // We're already at the start of this node, so there can be no line
+ // break before.
+ return result;
+ }
+ // If we began on a line break, we don't want to match it, so search
+ // from 1 before our anchor offset.
+ offset =
+ name.RFindChar('\n', acc == mAnchor ? mAnchorTextOffset - 1 : -1);
+ } else {
+ offset = name.FindChar('\n', acc == mAnchor ? mAnchorTextOffset : 0);
+ }
+ if (offset != -1) {
+ // Line ebreak!
+ mLastMatchTextOffset = offset;
+ result |= nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+ }
+
+ // This is only valid if the last match was a text leaf. It returns the
+ // offset of the line break character in that text leaf.
+ uint32_t GetLastMatchTextOffset() { return mLastMatchTextOffset; }
+
+ private:
+ LocalAccessible* mAnchor;
+ uint32_t mAnchorTextOffset;
+ nsDirection mDirection;
+ bool mSkipAnchorSubtree;
+ uint32_t mLastMatchTextOffset;
+};
+
+/**
+ * This class is used in HyperTextAccessible::FindParagraphStartOffset to
+ * search forward exactly one step from a match found by the above.
+ * It should only be initialized with a boundary, and it will skip that
+ * boundary's sub tree if it is a block element boundary.
+ */
+class SkipParagraphBoundaryRule : public PivotRule {
+ public:
+ explicit SkipParagraphBoundaryRule(Accessible* aBoundary)
+ : mBoundary(aBoundary) {}
+
+ virtual uint16_t Match(Accessible* aAcc) override {
+ MOZ_ASSERT(aAcc && aAcc->IsLocal());
+ // If matching the boundary, skip its sub tree.
+ if (aAcc == mBoundary) {
+ return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ private:
+ Accessible* mBoundary;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperTextAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc)
+ : AccessibleWrap(aNode, aDoc) {
+ mType = eHyperTextType;
+ mGenericTypes |= eHyperText;
+}
+
+role HyperTextAccessible::NativeRole() const {
+ a11y::role r = GetAccService()->MarkupRole(mContent);
+ if (r != roles::NOTHING) return r;
+
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->IsInlineFrame()) return roles::TEXT;
+
+ return roles::TEXT_CONTAINER;
+}
+
+uint64_t HyperTextAccessible::NativeState() const {
+ uint64_t states = AccessibleWrap::NativeState();
+
+ if (IsEditable()) {
+ states |= states::EDITABLE;
+
+ } else if (mContent->IsHTMLElement(nsGkAtoms::article)) {
+ // We want <article> to behave like a document in terms of readonly state.
+ states |= states::READONLY;
+ }
+
+ nsIFrame* frame = GetFrame();
+ if ((states & states::EDITABLE) || (frame && frame->IsSelectable(nullptr))) {
+ // If the accessible is editable the layout selectable state only disables
+ // mouse selection, but keyboard (shift+arrow) selection is still possible.
+ states |= states::SELECTABLE_TEXT;
+ }
+
+ return states;
+}
+
+bool HyperTextAccessible::IsEditable() const {
+ if (!mContent) {
+ return false;
+ }
+ return mContent->AsElement()->State().HasState(dom::ElementState::READWRITE);
+}
+
+LayoutDeviceIntRect HyperTextAccessible::GetBoundsInFrame(
+ nsIFrame* aFrame, uint32_t aStartRenderedOffset,
+ uint32_t aEndRenderedOffset) {
+ nsPresContext* presContext = mDoc->PresContext();
+ if (!aFrame->IsTextFrame()) {
+ return LayoutDeviceIntRect::FromAppUnitsToNearest(
+ aFrame->GetScreenRectInAppUnits(), presContext->AppUnitsPerDevPixel());
+ }
+
+ // Substring must be entirely within the same text node.
+ int32_t startContentOffset, endContentOffset;
+ nsresult rv = RenderedToContentOffset(aFrame, aStartRenderedOffset,
+ &startContentOffset);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+ rv = RenderedToContentOffset(aFrame, aEndRenderedOffset, &endContentOffset);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ nsIFrame* frame;
+ int32_t startContentOffsetInFrame;
+ // Get the right frame continuation -- not really a child, but a sibling of
+ // the primary frame passed in
+ rv = aFrame->GetChildFrameContainingOffset(
+ startContentOffset, false, &startContentOffsetInFrame, &frame);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ nsRect screenRect;
+ while (frame && startContentOffset < endContentOffset) {
+ // Start with this frame's screen rect, which we will shrink based on
+ // the substring we care about within it. We will then add that frame to
+ // the total screenRect we are returning.
+ nsRect frameScreenRect = frame->GetScreenRectInAppUnits();
+
+ // Get the length of the substring in this frame that we want the bounds for
+ auto [startFrameTextOffset, endFrameTextOffset] = frame->GetOffsets();
+ int32_t frameTotalTextLength = endFrameTextOffset - startFrameTextOffset;
+ int32_t seekLength = endContentOffset - startContentOffset;
+ int32_t frameSubStringLength =
+ std::min(frameTotalTextLength - startContentOffsetInFrame, seekLength);
+
+ // Add the point where the string starts to the frameScreenRect
+ nsPoint frameTextStartPoint;
+ rv = frame->GetPointFromOffset(startContentOffset, &frameTextStartPoint);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ // Use the point for the end offset to calculate the width
+ nsPoint frameTextEndPoint;
+ rv = frame->GetPointFromOffset(startContentOffset + frameSubStringLength,
+ &frameTextEndPoint);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ frameScreenRect.SetRectX(
+ frameScreenRect.X() +
+ std::min(frameTextStartPoint.x, frameTextEndPoint.x),
+ mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x));
+
+ screenRect.UnionRect(frameScreenRect, screenRect);
+
+ // Get ready to loop back for next frame continuation
+ startContentOffset += frameSubStringLength;
+ startContentOffsetInFrame = 0;
+ frame = frame->GetNextContinuation();
+ }
+
+ return LayoutDeviceIntRect::FromAppUnitsToNearest(
+ screenRect, presContext->AppUnitsPerDevPixel());
+}
+
+uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode,
+ int32_t aNodeOffset,
+ bool aIsEndOffset) const {
+ if (!aNode) return 0;
+
+ uint32_t offset = 0;
+ nsINode* findNode = nullptr;
+
+ if (aNodeOffset == -1) {
+ findNode = aNode;
+
+ } else if (aNode->IsText()) {
+ // For text nodes, aNodeOffset comes in as a character offset
+ // Text offset will be added at the end, if we find the offset in this
+ // hypertext We want the "skipped" offset into the text (rendered text
+ // without the extra whitespace)
+ nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
+ NS_ENSURE_TRUE(frame, 0);
+
+ nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ findNode = aNode;
+
+ } else {
+ // findNode could be null if aNodeOffset == # of child nodes, which means
+ // one of two things:
+ // 1) there are no children, and the passed-in node is not mContent -- use
+ // parentContent for the node to find
+ // 2) there are no children and the passed-in node is mContent, which means
+ // we're an empty nsIAccessibleText
+ // 3) there are children and we're at the end of the children
+
+ findNode = aNode->GetChildAt_Deprecated(aNodeOffset);
+ if (!findNode) {
+ if (aNodeOffset == 0) {
+ if (aNode == GetNode()) {
+ // Case #1: this accessible has no children and thus has empty text,
+ // we can only be at hypertext offset 0.
+ return 0;
+ }
+
+ // Case #2: there are no children, we're at this node.
+ findNode = aNode;
+ } else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) {
+ // Case #3: we're after the last child, get next node to this one.
+ for (nsINode* tmpNode = aNode;
+ !findNode && tmpNode && tmpNode != mContent;
+ tmpNode = tmpNode->GetParent()) {
+ findNode = tmpNode->GetNextSibling();
+ }
+ }
+ }
+ }
+
+ // Get accessible for this findNode, or if that node isn't accessible, use the
+ // accessible for the next DOM node which has one (based on forward depth
+ // first search)
+ LocalAccessible* descendant = nullptr;
+ if (findNode) {
+ dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode);
+ if (brElement && brElement->IsPaddingForEmptyEditor()) {
+ // This <br> is the hacky "padding <br> element" used when there is no
+ // text in the editor.
+ return 0;
+ }
+
+ descendant = mDoc->GetAccessible(findNode);
+ if (!descendant && findNode->IsContent()) {
+ LocalAccessible* container = mDoc->GetContainerAccessible(findNode);
+ if (container) {
+ TreeWalker walker(container, findNode->AsContent(),
+ TreeWalker::eWalkContextTree);
+ descendant = walker.Next();
+ if (!descendant) descendant = container;
+ }
+ }
+ }
+
+ return TransformOffset(descendant, offset, aIsEndOffset);
+}
+
+uint32_t HyperTextAccessible::TransformOffset(LocalAccessible* aDescendant,
+ uint32_t aOffset,
+ bool aIsEndOffset) const {
+ // From the descendant, go up and get the immediate child of this hypertext.
+ uint32_t offset = aOffset;
+ LocalAccessible* descendant = aDescendant;
+ while (descendant) {
+ LocalAccessible* parent = descendant->LocalParent();
+ if (parent == this) return GetChildOffset(descendant) + offset;
+
+ // This offset no longer applies because the passed-in text object is not
+ // a child of the hypertext. This happens when there are nested hypertexts,
+ // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
+ // to make it relative the hypertext.
+ // If the end offset is not supposed to be inclusive and the original point
+ // is not at 0 offset then the returned offset should be after an embedded
+ // character the original point belongs to.
+ if (aIsEndOffset) {
+ // Similar to our special casing in FindOffset, we add handling for
+ // bulleted lists here because PeekOffset returns the inner text node
+ // for a list when it should return the list bullet.
+ // We manually set the offset so the error doesn't propagate up.
+ if (offset == 0 && parent && parent->IsHTMLListItem() &&
+ descendant->LocalPrevSibling() &&
+ descendant->LocalPrevSibling() ==
+ parent->AsHTMLListItem()->Bullet()) {
+ offset = 0;
+ } else {
+ offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
+ }
+ } else {
+ offset = 0;
+ }
+
+ descendant = parent;
+ }
+
+ // If the given a11y point cannot be mapped into offset relative this
+ // hypertext offset then return length as fallback value.
+ return CharacterCount();
+}
+
+DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const {
+ // 0 offset is valid even if no children. In this case the associated editor
+ // is empty so return a DOM point for editor root element.
+ if (aOffset == 0) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ if (editorBase->IsEmpty()) {
+ return DOMPoint(editorBase->GetRoot(), 0);
+ }
+ }
+ }
+
+ int32_t childIdx = GetChildIndexAtOffset(aOffset);
+ if (childIdx == -1) return DOMPoint();
+
+ LocalAccessible* child = LocalChildAt(childIdx);
+ int32_t innerOffset = aOffset - GetChildOffset(childIdx);
+
+ // A text leaf case.
+ if (child->IsTextLeaf()) {
+ // The point is inside the text node. This is always true for any text leaf
+ // except a last child one. See assertion below.
+ if (aOffset < GetChildOffset(childIdx + 1)) {
+ nsIContent* content = child->GetContent();
+ int32_t idx = 0;
+ if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(),
+ innerOffset, &idx))) {
+ return DOMPoint();
+ }
+
+ return DOMPoint(content, idx);
+ }
+
+ // Set the DOM point right after the text node.
+ MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount());
+ innerOffset = 1;
+ }
+
+ // Case of embedded object. The point is either before or after the element.
+ NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!");
+ nsINode* node = child->GetNode();
+ nsINode* parentNode = node->GetParentNode();
+ return parentNode ? DOMPoint(parentNode,
+ parentNode->ComputeIndexOf_Deprecated(node) +
+ innerOffset)
+ : DOMPoint();
+}
+
+uint32_t HyperTextAccessible::FindOffset(uint32_t aOffset,
+ nsDirection aDirection,
+ nsSelectionAmount aAmount,
+ EWordMovementType aWordMovementType) {
+ NS_ASSERTION(aDirection == eDirPrevious || aAmount != eSelectBeginLine,
+ "eSelectBeginLine should only be used with eDirPrevious");
+
+ // Find a leaf accessible frame to start with. PeekOffset wants this.
+ HyperTextAccessible* text = this;
+ LocalAccessible* child = nullptr;
+ int32_t innerOffset = aOffset;
+
+ do {
+ int32_t childIdx = text->GetChildIndexAtOffset(innerOffset);
+
+ // We can have an empty text leaf as our only child. Since empty text
+ // leaves are not accessible we then have no children, but 0 is a valid
+ // innerOffset.
+ if (childIdx == -1) {
+ NS_ASSERTION(innerOffset == 0 && !text->ChildCount(), "No childIdx?");
+ return DOMPointToOffset(text->GetNode(), 0, aDirection == eDirNext);
+ }
+
+ child = text->LocalChildAt(childIdx);
+
+ // HTML list items may need special processing because PeekOffset doesn't
+ // work with list bullets.
+ if (text->IsHTMLListItem()) {
+ HTMLLIAccessible* li = text->AsHTMLListItem();
+ if (child == li->Bullet()) {
+ // XXX: the logic is broken for multichar bullets in moving by
+ // char/cluster/word cases.
+ if (text != this) {
+ return aDirection == eDirPrevious ? TransformOffset(text, 0, false)
+ : TransformOffset(text, 1, true);
+ }
+ if (aDirection == eDirPrevious) return 0;
+
+ uint32_t nextOffset = GetChildOffset(1);
+ if (nextOffset == 0) return 0;
+
+ switch (aAmount) {
+ case eSelectLine:
+ case eSelectEndLine:
+ // Ask a text leaf next (if not empty) to the bullet for an offset
+ // since list item may be multiline.
+ return nextOffset < CharacterCount()
+ ? FindOffset(nextOffset, aDirection, aAmount,
+ aWordMovementType)
+ : nextOffset;
+
+ default:
+ return nextOffset;
+ }
+ }
+ }
+
+ innerOffset -= text->GetChildOffset(childIdx);
+
+ text = child->AsHyperText();
+ } while (text);
+
+ nsIFrame* childFrame = child->GetFrame();
+ if (!childFrame) {
+ NS_ERROR("No child frame");
+ return 0;
+ }
+
+ int32_t innerContentOffset = innerOffset;
+ if (child->IsTextLeaf()) {
+ NS_ASSERTION(childFrame->IsTextFrame(), "Wrong frame!");
+ RenderedToContentOffset(childFrame, innerOffset, &innerContentOffset);
+ }
+
+ nsIFrame* frameAtOffset = childFrame;
+ int32_t unusedOffsetInFrame = 0;
+ childFrame->GetChildFrameContainingOffset(
+ innerContentOffset, true, &unusedOffsetInFrame, &frameAtOffset);
+
+ const bool kIsJumpLinesOk = true; // okay to jump lines
+ const bool kIsScrollViewAStop = false; // do not stop at scroll views
+ const bool kIsKeyboardSelect = true; // is keyboard selection
+ const bool kIsVisualBidi = false; // use visual order for bidi text
+ nsPeekOffsetStruct pos(
+ aAmount, aDirection, innerContentOffset, nsPoint(0, 0), kIsJumpLinesOk,
+ kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi, false,
+ nsPeekOffsetStruct::ForceEditableRegion::No, aWordMovementType, false);
+ nsresult rv = frameAtOffset->PeekOffset(&pos);
+
+ // PeekOffset fails on last/first lines of the text in certain cases.
+ bool fallBackToSelectEndLine = false;
+ if (NS_FAILED(rv) && aAmount == eSelectLine) {
+ fallBackToSelectEndLine = aDirection == eDirNext;
+ pos.mAmount = fallBackToSelectEndLine ? eSelectEndLine : eSelectBeginLine;
+ frameAtOffset->PeekOffset(&pos);
+ }
+ if (!pos.mResultContent) {
+ NS_ERROR("No result content!");
+ return 0;
+ }
+
+ // Turn the resulting DOM point into an offset.
+ uint32_t hyperTextOffset = DOMPointToOffset(
+ pos.mResultContent, pos.mContentOffset, aDirection == eDirNext);
+
+ if (fallBackToSelectEndLine && IsLineEndCharAt(hyperTextOffset)) {
+ // We used eSelectEndLine, but the caller requested eSelectLine.
+ // If there's a '\n' at the end of the line, eSelectEndLine will stop on
+ // it rather than after it. This is not what we want, since the caller
+ // wants the next line, not the same line.
+ ++hyperTextOffset;
+ }
+
+ if (aDirection == eDirPrevious) {
+ // If we reached the end during search, this means we didn't find the DOM
+ // point and we're actually at the start of the paragraph
+ if (hyperTextOffset == CharacterCount()) return 0;
+
+ // PeekOffset stops right before bullet so return 0 to workaround it.
+ if (IsHTMLListItem() && aAmount == eSelectBeginLine &&
+ hyperTextOffset > 0) {
+ LocalAccessible* prevOffsetChild = GetChildAtOffset(hyperTextOffset - 1);
+ if (prevOffsetChild == AsHTMLListItem()->Bullet()) return 0;
+ }
+ }
+
+ return hyperTextOffset;
+}
+
+uint32_t HyperTextAccessible::FindWordBoundary(
+ uint32_t aOffset, nsDirection aDirection,
+ EWordMovementType aWordMovementType) {
+ uint32_t orig =
+ FindOffset(aOffset, aDirection, eSelectWord, aWordMovementType);
+ if (aWordMovementType != eStartWord) {
+ return orig;
+ }
+ if (aDirection == eDirPrevious) {
+ // When layout.word_select.stop_at_punctuation is true (the default),
+ // for a word beginning with punctuation, layout treats the punctuation
+ // as the start of the word when moving next. However, when moving
+ // previous, layout stops *after* the punctuation. We want to be
+ // consistent regardless of movement direction and always treat punctuation
+ // as the start of a word.
+ if (!StaticPrefs::layout_word_select_stop_at_punctuation()) {
+ return orig;
+ }
+ // Case 1: Example: "a @"
+ // If aOffset is 2 or 3, orig will be 0, but it should be 2. That is,
+ // previous word moved back too far.
+ LocalAccessible* child = GetChildAtOffset(orig);
+ if (child && child->IsHyperText()) {
+ // For a multi-word embedded object, previous word correctly goes back
+ // to the start of the word (the embedded object). Next word (below)
+ // incorrectly stops after the embedded object in this case, so return
+ // the already correct result.
+ // Example: "a x y b", where "x y" is an embedded link
+ // If aOffset is 4, orig will be 2, which is correct.
+ // If we get the next word (below), we'll end up returning 3 instead.
+ return orig;
+ }
+ uint32_t next = FindOffset(orig, eDirNext, eSelectWord, eStartWord);
+ if (next < aOffset) {
+ // Next word stopped on punctuation.
+ return next;
+ }
+ // case 2: example: "a @@b"
+ // If aOffset is 2, 3 or 4, orig will be 4, but it should be 2. That is,
+ // previous word didn't go back far enough.
+ if (orig == 0) {
+ return orig;
+ }
+ // Walk backwards by offset, getting the next word.
+ // In the loop, o is unsigned, so o >= 0 will always be true and won't
+ // prevent us from decrementing at 0. Instead, we check that o doesn't
+ // wrap around.
+ for (uint32_t o = orig - 1; o < orig; --o) {
+ next = FindOffset(o, eDirNext, eSelectWord, eStartWord);
+ if (next == orig) {
+ // Next word and previous word were consistent. This
+ // punctuation problem isn't applicable here.
+ break;
+ }
+ if (next < orig) {
+ // Next word stopped on punctuation.
+ return next;
+ }
+ }
+ } else {
+ // When layout.word_select.stop_at_punctuation is true (the default),
+ // when positioned on punctuation in the middle of a word, next word skips
+ // the rest of the word. However, when positioned before the punctuation,
+ // next word moves just after the punctuation. We want to be consistent
+ // regardless of starting position and always stop just after the
+ // punctuation.
+ // Next word can move too far when positioned on white space too.
+ // Example: "a b@c"
+ // If aOffset is 3, orig will be 5, but it should be 4. That is, next word
+ // moved too far.
+ if (aOffset == 0) {
+ return orig;
+ }
+ uint32_t prev = FindOffset(orig, eDirPrevious, eSelectWord, eStartWord);
+ if (prev <= aOffset) {
+ // orig definitely isn't too far forward.
+ return orig;
+ }
+ // Walk backwards by offset, getting the next word.
+ // In the loop, o is unsigned, so o >= 0 will always be true and won't
+ // prevent us from decrementing at 0. Instead, we check that o doesn't
+ // wrap around.
+ for (uint32_t o = aOffset - 1; o < aOffset; --o) {
+ uint32_t next = FindOffset(o, eDirNext, eSelectWord, eStartWord);
+ if (next > aOffset && next < orig) {
+ return next;
+ }
+ if (next <= aOffset) {
+ break;
+ }
+ }
+ }
+ return orig;
+}
+
+uint32_t HyperTextAccessible::FindLineBoundary(
+ uint32_t aOffset, EWhichLineBoundary aWhichLineBoundary) {
+ // Note: empty last line doesn't have own frame (a previous line contains '\n'
+ // character instead) thus when it makes a difference we need to process this
+ // case separately (otherwise operations are performed on previous line).
+ switch (aWhichLineBoundary) {
+ case ePrevLineBegin: {
+ // Fetch a previous line and move to its start (as arrow up and home keys
+ // were pressed).
+ if (IsEmptyLastLineOffset(aOffset)) {
+ return FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
+ }
+
+ uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
+ return FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine);
+ }
+
+ case ePrevLineEnd: {
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset - 1;
+
+ // If offset is at first line then return 0 (first line start).
+ uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
+ if (tmpOffset == 0) return 0;
+
+ // Otherwise move to end of previous line (as arrow up and end keys were
+ // pressed).
+ tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
+ return FindOffset(tmpOffset, eDirNext, eSelectEndLine);
+ }
+
+ case eThisLineBegin: {
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset;
+
+ // Move to begin of the current line (as home key was pressed).
+ uint32_t thisLineBeginOffset =
+ FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
+ if (IsCharAt(thisLineBeginOffset, kEmbeddedObjectChar)) {
+ // We landed on an embedded character, don't mess with possible embedded
+ // line breaks, and assume the offset is correct.
+ return thisLineBeginOffset;
+ }
+
+ // Sometimes, there is the possibility layout returned an
+ // offset smaller than it should. Sanity-check by moving to the end of the
+ // previous line and see if that has a greater offset.
+ uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
+ tmpOffset = FindOffset(tmpOffset, eDirNext, eSelectEndLine);
+ if (tmpOffset > thisLineBeginOffset && tmpOffset < aOffset) {
+ // We found a previous line offset. Return the next character after it
+ // as our start offset if it points to a line end char.
+ return IsLineEndCharAt(tmpOffset) ? tmpOffset + 1 : tmpOffset;
+ }
+ return thisLineBeginOffset;
+ }
+
+ case eThisLineEnd:
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset;
+
+ // Move to end of the current line (as end key was pressed).
+ return FindOffset(aOffset, eDirNext, eSelectEndLine);
+
+ case eNextLineBegin: {
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset;
+
+ // Move to begin of the next line if any (arrow down and home keys),
+ // otherwise end of the current line (arrow down only).
+ uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine);
+ uint32_t characterCount = CharacterCount();
+ if (tmpOffset == characterCount) {
+ return tmpOffset;
+ }
+
+ // Now, simulate the Home key on the next line to get its real offset.
+ uint32_t nextLineBeginOffset =
+ FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine);
+ // Sometimes, there are line breaks inside embedded characters. If this
+ // is the case, the cursor is after the line break, but the offset will
+ // be that of the embedded character, which points to before the line
+ // break. We definitely want the line break included.
+ if (IsCharAt(nextLineBeginOffset, kEmbeddedObjectChar)) {
+ // We can determine if there is a line break by pressing End from
+ // the queried offset. If there is a line break, the offset will be 1
+ // greater, since this line ends with the embed. If there is not, the
+ // value will be different even if a line break follows right after the
+ // embed.
+ uint32_t thisLineEndOffset =
+ FindOffset(aOffset, eDirNext, eSelectEndLine);
+ if (thisLineEndOffset == nextLineBeginOffset + 1) {
+ // If we're querying the offset of the embedded character, we want
+ // the end offset of the parent line instead. Press End
+ // once more from the current position, which is after the embed.
+ if (nextLineBeginOffset == aOffset) {
+ uint32_t thisLineEndOffset2 =
+ FindOffset(thisLineEndOffset, eDirNext, eSelectEndLine);
+ // The above returns an offset exclusive the final line break, so we
+ // need to add 1 to it to return an inclusive end offset. Make sure
+ // we don't overshoot if we've started from another embedded
+ // character that has a line break, or landed on another embedded
+ // character, or if the result is the very end.
+ return (thisLineEndOffset2 == characterCount ||
+ (IsCharAt(thisLineEndOffset, kEmbeddedObjectChar) &&
+ thisLineEndOffset2 == thisLineEndOffset + 1) ||
+ IsCharAt(thisLineEndOffset2, kEmbeddedObjectChar))
+ ? thisLineEndOffset2
+ : thisLineEndOffset2 + 1;
+ }
+
+ return thisLineEndOffset;
+ }
+ return nextLineBeginOffset;
+ }
+
+ // If the resulting offset is not greater than the offset we started from,
+ // layout could not find the offset for us. This can happen with certain
+ // inline-block elements.
+ if (nextLineBeginOffset <= aOffset) {
+ // Walk forward from the offset we started from up to tmpOffset,
+ // stopping after a line end character.
+ nextLineBeginOffset = aOffset;
+ while (nextLineBeginOffset < tmpOffset) {
+ if (IsLineEndCharAt(nextLineBeginOffset)) {
+ return nextLineBeginOffset + 1;
+ }
+ nextLineBeginOffset++;
+ }
+ }
+
+ return nextLineBeginOffset;
+ }
+
+ case eNextLineEnd: {
+ if (IsEmptyLastLineOffset(aOffset)) return aOffset;
+
+ // Move to next line end (as down arrow and end key were pressed).
+ uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine);
+ if (tmpOffset == CharacterCount()) return tmpOffset;
+
+ return FindOffset(tmpOffset, eDirNext, eSelectEndLine);
+ }
+ }
+
+ return 0;
+}
+
+int32_t HyperTextAccessible::FindParagraphStartOffset(uint32_t aOffset) {
+ // Because layout often gives us offsets that are incompatible with
+ // accessibility API requirements, for example when a paragraph contains
+ // presentational line breaks as found in Google Docs, use the accessibility
+ // tree to find the start offset instead.
+ LocalAccessible* child = GetChildAtOffset(aOffset);
+ if (!child) {
+ return -1; // Invalid offset
+ }
+
+ // Use the pivot class to search for the start offset.
+ Pivot p = Pivot(this);
+ ParagraphBoundaryRule boundaryRule = ParagraphBoundaryRule(
+ child, child->IsTextLeaf() ? aOffset - GetChildOffset(child) : 0,
+ eDirPrevious);
+ Accessible* match = p.Prev(child, boundaryRule, true);
+ if (!match || match->AsLocal() == this) {
+ // Found nothing, or pivot found the root of the search, startOffset is 0.
+ // This will include all relevant text nodes.
+ return 0;
+ }
+
+ if (match == child) {
+ // We started out on a boundary.
+ if (match->Role() == roles::WHITESPACE) {
+ // We are on a line break boundary, so force pivot to find the previous
+ // boundary. What we want is any text before this, if any.
+ match = p.Prev(match, boundaryRule);
+ if (!match || match->AsLocal() == this) {
+ // Same as before, we landed on the root, so offset is definitely 0.
+ return 0;
+ }
+ } else if (!match->AsLocal()->IsTextLeaf()) {
+ // The match is a block element, which is always a starting point, so
+ // just return its offset.
+ return TransformOffset(match->AsLocal(), 0, false);
+ }
+ }
+
+ if (match->AsLocal()->IsTextLeaf()) {
+ // ParagraphBoundaryRule only returns a text leaf if it contains a line
+ // break. We want to stop after that.
+ return TransformOffset(match->AsLocal(),
+ boundaryRule.GetLastMatchTextOffset() + 1, false);
+ }
+
+ // This is a previous boundary, we don't want to include it itself.
+ // So, walk forward one accessible, excluding the descendants of this
+ // boundary if it is a block element. The below call to Next should always be
+ // initialized with a boundary.
+ SkipParagraphBoundaryRule goForwardOneRule = SkipParagraphBoundaryRule(match);
+ match = p.Next(match, goForwardOneRule);
+ // We already know that the search skipped over at least one accessible,
+ // so match can't be null. Get its transformed offset.
+ MOZ_ASSERT(match);
+ return TransformOffset(match->AsLocal(), 0, false);
+}
+
+int32_t HyperTextAccessible::FindParagraphEndOffset(uint32_t aOffset) {
+ // Because layout often gives us offsets that are incompatible with
+ // accessibility API requirements, for example when a paragraph contains
+ // presentational line breaks as found in Google Docs, use the accessibility
+ // tree to find the end offset instead.
+ LocalAccessible* child = GetChildAtOffset(aOffset);
+ if (!child) {
+ return -1; // invalid offset
+ }
+
+ // Use the pivot class to search for the end offset.
+ Pivot p = Pivot(this);
+ ParagraphBoundaryRule boundaryRule = ParagraphBoundaryRule(
+ child, child->IsTextLeaf() ? aOffset - GetChildOffset(child) : 0,
+ eDirNext,
+ // In order to encompass all paragraphs inside embedded objects, not just
+ // the first, we want to skip the anchor's subtree.
+ /* aSkipAnchorSubtree */ true);
+ // Search forward for the end offset, including child. We don't want
+ // to go beyond this point if this offset indicates a paragraph boundary.
+ Accessible* match = p.Next(child, boundaryRule, true);
+ if (match) {
+ // Found something of relevance, adjust end offset.
+ LocalAccessible* matchAcc = match->AsLocal();
+ uint32_t matchOffset;
+ if (matchAcc->IsTextLeaf()) {
+ // ParagraphBoundaryRule only returns a text leaf if it contains a line
+ // break.
+ matchOffset = boundaryRule.GetLastMatchTextOffset() + 1;
+ } else if (matchAcc->Role() != roles::WHITESPACE && matchAcc != child) {
+ // We found a block boundary that wasn't our origin. We want to stop
+ // right on it, not after it, since we don't want to include the content
+ // of the block.
+ matchOffset = 0;
+ } else {
+ matchOffset = nsAccUtils::TextLength(matchAcc);
+ }
+ return TransformOffset(matchAcc, matchOffset, true);
+ }
+
+ // Didn't find anything, end offset is character count.
+ return CharacterCount();
+}
+
+void HyperTextAccessible::TextBeforeOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset,
+ nsAString& aText) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // This isn't strictly related to caching, but this new text implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return HyperTextAccessibleBase::TextBeforeOffset(
+ aOffset, aBoundaryType, aStartOffset, aEndOffset, aText);
+ }
+
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) {
+ // Not supported, bail out with empty text.
+ return;
+ }
+
+ index_t convertedOffset = ConvertMagicOffset(aOffset);
+ if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) {
+ NS_ERROR("Wrong in offset!");
+ return;
+ }
+
+ uint32_t adjustedOffset = convertedOffset;
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ switch (aBoundaryType) {
+ case nsIAccessibleText::BOUNDARY_CHAR:
+ if (convertedOffset != 0) {
+ CharAt(convertedOffset - 1, aText, aStartOffset, aEndOffset);
+ }
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_START: {
+ // If the offset is a word start (except text length offset) then move
+ // backward to find a start offset (end offset is the given offset).
+ // Otherwise move backward twice to find both start and end offsets.
+ if (adjustedOffset == CharacterCount()) {
+ *aEndOffset =
+ FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
+ } else {
+ *aStartOffset =
+ FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord);
+ *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord);
+ if (*aEndOffset != static_cast<int32_t>(adjustedOffset)) {
+ *aEndOffset = *aStartOffset;
+ *aStartOffset =
+ FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
+ }
+ }
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+
+ case nsIAccessibleText::BOUNDARY_WORD_END: {
+ // Move word backward twice to find start and end offsets.
+ *aEndOffset = FindWordBoundary(convertedOffset, eDirPrevious, eEndWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+
+ case nsIAccessibleText::BOUNDARY_LINE_START:
+ *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineBegin);
+ *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineBegin);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_END: {
+ *aEndOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd);
+ int32_t tmpOffset = *aEndOffset;
+ // Adjust offset if line is wrapped.
+ if (*aEndOffset != 0 && !IsLineEndCharAt(*aEndOffset)) tmpOffset--;
+
+ *aStartOffset = FindLineBoundary(tmpOffset, ePrevLineEnd);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+ }
+}
+
+void HyperTextAccessible::TextAtOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset, nsAString& aText) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // This isn't strictly related to caching, but this new text implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return HyperTextAccessibleBase::TextAtOffset(
+ aOffset, aBoundaryType, aStartOffset, aEndOffset, aText);
+ }
+
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
+ if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
+ NS_ERROR("Wrong given offset!");
+ return;
+ }
+
+ switch (aBoundaryType) {
+ case nsIAccessibleText::BOUNDARY_CHAR:
+ // Return no char if caret is at the end of wrapped line (case of no line
+ // end character). Returning a next line char is confusing for AT.
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET &&
+ IsCaretAtEndOfLine()) {
+ *aStartOffset = *aEndOffset = adjustedOffset;
+ } else {
+ CharAt(adjustedOffset, aText, aStartOffset, aEndOffset);
+ }
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_START:
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_END:
+ // Ignore the spec and follow what WebKitGtk does because Orca expects it,
+ // i.e. return a next word at word end offset of the current word
+ // (WebKitGtk behavior) instead the current word (AKT spec).
+ *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eEndWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_START:
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineBegin);
+ *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineBegin);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_END:
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ // In contrast to word end boundary we follow the spec here.
+ *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd);
+ *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineEnd);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_PARAGRAPH: {
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ if (IsEmptyLastLineOffset(adjustedOffset)) {
+ // We are on the last line of a paragraph where there is no text.
+ // For example, in a textarea where a new line has just been inserted.
+ // In this case, return offsets for an empty line without text content.
+ *aStartOffset = *aEndOffset = adjustedOffset;
+ break;
+ }
+
+ *aStartOffset = FindParagraphStartOffset(adjustedOffset);
+ *aEndOffset = FindParagraphEndOffset(adjustedOffset);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+ }
+}
+
+void HyperTextAccessible::TextAfterOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset,
+ nsAString& aText) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // This isn't strictly related to caching, but this new text implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return HyperTextAccessibleBase::TextAfterOffset(
+ aOffset, aBoundaryType, aStartOffset, aEndOffset, aText);
+ }
+
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) {
+ // Not supported, bail out with empty text.
+ return;
+ }
+
+ index_t convertedOffset = ConvertMagicOffset(aOffset);
+ if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) {
+ NS_ERROR("Wrong in offset!");
+ return;
+ }
+
+ uint32_t adjustedOffset = convertedOffset;
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ adjustedOffset = AdjustCaretOffset(adjustedOffset);
+ }
+
+ switch (aBoundaryType) {
+ case nsIAccessibleText::BOUNDARY_CHAR:
+ // If caret is at the end of wrapped line (case of no line end character)
+ // then char after the offset is a first char at next line.
+ if (adjustedOffset >= CharacterCount()) {
+ *aStartOffset = *aEndOffset = CharacterCount();
+ } else {
+ CharAt(adjustedOffset + 1, aText, aStartOffset, aEndOffset);
+ }
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_START:
+ // Move word forward twice to find start and end offsets.
+ *aStartOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord);
+ *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_WORD_END:
+ // If the offset is a word end (except 0 offset) then move forward to find
+ // end offset (start offset is the given offset). Otherwise move forward
+ // twice to find both start and end offsets.
+ if (convertedOffset == 0) {
+ *aStartOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord);
+ *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord);
+ } else {
+ *aEndOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord);
+ *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
+ if (*aStartOffset != static_cast<int32_t>(convertedOffset)) {
+ *aStartOffset = *aEndOffset;
+ *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord);
+ }
+ }
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_START:
+ *aStartOffset = FindLineBoundary(adjustedOffset, eNextLineBegin);
+ *aEndOffset = FindLineBoundary(*aStartOffset, eNextLineBegin);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+
+ case nsIAccessibleText::BOUNDARY_LINE_END:
+ *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineEnd);
+ *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineEnd);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+ break;
+ }
+}
+
+already_AddRefed<AccAttributes> HyperTextAccessible::TextAttributes(
+ bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
+ // This isn't strictly related to caching, but this new text implementation
+ // is being developed to make caching feasible. We put it behind this pref
+ // to make it easy to test while it's still under development.
+ return HyperTextAccessibleBase::TextAttributes(aIncludeDefAttrs, aOffset,
+ aStartOffset, aEndOffset);
+ }
+
+ // 1. Get each attribute and its ranges one after another.
+ // 2. As we get each new attribute, we pass the current start and end offsets
+ // as in/out parameters. In other words, as attributes are collected,
+ // the attribute range itself can only stay the same or get smaller.
+
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+ *aStartOffset = *aEndOffset = 0;
+ index_t offset = ConvertMagicOffset(aOffset);
+ if (!offset.IsValid() || offset > CharacterCount()) {
+ NS_ERROR("Wrong in offset!");
+ return attributes.forget();
+ }
+
+ LocalAccessible* accAtOffset = GetChildAtOffset(offset);
+ if (!accAtOffset) {
+ // Offset 0 is correct offset when accessible has empty text. Include
+ // default attributes if they were requested, otherwise return empty set.
+ if (offset == 0) {
+ if (aIncludeDefAttrs) {
+ TextAttrsMgr textAttrsMgr(this);
+ textAttrsMgr.GetAttributes(attributes);
+ }
+ }
+ return attributes.forget();
+ }
+
+ int32_t accAtOffsetIdx = accAtOffset->IndexInParent();
+ uint32_t startOffset = GetChildOffset(accAtOffsetIdx);
+ uint32_t endOffset = GetChildOffset(accAtOffsetIdx + 1);
+ int32_t offsetInAcc = offset - startOffset;
+
+ TextAttrsMgr textAttrsMgr(this, aIncludeDefAttrs, accAtOffset,
+ accAtOffsetIdx);
+ textAttrsMgr.GetAttributes(attributes, &startOffset, &endOffset);
+
+ // Compute spelling attributes on text accessible only.
+ nsIFrame* offsetFrame = accAtOffset->GetFrame();
+ if (offsetFrame && offsetFrame->IsTextFrame()) {
+ int32_t nodeOffset = 0;
+ RenderedToContentOffset(offsetFrame, offsetInAcc, &nodeOffset);
+
+ // Set 'misspelled' text attribute.
+ // FYI: Max length of text in a text node is less than INT32_MAX (see
+ // NS_MAX_TEXT_FRAGMENT_LENGTH) so that nodeOffset should always
+ // be 0 or greater.
+ MOZ_DIAGNOSTIC_ASSERT(accAtOffset->GetNode()->IsText());
+ MOZ_DIAGNOSTIC_ASSERT(nodeOffset >= 0);
+ GetSpellTextAttr(accAtOffset->GetNode(), static_cast<uint32_t>(nodeOffset),
+ &startOffset, &endOffset, attributes);
+ }
+
+ *aStartOffset = startOffset;
+ *aEndOffset = endOffset;
+ return attributes.forget();
+}
+
+already_AddRefed<AccAttributes> HyperTextAccessible::DefaultTextAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+
+ TextAttrsMgr textAttrsMgr(this);
+ textAttrsMgr.GetAttributes(attributes);
+ return attributes.forget();
+}
+
+void HyperTextAccessible::SetMathMLXMLRoles(AccAttributes* aAttributes) {
+ // Add MathML xmlroles based on the position inside the parent.
+ LocalAccessible* parent = LocalParent();
+ if (parent) {
+ switch (parent->Role()) {
+ case roles::MATHML_CELL:
+ case roles::MATHML_ENCLOSED:
+ case roles::MATHML_ERROR:
+ case roles::MATHML_MATH:
+ case roles::MATHML_ROW:
+ case roles::MATHML_SQUARE_ROOT:
+ case roles::MATHML_STYLE:
+ if (Role() == roles::MATHML_OPERATOR) {
+ // This is an operator inside an <mrow> (or an inferred <mrow>).
+ // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow
+ // XXX We should probably do something similar for MATHML_FENCED, but
+ // operators do not appear in the accessible tree. See bug 1175747.
+ nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame());
+ if (mathMLFrame) {
+ nsEmbellishData embellishData;
+ mathMLFrame->GetEmbellishData(embellishData);
+ if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData.flags)) {
+ if (!LocalPrevSibling()) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ nsGkAtoms::open_fence);
+ } else if (!LocalNextSibling()) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ nsGkAtoms::close_fence);
+ }
+ }
+ if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ nsGkAtoms::separator_);
+ }
+ }
+ }
+ break;
+ case roles::MATHML_FRACTION:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles, IndexInParent() == 0 ? nsGkAtoms::numerator
+ : nsGkAtoms::denominator);
+ break;
+ case roles::MATHML_ROOT:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index);
+ break;
+ case roles::MATHML_SUB:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript);
+ break;
+ case roles::MATHML_SUP:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript);
+ break;
+ case roles::MATHML_SUB_SUP: {
+ int32_t index = IndexInParent();
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ index == 0
+ ? nsGkAtoms::base
+ : (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript));
+ } break;
+ case roles::MATHML_UNDER:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript);
+ break;
+ case roles::MATHML_OVER:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript);
+ break;
+ case roles::MATHML_UNDER_OVER: {
+ int32_t index = IndexInParent();
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ index == 0
+ ? nsGkAtoms::base
+ : (index == 1 ? nsGkAtoms::underscript
+ : nsGkAtoms::overscript));
+ } break;
+ case roles::MATHML_MULTISCRIPTS: {
+ // Get the <multiscripts> base.
+ nsIContent* child;
+ bool baseFound = false;
+ for (child = parent->GetContent()->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsMathMLElement()) {
+ baseFound = true;
+ break;
+ }
+ }
+ if (baseFound) {
+ nsIContent* content = GetContent();
+ if (child == content) {
+ // We are the base.
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::base);
+ } else {
+ // Browse the list of scripts to find us and determine our type.
+ bool postscript = true;
+ bool subscript = true;
+ for (child = child->GetNextSibling(); child;
+ child = child->GetNextSibling()) {
+ if (!child->IsMathMLElement()) continue;
+ if (child->IsMathMLElement(nsGkAtoms::mprescripts_)) {
+ postscript = false;
+ subscript = true;
+ continue;
+ }
+ if (child == content) {
+ if (postscript) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ subscript ? nsGkAtoms::subscript
+ : nsGkAtoms::superscript);
+ } else {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ subscript
+ ? nsGkAtoms::presubscript
+ : nsGkAtoms::presuperscript);
+ }
+ break;
+ }
+ subscript = !subscript;
+ }
+ }
+ }
+ } break;
+ default:
+ break;
+ }
+ }
+}
+
+already_AddRefed<AccAttributes> HyperTextAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes();
+
+ // 'formatting' attribute is deprecated, 'display' attribute should be
+ // instead.
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->IsBlockFrame()) {
+ attributes->SetAttribute(nsGkAtoms::formatting, nsGkAtoms::block);
+ }
+
+ if (FocusMgr()->IsFocused(this)) {
+ int32_t lineNumber = CaretLineNumber();
+ if (lineNumber >= 1) {
+ attributes->SetAttribute(nsGkAtoms::lineNumber, lineNumber);
+ }
+ }
+
+ if (HasOwnContent()) {
+ GetAccService()->MarkupAttributes(this, attributes);
+ if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes);
+ }
+
+ return attributes.forget();
+}
+
+int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY,
+ uint32_t aCoordType) {
+ nsIFrame* hyperFrame = GetFrame();
+ if (!hyperFrame) return -1;
+
+ LayoutDeviceIntPoint coords =
+ nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this);
+
+ nsPresContext* presContext = mDoc->PresContext();
+ nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
+ coords, presContext->AppUnitsPerDevPixel());
+
+ nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits();
+ if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y)) {
+ return -1; // Not found
+ }
+
+ nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(),
+ coordsInAppUnits.y - frameScreenRect.Y());
+
+ // Go through the frames to check if each one has the point.
+ // When one does, add up the character offsets until we have a match
+
+ // We have an point in an accessible child of this, now we need to add up the
+ // offsets before it to what we already have
+ int32_t offset = 0;
+ uint32_t childCount = ChildCount();
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ LocalAccessible* childAcc = mChildren[childIdx];
+
+ nsIFrame* primaryFrame = childAcc->GetFrame();
+ NS_ENSURE_TRUE(primaryFrame, -1);
+
+ nsIFrame* frame = primaryFrame;
+ while (frame) {
+ nsIContent* content = frame->GetContent();
+ NS_ENSURE_TRUE(content, -1);
+ nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame);
+ nsSize frameSize = frame->GetSize();
+ if (pointInFrame.x < frameSize.width &&
+ pointInFrame.y < frameSize.height) {
+ // Finished
+ if (frame->IsTextFrame()) {
+ nsIFrame::ContentOffsets contentOffsets =
+ frame->GetContentOffsetsFromPointExternal(
+ pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE);
+ if (contentOffsets.IsNull() || contentOffsets.content != content) {
+ return -1; // Not found
+ }
+ uint32_t addToOffset;
+ nsresult rv = ContentToRenderedOffset(
+ primaryFrame, contentOffsets.offset, &addToOffset);
+ NS_ENSURE_SUCCESS(rv, -1);
+ offset += addToOffset;
+ }
+ return offset;
+ }
+ frame = frame->GetNextContinuation();
+ }
+
+ offset += nsAccUtils::TextLength(childAcc);
+ }
+
+ return -1; // Not found
+}
+
+LayoutDeviceIntRect HyperTextAccessible::TextBounds(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aCoordType) {
+ index_t startOffset = ConvertMagicOffset(aStartOffset);
+ index_t endOffset = ConvertMagicOffset(aEndOffset);
+ if (!startOffset.IsValid() || !endOffset.IsValid() ||
+ startOffset > endOffset || endOffset > CharacterCount()) {
+ NS_ERROR("Wrong in offset");
+ return LayoutDeviceIntRect();
+ }
+
+ if (CharacterCount() == 0) {
+ nsPresContext* presContext = mDoc->PresContext();
+ // Empty content, use our own bound to at least get x,y coordinates
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ return LayoutDeviceIntRect();
+ }
+ return LayoutDeviceIntRect::FromAppUnitsToNearest(
+ frame->GetScreenRectInAppUnits(), presContext->AppUnitsPerDevPixel());
+ }
+
+ int32_t childIdx = GetChildIndexAtOffset(startOffset);
+ if (childIdx == -1) return LayoutDeviceIntRect();
+
+ LayoutDeviceIntRect bounds;
+ int32_t prevOffset = GetChildOffset(childIdx);
+ int32_t offset1 = startOffset - prevOffset;
+
+ while (childIdx < static_cast<int32_t>(ChildCount())) {
+ nsIFrame* frame = LocalChildAt(childIdx++)->GetFrame();
+ if (!frame) {
+ MOZ_ASSERT_UNREACHABLE("No frame for a child!");
+ continue;
+ }
+
+ int32_t nextOffset = GetChildOffset(childIdx);
+ if (nextOffset >= static_cast<int32_t>(endOffset)) {
+ bounds.UnionRect(
+ bounds, GetBoundsInFrame(frame, offset1, endOffset - prevOffset));
+ break;
+ }
+
+ bounds.UnionRect(bounds,
+ GetBoundsInFrame(frame, offset1, nextOffset - prevOffset));
+
+ prevOffset = nextOffset;
+ offset1 = 0;
+ }
+
+ // This document may have a resolution set, we will need to multiply
+ // the document-relative coordinates by that value and re-apply the doc's
+ // screen coordinates.
+ nsPresContext* presContext = mDoc->PresContext();
+ nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
+ LayoutDeviceIntRect orgRectPixels =
+ LayoutDeviceIntRect::FromAppUnitsToNearest(
+ rootFrame->GetScreenRectInAppUnits(),
+ presContext->AppUnitsPerDevPixel());
+ bounds.MoveBy(-orgRectPixels.X(), -orgRectPixels.Y());
+ bounds.ScaleRoundOut(presContext->PresShell()->GetResolution());
+ bounds.MoveBy(orgRectPixels.X(), orgRectPixels.Y());
+
+ auto boundsX = bounds.X();
+ auto boundsY = bounds.Y();
+ nsAccUtils::ConvertScreenCoordsTo(&boundsX, &boundsY, aCoordType, this);
+ bounds.MoveTo(boundsX, boundsY);
+ return bounds;
+}
+
+already_AddRefed<EditorBase> HyperTextAccessible::GetEditor() const {
+ if (!mContent->HasFlag(NODE_IS_EDITABLE)) {
+ // If we're inside an editable container, then return that container's
+ // editor
+ LocalAccessible* ancestor = LocalParent();
+ while (ancestor) {
+ HyperTextAccessible* hyperText = ancestor->AsHyperText();
+ if (hyperText) {
+ // Recursion will stop at container doc because it has its own impl
+ // of GetEditor()
+ return hyperText->GetEditor();
+ }
+
+ ancestor = ancestor->LocalParent();
+ }
+
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent);
+ nsCOMPtr<nsIEditingSession> editingSession;
+ docShell->GetEditingSession(getter_AddRefs(editingSession));
+ if (!editingSession) return nullptr; // No editing session interface
+
+ dom::Document* docNode = mDoc->DocumentNode();
+ RefPtr<HTMLEditor> htmlEditor =
+ editingSession->GetHTMLEditorForWindow(docNode->GetWindow());
+ return htmlEditor.forget();
+}
+
+/**
+ * =================== Caret & Selection ======================
+ */
+
+nsresult HyperTextAccessible::SetSelectionRange(int32_t aStartPos,
+ int32_t aEndPos) {
+ // Before setting the selection range, we need to ensure that the editor
+ // is initialized. (See bug 804927.)
+ // Otherwise, it's possible that lazy editor initialization will override
+ // the selection we set here and leave the caret at the end of the text.
+ // By calling GetEditor here, we ensure that editor initialization is
+ // completed before we set the selection.
+ RefPtr<EditorBase> editorBase = GetEditor();
+
+ bool isFocusable = InteractiveState() & states::FOCUSABLE;
+
+ // If accessible is focusable then focus it before setting the selection to
+ // neglect control's selection changes on focus if any (for example, inputs
+ // that do select all on focus).
+ // some input controls
+ if (isFocusable) TakeFocus();
+
+ RefPtr<dom::Selection> domSel = DOMSelection();
+ NS_ENSURE_STATE(domSel);
+
+ // Set up the selection.
+ for (const uint32_t idx : Reversed(IntegerRange(1u, domSel->RangeCount()))) {
+ MOZ_ASSERT(domSel->RangeCount() == idx + 1);
+ RefPtr<nsRange> range{domSel->GetRangeAt(idx)};
+ if (!range) {
+ break; // The range count has been changed by somebody else.
+ }
+ domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
+ IgnoreErrors());
+ }
+ SetSelectionBoundsAt(0, aStartPos, aEndPos);
+
+ // Make sure it is visible
+ domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
+ ScrollAxis(), ScrollAxis(),
+ dom::Selection::SCROLL_FOR_CARET_MOVE |
+ dom::Selection::SCROLL_OVERFLOW_HIDDEN);
+
+ // When selection is done, move the focus to the selection if accessible is
+ // not focusable. That happens when selection is set within hypertext
+ // accessible.
+ if (isFocusable) return NS_OK;
+
+ nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
+ if (DOMFocusManager) {
+ NS_ENSURE_TRUE(mDoc, NS_ERROR_FAILURE);
+ dom::Document* docNode = mDoc->DocumentNode();
+ NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE);
+ nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow();
+ RefPtr<dom::Element> result;
+ DOMFocusManager->MoveFocus(
+ window, nullptr, nsIFocusManager::MOVEFOCUS_CARET,
+ nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result));
+ }
+
+ return NS_OK;
+}
+
+int32_t HyperTextAccessible::CaretOffset() const {
+ // Not focused focusable accessible except document accessible doesn't have
+ // a caret.
+ if (!IsDoc() && !FocusMgr()->IsFocused(this) &&
+ (InteractiveState() & states::FOCUSABLE)) {
+ return -1;
+ }
+
+ // Check cached value.
+ int32_t caretOffset = -1;
+ HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset);
+
+ // Use cached value if it corresponds to this accessible.
+ if (caretOffset != -1) {
+ if (text == this) return caretOffset;
+
+ nsINode* textNode = text->GetNode();
+ // Ignore offset if cached accessible isn't a text leaf.
+ if (nsCoreUtils::IsAncestorOf(GetNode(), textNode)) {
+ return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false);
+ }
+ }
+
+ // No caret if the focused node is not inside this DOM node and this DOM node
+ // is not inside of focused node.
+ FocusManager::FocusDisposition focusDisp =
+ FocusMgr()->IsInOrContainsFocus(this);
+ if (focusDisp == FocusManager::eNone) return -1;
+
+ // Turn the focus node and offset of the selection into caret hypretext
+ // offset.
+ dom::Selection* domSel = DOMSelection();
+ NS_ENSURE_TRUE(domSel, -1);
+
+ nsINode* focusNode = domSel->GetFocusNode();
+ uint32_t focusOffset = domSel->FocusOffset();
+
+ // No caret if this DOM node is inside of focused node but the selection's
+ // focus point is not inside of this DOM node.
+ if (focusDisp == FocusManager::eContainedByFocus) {
+ nsINode* resultNode =
+ nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset);
+
+ nsINode* thisNode = GetNode();
+ if (resultNode != thisNode &&
+ !nsCoreUtils::IsAncestorOf(thisNode, resultNode)) {
+ return -1;
+ }
+ }
+
+ return DOMPointToOffset(focusNode, focusOffset);
+}
+
+int32_t HyperTextAccessible::CaretLineNumber() {
+ // Provide the line number for the caret, relative to the
+ // currently focused node. Use a 1-based index
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ if (!frameSelection) return -1;
+
+ dom::Selection* domSel = frameSelection->GetSelection(SelectionType::eNormal);
+ if (!domSel) return -1;
+
+ nsINode* caretNode = domSel->GetFocusNode();
+ if (!caretNode || !caretNode->IsContent()) return -1;
+
+ nsIContent* caretContent = caretNode->AsContent();
+ if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1;
+
+ int32_t returnOffsetUnused;
+ uint32_t caretOffset = domSel->FocusOffset();
+ CaretAssociationHint hint = frameSelection->GetHint();
+ nsIFrame* caretFrame = frameSelection->GetFrameForNodeOffset(
+ caretContent, caretOffset, hint, &returnOffsetUnused);
+ NS_ENSURE_TRUE(caretFrame, -1);
+
+ AutoAssertNoDomMutations guard; // The nsILineIterators below will break if
+ // the DOM is modified while they're in use!
+ int32_t lineNumber = 1;
+ nsILineIterator* lineIterForCaret = nullptr;
+ nsIContent* hyperTextContent = IsContent() ? mContent.get() : nullptr;
+ while (caretFrame) {
+ if (hyperTextContent == caretFrame->GetContent()) {
+ return lineNumber; // Must be in a single line hyper text, there is no
+ // line iterator
+ }
+ nsContainerFrame* parentFrame = caretFrame->GetParent();
+ if (!parentFrame) break;
+
+ // Add lines for the sibling frames before the caret
+ nsIFrame* sibling = parentFrame->PrincipalChildList().FirstChild();
+ while (sibling && sibling != caretFrame) {
+ nsILineIterator* lineIterForSibling = sibling->GetLineIterator();
+ if (lineIterForSibling) {
+ // For the frames before that grab all the lines
+ int32_t addLines = lineIterForSibling->GetNumLines();
+ lineNumber += addLines;
+ }
+ sibling = sibling->GetNextSibling();
+ }
+
+ // Get the line number relative to the container with lines
+ if (!lineIterForCaret) { // Add the caret line just once
+ lineIterForCaret = parentFrame->GetLineIterator();
+ if (lineIterForCaret) {
+ // Ancestor of caret
+ int32_t addLines = lineIterForCaret->FindLineContaining(caretFrame);
+ lineNumber += addLines;
+ }
+ }
+
+ caretFrame = parentFrame;
+ }
+
+ MOZ_ASSERT_UNREACHABLE(
+ "DOM ancestry had this hypertext but frame ancestry didn't");
+ return lineNumber;
+}
+
+LayoutDeviceIntRect HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) {
+ *aWidget = nullptr;
+
+ RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret();
+ NS_ENSURE_TRUE(caret, LayoutDeviceIntRect());
+
+ bool isVisible = caret->IsVisible();
+ if (!isVisible) return LayoutDeviceIntRect();
+
+ nsRect rect;
+ nsIFrame* frame = caret->GetGeometry(&rect);
+ if (!frame || rect.IsEmpty()) return LayoutDeviceIntRect();
+
+ nsPoint offset;
+ // Offset from widget origin to the frame origin, which includes chrome
+ // on the widget.
+ *aWidget = frame->GetNearestWidget(offset);
+ NS_ENSURE_TRUE(*aWidget, LayoutDeviceIntRect());
+ rect.MoveBy(offset);
+
+ LayoutDeviceIntRect caretRect = LayoutDeviceIntRect::FromUnknownRect(
+ rect.ToOutsidePixels(frame->PresContext()->AppUnitsPerDevPixel()));
+ // clang-format off
+ // ((content screen origin) - (content offset in the widget)) = widget origin on the screen
+ // clang-format on
+ caretRect.MoveBy((*aWidget)->WidgetToScreenOffset() -
+ (*aWidget)->GetClientOffset());
+
+ // Correct for character size, so that caret always matches the size of
+ // the character. This is important for font size transitions, and is
+ // necessary because the Gecko caret uses the previous character's size as
+ // the user moves forward in the text by character.
+ int32_t caretOffset = CaretOffset();
+ if (NS_WARN_IF(caretOffset == -1)) {
+ // The caret offset will be -1 if this Accessible isn't focused. Note that
+ // the DOM node contaning the caret might be focused, but the Accessible
+ // might not be; e.g. due to an autocomplete popup suggestion having a11y
+ // focus.
+ return LayoutDeviceIntRect();
+ }
+ LayoutDeviceIntRect charRect = CharBounds(
+ caretOffset, nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE);
+ if (!charRect.IsEmpty()) {
+ caretRect.SetTopEdge(charRect.Y());
+ }
+ return caretRect;
+}
+
+void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType,
+ nsTArray<nsRange*>* aRanges) {
+ // Ignore selection if it is not visible.
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ if (!frameSelection || frameSelection->GetDisplaySelection() <=
+ nsISelectionController::SELECTION_HIDDEN) {
+ return;
+ }
+
+ dom::Selection* domSel = frameSelection->GetSelection(aSelectionType);
+ if (!domSel) return;
+
+ nsINode* startNode = GetNode();
+
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ startNode = editorBase->GetRoot();
+ }
+
+ if (!startNode) return;
+
+ uint32_t childCount = startNode->GetChildCount();
+ nsresult rv = domSel->GetRangesForIntervalArray(startNode, 0, startNode,
+ childCount, true, aRanges);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Remove collapsed ranges
+ aRanges->RemoveElementsBy(
+ [](const auto& range) { return range->Collapsed(); });
+}
+
+int32_t HyperTextAccessible::SelectionCount() {
+ nsTArray<nsRange*> ranges;
+ GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
+ return ranges.Length();
+}
+
+bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ *aStartOffset = *aEndOffset = 0;
+
+ nsTArray<nsRange*> ranges;
+ GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
+
+ uint32_t rangeCount = ranges.Length();
+ if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(rangeCount)) {
+ return false;
+ }
+
+ nsRange* range = ranges[aSelectionNum];
+
+ // Get start and end points.
+ nsINode* startNode = range->GetStartContainer();
+ nsINode* endNode = range->GetEndContainer();
+ uint32_t startOffset = range->StartOffset();
+ uint32_t endOffset = range->EndOffset();
+
+ // Make sure start is before end, by swapping DOM points. This occurs when
+ // the user selects backwards in the text.
+ const Maybe<int32_t> order =
+ nsContentUtils::ComparePoints(endNode, endOffset, startNode, startOffset);
+
+ if (!order) {
+ MOZ_ASSERT_UNREACHABLE();
+ return false;
+ }
+
+ if (*order < 0) {
+ std::swap(startNode, endNode);
+ std::swap(startOffset, endOffset);
+ }
+
+ if (!startNode->IsInclusiveDescendantOf(mContent)) {
+ *aStartOffset = 0;
+ } else {
+ *aStartOffset =
+ DOMPointToOffset(startNode, AssertedCast<int32_t>(startOffset));
+ }
+
+ if (!endNode->IsInclusiveDescendantOf(mContent)) {
+ *aEndOffset = CharacterCount();
+ } else {
+ *aEndOffset =
+ DOMPointToOffset(endNode, AssertedCast<int32_t>(endOffset), true);
+ }
+ return true;
+}
+
+bool HyperTextAccessible::SetSelectionBoundsAt(int32_t aSelectionNum,
+ int32_t aStartOffset,
+ int32_t aEndOffset) {
+ index_t startOffset = ConvertMagicOffset(aStartOffset);
+ index_t endOffset = ConvertMagicOffset(aEndOffset);
+ if (!startOffset.IsValid() || !endOffset.IsValid() ||
+ std::max(startOffset, endOffset) > CharacterCount()) {
+ NS_ERROR("Wrong in offset");
+ return false;
+ }
+
+ TextRange range(this, this, startOffset, this, endOffset);
+ return range.SetSelectionAt(aSelectionNum);
+}
+
+bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
+ RefPtr<dom::Selection> domSel = DOMSelection();
+ if (!domSel) return false;
+
+ if (aSelectionNum < 0 ||
+ aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) {
+ return false;
+ }
+
+ const RefPtr<nsRange> range{
+ domSel->GetRangeAt(static_cast<uint32_t>(aSelectionNum))};
+ domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
+ IgnoreErrors());
+ return true;
+}
+
+void HyperTextAccessible::ScrollSubstringTo(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aScrollType) {
+ TextRange range(this, this, aStartOffset, this, aEndOffset);
+ range.ScrollIntoView(aScrollType);
+}
+
+void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aCoordinateType,
+ int32_t aX, int32_t aY) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return;
+
+ LayoutDeviceIntPoint coords =
+ nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
+
+ RefPtr<nsRange> domRange = nsRange::Create(mContent);
+ TextRange range(this, this, aStartOffset, this, aEndOffset);
+ if (!range.AssignDOMRange(domRange)) {
+ return;
+ }
+
+ nsPresContext* presContext = frame->PresContext();
+ nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
+ coords, presContext->AppUnitsPerDevPixel());
+
+ bool initialScrolled = false;
+ nsIFrame* parentFrame = frame;
+ while ((parentFrame = parentFrame->GetParent())) {
+ nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
+ if (scrollableFrame) {
+ if (!initialScrolled) {
+ // Scroll substring to the given point. Turn the point into percents
+ // relative scrollable area to use nsCoreUtils::ScrollSubstringTo.
+ nsRect frameRect = parentFrame->GetScreenRectInAppUnits();
+ nscoord offsetPointX = coordsInAppUnits.x - frameRect.X();
+ nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y();
+
+ nsSize size(parentFrame->GetSize());
+
+ // avoid divide by zero
+ size.width = size.width ? size.width : 1;
+ size.height = size.height ? size.height : 1;
+
+ int16_t hPercent = offsetPointX * 100 / size.width;
+ int16_t vPercent = offsetPointY * 100 / size.height;
+
+ nsresult rv = nsCoreUtils::ScrollSubstringTo(
+ frame, domRange,
+ ScrollAxis(WhereToScroll(vPercent), WhenToScroll::Always),
+ ScrollAxis(WhereToScroll(hPercent), WhenToScroll::Always));
+ if (NS_FAILED(rv)) return;
+
+ initialScrolled = true;
+ } else {
+ // Substring was scrolled to the given point already inside its closest
+ // scrollable area. If there are nested scrollable areas then make
+ // sure we scroll lower areas to the given point inside currently
+ // traversed scrollable area.
+ nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
+ }
+ }
+ frame = parentFrame;
+ }
+}
+
+void HyperTextAccessible::EnclosingRange(a11y::TextRange& aRange) const {
+ if (IsTextField()) {
+ aRange.Set(mDoc, const_cast<HyperTextAccessible*>(this), 0,
+ const_cast<HyperTextAccessible*>(this), CharacterCount());
+ } else {
+ aRange.Set(mDoc, mDoc, 0, mDoc, mDoc->CharacterCount());
+ }
+}
+
+void HyperTextAccessible::SelectionRanges(
+ nsTArray<a11y::TextRange>* aRanges) const {
+ dom::Selection* sel = DOMSelection();
+ if (!sel) {
+ return;
+ }
+
+ TextRange::TextRangesFromSelection(sel, aRanges);
+}
+
+void HyperTextAccessible::VisibleRanges(
+ nsTArray<a11y::TextRange>* aRanges) const {}
+
+void HyperTextAccessible::RangeByChild(LocalAccessible* aChild,
+ a11y::TextRange& aRange) const {
+ HyperTextAccessible* ht = aChild->AsHyperText();
+ if (ht) {
+ aRange.Set(mDoc, ht, 0, ht, ht->CharacterCount());
+ return;
+ }
+
+ LocalAccessible* child = aChild;
+ LocalAccessible* parent = nullptr;
+ while ((parent = child->LocalParent()) && !(ht = parent->AsHyperText())) {
+ child = parent;
+ }
+
+ // If no text then return collapsed text range, otherwise return a range
+ // containing the text enclosed by the given child.
+ if (ht) {
+ int32_t childIdx = child->IndexInParent();
+ int32_t startOffset = ht->GetChildOffset(childIdx);
+ int32_t endOffset =
+ child->IsTextLeaf() ? ht->GetChildOffset(childIdx + 1) : startOffset;
+ aRange.Set(mDoc, ht, startOffset, ht, endOffset);
+ }
+}
+
+void HyperTextAccessible::RangeAtPoint(int32_t aX, int32_t aY,
+ a11y::TextRange& aRange) const {
+ LocalAccessible* child =
+ mDoc->LocalChildAtPoint(aX, aY, EWhichChildAtPoint::DeepestChild);
+ if (!child) return;
+
+ LocalAccessible* parent = nullptr;
+ while ((parent = child->LocalParent()) && !parent->IsHyperText()) {
+ child = parent;
+ }
+
+ // Return collapsed text range for the point.
+ if (parent) {
+ HyperTextAccessible* ht = parent->AsHyperText();
+ int32_t offset = ht->GetChildOffset(child);
+ aRange.Set(mDoc, ht, offset, ht, offset);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public
+
+// LocalAccessible protected
+ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const {
+ // Check @alt attribute for invalid img elements.
+ if (mContent->IsHTMLElement(nsGkAtoms::img)) {
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::alt, aName);
+ if (!aName.IsEmpty()) return eNameOK;
+ }
+
+ ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ // Get name from title attribute for HTML abbr and acronym elements making it
+ // a valid name from markup. Otherwise their name isn't picked up by recursive
+ // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
+ if (IsAbbreviation() && mContent->AsElement()->GetAttr(
+ kNameSpaceID_None, nsGkAtoms::title, aName)) {
+ aName.CompressWhitespace();
+ }
+
+ return eNameOK;
+}
+
+void HyperTextAccessible::Shutdown() {
+ mOffsets.Clear();
+ AccessibleWrap::Shutdown();
+}
+
+bool HyperTextAccessible::RemoveChild(LocalAccessible* aAccessible) {
+ const int32_t childIndex = aAccessible->IndexInParent();
+ if (childIndex < static_cast<int32_t>(mOffsets.Length())) {
+ mOffsets.RemoveLastElements(mOffsets.Length() - childIndex);
+ }
+
+ return AccessibleWrap::RemoveChild(aAccessible);
+}
+
+bool HyperTextAccessible::InsertChildAt(uint32_t aIndex,
+ LocalAccessible* aChild) {
+ if (aIndex < mOffsets.Length()) {
+ mOffsets.RemoveLastElements(mOffsets.Length() - aIndex);
+ }
+
+ return AccessibleWrap::InsertChildAt(aIndex, aChild);
+}
+
+Relation HyperTextAccessible::RelationByType(RelationType aType) const {
+ Relation rel = LocalAccessible::RelationByType(aType);
+
+ switch (aType) {
+ case RelationType::NODE_CHILD_OF:
+ if (HasOwnContent() && mContent->IsMathMLElement()) {
+ LocalAccessible* parent = LocalParent();
+ if (parent) {
+ nsIContent* parentContent = parent->GetContent();
+ if (parentContent &&
+ parentContent->IsMathMLElement(nsGkAtoms::mroot_)) {
+ // Add a relation pointing to the parent <mroot>.
+ rel.AppendTarget(parent);
+ }
+ }
+ }
+ break;
+ case RelationType::NODE_PARENT_OF:
+ if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot_)) {
+ LocalAccessible* base = LocalChildAt(0);
+ LocalAccessible* index = LocalChildAt(1);
+ if (base && index) {
+ // Append the <mroot> children in the order index, base.
+ rel.AppendTarget(index);
+ rel.AppendTarget(base);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return rel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperTextAccessible public static
+
+nsresult HyperTextAccessible::ContentToRenderedOffset(
+ nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const {
+ if (!aFrame) {
+ // Current frame not rendered -- this can happen if text is set on
+ // something with display: none
+ *aRenderedOffset = 0;
+ return NS_OK;
+ }
+
+ if (IsTextField()) {
+ *aRenderedOffset = aContentOffset;
+ return NS_OK;
+ }
+
+ NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
+ NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
+ "Call on primary frame only");
+
+ nsIFrame::RenderedText text =
+ aFrame->GetRenderedText(aContentOffset, aContentOffset + 1,
+ nsIFrame::TextOffsetType::OffsetsInContentText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ *aRenderedOffset = text.mOffsetWithinNodeRenderedText;
+
+ return NS_OK;
+}
+
+nsresult HyperTextAccessible::RenderedToContentOffset(
+ nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const {
+ if (IsTextField()) {
+ *aContentOffset = aRenderedOffset;
+ return NS_OK;
+ }
+
+ *aContentOffset = 0;
+ NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE);
+
+ NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
+ NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
+ "Call on primary frame only");
+
+ nsIFrame::RenderedText text =
+ aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
+ nsIFrame::TextOffsetType::OffsetsInRenderedText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ *aContentOffset = text.mOffsetWithinNodeText;
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperTextAccessible protected
+
+nsresult HyperTextAccessible::GetDOMPointByFrameOffset(
+ nsIFrame* aFrame, int32_t aOffset, LocalAccessible* aAccessible,
+ DOMPoint* aPoint) {
+ NS_ENSURE_ARG(aAccessible);
+
+ if (!aFrame) {
+ // If the given frame is null then set offset after the DOM node of the
+ // given accessible.
+ NS_ASSERTION(!aAccessible->IsDoc(),
+ "Shouldn't be called on document accessible!");
+
+ nsIContent* content = aAccessible->GetContent();
+ NS_ASSERTION(content, "Shouldn't operate on defunct accessible!");
+
+ nsIContent* parent = content->GetParent();
+
+ aPoint->idx = parent->ComputeIndexOf_Deprecated(content) + 1;
+ aPoint->node = parent;
+
+ } else if (aFrame->IsTextFrame()) {
+ nsIContent* content = aFrame->GetContent();
+ NS_ENSURE_STATE(content);
+
+ nsIFrame* primaryFrame = content->GetPrimaryFrame();
+ nsresult rv =
+ RenderedToContentOffset(primaryFrame, aOffset, &(aPoint->idx));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aPoint->node = content;
+
+ } else {
+ nsIContent* content = aFrame->GetContent();
+ NS_ENSURE_STATE(content);
+
+ nsIContent* parent = content->GetParent();
+ NS_ENSURE_STATE(parent);
+
+ aPoint->idx = parent->ComputeIndexOf_Deprecated(content);
+ aPoint->node = parent;
+ }
+
+ return NS_OK;
+}
+
+// HyperTextAccessible
+void HyperTextAccessible::GetSpellTextAttr(nsINode* aNode, uint32_t aNodeOffset,
+ uint32_t* aStartOffset,
+ uint32_t* aEndOffset,
+ AccAttributes* aAttributes) {
+ RefPtr<nsFrameSelection> fs = FrameSelection();
+ if (!fs) return;
+
+ dom::Selection* domSel = fs->GetSelection(SelectionType::eSpellCheck);
+ if (!domSel) return;
+
+ const uint32_t rangeCount = domSel->RangeCount();
+ if (!rangeCount) {
+ return;
+ }
+
+ uint32_t startOffset = 0, endOffset = 0;
+ for (const uint32_t idx : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(domSel->RangeCount() == rangeCount);
+ const nsRange* range = domSel->GetRangeAt(idx);
+ MOZ_ASSERT(range);
+ if (range->Collapsed()) continue;
+
+ // See if the point comes after the range in which case we must continue in
+ // case there is another range after this one.
+ nsINode* endNode = range->GetEndContainer();
+ uint32_t endNodeOffset = range->EndOffset();
+ Maybe<int32_t> order = nsContentUtils::ComparePoints(
+ aNode, aNodeOffset, endNode, endNodeOffset);
+ if (NS_WARN_IF(!order)) {
+ continue;
+ }
+
+ if (*order >= 0) {
+ continue;
+ }
+
+ // At this point our point is either in this range or before it but after
+ // the previous range. So we check to see if the range starts before the
+ // point in which case the point is in the missspelled range, otherwise it
+ // must be before the range and after the previous one if any.
+ nsINode* startNode = range->GetStartContainer();
+ int32_t startNodeOffset = range->StartOffset();
+ order = nsContentUtils::ComparePoints(startNode, startNodeOffset, aNode,
+ aNodeOffset);
+ if (!order) {
+ // As (`aNode`, `aNodeOffset`) is comparable to the end of the range, it
+ // should also be comparable to the range's start. Returning here
+ // prevents crashes in release builds.
+ MOZ_ASSERT_UNREACHABLE();
+ return;
+ }
+
+ if (*order <= 0) {
+ startOffset = DOMPointToOffset(startNode, startNodeOffset);
+
+ endOffset = DOMPointToOffset(endNode, endNodeOffset);
+
+ if (startOffset > *aStartOffset) *aStartOffset = startOffset;
+
+ if (endOffset < *aEndOffset) *aEndOffset = endOffset;
+
+ aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::spelling);
+
+ return;
+ }
+
+ // This range came after the point.
+ endOffset = DOMPointToOffset(startNode, startNodeOffset);
+
+ if (idx > 0) {
+ const nsRange* prevRange = domSel->GetRangeAt(idx - 1);
+ startOffset = DOMPointToOffset(prevRange->GetEndContainer(),
+ prevRange->EndOffset());
+ }
+
+ // The previous range might not be within this accessible. In that case,
+ // DOMPointToOffset returns length as a fallback. We don't want to use
+ // that offset if so, hence the startOffset < *aEndOffset check.
+ if (startOffset > *aStartOffset && startOffset < *aEndOffset) {
+ *aStartOffset = startOffset;
+ }
+
+ if (endOffset < *aEndOffset) *aEndOffset = endOffset;
+
+ return;
+ }
+
+ // We never found a range that ended after the point, therefore we know that
+ // the point is not in a range, that we do not need to compute an end offset,
+ // and that we should use the end offset of the last range to compute the
+ // start offset of the text attribute range.
+ const nsRange* prevRange = domSel->GetRangeAt(rangeCount - 1);
+ startOffset =
+ DOMPointToOffset(prevRange->GetEndContainer(), prevRange->EndOffset());
+
+ // The previous range might not be within this accessible. In that case,
+ // DOMPointToOffset returns length as a fallback. We don't want to use
+ // that offset if so, hence the startOffset < *aEndOffset check.
+ if (startOffset > *aStartOffset && startOffset < *aEndOffset) {
+ *aStartOffset = startOffset;
+ }
+}