/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* 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 "TextRange-inl.h" #include "LocalAccessible-inl.h" #include "HyperTextAccessible-inl.h" #include "mozilla/IntegerRange.h" #include "mozilla/dom/Selection.h" #include "nsAccUtils.h" namespace mozilla { namespace a11y { /** * Returns a text point for aAcc within aContainer. */ static void ToTextPoint(Accessible* aAcc, Accessible** aContainer, int32_t* aOffset, bool aIsBefore = true) { if (aAcc->IsHyperText()) { *aContainer = aAcc; *aOffset = aIsBefore ? 0 : static_cast(aAcc->AsHyperTextBase()->CharacterCount()); return; } Accessible* child = nullptr; Accessible* parent = aAcc; do { child = parent; parent = parent->Parent(); } while (parent && !parent->IsHyperText()); if (parent) { *aContainer = parent; *aOffset = parent->AsHyperTextBase()->GetChildOffset( child->IndexInParent() + static_cast(!aIsBefore)); } } //////////////////////////////////////////////////////////////////////////////// // TextPoint bool TextPoint::operator<(const TextPoint& aPoint) const { if (mContainer == aPoint.mContainer) return mOffset < aPoint.mOffset; // Build the chain of parents Accessible* p1 = mContainer; Accessible* p2 = aPoint.mContainer; AutoTArray parents1, parents2; do { parents1.AppendElement(p1); p1 = p1->Parent(); } while (p1); do { parents2.AppendElement(p2); p2 = p2->Parent(); } while (p2); // Find where the parent chain differs uint32_t pos1 = parents1.Length(), pos2 = parents2.Length(); for (uint32_t len = std::min(pos1, pos2); len > 0; --len) { Accessible* child1 = parents1.ElementAt(--pos1); Accessible* child2 = parents2.ElementAt(--pos2); if (child1 != child2) { return child1->IndexInParent() < child2->IndexInParent(); } } if (pos1 != 0) { // If parents1 is a superset of parents2 then mContainer is a // descendant of aPoint.mContainer. The next element down in parents1 // is mContainer's ancestor that is the child of aPoint.mContainer. // We compare its end offset in aPoint.mContainer with aPoint.mOffset. Accessible* child = parents1.ElementAt(pos1 - 1); MOZ_ASSERT(child->Parent() == aPoint.mContainer); return child->EndOffset() < static_cast(aPoint.mOffset); } if (pos2 != 0) { // If parents2 is a superset of parents1 then aPoint.mContainer is a // descendant of mContainer. The next element down in parents2 // is aPoint.mContainer's ancestor that is the child of mContainer. // We compare its start offset in mContainer with mOffset. Accessible* child = parents2.ElementAt(pos2 - 1); MOZ_ASSERT(child->Parent() == mContainer); return static_cast(mOffset) < child->StartOffset(); } NS_ERROR("Broken tree?!"); return false; } //////////////////////////////////////////////////////////////////////////////// // TextRange TextRange::TextRange(Accessible* aRoot, Accessible* aStartContainer, int32_t aStartOffset, Accessible* aEndContainer, int32_t aEndOffset) : mRoot(aRoot), mStartContainer(aStartContainer), mEndContainer(aEndContainer), mStartOffset(aStartOffset), mEndOffset(aEndOffset) {} void TextRange::EmbeddedChildren(nsTArray* aChildren) const { HyperTextAccessibleBase* startHyper = mStartContainer->AsHyperTextBase(); if (mStartContainer == mEndContainer) { int32_t startIdx = startHyper->GetChildIndexAtOffset(mStartOffset); int32_t endIdx = startHyper->GetChildIndexAtOffset(mEndOffset); for (int32_t idx = startIdx; idx <= endIdx; idx++) { Accessible* child = mStartContainer->ChildAt(idx); if (!child->IsText()) { aChildren->AppendElement(child); } } return; } Accessible* p1 = startHyper->GetChildAtOffset(mStartOffset); HyperTextAccessibleBase* endHyper = mEndContainer->AsHyperTextBase(); Accessible* p2 = endHyper->GetChildAtOffset(mEndOffset); uint32_t pos1 = 0, pos2 = 0; AutoTArray parents1, parents2; Accessible* container = CommonParent(p1, p2, &parents1, &pos1, &parents2, &pos2); // Traverse the tree up to the container and collect embedded objects. for (uint32_t idx = 0; idx < pos1 - 1; idx++) { Accessible* parent = parents1[idx + 1]; Accessible* child = parents1[idx]; uint32_t childCount = parent->ChildCount(); for (uint32_t childIdx = child->IndexInParent(); childIdx < childCount; childIdx++) { Accessible* next = parent->ChildAt(childIdx); if (!next->IsText()) { aChildren->AppendElement(next); } } } // Traverse through direct children in the container. int32_t endIdx = parents2[pos2 - 1]->IndexInParent(); int32_t childIdx = parents1[pos1 - 1]->IndexInParent() + 1; for (; childIdx < endIdx; childIdx++) { Accessible* next = container->ChildAt(childIdx); if (!next->IsText()) { aChildren->AppendElement(next); } } // Traverse down from the container to end point. for (int32_t idx = pos2 - 2; idx > 0; idx--) { Accessible* parent = parents2[idx]; Accessible* child = parents2[idx - 1]; int32_t endIdx = child->IndexInParent(); for (int32_t childIdx = 0; childIdx < endIdx; childIdx++) { Accessible* next = parent->ChildAt(childIdx); if (!next->IsText()) { aChildren->AppendElement(next); } } } } void TextRange::Text(nsAString& aText) const { HyperTextAccessibleBase* startHyper = mStartContainer->AsHyperTextBase(); Accessible* current = startHyper->GetChildAtOffset(mStartOffset); uint32_t startIntlOffset = mStartOffset - startHyper->GetChildOffset(current); while (current && TextInternal(aText, current, startIntlOffset)) { current = current->Parent(); if (!current) break; current = current->NextSibling(); } } bool TextRange::Crop(Accessible* aContainer) { uint32_t boundaryPos = 0, containerPos = 0; AutoTArray boundaryParents, containerParents; // Crop the start boundary. Accessible* container = nullptr; HyperTextAccessibleBase* startHyper = mStartContainer->AsHyperTextBase(); Accessible* boundary = startHyper->GetChildAtOffset(mStartOffset); if (boundary != aContainer) { CommonParent(boundary, aContainer, &boundaryParents, &boundaryPos, &containerParents, &containerPos); if (boundaryPos == 0) { if (containerPos != 0) { // The container is contained by the start boundary, reduce the range to // the point starting at the container. ToTextPoint(aContainer, &mStartContainer, &mStartOffset); } else { // The start boundary and the container are siblings. container = aContainer; } } else { // The container does not contain the start boundary. boundary = boundaryParents[boundaryPos]; container = containerParents[containerPos]; } if (container) { // If the range start is after the container, then make the range invalid. if (boundary->IndexInParent() > container->IndexInParent()) { return !!(mRoot = nullptr); } // If the range starts before the container, then reduce the range to // the point starting at the container. if (boundary->IndexInParent() < container->IndexInParent()) { ToTextPoint(container, &mStartContainer, &mStartOffset); } } boundaryParents.SetLengthAndRetainStorage(0); containerParents.SetLengthAndRetainStorage(0); } HyperTextAccessibleBase* endHyper = mEndContainer->AsHyperTextBase(); boundary = endHyper->GetChildAtOffset(mEndOffset); if (boundary == aContainer) { return true; } // Crop the end boundary. container = nullptr; CommonParent(boundary, aContainer, &boundaryParents, &boundaryPos, &containerParents, &containerPos); if (boundaryPos == 0) { if (containerPos != 0) { ToTextPoint(aContainer, &mEndContainer, &mEndOffset, false); } else { container = aContainer; } } else { boundary = boundaryParents[boundaryPos]; container = containerParents[containerPos]; } if (!container) { return true; } if (boundary->IndexInParent() < container->IndexInParent()) { return !!(mRoot = nullptr); } if (boundary->IndexInParent() > container->IndexInParent()) { ToTextPoint(container, &mEndContainer, &mEndOffset, false); } return true; } bool TextRange::SetSelectionAt(int32_t aSelectionNum) const { HyperTextAccessible* root = mRoot->AsLocal()->AsHyperText(); if (!root) { MOZ_ASSERT_UNREACHABLE("Not supported for RemoteAccessible"); return false; } RefPtr domSel = root->DOMSelection(); if (!domSel) { return false; } RefPtr range = nsRange::Create(root->GetContent()); uint32_t rangeCount = domSel->RangeCount(); if (aSelectionNum == static_cast(rangeCount)) { range = nsRange::Create(root->GetContent()); } else { range = domSel->GetRangeAt(AssertedCast(aSelectionNum)); } if (!range) { return false; } bool reversed; AssignDOMRange(range, &reversed); // If this is not a new range, notify selection listeners that the existing // selection range has changed. Otherwise, just add the new range. if (aSelectionNum != static_cast(rangeCount)) { domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, IgnoreErrors()); } IgnoredErrorResult err; domSel->AddRangeAndSelectFramesAndNotifyListeners(*range, err); if (!err.Failed()) { // Changing the direction of the selection assures that the caret // will be at the logical end of the selection. domSel->SetDirection(reversed ? eDirPrevious : eDirNext); return true; } return false; } void TextRange::ScrollIntoView(uint32_t aScrollType) const { LocalAccessible* root = mRoot->AsLocal(); if (!root) { MOZ_ASSERT_UNREACHABLE("Not supported for RemoteAccessible"); return; } RefPtr range = nsRange::Create(root->GetContent()); if (AssignDOMRange(range)) { nsCoreUtils::ScrollSubstringTo(mStartContainer->AsLocal()->GetFrame(), range, aScrollType); } } /** * Convert the given DOM point to a DOM point in non-generated contents. * * If aDOMPoint is in ::before, the result is immediately after it. * If aDOMPoint is in ::after, the result is immediately before it. */ static DOMPoint ClosestNotGeneratedDOMPoint(const DOMPoint& aDOMPoint, nsIContent* aElementContent) { MOZ_ASSERT(aDOMPoint.node, "The node must not be null"); // ::before pseudo element if (aElementContent && aElementContent->IsGeneratedContentContainerForBefore()) { MOZ_ASSERT(aElementContent->GetParent(), "::before must have parent element"); // The first child of its parent (i.e., immediately after the ::before) is // good point for a DOM range. return DOMPoint(aElementContent->GetParent(), 0); } // ::after pseudo element if (aElementContent && aElementContent->IsGeneratedContentContainerForAfter()) { MOZ_ASSERT(aElementContent->GetParent(), "::after must have parent element"); // The end of its parent (i.e., immediately before the ::after) is good // point for a DOM range. return DOMPoint(aElementContent->GetParent(), aElementContent->GetParent()->GetChildCount()); } return aDOMPoint; } /** * GetElementAsContentOf() returns a content representing an element which is * or includes aNode. * * XXX This method is enough to retrieve ::before or ::after pseudo element. * So, if you want to use this for other purpose, you might need to check * ancestors too. */ static nsIContent* GetElementAsContentOf(nsINode* aNode) { if (auto* element = dom::Element::FromNode(aNode)) { return element; } return aNode->GetParentElement(); } bool TextRange::AssignDOMRange(nsRange* aRange, bool* aReversed) const { MOZ_ASSERT(mRoot->IsLocal(), "Not supported for RemoteAccessible"); bool reversed = EndPoint() < StartPoint(); if (aReversed) { *aReversed = reversed; } HyperTextAccessible* startHyper = mStartContainer->AsLocal()->AsHyperText(); HyperTextAccessible* endHyper = mEndContainer->AsLocal()->AsHyperText(); DOMPoint startPoint = reversed ? endHyper->OffsetToDOMPoint(mEndOffset) : startHyper->OffsetToDOMPoint(mStartOffset); if (!startPoint.node) { return false; } // HyperTextAccessible manages pseudo elements generated by ::before or // ::after. However, contents of them are not in the DOM tree normally. // Therefore, they are not selectable and editable. So, when this creates // a DOM range, it should not start from nor end in any pseudo contents. nsIContent* container = GetElementAsContentOf(startPoint.node); DOMPoint startPointForDOMRange = ClosestNotGeneratedDOMPoint(startPoint, container); aRange->SetStart(startPointForDOMRange.node, startPointForDOMRange.idx); // If the caller wants collapsed range, let's collapse the range to its start. if (mEndContainer == mStartContainer && mEndOffset == mStartOffset) { aRange->Collapse(true); return true; } DOMPoint endPoint = reversed ? startHyper->OffsetToDOMPoint(mStartOffset) : endHyper->OffsetToDOMPoint(mEndOffset); if (!endPoint.node) { return false; } if (startPoint.node != endPoint.node) { container = GetElementAsContentOf(endPoint.node); } DOMPoint endPointForDOMRange = ClosestNotGeneratedDOMPoint(endPoint, container); aRange->SetEnd(endPointForDOMRange.node, endPointForDOMRange.idx); return true; } void TextRange::TextRangesFromSelection(dom::Selection* aSelection, nsTArray* aRanges) { MOZ_ASSERT(aRanges->Length() == 0, "TextRange array supposed to be empty"); aRanges->SetCapacity(aSelection->RangeCount()); const uint32_t rangeCount = aSelection->RangeCount(); for (const uint32_t idx : IntegerRange(rangeCount)) { MOZ_ASSERT(aSelection->RangeCount() == rangeCount); const nsRange* DOMRange = aSelection->GetRangeAt(idx); MOZ_ASSERT(DOMRange); HyperTextAccessible* startContainer = nsAccUtils::GetTextContainer(DOMRange->GetStartContainer()); HyperTextAccessible* endContainer = nsAccUtils::GetTextContainer(DOMRange->GetEndContainer()); HyperTextAccessible* commonAncestor = nsAccUtils::GetTextContainer( DOMRange->GetClosestCommonInclusiveAncestor()); if (!startContainer || !endContainer) { continue; } int32_t startOffset = startContainer->DOMPointToOffset( DOMRange->GetStartContainer(), DOMRange->StartOffset(), false); int32_t endOffset = endContainer->DOMPointToOffset( DOMRange->GetEndContainer(), DOMRange->EndOffset(), true); TextRange tr(commonAncestor && commonAncestor->IsTextField() ? commonAncestor : startContainer->Document(), startContainer, startOffset, endContainer, endOffset); *(aRanges->AppendElement()) = std::move(tr); } } //////////////////////////////////////////////////////////////////////////////// // pivate void TextRange::Set(Accessible* aRoot, Accessible* aStartContainer, int32_t aStartOffset, Accessible* aEndContainer, int32_t aEndOffset) { mRoot = aRoot; mStartContainer = aStartContainer; mEndContainer = aEndContainer; mStartOffset = aStartOffset; mEndOffset = aEndOffset; } bool TextRange::TextInternal(nsAString& aText, Accessible* aCurrent, uint32_t aStartIntlOffset) const { bool moveNext = true; int32_t endIntlOffset = -1; HyperTextAccessibleBase* endHyper = mEndContainer->AsHyperTextBase(); if (aCurrent->Parent() == mEndContainer && endHyper->GetChildAtOffset(mEndOffset) == aCurrent) { uint32_t currentStartOffset = endHyper->GetChildOffset(aCurrent); endIntlOffset = mEndOffset - currentStartOffset; if (endIntlOffset == 0) return false; moveNext = false; } if (aCurrent->IsTextLeaf()) { aCurrent->AppendTextTo(aText, aStartIntlOffset, endIntlOffset - aStartIntlOffset); if (!moveNext) return false; } Accessible* next = aCurrent->FirstChild(); if (next) { if (!TextInternal(aText, next, 0)) return false; } next = aCurrent->NextSibling(); if (next) { if (!TextInternal(aText, next, 0)) return false; } return moveNext; } Accessible* TextRange::CommonParent(Accessible* aAcc1, Accessible* aAcc2, nsTArray* aParents1, uint32_t* aPos1, nsTArray* aParents2, uint32_t* aPos2) const { if (aAcc1 == aAcc2) { return aAcc1; } MOZ_ASSERT(aParents1->Length() == 0 || aParents2->Length() == 0, "Wrong arguments"); // Build the chain of parents. Accessible* p1 = aAcc1; Accessible* p2 = aAcc2; do { aParents1->AppendElement(p1); p1 = p1->Parent(); } while (p1); do { aParents2->AppendElement(p2); p2 = p2->Parent(); } while (p2); // Find where the parent chain differs *aPos1 = aParents1->Length(); *aPos2 = aParents2->Length(); Accessible* parent = nullptr; uint32_t len = 0; for (len = std::min(*aPos1, *aPos2); len > 0; --len) { Accessible* child1 = aParents1->ElementAt(--(*aPos1)); Accessible* child2 = aParents2->ElementAt(--(*aPos2)); if (child1 != child2) break; parent = child1; } return parent; } } // namespace a11y } // namespace mozilla