Files
Masayuki Nakano d3828ed70b Bug 2014622 - part 4: Rename RangeBoundaryBase::AsRangeBoundaryInFlatTree() r=smaug
It may converts a `TreeKind::DOM` point to a point whose container is
a non-flattened node which is a descendant of a shadow host child or
a `<slot>` in `TreeKind::DOM`. Therefore, the name should explain it
may return non-flattened node point.

Differential Revision: https://phabricator.services.mozilla.com/D309548
2026-07-05 23:40:43 +00:00

429 lines
17 KiB
C++

/* 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 "RangeUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/dom/AbstractRange.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/HTMLSlotElement.h"
#include "mozilla/dom/ShadowRoot.h"
#include "nsContentUtils.h"
#include "nsFrameSelection.h"
#include "nsIContentInlines.h"
namespace mozilla {
using namespace dom;
template bool RangeUtils::IsValidPoints(const RangeBoundary&,
const RangeBoundary&);
template bool RangeUtils::IsValidPoints(const RangeBoundary&,
const RawRangeBoundary&);
template bool RangeUtils::IsValidPoints(const RawRangeBoundary&,
const RangeBoundary&);
template bool RangeUtils::IsValidPoints(const RawRangeBoundary&,
const RawRangeBoundary&);
template nsresult
RangeUtils::CompareNodeToRangeBoundaries<TreeKind::ShadowIncludingDOM>(
const nsINode*, const RangeBoundary&, const RangeBoundary&, bool*, bool*);
template nsresult
RangeUtils::CompareNodeToRangeBoundaries<TreeKind::FlatForSelection>(
const nsINode*, const RangeBoundary&, const RangeBoundary&, bool*, bool*);
template nsresult RangeUtils::CompareNodeToRangeBoundaries<
TreeKind::ShadowIncludingDOM>(const nsINode*, const RangeBoundary&,
const RawRangeBoundary&, bool*, bool*);
template nsresult RangeUtils::CompareNodeToRangeBoundaries<
TreeKind::FlatForSelection>(const nsINode*, const RangeBoundary&,
const RawRangeBoundary&, bool*, bool*);
template nsresult RangeUtils::CompareNodeToRangeBoundaries<
TreeKind::ShadowIncludingDOM>(const nsINode*, const RawRangeBoundary&,
const RangeBoundary&, bool*, bool*);
template nsresult RangeUtils::CompareNodeToRangeBoundaries<
TreeKind::FlatForSelection>(const nsINode*, const RawRangeBoundary&,
const RangeBoundary&, bool*, bool*);
template nsresult RangeUtils::CompareNodeToRangeBoundaries<
TreeKind::ShadowIncludingDOM>(const nsINode*, const RawRangeBoundary&,
const RawRangeBoundary&, bool*, bool*);
template nsresult RangeUtils::CompareNodeToRangeBoundaries<
TreeKind::FlatForSelection>(const nsINode*, const RawRangeBoundary&,
const RawRangeBoundary&, bool*, bool*);
template nsresult RangeUtils::CompareNodeToRange<TreeKind::ShadowIncludingDOM>(
const nsINode*, const AbstractRange*, bool*, bool*);
template nsresult RangeUtils::CompareNodeToRange<TreeKind::FlatForSelection>(
const nsINode*, const AbstractRange*, bool*, bool*);
template Maybe<bool> RangeUtils::IsNodeContainedInRange<
TreeKind::ShadowIncludingDOM>(const nsINode&, const AbstractRange*);
template Maybe<bool> RangeUtils::IsNodeContainedInRange<
TreeKind::FlatForSelection>(const nsINode&, const AbstractRange*);
[[nodiscard]] static inline bool ParentNodeIsInSameSelection(
const nsINode& aNode) {
// Currently, independent selection root is always the anonymous <div> in a
// text control which is an native anonymous subtree root. Therefore, we
// can skip most checks if the node is not a root of native anonymous subtree.
if (!aNode.IsRootOfNativeAnonymousSubtree()) {
return true;
}
// If the node returns nullptr for frame selection, it means that it's not the
// anonymous <div> of the editable content root of a text control or just not
// in composed doc.
const nsFrameSelection* frameSelection = aNode.GetFrameSelection();
if (!frameSelection || frameSelection->IsIndependentSelection()) {
MOZ_ASSERT_IF(aNode.GetClosestNativeAnonymousSubtreeRootParentOrHost(),
aNode.GetClosestNativeAnonymousSubtreeRootParentOrHost()
->IsTextControlElement());
return false;
}
return true;
}
// static
nsINode* RangeUtils::ComputeRootNode(nsINode* aNode) {
if (!aNode) {
return nullptr;
}
if (aNode->IsContent()) {
if (aNode->NodeInfo()->NameAtom() == nsGkAtoms::documentTypeNodeName) {
return nullptr;
}
nsIContent* content = aNode->AsContent();
// If the node is in a shadow tree then the ShadowRoot is the root.
//
// FIXME(emilio): Should this be after the NAC check below? We can have NAC
// inside Shadow DOM which will peek this path rather than the one below.
if (ShadowRoot* containingShadow = content->GetContainingShadow()) {
return containingShadow;
}
// If the node is in NAC, then the NAC parent should be the root.
if (nsINode* root =
content->GetClosestNativeAnonymousSubtreeRootParentOrHost()) {
return root;
}
}
// Elements etc. must be in document or in document fragment,
// text nodes in document, in document fragment or in attribute.
if (nsINode* root = aNode->GetUncomposedDoc()) {
return root;
}
NS_ASSERTION(!aNode->SubtreeRoot()->IsDocument(),
"GetUncomposedDoc should have returned a doc");
// We allow this because of backward compatibility.
return aNode->SubtreeRoot();
}
// static
template <typename SPT, typename SRT, typename EPT, typename ERT>
bool RangeUtils::IsValidPoints(
const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
const RangeBoundaryBase<EPT, ERT>& aEndBoundary) {
// Use NS_WARN_IF() only for the cases where the arguments are unexpected.
if (NS_WARN_IF(!aStartBoundary.IsSetAndValid()) ||
NS_WARN_IF(!aEndBoundary.IsSetAndValid())) {
return false;
}
MOZ_ASSERT(aStartBoundary.GetTreeKind() == aEndBoundary.GetTreeKind());
// Otherwise, don't use NS_WARN_IF() for preventing to make console messy.
// Instead, check one by one since it is easier to catch the error reason
// with debugger.
if (ComputeRootNode(aStartBoundary.GetContainer()) !=
ComputeRootNode(aEndBoundary.GetContainer())) {
return false;
}
const Maybe<int32_t> order =
aStartBoundary.GetTreeKind() == TreeKind::FlatForSelection
? nsContentUtils::ComparePoints<TreeKind::FlatForSelection>(
aStartBoundary, aEndBoundary)
: nsContentUtils::ComparePoints<TreeKind::DOM>(aStartBoundary,
aEndBoundary);
if (!order) {
MOZ_ASSERT_UNREACHABLE();
return false;
}
return *order != 1;
}
// static
template <TreeKind aKind, typename Dummy>
Maybe<bool> RangeUtils::IsNodeContainedInRange(
const nsINode& aNode, const AbstractRange* aAbstractRange) {
bool nodeIsBeforeRange{false};
bool nodeIsAfterRange{false};
const nsresult rv = CompareNodeToRange<aKind>(
&aNode, aAbstractRange, &nodeIsBeforeRange, &nodeIsAfterRange);
if (NS_FAILED(rv)) {
return Nothing();
}
return Some(!nodeIsBeforeRange && !nodeIsAfterRange);
}
// Utility routine to detect if a content node is completely contained in a
// range If outNodeBefore is returned true, then the node starts before the
// range does. If outNodeAfter is returned true, then the node ends after the
// range does. Note that both of the above might be true. If neither are true,
// the node is contained inside of the range.
// XXX - callers responsibility to ensure node in same doc as range!
// static
template <TreeKind aKind, typename Dummy>
nsresult RangeUtils::CompareNodeToRange(const nsINode* aNode,
const AbstractRange* aAbstractRange,
bool* aNodeIsBeforeRange,
bool* aNodeIsAfterRange) {
if (NS_WARN_IF(!aAbstractRange) ||
NS_WARN_IF(!aAbstractRange->IsPositioned())) {
return NS_ERROR_INVALID_ARG;
}
return CompareNodeToRangeBoundaries<aKind>(
aNode, aAbstractRange->MayCrossShadowBoundaryStartRef(),
aAbstractRange->MayCrossShadowBoundaryEndRef(), aNodeIsBeforeRange,
aNodeIsAfterRange);
}
template <TreeKind aKind, typename SPT, typename SRT, typename EPT,
typename ERT, typename Dummy>
nsresult RangeUtils::CompareNodeToRangeBoundaries(
const nsINode* aNode, const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
const RangeBoundaryBase<EPT, ERT>& aEndBoundary, bool* aNodeIsBeforeRange,
bool* aNodeIsAfterRange) {
MOZ_ASSERT(aNodeIsBeforeRange);
MOZ_ASSERT(aNodeIsAfterRange);
MOZ_ASSERT(aStartBoundary.GetTreeKind() == aEndBoundary.GetTreeKind());
if (NS_WARN_IF(!aNode) ||
NS_WARN_IF(!aStartBoundary.IsSet() || !aEndBoundary.IsSet())) {
return NS_ERROR_INVALID_ARG;
}
constexpr TreeKind boundaryKind = aKind == TreeKind::FlatForSelection
? TreeKind::FlatForSelection
: TreeKind::DOM;
// create a pair of dom points that expresses location of node:
// NODE(start), NODE(end)
// Let incoming range be:
// {RANGE(start), RANGE(end)}
// if (RANGE(start) <= NODE(start)) and (RANGE(end) => NODE(end))
// then the Node is contained (completely) by the Range.
// gather up the dom point info
ConstRawRangeBoundary nodeStart(boundaryKind);
ConstRawRangeBoundary nodeEnd(boundaryKind);
// ShadowRoot has no parent, nor can be represented by parent/offset pair.
nsINode* const parentNodeInSameSelection = [&]() -> nsINode* {
if (aNode->IsShadowRoot()) {
return nullptr;
}
return ShadowDOMSelectionHelpers::GetParentNodeInSameSelection(
*aNode, aKind == TreeKind::FlatForSelection
? AllowRangeCrossShadowBoundary::Yes
: AllowRangeCrossShadowBoundary::No);
}();
if (!parentNodeInSameSelection) {
// can't make a parent/offset pair to represent start or
// end of the root node, because it has no parent.
// so instead represent it by (node,0) and (node,numChildren)
nodeStart = ConstRawRangeBoundary::StartOfParent(
*aNode, RangeBoundarySetBy::Ref, boundaryKind);
nodeEnd = ConstRawRangeBoundary::EndOfParent(
*aNode, RangeBoundarySetBy::Ref, boundaryKind);
} else if (const auto* slotAsParent =
parentNodeInSameSelection
->GetAsHTMLSlotElementIfFilledForSelection();
slotAsParent && aKind == TreeKind::FlatForSelection) {
// aNode is a slotted content, use the index in the assigned nodes
// to represent this node.
auto index = slotAsParent->AssignedNodes().IndexOf(aNode);
nodeStart =
ConstRawRangeBoundary(slotAsParent, index, RangeBoundarySetBy::Offset,
TreeKind::FlatForSelection);
nodeEnd = ConstRawRangeBoundary(slotAsParent, index + 1,
RangeBoundarySetBy::Offset,
TreeKind::FlatForSelection);
} else {
nodeStart =
ConstRawRangeBoundary::FromChild(*aNode->AsContent(), boundaryKind);
nodeEnd = ConstRawRangeBoundary::After(*aNode->AsContent(), boundaryKind);
if (boundaryKind == TreeKind::FlatForSelection && !nodeStart.IsSet() &&
!nodeEnd.IsSet()) {
if (ShadowRoot* const shadowRoot =
parentNodeInSameSelection->GetShadowRootForSelection()) {
// In this case, aNode must be a child node which is not in the shadow
// hosted by the parent node.
if (aNode == parentNodeInSameSelection->GetFirstChild()) {
nodeStart = nodeEnd = ConstRawRangeBoundary::StartOfParent(
*shadowRoot, RangeBoundarySetBy::Ref, TreeKind::FlatForSelection);
} else {
nodeStart = nodeEnd = ConstRawRangeBoundary::EndOfParent(
*shadowRoot, RangeBoundarySetBy::Ref, TreeKind::FlatForSelection);
}
}
}
}
// XXX nsContentUtils::ComparePoints() may be expensive. If some callers
// just want one of aNodeIsBeforeRange or aNodeIsAfterRange, we can
// skip the other comparison.
// In the ComparePoints calls below we use a container & offset instead of
// a range boundary because the range boundary constructor warns if you pass
// in a -1 offset and the ComputeIndexOf call above can return -1 if aNode
// is native anonymous content. ComparePoints has comments about offsets
// being -1 and it seems to deal with it, or at least we aren't aware of any
// problems arising because of it. We don't have a better idea how to get
// rid of the warning without much larger changes so we do this just to
// silence the warning. (Bug 1438996)
const ConstRawRangeBoundary startBoundary =
aStartBoundary.GetTreeKind() == boundaryKind
? aStartBoundary.AsConstRaw()
: (boundaryKind == TreeKind::DOM
? aStartBoundary.AsConstRaw().AsRangeBoundaryInDOMTree()
: aStartBoundary.AsConstRaw()
.AsRangeBoundaryInFlatTreeOrNonFlattenedNode(
aStartBoundary == aEndBoundary
? RangeBoundaryFor::Collapsed
: RangeBoundaryFor::Start));
// is RANGE(start) <= NODE(start) ?
Maybe<int32_t> order =
nsContentUtils::ComparePoints<aKind>(startBoundary, nodeStart);
if (NS_WARN_IF(!order)) {
return NS_ERROR_DOM_WRONG_DOCUMENT_ERR;
}
*aNodeIsBeforeRange = *order > 0;
const ConstRawRangeBoundary endBoundary =
aEndBoundary.GetTreeKind() == boundaryKind
? aEndBoundary.AsConstRaw()
: (boundaryKind == TreeKind::DOM
? aEndBoundary.AsConstRaw().AsRangeBoundaryInDOMTree()
: aEndBoundary.AsConstRaw()
.AsRangeBoundaryInFlatTreeOrNonFlattenedNode(
aStartBoundary == aEndBoundary
? RangeBoundaryFor::Collapsed
: RangeBoundaryFor::End));
// is RANGE(end) >= NODE(end) ?
order = nsContentUtils::ComparePoints<aKind>(endBoundary, nodeEnd);
if (NS_WARN_IF(!order)) {
return NS_ERROR_DOM_WRONG_DOCUMENT_ERR;
}
*aNodeIsAfterRange = *order < 0;
return NS_OK;
}
// static
RawRangeBoundary ShadowDOMSelectionHelpers::StartRef(
const AbstractRange* aRange,
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
MOZ_ASSERT(aRange);
return (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes)
? aRange->MayCrossShadowBoundaryStartRef().AsRaw()
: aRange->StartRef().AsRaw();
}
// static
nsINode* ShadowDOMSelectionHelpers::GetStartContainer(
const AbstractRange* aRange,
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
MOZ_ASSERT(aRange);
return (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes)
? aRange->GetMayCrossShadowBoundaryStartContainer()
: aRange->GetStartContainer();
}
// static
uint32_t ShadowDOMSelectionHelpers::StartOffset(
const AbstractRange* aRange,
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
MOZ_ASSERT(aRange);
return (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes)
? aRange->MayCrossShadowBoundaryStartOffset()
: aRange->StartOffset();
}
// static
RawRangeBoundary ShadowDOMSelectionHelpers::EndRef(
const AbstractRange* aRange,
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
MOZ_ASSERT(aRange);
return (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes)
? aRange->MayCrossShadowBoundaryEndRef().AsRaw()
: aRange->EndRef().AsRaw();
}
// static
nsINode* ShadowDOMSelectionHelpers::GetEndContainer(
const AbstractRange* aRange,
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
MOZ_ASSERT(aRange);
return (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes)
? aRange->GetMayCrossShadowBoundaryEndContainer()
: aRange->GetEndContainer();
}
// static
uint32_t ShadowDOMSelectionHelpers::EndOffset(
const AbstractRange* aRange,
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
MOZ_ASSERT(aRange);
return (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes)
? aRange->MayCrossShadowBoundaryEndOffset()
: aRange->EndOffset();
}
// static
nsINode* ShadowDOMSelectionHelpers::GetParentNodeInSameSelection(
const nsINode& aNode,
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
if (!ParentNodeIsInSameSelection(aNode)) {
return nullptr;
}
if (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) {
if (aNode.IsContent()) {
if (HTMLSlotElement* const slot =
aNode.AsContent()->GetAssignedSlotForSelection()) {
return slot;
}
}
return aNode.GetParentOrShadowHostNode();
}
return aNode.GetParentNode();
}
// static
ShadowRoot* ShadowDOMSelectionHelpers::GetShadowRoot(
const nsINode* aNode,
AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) {
MOZ_ASSERT(aNode);
return (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes)
? aNode->GetShadowRootForSelection()
: nullptr;
} // namespace dom
} // namespace mozilla