/* clang-format off */ /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* clang-format on */ /* 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 "HyperTextAccessibleWrap.h" #include "Accessible-inl.h" #include "HTMLListAccessible.h" #include "nsAccUtils.h" #include "nsFrameSelection.h" #include "TextRange.h" #include "TreeWalker.h" using namespace mozilla; using namespace mozilla::a11y; // HyperTextIterator class HyperTextIterator { public: HyperTextIterator(HyperTextAccessible* aStartContainer, int32_t aStartOffset, HyperTextAccessible* aEndContainer, int32_t aEndOffset) : mCurrentContainer(aStartContainer), mCurrentStartOffset(aStartOffset), mCurrentEndOffset(aStartOffset), mEndContainer(aEndContainer), mEndOffset(aEndOffset) {} bool Next(); int32_t SegmentLength(); // If offset is set to a child hyperlink, adjust it so it set on the first // offset in the deepest link. Or, if the offset to the last character, set it // to the outermost end offset in an ancestor. Returns true if iterator was // mutated. bool NormalizeForward(); // If offset is set right after child hyperlink, adjust it so it set on the // last offset in the deepest link. Or, if the offset is on the first // character of a link, set it to the outermost start offset in an ancestor. // Returns true if iterator was mutated. bool NormalizeBackward(); HyperTextAccessible* mCurrentContainer; int32_t mCurrentStartOffset; int32_t mCurrentEndOffset; private: int32_t NextLinkOffset(); HyperTextAccessible* mEndContainer; int32_t mEndOffset; }; bool HyperTextIterator::NormalizeForward() { if (mCurrentStartOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT || mCurrentStartOffset >= static_cast(mCurrentContainer->CharacterCount())) { // If this is the end of the current container, mutate to its parent's // end offset. if (!mCurrentContainer->IsLink()) { // If we are not a link, it is a root hypertext accessible. return false; } if (!mCurrentContainer->Parent() || !mCurrentContainer->Parent()->IsHyperText()) { // If we are a link, but our parent is not a hypertext accessible // treat the current container as the root hypertext accessible. // This can be the case with some XUL containers that are not // hypertext accessibles. return false; } uint32_t endOffset = mCurrentContainer->EndOffset(); if (endOffset != 0) { mCurrentContainer = mCurrentContainer->Parent()->AsHyperText(); mCurrentStartOffset = endOffset; if (mCurrentContainer == mEndContainer && mCurrentStartOffset >= mEndOffset) { // Reached end boundary. return false; } // Call NormalizeForward recursively to get top-most link if at the end of // one, or innermost link if at the beginning. NormalizeForward(); return true; } } else { Accessible* link = mCurrentContainer->LinkAt( mCurrentContainer->LinkIndexAtOffset(mCurrentStartOffset)); // If there is a link at this offset, mutate into it. if (link && link->IsHyperText()) { if (mCurrentStartOffset > 0 && mCurrentContainer->LinkIndexAtOffset(mCurrentStartOffset) == mCurrentContainer->LinkIndexAtOffset(mCurrentStartOffset - 1)) { MOZ_ASSERT_UNREACHABLE("Same link for previous offset"); return false; } mCurrentContainer = link->AsHyperText(); if (link->IsHTMLListItem()) { Accessible* bullet = link->AsHTMLListItem()->Bullet(); mCurrentStartOffset = bullet ? nsAccUtils::TextLength(bullet) : 0; } else { mCurrentStartOffset = 0; } if (mCurrentContainer == mEndContainer && mCurrentStartOffset >= mEndOffset) { // Reached end boundary. return false; } // Call NormalizeForward recursively to get top-most embedding ancestor // if at the end of one, or innermost link if at the beginning. NormalizeForward(); return true; } } return false; } bool HyperTextIterator::NormalizeBackward() { if (mCurrentStartOffset == 0) { // If this is the start of the current container, mutate to its parent's // start offset. if (!mCurrentContainer->IsLink()) { // If we are not a link, it is a root hypertext accessible. return false; } if (!mCurrentContainer->Parent() || !mCurrentContainer->Parent()->IsHyperText()) { // If we are a link, but our parent is not a hypertext accessible // treat the current container as the root hypertext accessible. // This can be the case with some XUL containers that are not // hypertext accessibles. return false; } uint32_t startOffset = mCurrentContainer->StartOffset(); mCurrentContainer = mCurrentContainer->Parent()->AsHyperText(); mCurrentStartOffset = startOffset; // Call NormalizeBackward recursively to get top-most link if at the // beginning of one, or innermost link if at the end. NormalizeBackward(); return true; } else { Accessible* link = mCurrentContainer->GetChildAtOffset(mCurrentStartOffset - 1); // If there is a link before this offset, mutate into it, // and set the offset to its last character. if (link && link->IsHyperText()) { mCurrentContainer = link->AsHyperText(); mCurrentStartOffset = mCurrentContainer->CharacterCount(); // Call NormalizeBackward recursively to get top-most top-most embedding // ancestor if at the beginning of one, or innermost link if at the end. NormalizeBackward(); return true; } if (mCurrentContainer->IsHTMLListItem() && mCurrentContainer->AsHTMLListItem()->Bullet() == link) { mCurrentStartOffset = 0; NormalizeBackward(); return true; } } return false; } int32_t HyperTextIterator::SegmentLength() { int32_t endOffset = mCurrentEndOffset < 0 ? mCurrentContainer->CharacterCount() : mCurrentEndOffset; return endOffset - mCurrentStartOffset; } int32_t HyperTextIterator::NextLinkOffset() { int32_t linkCount = mCurrentContainer->LinkCount(); for (int32_t i = 0; i < linkCount; i++) { Accessible* link = mCurrentContainer->LinkAt(i); MOZ_ASSERT(link); int32_t linkStartOffset = link->StartOffset(); if (mCurrentStartOffset < linkStartOffset) { return linkStartOffset; } } return -1; } bool HyperTextIterator::Next() { if (!mCurrentContainer->Document()->HasLoadState( DocAccessible::eTreeConstructed)) { // If the accessible tree is still being constructed the text tree // is not in a traversable state yet. return false; } if (mCurrentContainer == mEndContainer && (mCurrentEndOffset == -1 || mEndOffset <= mCurrentEndOffset)) { return false; } else { mCurrentStartOffset = mCurrentEndOffset; NormalizeForward(); } int32_t nextLinkOffset = NextLinkOffset(); if (mCurrentContainer == mEndContainer && (nextLinkOffset == -1 || nextLinkOffset > mEndOffset)) { mCurrentEndOffset = mEndOffset < 0 ? mEndContainer->CharacterCount() : mEndOffset; } else { mCurrentEndOffset = nextLinkOffset < 0 ? mCurrentContainer->CharacterCount() : nextLinkOffset; } return mCurrentStartOffset != mCurrentEndOffset; } void HyperTextAccessibleWrap::TextForRange(nsAString& aText, int32_t aStartOffset, HyperTextAccessible* aEndContainer, int32_t aEndOffset) { if (IsHTMLListItem()) { Accessible* maybeBullet = GetChildAtOffset(aStartOffset - 1); if (maybeBullet) { Accessible* bullet = AsHTMLListItem()->Bullet(); if (maybeBullet == bullet) { TextSubstring(0, nsAccUtils::TextLength(bullet), aText); } } } HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset); while (iter.Next()) { nsAutoString text; iter.mCurrentContainer->TextSubstring(iter.mCurrentStartOffset, iter.mCurrentEndOffset, text); aText.Append(text); } } nsIntRect HyperTextAccessibleWrap::BoundsForRange( int32_t aStartOffset, HyperTextAccessible* aEndContainer, int32_t aEndOffset) { nsIntRect rect; HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset); while (iter.Next()) { nsIntRect stringRect = iter.mCurrentContainer->TextBounds( iter.mCurrentStartOffset, iter.mCurrentEndOffset); rect.UnionRect(rect, stringRect); } return rect; } int32_t HyperTextAccessibleWrap::LengthForRange( int32_t aStartOffset, HyperTextAccessible* aEndContainer, int32_t aEndOffset) { int32_t length = 0; HyperTextIterator iter(this, aStartOffset, aEndContainer, aEndOffset); while (iter.Next()) { length += iter.SegmentLength(); } return length; } void HyperTextAccessibleWrap::OffsetAtIndex(int32_t aIndex, HyperTextAccessible** aContainer, int32_t* aOffset) { int32_t index = aIndex; HyperTextIterator iter(this, 0, this, CharacterCount()); while (iter.Next()) { int32_t segmentLength = iter.SegmentLength(); if (index <= segmentLength) { *aContainer = iter.mCurrentContainer; *aOffset = iter.mCurrentStartOffset + index; break; } index -= segmentLength; } } void HyperTextAccessibleWrap::RangeAt(int32_t aOffset, EWhichRange aRangeType, HyperTextAccessible** aStartContainer, int32_t* aStartOffset, HyperTextAccessible** aEndContainer, int32_t* aEndOffset) { switch (aRangeType) { case EWhichRange::eLeftWord: LeftWordAt(aOffset, aStartContainer, aStartOffset, aEndContainer, aEndOffset); break; case EWhichRange::eRightWord: RightWordAt(aOffset, aStartContainer, aStartOffset, aEndContainer, aEndOffset); break; case EWhichRange::eLine: case EWhichRange::eLeftLine: LineAt(aOffset, false, aStartContainer, aStartOffset, aEndContainer, aEndOffset); break; case EWhichRange::eRightLine: LineAt(aOffset, true, aStartContainer, aStartOffset, aEndContainer, aEndOffset); break; case EWhichRange::eParagraph: ParagraphAt(aOffset, aStartContainer, aStartOffset, aEndContainer, aEndOffset); break; case EWhichRange::eStyle: StyleAt(aOffset, aStartContainer, aStartOffset, aEndContainer, aEndOffset); break; default: break; } } void HyperTextAccessibleWrap::LeftWordAt(int32_t aOffset, HyperTextAccessible** aStartContainer, int32_t* aStartOffset, HyperTextAccessible** aEndContainer, int32_t* aEndOffset) { TextPoint here(this, aOffset); TextPoint start = FindTextPoint(aOffset, eDirPrevious, eSelectWord, eStartWord); if (!start.mContainer) { return; } if ((NativeState() & states::EDITABLE) && !(start.mContainer->NativeState() & states::EDITABLE)) { // The word search crossed an editable boundary. Return the first word of // the editable root. return EditableRoot()->RightWordAt(0, aStartContainer, aStartOffset, aEndContainer, aEndOffset); } TextPoint end = static_cast(start.mContainer) ->FindTextPoint(start.mOffset, eDirNext, eSelectWord, eEndWord); if (end < here) { *aStartContainer = end.mContainer; *aEndContainer = here.mContainer; *aStartOffset = end.mOffset; *aEndOffset = here.mOffset; } else { *aStartContainer = start.mContainer; *aEndContainer = end.mContainer; *aStartOffset = start.mOffset; *aEndOffset = end.mOffset; } } void HyperTextAccessibleWrap::RightWordAt(int32_t aOffset, HyperTextAccessible** aStartContainer, int32_t* aStartOffset, HyperTextAccessible** aEndContainer, int32_t* aEndOffset) { TextPoint here(this, aOffset); TextPoint end = FindTextPoint(aOffset, eDirNext, eSelectWord, eEndWord); if (!end.mContainer || end < here || here == end) { // If we didn't find a word end, or if we wrapped around (bug 1652833), // return with no result. return; } if ((NativeState() & states::EDITABLE) && !(end.mContainer->NativeState() & states::EDITABLE)) { // The word search crossed an editable boundary. Return with no result. return; } TextPoint start = static_cast(end.mContainer) ->FindTextPoint(end.mOffset, eDirPrevious, eSelectWord, eStartWord); if (here < start) { *aStartContainer = here.mContainer; *aEndContainer = start.mContainer; *aStartOffset = here.mOffset; *aEndOffset = start.mOffset; } else { *aStartContainer = start.mContainer; *aEndContainer = end.mContainer; *aStartOffset = start.mOffset; *aEndOffset = end.mOffset; } } void HyperTextAccessibleWrap::LineAt(int32_t aOffset, bool aNextLine, HyperTextAccessible** aStartContainer, int32_t* aStartOffset, HyperTextAccessible** aEndContainer, int32_t* aEndOffset) { TextPoint here(this, aOffset); TextPoint end = FindTextPoint(aOffset, eDirNext, eSelectEndLine, eDefaultBehavior); if (!end.mContainer || end < here) { // If we didn't find a word end, or if we wrapped around (bug 1652833), // return with no result. return; } TextPoint start = static_cast(end.mContainer) ->FindTextPoint(end.mOffset, eDirPrevious, eSelectBeginLine, eDefaultBehavior); if (!aNextLine && here < start) { start = FindTextPoint(aOffset, eDirPrevious, eSelectBeginLine, eDefaultBehavior); if (!start.mContainer) { return; } end = static_cast(start.mContainer) ->FindTextPoint(start.mOffset, eDirNext, eSelectEndLine, eDefaultBehavior); } *aStartContainer = start.mContainer; *aEndContainer = end.mContainer; *aStartOffset = start.mOffset; *aEndOffset = end.mOffset; } void HyperTextAccessibleWrap::ParagraphAt(int32_t aOffset, HyperTextAccessible** aStartContainer, int32_t* aStartOffset, HyperTextAccessible** aEndContainer, int32_t* aEndOffset) { TextPoint here(this, aOffset); TextPoint end = FindTextPoint(aOffset, eDirNext, eSelectParagraph, eDefaultBehavior); if (!end.mContainer || end < here) { // If we didn't find a word end, or if we wrapped around (bug 1652833), // return with no result. return; } if (end.mOffset == -1 && Parent() && Parent()->IsHyperText()) { // If end offset is -1 we didn't find a paragraph boundary. // This must be an inline container, go to its parent to // retrieve paragraph boundaries. static_cast(Parent()->AsHyperText()) ->ParagraphAt(StartOffset(), aStartContainer, aStartOffset, aEndContainer, aEndOffset); return; } TextPoint start = static_cast(end.mContainer) ->FindTextPoint(end.mOffset, eDirPrevious, eSelectParagraph, eDefaultBehavior); *aStartContainer = start.mContainer; *aEndContainer = end.mContainer; *aStartOffset = start.mOffset; *aEndOffset = end.mOffset; } void HyperTextAccessibleWrap::StyleAt(int32_t aOffset, HyperTextAccessible** aStartContainer, int32_t* aStartOffset, HyperTextAccessible** aEndContainer, int32_t* aEndOffset) { // Get the range of the text leaf at this offset. // A text leaf represents a stretch of like-styled text. auto leaf = LeafAtOffset(aOffset); if (!leaf) { return; } MOZ_ASSERT(leaf->Parent()->IsHyperText()); HyperTextAccessibleWrap* container = static_cast(leaf->Parent()->AsHyperText()); if (!container) { return; } *aStartContainer = *aEndContainer = container; container->RangeOfChild(leaf, aStartOffset, aEndOffset); } void HyperTextAccessibleWrap::NextClusterAt( int32_t aOffset, HyperTextAccessible** aNextContainer, int32_t* aNextOffset) { TextPoint here(this, aOffset); TextPoint next = FindTextPoint(aOffset, eDirNext, eSelectCluster, eDefaultBehavior); if ((next.mOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT && next.mContainer == Document()) || (next < here)) { // If we reached the end of the doc, or if we wrapped to the start of the // doc return given offset as-is. *aNextContainer = this; *aNextOffset = aOffset; } else { *aNextContainer = next.mContainer; *aNextOffset = next.mOffset; } } void HyperTextAccessibleWrap::PreviousClusterAt( int32_t aOffset, HyperTextAccessible** aPrevContainer, int32_t* aPrevOffset) { TextPoint prev = FindTextPoint(aOffset, eDirPrevious, eSelectCluster, eDefaultBehavior); *aPrevContainer = prev.mContainer; *aPrevOffset = prev.mOffset; } void HyperTextAccessibleWrap::RangeOfChild(Accessible* aChild, int32_t* aStartOffset, int32_t* aEndOffset) { MOZ_ASSERT(aChild->Parent() == this); *aStartOffset = *aEndOffset = -1; int32_t index = GetIndexOf(aChild); if (index != -1) { *aStartOffset = GetChildOffset(index); // If this is the last child index + 1 will return the total // chracter count. *aEndOffset = GetChildOffset(index + 1); } } Accessible* HyperTextAccessibleWrap::LeafAtOffset(int32_t aOffset) { HyperTextAccessible* text = this; Accessible* child = nullptr; // The offset needed should "attach" the previous accessible if // in between two accessibles. int32_t innerOffset = aOffset > 0 ? aOffset - 1 : aOffset; do { int32_t childIdx = text->GetChildIndexAtOffset(innerOffset); if (childIdx == -1) { return text; } child = text->GetChildAt(childIdx); if (!child || nsAccUtils::MustPrune(text)) { return text; } innerOffset -= text->GetChildOffset(childIdx); text = child->AsHyperText(); } while (text); return child; } void HyperTextAccessibleWrap::SelectRange(int32_t aStartOffset, HyperTextAccessible* aEndContainer, int32_t aEndOffset) { TextRange range(this, this, aStartOffset, aEndContainer, aEndOffset); range.SetSelectionAt(0); } TextPoint HyperTextAccessibleWrap::FindTextPoint( int32_t aOffset, nsDirection aDirection, nsSelectionAmount aAmount, EWordMovementType aWordMovementType) { // Layout can remain trapped in an editable. We normalize out of // it if we are in its last offset. HyperTextIterator iter(this, aOffset, this, CharacterCount()); if (aDirection == eDirNext) { iter.NormalizeForward(); } else { iter.NormalizeBackward(); } // Find a leaf accessible frame to start with. PeekOffset wants this. HyperTextAccessible* text = iter.mCurrentContainer; Accessible* child = nullptr; int32_t innerOffset = iter.mCurrentStartOffset; 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 TextPoint(text, 0); } child = text->GetChildAt(childIdx); if (child->IsHyperText() && !child->ChildCount()) { // If this is a childless hypertext, jump to its // previous or next sibling, depending on // direction. if (aDirection == eDirPrevious && childIdx > 0) { child = text->GetChildAt(--childIdx); } else if (aDirection == eDirNext && childIdx + 1 < static_cast(text->ChildCount())) { child = text->GetChildAt(++childIdx); } } int32_t childOffset = text->GetChildOffset(childIdx); if (child->IsHyperText() && aDirection == eDirPrevious && childIdx > 0 && innerOffset - childOffset == 0) { // If we are searching backwards, and this is the begining of a // segment, get the previous sibling so that layout will start // its search there. childIdx--; innerOffset -= text->GetChildOffset(childIdx); child = text->GetChildAt(childIdx); } else { innerOffset -= childOffset; } text = child->AsHyperText(); } while (text); nsIFrame* childFrame = child->GetFrame(); if (!childFrame) { NS_ERROR("No child frame"); return TextPoint(this, aOffset); } int32_t innerContentOffset = innerOffset; if (child->IsTextLeaf()) { NS_ASSERTION(childFrame->IsTextFrame(), "Wrong frame!"); RenderedToContentOffset(childFrame, innerOffset, &innerContentOffset); } nsIFrame* frameAtOffset = childFrame; int32_t offsetInFrame = 0; childFrame->GetChildFrameContainingOffset(innerContentOffset, true, &offsetInFrame, &frameAtOffset); if (aDirection == eDirPrevious && offsetInFrame == 0) { // If we are searching backwards, and we are at the start of a frame, // get the previous continuation frame. if (nsIFrame* prevInContinuation = frameAtOffset->GetPrevContinuation()) { frameAtOffset = prevInContinuation; } } 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. if (NS_FAILED(rv) && aAmount == eSelectLine) { pos.mAmount = aDirection == eDirNext ? eSelectEndLine : eSelectBeginLine; frameAtOffset->PeekOffset(&pos); } if (!pos.mResultContent) { NS_ERROR("No result content!"); return TextPoint(this, aOffset); } if (aDirection == eDirNext && nsContentUtils::PositionIsBefore(pos.mResultContent, mContent, nullptr, nullptr)) { // Bug 1652833 makes us sometimes return the first element on the doc. return TextPoint(Document(), nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT); } HyperTextAccessible* container = nsAccUtils::GetTextContainer(pos.mResultContent); int32_t offset = container ? container->DOMPointToOffset( pos.mResultContent, pos.mContentOffset, aDirection == eDirNext) : 0; return TextPoint(container, offset); } HyperTextAccessibleWrap* HyperTextAccessibleWrap::EditableRoot() { Accessible* editable = nullptr; for (Accessible* acc = this; acc && acc != Document(); acc = acc->Parent()) { if (acc->NativeState() & states::EDITABLE) { editable = acc; } else { break; } } return static_cast(editable->AsHyperText()); }