RangeUtils.cpp (14821B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "RangeUtils.h" 8 9 #include "mozilla/Assertions.h" 10 #include "mozilla/dom/AbstractRange.h" 11 #include "mozilla/dom/Document.h" 12 #include "mozilla/dom/HTMLSlotElement.h" 13 #include "mozilla/dom/ShadowRoot.h" 14 #include "nsContentUtils.h" 15 #include "nsFrameSelection.h" 16 17 namespace mozilla { 18 19 using namespace dom; 20 21 template bool RangeUtils::IsValidPoints(const RangeBoundary&, 22 const RangeBoundary&); 23 template bool RangeUtils::IsValidPoints(const RangeBoundary&, 24 const RawRangeBoundary&); 25 template bool RangeUtils::IsValidPoints(const RawRangeBoundary&, 26 const RangeBoundary&); 27 template bool RangeUtils::IsValidPoints(const RawRangeBoundary&, 28 const RawRangeBoundary&); 29 30 template nsresult 31 RangeUtils::CompareNodeToRangeBoundaries<TreeKind::ShadowIncludingDOM>( 32 const nsINode*, const RangeBoundary&, const RangeBoundary&, bool*, bool*); 33 template nsresult RangeUtils::CompareNodeToRangeBoundaries<TreeKind::Flat>( 34 const nsINode*, const RangeBoundary&, const RangeBoundary&, bool*, bool*); 35 36 template nsresult RangeUtils::CompareNodeToRangeBoundaries< 37 TreeKind::ShadowIncludingDOM>(const nsINode*, const RangeBoundary&, 38 const RawRangeBoundary&, bool*, bool*); 39 template nsresult RangeUtils::CompareNodeToRangeBoundaries<TreeKind::Flat>( 40 const nsINode*, const RangeBoundary&, const RawRangeBoundary&, bool*, 41 bool*); 42 43 template nsresult RangeUtils::CompareNodeToRangeBoundaries< 44 TreeKind::ShadowIncludingDOM>(const nsINode*, const RawRangeBoundary&, 45 const RangeBoundary&, bool*, bool*); 46 template nsresult RangeUtils::CompareNodeToRangeBoundaries<TreeKind::Flat>( 47 const nsINode*, const RawRangeBoundary&, const RangeBoundary&, bool*, 48 bool*); 49 50 template nsresult RangeUtils::CompareNodeToRangeBoundaries< 51 TreeKind::ShadowIncludingDOM>(const nsINode*, const RawRangeBoundary&, 52 const RawRangeBoundary&, bool*, bool*); 53 template nsresult RangeUtils::CompareNodeToRangeBoundaries<TreeKind::Flat>( 54 const nsINode*, const RawRangeBoundary&, const RawRangeBoundary&, bool*, 55 bool*); 56 57 template nsresult RangeUtils::CompareNodeToRange<TreeKind::ShadowIncludingDOM>( 58 const nsINode*, const AbstractRange*, bool*, bool*); 59 template nsresult RangeUtils::CompareNodeToRange<TreeKind::Flat>( 60 const nsINode*, const AbstractRange*, bool*, bool*); 61 62 template Maybe<bool> RangeUtils::IsNodeContainedInRange< 63 TreeKind::ShadowIncludingDOM>(const nsINode&, const AbstractRange*); 64 template Maybe<bool> RangeUtils::IsNodeContainedInRange<TreeKind::Flat>( 65 const nsINode&, const AbstractRange*); 66 67 [[nodiscard]] static inline bool ParentNodeIsInSameSelection( 68 const nsINode& aNode) { 69 // Currently, independent selection root is always the anonymous <div> in a 70 // text control which is an native anonymous subtree root. Therefore, we 71 // can skip most checks if the node is not a root of native anonymous subtree. 72 if (!aNode.IsRootOfNativeAnonymousSubtree()) { 73 return true; 74 } 75 // If the node returns nullptr for frame selection, it means that it's not the 76 // anonymous <div> of the editable content root of a text control or just not 77 // in composed doc. 78 const nsFrameSelection* frameSelection = aNode.GetFrameSelection(); 79 if (!frameSelection || frameSelection->IsIndependentSelection()) { 80 MOZ_ASSERT_IF(aNode.GetClosestNativeAnonymousSubtreeRootParentOrHost(), 81 aNode.GetClosestNativeAnonymousSubtreeRootParentOrHost() 82 ->IsTextControlElement()); 83 return false; 84 } 85 return true; 86 } 87 88 // static 89 nsINode* RangeUtils::ComputeRootNode(nsINode* aNode) { 90 if (!aNode) { 91 return nullptr; 92 } 93 94 if (aNode->IsContent()) { 95 if (aNode->NodeInfo()->NameAtom() == nsGkAtoms::documentTypeNodeName) { 96 return nullptr; 97 } 98 99 nsIContent* content = aNode->AsContent(); 100 101 // If the node is in a shadow tree then the ShadowRoot is the root. 102 // 103 // FIXME(emilio): Should this be after the NAC check below? We can have NAC 104 // inside Shadow DOM which will peek this path rather than the one below. 105 if (ShadowRoot* containingShadow = content->GetContainingShadow()) { 106 return containingShadow; 107 } 108 109 // If the node is in NAC, then the NAC parent should be the root. 110 if (nsINode* root = 111 content->GetClosestNativeAnonymousSubtreeRootParentOrHost()) { 112 return root; 113 } 114 } 115 116 // Elements etc. must be in document or in document fragment, 117 // text nodes in document, in document fragment or in attribute. 118 if (nsINode* root = aNode->GetUncomposedDoc()) { 119 return root; 120 } 121 122 NS_ASSERTION(!aNode->SubtreeRoot()->IsDocument(), 123 "GetUncomposedDoc should have returned a doc"); 124 125 // We allow this because of backward compatibility. 126 return aNode->SubtreeRoot(); 127 } 128 129 // static 130 template <typename SPT, typename SRT, typename EPT, typename ERT> 131 bool RangeUtils::IsValidPoints( 132 const RangeBoundaryBase<SPT, SRT>& aStartBoundary, 133 const RangeBoundaryBase<EPT, ERT>& aEndBoundary) { 134 // Use NS_WARN_IF() only for the cases where the arguments are unexpected. 135 if (NS_WARN_IF(!aStartBoundary.IsSetAndValid()) || 136 NS_WARN_IF(!aEndBoundary.IsSetAndValid())) { 137 return false; 138 } 139 140 MOZ_ASSERT(aStartBoundary.GetTreeKind() == aEndBoundary.GetTreeKind()); 141 142 // Otherwise, don't use NS_WARN_IF() for preventing to make console messy. 143 // Instead, check one by one since it is easier to catch the error reason 144 // with debugger. 145 146 if (ComputeRootNode(aStartBoundary.GetContainer()) != 147 ComputeRootNode(aEndBoundary.GetContainer())) { 148 return false; 149 } 150 151 const Maybe<int32_t> order = 152 nsContentUtils::ComparePoints(aStartBoundary, aEndBoundary); 153 if (!order) { 154 MOZ_ASSERT_UNREACHABLE(); 155 return false; 156 } 157 158 return *order != 1; 159 } 160 161 // static 162 template <TreeKind aKind, typename Dummy> 163 Maybe<bool> RangeUtils::IsNodeContainedInRange( 164 const nsINode& aNode, const AbstractRange* aAbstractRange) { 165 bool nodeIsBeforeRange{false}; 166 bool nodeIsAfterRange{false}; 167 168 const nsresult rv = CompareNodeToRange<aKind>( 169 &aNode, aAbstractRange, &nodeIsBeforeRange, &nodeIsAfterRange); 170 if (NS_FAILED(rv)) { 171 return Nothing(); 172 } 173 174 return Some(!nodeIsBeforeRange && !nodeIsAfterRange); 175 } 176 177 // Utility routine to detect if a content node is completely contained in a 178 // range If outNodeBefore is returned true, then the node starts before the 179 // range does. If outNodeAfter is returned true, then the node ends after the 180 // range does. Note that both of the above might be true. If neither are true, 181 // the node is contained inside of the range. 182 // XXX - callers responsibility to ensure node in same doc as range! 183 184 // static 185 template <TreeKind aKind, typename Dummy> 186 nsresult RangeUtils::CompareNodeToRange(const nsINode* aNode, 187 const AbstractRange* aAbstractRange, 188 bool* aNodeIsBeforeRange, 189 bool* aNodeIsAfterRange) { 190 if (NS_WARN_IF(!aAbstractRange) || 191 NS_WARN_IF(!aAbstractRange->IsPositioned())) { 192 return NS_ERROR_INVALID_ARG; 193 } 194 return CompareNodeToRangeBoundaries<aKind>( 195 aNode, aAbstractRange->MayCrossShadowBoundaryStartRef(), 196 aAbstractRange->MayCrossShadowBoundaryEndRef(), aNodeIsBeforeRange, 197 aNodeIsAfterRange); 198 } 199 200 template <TreeKind aKind, typename SPT, typename SRT, typename EPT, 201 typename ERT, typename Dummy> 202 nsresult RangeUtils::CompareNodeToRangeBoundaries( 203 const nsINode* aNode, const RangeBoundaryBase<SPT, SRT>& aStartBoundary, 204 const RangeBoundaryBase<EPT, ERT>& aEndBoundary, bool* aNodeIsBeforeRange, 205 bool* aNodeIsAfterRange) { 206 MOZ_ASSERT(aNodeIsBeforeRange); 207 MOZ_ASSERT(aNodeIsAfterRange); 208 MOZ_ASSERT(aStartBoundary.GetTreeKind() == aEndBoundary.GetTreeKind()); 209 210 if (NS_WARN_IF(!aNode) || 211 NS_WARN_IF(!aStartBoundary.IsSet() || !aEndBoundary.IsSet())) { 212 return NS_ERROR_INVALID_ARG; 213 } 214 215 // create a pair of dom points that expresses location of node: 216 // NODE(start), NODE(end) 217 // Let incoming range be: 218 // {RANGE(start), RANGE(end)} 219 // if (RANGE(start) <= NODE(start)) and (RANGE(end) => NODE(end)) 220 // then the Node is contained (completely) by the Range. 221 222 // gather up the dom point info 223 int32_t nodeStart; 224 uint32_t nodeEnd; 225 const nsINode* parent = nullptr; 226 227 MOZ_ASSERT_IF(aKind == TreeKind::Flat, 228 StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()); 229 // ShadowRoot has no parent, nor can be represented by parent/offset pair. 230 if (!aNode->IsShadowRoot()) { 231 parent = ShadowDOMSelectionHelpers::GetParentNodeInSameSelection( 232 *aNode, aKind == TreeKind::Flat ? AllowRangeCrossShadowBoundary::Yes 233 : AllowRangeCrossShadowBoundary::No); 234 } 235 236 if (!parent) { 237 // can't make a parent/offset pair to represent start or 238 // end of the root node, because it has no parent. 239 // so instead represent it by (node,0) and (node,numChildren) 240 parent = aNode; 241 nodeStart = 0; 242 nodeEnd = aNode->GetChildCount(); 243 } else if (const auto* slotAsParent = HTMLSlotElement::FromNode(parent); 244 slotAsParent && aKind == TreeKind::Flat) { 245 // aNode is a slotted content, use the index in the assigned nodes 246 // to represent this node. 247 auto index = slotAsParent->AssignedNodes().IndexOf(aNode); 248 nodeStart = index; 249 nodeEnd = nodeStart + 1; 250 } else { 251 nodeStart = parent->ComputeIndexOf_Deprecated(aNode); 252 NS_WARNING_ASSERTION( 253 nodeStart >= 0, 254 "aNode has the parent node but it does not have aNode!"); 255 nodeEnd = nodeStart + 1u; 256 MOZ_ASSERT(nodeStart < 0 || static_cast<uint32_t>(nodeStart) < nodeEnd, 257 "nodeStart should be less than nodeEnd"); 258 } 259 260 // XXX nsContentUtils::ComparePoints() may be expensive. If some callers 261 // just want one of aNodeIsBeforeRange or aNodeIsAfterRange, we can 262 // skip the other comparison. 263 264 // In the ComparePoints calls below we use a container & offset instead of 265 // a range boundary because the range boundary constructor warns if you pass 266 // in a -1 offset and the ComputeIndexOf call above can return -1 if aNode 267 // is native anonymous content. ComparePoints has comments about offsets 268 // being -1 and it seems to deal with it, or at least we aren't aware of any 269 // problems arising because of it. We don't have a better idea how to get 270 // rid of the warning without much larger changes so we do this just to 271 // silence the warning. (Bug 1438996) 272 273 // is RANGE(start) <= NODE(start) ? 274 Maybe<int32_t> order = 275 nsContentUtils::ComparePoints_AllowNegativeOffsets<aKind>( 276 aStartBoundary.GetContainer(), 277 *aStartBoundary.Offset( 278 RangeBoundaryBase<SPT, 279 SRT>::OffsetFilter::kValidOrInvalidOffsets), 280 parent, nodeStart); 281 if (NS_WARN_IF(!order)) { 282 return NS_ERROR_DOM_WRONG_DOCUMENT_ERR; 283 } 284 *aNodeIsBeforeRange = *order > 0; 285 // is RANGE(end) >= NODE(end) ? 286 order = nsContentUtils::ComparePointsWithIndices<aKind>( 287 aEndBoundary.GetContainer(), 288 *aEndBoundary.Offset( 289 RangeBoundaryBase<EPT, ERT>::OffsetFilter::kValidOrInvalidOffsets), 290 parent, nodeEnd); 291 292 if (NS_WARN_IF(!order)) { 293 return NS_ERROR_DOM_WRONG_DOCUMENT_ERR; 294 } 295 *aNodeIsAfterRange = *order < 0; 296 297 return NS_OK; 298 } 299 300 // static 301 nsINode* ShadowDOMSelectionHelpers::GetStartContainer( 302 const AbstractRange* aRange, 303 AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { 304 MOZ_ASSERT(aRange); 305 return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && 306 aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) 307 ? aRange->GetMayCrossShadowBoundaryStartContainer() 308 : aRange->GetStartContainer(); 309 } 310 311 // static 312 uint32_t ShadowDOMSelectionHelpers::StartOffset( 313 const AbstractRange* aRange, 314 AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { 315 MOZ_ASSERT(aRange); 316 return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && 317 aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) 318 ? aRange->MayCrossShadowBoundaryStartOffset() 319 : aRange->StartOffset(); 320 } 321 322 // static 323 nsINode* ShadowDOMSelectionHelpers::GetEndContainer( 324 const AbstractRange* aRange, 325 AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { 326 MOZ_ASSERT(aRange); 327 return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && 328 aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) 329 ? aRange->GetMayCrossShadowBoundaryEndContainer() 330 : aRange->GetEndContainer(); 331 } 332 333 // static 334 uint32_t ShadowDOMSelectionHelpers::EndOffset( 335 const AbstractRange* aRange, 336 AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { 337 MOZ_ASSERT(aRange); 338 return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && 339 aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) 340 ? aRange->MayCrossShadowBoundaryEndOffset() 341 : aRange->EndOffset(); 342 } 343 344 // static 345 nsINode* ShadowDOMSelectionHelpers::GetParentNodeInSameSelection( 346 const nsINode& aNode, 347 AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { 348 if (!ParentNodeIsInSameSelection(aNode)) { 349 return nullptr; 350 } 351 352 if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && 353 aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) { 354 if (aNode.IsContent()) { 355 if (HTMLSlotElement* slot = aNode.AsContent()->GetAssignedSlot(); 356 slot && GetShadowRoot(slot->GetContainingShadowHost(), 357 aAllowCrossShadowBoundary)) { 358 return slot; 359 } 360 } 361 return aNode.GetParentOrShadowHostNode(); 362 } 363 return aNode.GetParentNode(); 364 } 365 366 // static 367 ShadowRoot* ShadowDOMSelectionHelpers::GetShadowRoot( 368 const nsINode* aNode, 369 AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { 370 MOZ_ASSERT(aNode); 371 return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && 372 aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) 373 ? aNode->GetShadowRootForSelection() 374 : nullptr; 375 } // namespace dom 376 377 } // namespace mozilla