summaryrefslogtreecommitdiffstats
path: root/accessible/base/TextRange.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/base/TextRange.cpp')
-rw-r--r--accessible/base/TextRange.cpp541
1 files changed, 541 insertions, 0 deletions
diff --git a/accessible/base/TextRange.cpp b/accessible/base/TextRange.cpp
new file mode 100644
index 0000000000..75f91f4932
--- /dev/null
+++ b/accessible/base/TextRange.cpp
@@ -0,0 +1,541 @@
+/* -*- 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<int32_t>(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<int32_t>(!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<Accessible*, 30> 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<uint32_t>(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<uint32_t>(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<Accessible*>* 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<Accessible*, 30> 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<Accessible*, 30> 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<dom::Selection> domSel = root->DOMSelection();
+ if (!domSel) {
+ return false;
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(root->GetContent());
+ uint32_t rangeCount = domSel->RangeCount();
+ if (aSelectionNum == static_cast<int32_t>(rangeCount)) {
+ range = nsRange::Create(root->GetContent());
+ } else {
+ range = domSel->GetRangeAt(AssertedCast<uint32_t>(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<int32_t>(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<nsRange> 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<TextRange>* 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<Accessible*>* aParents1,
+ uint32_t* aPos1,
+ nsTArray<Accessible*>* 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