DirectionalityUtils.cpp (33920B)
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 /* 8 This file contains most of the code to implement html directionality. 9 This includes default direction, inheritance, and auto directionality. 10 11 A useful perspective is separating the static and dynamic case. 12 In the static case, directionality is computed based on the current DOM, 13 closely following the specification, e.g. in ComputeAutoDirectionality. 14 Dynamic changes, e.g. OnSetDirAttr, are propagated to the impacted nodes, 15 for which the static case is re-run. 16 17 To minimize searching for dir=auto nodes impacted by a dynamic change, several 18 flags are maintained (see their declaration for documentation): 19 - NodeAncestorHasDirAuto and NodeAffectsDirAutoSlot apply to all nodes. 20 They are set when a node is placed somewhere in the tree and set or cleared 21 when a dir attribute changes. 22 - NS_MAY_SET_DIR_AUTO applies to text. It is set whenever a text node might be 23 responsible for the auto directionality of a dir=auto element. It is cleared 24 when the element is unbound. 25 */ 26 27 #include "mozilla/dom/DirectionalityUtils.h" 28 29 #include "mozilla/Maybe.h" 30 #include "mozilla/dom/CharacterDataBuffer.h" 31 #include "mozilla/dom/Document.h" 32 #include "mozilla/dom/Element.h" 33 #include "mozilla/dom/HTMLInputElement.h" 34 #include "mozilla/dom/HTMLSlotElement.h" 35 #include "mozilla/dom/HTMLTextAreaElement.h" 36 #include "mozilla/dom/ShadowRoot.h" 37 #include "mozilla/dom/Text.h" 38 #include "mozilla/dom/UnbindContext.h" 39 #include "mozilla/intl/UnicodeProperties.h" 40 #include "nsAttrValue.h" 41 #include "nsIContent.h" 42 #include "nsIContentInlines.h" 43 #include "nsINode.h" 44 #include "nsUnicodeProperties.h" 45 46 namespace mozilla { 47 48 using mozilla::dom::Element; 49 using mozilla::dom::HTMLInputElement; 50 using mozilla::dom::HTMLSlotElement; 51 using mozilla::dom::ShadowRoot; 52 using mozilla::dom::Text; 53 54 /** 55 * Returns true if aElement is one of the elements whose text content should 56 * affect its own direction, or the direction of ancestors with dir=auto. 57 * 58 * Note that this does not include <bdi>, whose content does affect its own 59 * direction when it has dir=auto (which it has by default), so one needs to 60 * test for it separately, e.g. with EstablishesOwnDirection. 61 * It *does* include textarea, because even if a textarea has dir=auto, it has 62 * unicode-bidi: plaintext and is handled automatically in bidi resolution. 63 * It also includes `input`, because it takes the `dir` value from its value 64 * attribute, instead of the child nodes. 65 */ 66 static bool ParticipatesInAutoDirection(const nsIContent* aContent) { 67 if (aContent->IsInNativeAnonymousSubtree()) { 68 return false; 69 } 70 if (aContent->IsShadowRoot()) { 71 return true; 72 } 73 return !aContent->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::style, 74 nsGkAtoms::input, nsGkAtoms::textarea); 75 } 76 77 static bool IsAutoDirectionalityFormAssociatedElement(Element* aElement) { 78 if (HTMLInputElement* input = HTMLInputElement::FromNode(aElement)) { 79 return input->IsAutoDirectionalityAssociated(); 80 } 81 return aElement->IsHTMLElement(nsGkAtoms::textarea); 82 } 83 84 static Maybe<nsAutoString> GetValueIfFormAssociatedElement(Element* aElement) { 85 Maybe<nsAutoString> result; 86 if (HTMLInputElement* input = HTMLInputElement::FromNode(aElement)) { 87 if (input->IsAutoDirectionalityAssociated()) { 88 // It's unclear if per spec we should use the sanitized or unsanitized 89 // value to set the directionality. But input may provide a known value 90 // to us, which is unsanitized, so be consistent. Using what the user is 91 // seeing to determine directionality instead of the sanitized 92 // (empty if invalid) value probably makes more sense. 93 result.emplace(); 94 input->GetValueInternal(*result, dom::CallerType::System); 95 } 96 } else if (dom::HTMLTextAreaElement* ta = 97 dom::HTMLTextAreaElement::FromNode(aElement)) { 98 result.emplace(); 99 ta->GetValue(*result); 100 } 101 return result; 102 } 103 104 /** 105 * Returns the directionality of a Unicode character 106 */ 107 static Directionality GetDirectionFromChar(uint32_t ch) { 108 switch (intl::UnicodeProperties::GetBidiClass(ch)) { 109 case intl::BidiClass::RightToLeft: 110 case intl::BidiClass::RightToLeftArabic: 111 return Directionality::Rtl; 112 113 case intl::BidiClass::LeftToRight: 114 return Directionality::Ltr; 115 116 default: 117 return Directionality::Unset; 118 } 119 } 120 121 /** 122 * Returns true if aElement establishes its own direction or does not have one. 123 * 124 * From https://html.spec.whatwg.org/#auto-directionality step 3.1., this is 125 * bdi, script, style, textarea, and elements with auto, ltr or rtl dir. 126 * Additionally, it includes input as the class handles directionality itself. 127 */ 128 inline static bool EstablishesOwnDirection(const Element* aElement) { 129 return !ParticipatesInAutoDirection(aElement) || 130 aElement->IsHTMLElement(nsGkAtoms::bdi) || aElement->HasFixedDir() || 131 aElement->HasDirAuto(); 132 } 133 134 /** 135 * Returns true if aContent is dir=auto, affects a dir=auto ancestor, is 136 * assigned to a dir=auto slot, or has an ancestor assigned to a dir=auto slot. 137 * 138 * It's false for input and textarea as they handle their directionality 139 * themselves. We are concerned about steps 2 and 3 of 140 * https://html.spec.whatwg.org/#auto-directionality 141 */ 142 inline static bool AffectsDirAutoElement(nsIContent* aContent) { 143 return aContent && ParticipatesInAutoDirection(aContent) && 144 (aContent->NodeOrAncestorHasDirAuto() || 145 aContent->AffectsDirAutoSlot()); 146 } 147 148 Directionality GetDirectionFromText(const char16_t* aText, 149 const uint32_t aLength, 150 uint32_t* aFirstStrong) { 151 const char16_t* start = aText; 152 const char16_t* end = aText + aLength; 153 154 while (start < end) { 155 uint32_t current = start - aText; 156 uint32_t ch = *start++; 157 158 if (start < end && NS_IS_SURROGATE_PAIR(ch, *start)) { 159 ch = SURROGATE_TO_UCS4(ch, *start++); 160 current++; 161 } 162 163 // Just ignore lone surrogates 164 if (!IS_SURROGATE(ch)) { 165 Directionality dir = GetDirectionFromChar(ch); 166 if (dir != Directionality::Unset) { 167 if (aFirstStrong) { 168 *aFirstStrong = current; 169 } 170 return dir; 171 } 172 } 173 } 174 175 if (aFirstStrong) { 176 *aFirstStrong = UINT32_MAX; 177 } 178 return Directionality::Unset; 179 } 180 181 static Directionality GetDirectionFromText(const char* aText, 182 const uint32_t aLength, 183 uint32_t* aFirstStrong = nullptr) { 184 const char* start = aText; 185 const char* end = aText + aLength; 186 187 while (start < end) { 188 uint32_t current = start - aText; 189 unsigned char ch = (unsigned char)*start++; 190 191 Directionality dir = GetDirectionFromChar(ch); 192 if (dir != Directionality::Unset) { 193 if (aFirstStrong) { 194 *aFirstStrong = current; 195 } 196 return dir; 197 } 198 } 199 200 if (aFirstStrong) { 201 *aFirstStrong = UINT32_MAX; 202 } 203 return Directionality::Unset; 204 } 205 206 static Directionality GetDirectionFromText(const Text* aTextNode, 207 uint32_t* aFirstStrong = nullptr) { 208 const dom::CharacterDataBuffer* characterDataBuffer = 209 &aTextNode->DataBuffer(); 210 if (characterDataBuffer->Is2b()) { 211 return GetDirectionFromText(characterDataBuffer->Get2b(), 212 characterDataBuffer->GetLength(), aFirstStrong); 213 } 214 215 return GetDirectionFromText(characterDataBuffer->Get1b(), 216 characterDataBuffer->GetLength(), aFirstStrong); 217 } 218 219 /** 220 * Compute auto direction for aRoot. If aCanExcludeRoot is true and aRoot 221 * establishes its own directionality, return early. 222 * https://html.spec.whatwg.org/#contained-text-auto-directionality 223 */ 224 Directionality ContainedTextAutoDirectionality(nsINode* aRoot, 225 bool aCanExcludeRoot) { 226 MOZ_ASSERT_IF(aCanExcludeRoot, aRoot->IsElement()); 227 if (aCanExcludeRoot && EstablishesOwnDirection(aRoot->AsElement())) { 228 return Directionality::Unset; 229 } 230 231 nsIContent* child = aRoot->GetFirstChild(); 232 while (child) { 233 if (child->IsElement() && EstablishesOwnDirection(child->AsElement())) { 234 child = child->GetNextNonChildNode(aRoot); 235 continue; 236 } 237 238 // Step 1.2. If descendant is a slot element whose root is a shadow root, 239 // then return the directionality of that shadow root's host. 240 if (auto* slot = HTMLSlotElement::FromNode(child)) { 241 if (const ShadowRoot* sr = slot->GetContainingShadow()) { 242 Element* host = sr->GetHost(); 243 MOZ_ASSERT(host); 244 return host->GetDirectionality(); 245 } 246 } 247 248 // Step 1.3-5. If descendant is a Text node, return its 249 // text node directionality. 250 if (auto* text = Text::FromNode(child)) { 251 Directionality textNodeDir = GetDirectionFromText(text); 252 if (textNodeDir != Directionality::Unset) { 253 text->SetMaySetDirAuto(); 254 return textNodeDir; 255 } 256 } 257 child = child->GetNextNode(aRoot); 258 } 259 260 return Directionality::Unset; 261 } 262 263 static Directionality ComputeAutoDirectionality(Element* aElement, 264 bool aNotify); 265 266 /** 267 * Compute auto direction aSlot should have based on assigned nodes 268 * https://html.spec.whatwg.org/#auto-directionality step 2 269 */ 270 Directionality ComputeAutoDirectionFromAssignedNodes( 271 HTMLSlotElement* aSlot, Span<const RefPtr<nsINode>> aAssignedNodes, 272 bool aNotify) { 273 // Step 2.1. For each node child of element's assigned nodes: 274 for (const RefPtr<nsINode>& assignedNode : aAssignedNodes) { 275 // Step 2.1.1. Let childDirection be null. 276 Directionality childDirection = Directionality::Unset; 277 278 // Step 2.1.2. If child is a Text node... 279 if (auto* text = Text::FromNode(assignedNode)) { 280 childDirection = GetDirectionFromText(text); 281 if (childDirection != Directionality::Unset) { 282 text->SetMaySetDirAuto(); 283 } 284 } else { 285 // Step 2.1.3.1. Assert: child is an Element node. 286 Element* assignedElement = Element::FromNode(assignedNode); 287 MOZ_ASSERT(assignedElement); 288 289 // Step 2.1.3.2. 290 childDirection = ContainedTextAutoDirectionality(assignedElement, true); 291 } 292 293 // Step 2.1.4. If childDirection is not null, then return childDirection. 294 if (childDirection != Directionality::Unset) { 295 return childDirection; 296 } 297 } 298 // Step 2.2. Return null. 299 return Directionality::Unset; 300 } 301 302 /** 303 * Set the directionality of a node with dir=auto as defined in 304 * https://html.spec.whatwg.org/#auto-directionality, 305 * not including step 1: auto-directionality form-associated elements, this is 306 * implemented by the elements themselves. 307 * 308 * Sets NodeMaySetDirAuto on the text node that determined the direction. 309 */ 310 static Directionality ComputeAutoDirectionality(Element* aElement, 311 bool aNotify) { 312 MOZ_ASSERT(aElement, "Must have an element"); 313 MOZ_ASSERT(ParticipatesInAutoDirection(aElement), 314 "Cannot compute auto directionality of this element"); 315 316 // Step 2. If element is a slot element whose root is a shadow root and 317 // element's assigned nodes are not empty: 318 if (auto* slot = HTMLSlotElement::FromNode(aElement)) { 319 const Span assignedNodes = slot->AssignedNodes(); 320 if (!assignedNodes.IsEmpty()) { 321 MOZ_ASSERT(slot->IsInShadowTree()); 322 return ComputeAutoDirectionFromAssignedNodes(slot, assignedNodes, 323 aNotify); 324 } 325 } 326 327 // Step 3. find first text or slot that determines the direction 328 Directionality nodeDir = ContainedTextAutoDirectionality(aElement, false); 329 if (nodeDir != Directionality::Unset) { 330 return nodeDir; 331 } 332 333 // Step 4. return null 334 return Directionality::Unset; 335 } 336 337 Directionality GetParentDirectionality(const Element* aElement) { 338 if (nsIContent* parent = aElement->GetParent()) { 339 if (ShadowRoot* shadow = ShadowRoot::FromNode(parent)) { 340 parent = shadow->GetHost(); 341 } 342 if (parent && parent->IsElement()) { 343 // If the node doesn't have an explicit dir attribute with a valid value, 344 // the directionality is the same as the parent element (but don't 345 // propagate the parent directionality if it isn't set yet). 346 Directionality parentDir = parent->AsElement()->GetDirectionality(); 347 if (parentDir != Directionality::Unset) { 348 return parentDir; 349 } 350 } 351 } 352 return Directionality::Ltr; 353 } 354 355 Directionality RecomputeDirectionality(Element* aElement, bool aNotify) { 356 MOZ_ASSERT(!aElement->HasDirAuto(), 357 "RecomputeDirectionality called with dir=auto"); 358 359 if (aElement->HasValidDir()) { 360 return aElement->GetDirectionality(); 361 } 362 363 // https://html.spec.whatwg.org/multipage/dom.html#the-directionality: 364 // 365 // If the element is an input element whose type attribute is in the 366 // Telephone state, and the dir attribute is not in a defined state 367 // (i.e. it is not present or has an invalid value) 368 // 369 // The directionality of the element is 'ltr'. 370 if (auto* input = HTMLInputElement::FromNode(*aElement)) { 371 if (input->ControlType() == FormControlType::InputTel) { 372 aElement->SetDirectionality(Directionality::Ltr, aNotify); 373 return Directionality::Ltr; 374 } 375 } 376 377 const Directionality dir = GetParentDirectionality(aElement); 378 aElement->SetDirectionality(dir, aNotify); 379 return dir; 380 } 381 382 // Whether the element establishes its own directionality and the one of its 383 // descendants. 384 static inline bool IsBoundary(const Element& aElement) { 385 return aElement.HasValidDir() || aElement.HasDirAuto(); 386 } 387 388 static void ResetAutoDirection(Element* aElement, bool aNotify); 389 390 /** 391 * Called when shadow root host changes direction. Reset auto directionality 392 * for dir=auto descendants whose direction may depend on the host 393 * directionality through a slot element. 394 * 395 * Dynamic update for https://html.spec.whatwg.org/#auto-directionality step 3.2 396 * If descendant is a slot element whose root is a shadow root, then return 397 * the directionality of that shadow root's host. 398 */ 399 static void ResetAutoDirectionForAncestorsOfSlotDescendants(ShadowRoot* aShadow, 400 Directionality aDir, 401 bool aNotify) { 402 // For now, reset auto directionality for all descendants, not only those 403 // that have a slot descendant. 404 for (nsIContent* cur = aShadow->GetFirstChild(); cur; 405 cur = cur->GetNextNode(aShadow)) { 406 if (Element* element = Element::FromNode(cur)) { 407 if (element->HasDirAuto() && element->GetDirectionality() != aDir && 408 ParticipatesInAutoDirection(element)) { 409 ResetAutoDirection(element, aNotify); 410 } 411 } 412 } 413 } 414 415 static void SetDirectionalityOnDescendantsInternal(nsINode* aNode, 416 Directionality aDir, 417 bool aNotify) { 418 if (auto* element = Element::FromNode(aNode)) { 419 if (ShadowRoot* shadow = element->GetShadowRoot()) { 420 // host direction changed, propagate it through slots to dir=auto elements 421 ResetAutoDirectionForAncestorsOfSlotDescendants(shadow, aDir, aNotify); 422 423 SetDirectionalityOnDescendantsInternal(shadow, aDir, aNotify); 424 } 425 } 426 427 for (nsIContent* child = aNode->GetFirstChild(); child;) { 428 auto* element = Element::FromNode(child); 429 if (!element) { 430 child = child->GetNextNode(aNode); 431 continue; 432 } 433 434 if (IsBoundary(*element) || element->GetDirectionality() == aDir) { 435 // If the element is a directionality boundary, or already 436 // has the right directionality, then we can skip the whole subtree. 437 child = child->GetNextNonChildNode(aNode); 438 continue; 439 } 440 441 element->SetDirectionality(aDir, aNotify); 442 443 if (ShadowRoot* shadow = element->GetShadowRoot()) { 444 ResetAutoDirectionForAncestorsOfSlotDescendants(shadow, aDir, aNotify); 445 446 SetDirectionalityOnDescendantsInternal(shadow, aDir, aNotify); 447 } 448 449 child = child->GetNextNode(aNode); 450 } 451 } 452 453 // We want the public version of this only to acc 454 void SetDirectionalityOnDescendants(Element* aElement, Directionality aDir, 455 bool aNotify) { 456 return SetDirectionalityOnDescendantsInternal(aElement, aDir, aNotify); 457 } 458 459 static void ResetAutoDirection(Element* aElement, bool aNotify) { 460 MOZ_ASSERT(aElement->HasDirAuto()); 461 Directionality dir = ComputeAutoDirectionality(aElement, aNotify); 462 if (dir == Directionality::Unset) { 463 dir = Directionality::Ltr; 464 } 465 if (dir != aElement->GetDirectionality()) { 466 aElement->SetDirectionality(dir, aNotify); 467 SetDirectionalityOnDescendants(aElement, aElement->GetDirectionality(), 468 aNotify); 469 } 470 } 471 472 /** 473 * Reset auto direction of the dir=auto elements that aElement might impact. 474 * Walk the parent chain till a dir=auto element is found, also reset dir=auto 475 * slots an ancestor might be assigned to. 476 */ 477 static void WalkAncestorsResetAutoDirection(Element* aElement, bool aNotify) { 478 for (nsIContent* ancestor = aElement; AffectsDirAutoElement(ancestor); 479 ancestor = ancestor->GetParent()) { 480 if (HTMLSlotElement* slot = ancestor->GetAssignedSlot()) { 481 if (slot->HasDirAuto()) { 482 ResetAutoDirection(slot, aNotify); 483 } 484 } 485 486 auto* ancestorElement = Element::FromNode(*ancestor); 487 if (ancestorElement && ancestorElement->HasDirAuto()) { 488 ResetAutoDirection(ancestorElement, aNotify); 489 } 490 } 491 } 492 493 void SlotStateChanged(HTMLSlotElement* aSlot) { 494 if (aSlot->HasDirAuto()) { 495 ResetAutoDirection(aSlot, true); 496 } 497 } 498 499 static void DownwardPropagateDirAutoFlags(nsINode* aRoot) { 500 bool affectsAncestor = aRoot->NodeOrAncestorHasDirAuto(), 501 affectsSlot = aRoot->AffectsDirAutoSlot(); 502 if (!affectsAncestor && !affectsSlot) { 503 return; 504 } 505 506 nsIContent* child = aRoot->GetFirstChild(); 507 while (child) { 508 if (child->IsElement() && EstablishesOwnDirection(child->AsElement())) { 509 child = child->GetNextNonChildNode(aRoot); 510 continue; 511 } 512 513 if (affectsAncestor) { 514 child->SetAncestorHasDirAuto(); 515 } 516 if (affectsSlot) { 517 child->SetAffectsDirAutoSlot(); 518 } 519 child = child->GetNextNode(aRoot); 520 } 521 } 522 523 /** 524 * aContent no longer affects the auto directionality of it's assigned slot, 525 * e.g. as it is removed from the slot or the slot no longer has dir=auto. 526 * Check if aContent impacts another slot and otherwise clear the flag. 527 */ 528 static void MaybeClearAffectsDirAutoSlot(nsIContent* aContent) { 529 DebugOnly<HTMLSlotElement*> slot = aContent->GetAssignedSlot(); 530 MOZ_ASSERT(!slot || !slot->HasDirAuto(), 531 "Function expects aContent not to impact its assigned slot"); 532 // check if aContent still inherits the flag from its parent 533 if (Element* parent = aContent->GetParentElement()) { 534 // do not check EstablishesOwnDirection(parent), as it is only true despite 535 // AffectsDirAutoSlot if parent is directly assigned to a dir=auto slot 536 if (parent->AffectsDirAutoSlot() && 537 !(aContent->IsElement() && 538 EstablishesOwnDirection(aContent->AsElement()))) { 539 MOZ_ASSERT(aContent->AffectsDirAutoSlot()); 540 return; 541 } 542 } 543 544 aContent->ClearAffectsDirAutoSlot(); 545 546 nsIContent* child = aContent->GetFirstChild(); 547 while (child) { 548 if (child->IsElement() && EstablishesOwnDirection(child->AsElement())) { 549 child = child->GetNextNonChildNode(aContent); 550 continue; 551 } 552 if (HTMLSlotElement* slot = child->GetAssignedSlot()) { 553 if (slot->HasDirAuto()) { 554 child = child->GetNextNonChildNode(aContent); 555 continue; 556 } 557 } 558 559 child->ClearAffectsDirAutoSlot(); 560 child = child->GetNextNode(aContent); 561 } 562 } 563 564 void SlotAssignedNodeAdded(HTMLSlotElement* aSlot, nsIContent& aAssignedNode) { 565 MOZ_ASSERT(aSlot); 566 if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { 567 if (aSlot->IsMaybeSelected()) { 568 // Normally it's nsRange::ContentAppended's responsibility to 569 // mark new descendants, however this doesn't work for slotted 570 // content because nsRange observes the common ancestor of 571 // start/end, whereas slotted element may not have the same 572 // ancestor as them. 573 dom::AbstractRange::UpdateDescendantsInFlattenedTree( 574 aAssignedNode, true /* aMarkDesendants*/); 575 } 576 } 577 578 if (aSlot->HasDirAuto()) { 579 aAssignedNode.SetAffectsDirAutoSlot(); 580 DownwardPropagateDirAutoFlags(&aAssignedNode); 581 } 582 SlotStateChanged(aSlot); 583 } 584 585 void SlotAssignedNodeRemoved(HTMLSlotElement* aSlot, 586 nsIContent& aUnassignedNode) { 587 if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && 588 aUnassignedNode.IsMaybeSelected()) { 589 // Normally, this shouldn't happen because nsRange::ContentRemoved 590 // should be called for content removal, and then 591 // AbstractRange::UnmarkDescendants will be used to clear the flags. 592 // Though this doesn't work for slotted element because nsRange 593 // observers the common ancestor of start/end, whereas slotted element 594 // may not have the same ancestor as them, so we have to clear 595 // the flags manually here. 596 dom::AbstractRange::UpdateDescendantsInFlattenedTree( 597 aUnassignedNode, false /* aMarkDesendants*/); 598 } 599 600 if (aSlot->HasDirAuto()) { 601 MaybeClearAffectsDirAutoSlot(&aUnassignedNode); 602 } 603 SlotStateChanged(aSlot); 604 } 605 606 /** 607 * When dir=auto was set on aElement, reset it's auto direction and set the 608 * flag on descendants 609 */ 610 void WalkDescendantsSetDirAuto(Element* aElement, bool aNotify) { 611 MOZ_ASSERT(aElement->HasDirAuto()); 612 // Only test for ParticipatesInAutoDirection -- in other words, if aElement is 613 // a <bdi> which is having its dir attribute set to auto (or 614 // removed or set to an invalid value, which are equivalent to dir=auto for 615 // <bdi>, we *do* want to set AncestorHasDirAuto on its descendants, unlike 616 // in SetDirOnBind where we don't propagate AncestorHasDirAuto to a <bdi> 617 // being bound to an existing node with dir=auto. 618 if (ParticipatesInAutoDirection(aElement) && 619 !aElement->AncestorHasDirAuto()) { 620 DownwardPropagateDirAutoFlags(aElement); 621 } 622 623 ResetAutoDirection(aElement, aNotify); 624 } 625 626 void WalkDescendantsClearAncestorDirAuto(nsIContent* aContent) { 627 nsIContent* child = aContent->GetFirstChild(); 628 while (child) { 629 if (child->IsElement() && EstablishesOwnDirection(child->AsElement())) { 630 child = child->GetNextNonChildNode(aContent); 631 continue; 632 } 633 634 child->ClearAncestorHasDirAuto(); 635 child = child->GetNextNode(aContent); 636 } 637 } 638 639 /** 640 * Returns whether answer is definitive, i.e. whether we found all dir=auto 641 * elements impacted by aContent. 642 * This is false when we hit the top of an ancestor chain without finding a 643 * dir=auto element or an element with a fixed direction. 644 * This is useful when processing node removals, since we might need to look at 645 * the subtree we're removing from. 646 */ 647 static bool FindDirAutoElementsFrom(nsIContent* aContent, 648 nsTArray<Element*>& aElements) { 649 if (!AffectsDirAutoElement(aContent)) { 650 return true; 651 } 652 653 for (nsIContent* ancestor = aContent; AffectsDirAutoElement(ancestor); 654 ancestor = ancestor->GetParent()) { 655 if (HTMLSlotElement* slot = ancestor->GetAssignedSlot()) { 656 if (slot->HasDirAuto()) { 657 aElements.AppendElement(slot); 658 // need to check whether there are more dir=auto slots or ancestors 659 nsIContent* parent = ancestor->GetParent(); 660 MOZ_ASSERT(parent, "Slotted content must have a parent"); 661 if (!parent->AffectsDirAutoSlot() && 662 !ancestor->NodeOrAncestorHasDirAuto()) { 663 return true; 664 } 665 } 666 } 667 668 auto* ancestorElement = Element::FromNode(*ancestor); 669 if (ancestorElement && ancestorElement->HasDirAuto()) { 670 aElements.AppendElement(ancestorElement); 671 return true; 672 } 673 if (ancestorElement && ancestorElement->IsInShadowTree() && 674 ancestorElement->IsHTMLElement(nsGkAtoms::slot)) { 675 // further ancestors will inherit directionality from shadow host 676 // https://html.spec.whatwg.org/#auto-directionality step 3.2 677 // if descendant is a slot in a shadow DOM, return host directionality 678 return true; 679 } 680 } 681 682 return false; 683 } 684 685 /** 686 * Reset auto directionality of ancestors of aTextNode 687 */ 688 static void SetAncestorDirectionIfAuto(Text* aTextNode, Directionality aDir, 689 bool aNotify = true) { 690 AutoTArray<Element*, 4> autoElements; 691 FindDirAutoElementsFrom(aTextNode, autoElements); 692 for (Element* autoElement : autoElements) { 693 if (autoElement->GetDirectionality() == aDir) { 694 // If we know that the directionality is already correct, we don't need to 695 // reset it. But we might be responsible for the directionality of 696 // parentElement. 697 MOZ_ASSERT(aDir != Directionality::Unset); 698 aTextNode->SetMaySetDirAuto(); 699 } else { 700 // Otherwise recompute the directionality of parentElement. 701 ResetAutoDirection(autoElement, aNotify); 702 } 703 } 704 } 705 706 bool TextNodeWillChangeDirection(Text* aTextNode, Directionality* aOldDir, 707 uint32_t aOffset) { 708 if (!AffectsDirAutoElement(aTextNode)) { 709 return false; 710 } 711 712 // If the change has happened after the first character with strong 713 // directionality in the text node, do nothing. 714 uint32_t firstStrong; 715 *aOldDir = GetDirectionFromText(aTextNode, &firstStrong); 716 return (aOffset <= firstStrong); 717 } 718 719 void TextNodeChangedDirection(Text* aTextNode, Directionality aOldDir, 720 bool aNotify) { 721 MOZ_ASSERT(AffectsDirAutoElement(aTextNode), "Caller should check"); 722 Directionality newDir = GetDirectionFromText(aTextNode); 723 if (newDir == aOldDir) { 724 return; 725 } 726 // If the old directionality is Unset, we might determine now dir=auto 727 // ancestor direction now, even if we don't have the MaySetDirAuto flag. 728 // 729 // Otherwise we used to have a strong directionality and either no longer 730 // does, or it changed. We might need to reset the direction. 731 if (aOldDir == Directionality::Unset || aTextNode->MaySetDirAuto()) { 732 SetAncestorDirectionIfAuto(aTextNode, newDir, aNotify); 733 } 734 } 735 736 void SetDirectionFromNewTextNode(Text* aTextNode) { 737 // Need to check parent as aTextNode does not yet have flags set 738 if (!AffectsDirAutoElement(aTextNode->GetParent())) { 739 return; 740 } 741 742 nsIContent* parent = aTextNode->GetParent(); 743 MOZ_ASSERT(parent); 744 if (parent->NodeOrAncestorHasDirAuto()) { 745 aTextNode->SetAncestorHasDirAuto(); 746 } 747 if (parent->AffectsDirAutoSlot()) { 748 aTextNode->SetAffectsDirAutoSlot(); 749 } 750 751 Directionality dir = GetDirectionFromText(aTextNode); 752 if (dir != Directionality::Unset) { 753 SetAncestorDirectionIfAuto(aTextNode, dir); 754 } 755 } 756 757 /** 758 * Reset auto directionality for impacted elements when aTextNode is removed 759 */ 760 void ResetDirectionSetByTextNode(Text* aTextNode, 761 dom::UnbindContext& aContext) { 762 MOZ_ASSERT(!aTextNode->IsInComposedDoc(), "Should be disconnected already"); 763 if (!aTextNode->MaySetDirAuto()) { 764 return; 765 } 766 AutoTArray<Element*, 4> autoElements; 767 bool answerIsDefinitive = FindDirAutoElementsFrom(aTextNode, autoElements); 768 769 if (answerIsDefinitive) { 770 // All dir=auto elements are in our (now detached) subtree. We're done, as 771 // nothing really changed for our purposes. 772 return; 773 } 774 // The dir=auto element might have been on the element we're unbinding from. 775 // In any case, this text node is clearly no longer what determines its 776 // directionality. 777 aTextNode->ClearMaySetDirAuto(); 778 auto* unboundFrom = 779 nsIContent::FromNodeOrNull(aContext.GetOriginalSubtreeParent()); 780 if (!unboundFrom || !AffectsDirAutoElement(unboundFrom)) { 781 return; 782 } 783 784 Directionality dir = GetDirectionFromText(aTextNode); 785 if (dir == Directionality::Unset) { 786 return; 787 } 788 789 autoElements.Clear(); 790 FindDirAutoElementsFrom(unboundFrom, autoElements); 791 for (Element* autoElement : autoElements) { 792 if (autoElement->GetDirectionality() != dir) { 793 // it's dir was not determined by this text node 794 continue; 795 } 796 ResetAutoDirection(autoElement, /* aNotify = */ true); 797 } 798 } 799 800 void ResetDirFormAssociatedElement(Element* aElement, bool aNotify, 801 bool aHasDirAuto, 802 const nsAString* aKnownValue) { 803 if (aHasDirAuto) { 804 Directionality dir = Directionality::Unset; 805 806 if (aKnownValue && IsAutoDirectionalityFormAssociatedElement(aElement)) { 807 dir = GetDirectionFromText(aKnownValue->BeginReading(), 808 aKnownValue->Length()); 809 } else if (!aKnownValue) { 810 if (Maybe<nsAutoString> maybe = 811 GetValueIfFormAssociatedElement(aElement)) { 812 dir = GetDirectionFromText(maybe.value().BeginReading(), 813 maybe.value().Length()); 814 } 815 } 816 817 // https://html.spec.whatwg.org/#the-directionality 818 // If auto directionality returns null, then return ltr 819 if (dir == Directionality::Unset) { 820 dir = Directionality::Ltr; 821 } 822 823 if (aElement->GetDirectionality() != dir) { 824 aElement->SetDirectionality(dir, aNotify); 825 } 826 } 827 828 // If aElement is assigned to a dir=auto slot, it might determine its auto 829 // directionality 830 if (HTMLSlotElement* slot = aElement->GetAssignedSlot()) { 831 if (slot->HasDirAuto() && 832 slot->GetDirectionality() != aElement->GetDirectionality()) { 833 ResetAutoDirection(slot, aNotify); 834 } 835 } 836 } 837 838 void OnSetDirAttr(Element* aElement, const nsAttrValue* aNewValue, 839 bool hadValidDir, bool hadDirAuto, bool aNotify) { 840 if (!ParticipatesInAutoDirection(aElement)) { 841 return; 842 } 843 844 auto* elementAsSlot = HTMLSlotElement::FromNode(aElement); 845 846 // If element was a boundary but is no more, inherit flags to subtree 847 if ((hadDirAuto || hadValidDir) && !EstablishesOwnDirection(aElement)) { 848 if (auto* slot = aElement->GetAssignedSlot()) { 849 if (slot->HasDirAuto()) { 850 aElement->SetAffectsDirAutoSlot(); 851 } 852 } 853 if (auto* parent = aElement->GetParent()) { 854 DownwardPropagateDirAutoFlags(parent); 855 } 856 } 857 858 if (AffectsDirAutoElement(aElement)) { 859 // The element is a descendant of an element with dir = auto, is having its 860 // dir attribute changed. Reset the direction of any of its ancestors whose 861 // direction might be determined by a text node descendant 862 WalkAncestorsResetAutoDirection(aElement, aNotify); 863 } else if (hadDirAuto && !aElement->HasDirAuto()) { 864 // The element isn't a descendant of an element with dir = auto, and is 865 // having its dir attribute set to something other than auto. 866 // Walk the descendant tree and clear the AncestorHasDirAuto flag. 867 // 868 // N.B: For elements other than <bdi> it would be enough to test that the 869 // current value of dir was "auto" in BeforeSetAttr to know that we 870 // were unsetting dir="auto". For <bdi> things are more complicated, 871 // since it behaves like dir="auto" whenever the dir attribute is 872 // empty or invalid, so we would have to check whether the old value 873 // was not either "ltr" or "rtl", and the new value was either "ltr" 874 // or "rtl". Element::HasDirAuto() encapsulates all that, so doing it 875 // here is simpler. 876 WalkDescendantsClearAncestorDirAuto(aElement); 877 if (elementAsSlot) { 878 for (const auto& assignedNode : elementAsSlot->AssignedNodes()) { 879 MaybeClearAffectsDirAutoSlot(assignedNode->AsContent()); 880 } 881 } 882 } 883 884 if (aElement->HasDirAuto()) { 885 if (elementAsSlot) { 886 for (const auto& assignedNode : elementAsSlot->AssignedNodes()) { 887 assignedNode->SetAffectsDirAutoSlot(); 888 DownwardPropagateDirAutoFlags(assignedNode); 889 } 890 } 891 MaybeClearAffectsDirAutoSlot(aElement); 892 WalkDescendantsSetDirAuto(aElement, aNotify); 893 } else { 894 Directionality oldDir = aElement->GetDirectionality(); 895 Directionality dir = RecomputeDirectionality(aElement, aNotify); 896 if (oldDir != dir) { 897 SetDirectionalityOnDescendants(aElement, dir, aNotify); 898 } 899 } 900 } 901 902 void SetDirOnBind(Element* aElement, nsIContent* aParent) { 903 // Propagate flags from parent to new element 904 if (!EstablishesOwnDirection(aElement) && AffectsDirAutoElement(aParent)) { 905 if (aParent->NodeOrAncestorHasDirAuto()) { 906 aElement->SetAncestorHasDirAuto(); 907 } 908 if (aParent->AffectsDirAutoSlot()) { 909 aElement->SetAffectsDirAutoSlot(); 910 } 911 DownwardPropagateDirAutoFlags(aElement); 912 913 if (aElement->GetFirstChild() || 914 (aElement->IsInShadowTree() && !aElement->HasValidDir() && 915 aElement->IsHTMLElement(nsGkAtoms::slot))) { 916 // We may also need to reset the direction of an ancestor with dir=auto 917 // as we are either an element with possible text descendants 918 // or a slot that provides it's host directionality 919 WalkAncestorsResetAutoDirection(aElement, true); 920 } 921 } 922 923 if (!aElement->HasDirAuto()) { 924 // if the element doesn't have dir=auto, set its own directionality from 925 // the dir attribute or by inheriting from its ancestors. 926 RecomputeDirectionality(aElement, false); 927 } 928 } 929 930 void ResetDir(Element* aElement) { 931 if (!aElement->HasDirAuto()) { 932 RecomputeDirectionality(aElement, false); 933 } 934 } 935 936 } // end namespace mozilla