Bug 1700051: part 35) Reduce accessibility of `mSoftText.mDOMMapping` to `private...
[gecko.git] / dom / base / DirectionalityUtils.cpp
blob40c24454317d859505617005924b656a871f3b95
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/. */
7 /*
8 Implementation description from https://etherpad.mozilla.org/dir-auto
10 Static case
11 ===========
12 When we see a new content node with @dir=auto from the parser, we set the
13 NodeHasDirAuto flag on the node. We won't have enough information to
14 decide the directionality of the node at this point.
16 When we bind a new content node to the document, if its parent has either of
17 the NodeAncestorHasDirAuto or NodeHasDirAuto flags, we set the
18 NodeAncestorHasDirAuto flag on the node.
20 When a new input with @type=text/search/tel/url/email and @dir=auto is added
21 from the parser, we resolve the directionality based on its @value.
23 When a new text node with non-neutral content is appended to a textarea
24 element with NodeHasDirAuto, if the directionality of the textarea element
25 is still unresolved, it is resolved based on the value of the text node.
26 Elements with unresolved directionality behave as LTR.
28 When a new text node with non-neutral content is appended to an element that
29 is not a textarea but has either of the NodeAncestorHasDirAuto or
30 NodeHasDirAuto flags, we walk up the parent chain while the
31 NodeAncestorHasDirAuto flag is present, and when we reach an element with
32 NodeHasDirAuto and no resolved directionality, we resolve the directionality
33 based on the contents of the text node and cease walking the parent chain.
34 Note that we should ignore elements with NodeHasDirAuto with resolved
35 directionality, so that the second text node in this example tree doesn't
36 affect the directionality of the div:
38 <div dir=auto>
39 <span>foo</span>
40 <span>بار</span>
41 </div>
43 The parent chain walk will be aborted if we hit a script or style element, or
44 if we hit an element with @dir=ltr or @dir=rtl.
46 I will call this algorithm "upward propagation".
48 Each text node should maintain a list of elements which have their
49 directionality determined by the first strong character of that text node.
50 This is useful to make dynamic changes more efficient. One way to implement
51 this is to have a per-document hash table mapping a text node to a set of
52 elements. I'll call this data structure TextNodeDirectionalityMap. The
53 algorithm for appending a new text node above needs to update this data
54 structure.
56 *IMPLEMENTATION NOTE*
57 In practice, the implementation uses two per-node properties:
59 dirAutoSetBy, which is set on a node with auto-directionality, and points to
60 the textnode that contains the strong character which determines the
61 directionality of the node.
63 textNodeDirectionalityMap, which is set on a text node and points to a hash
64 table listing the nodes whose directionality is determined by the text node.
66 Handling dynamic changes
67 ========================
69 We need to handle the following cases:
71 1. When the value of an input element with @type=text/search/tel/url/email is
72 changed, if it has NodeHasDirAuto, we update the resolved directionality.
74 2. When the dir attribute is changed from something else (including the case
75 where it doesn't exist) to auto on a textarea or an input element with
76 @type=text/search/tel/url/email, we set the NodeHasDirAuto flag and resolve
77 the directionality based on the value of the element.
79 3. When the dir attribute is changed from something else (including the case
80 where it doesn't exist) to auto on any element except case 1 above and the bdi
81 element, we run the following algorithm:
82 * We set the NodeHasDirAuto flag.
83 * If the element doesn't have the NodeAncestorHasDirAuto flag, we set the
84 NodeAncestorHasDirAuto flag on all of its child nodes. (Note that if the
85 element does have NodeAncestorHasDirAuto, all of its children should
86 already have this flag too. We can assert this in debug builds.)
87 * To resolve the directionality of the element, we run the algorithm explained
89 http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#the-dir-attribute
90 (I'll call this the "downward propagation algorithm".) by walking the child
91 subtree in tree order. Note that an element with @dir=auto should not affect
92 other elements in its document with @dir=auto. So there is no need to walk up
93 the parent chain in this case. TextNodeDirectionalityMap needs to be updated
94 as appropriate.
96 3a. When the dir attribute is set to any valid value on an element that didn't
97 have a valid dir attribute before, this means that any descendant of that
98 element will not affect the directionality of any of its ancestors. So we need
99 to check whether any text node descendants of the element are listed in
100 TextNodeDirectionalityMap, and whether the elements whose direction they set
101 are ancestors of the element. If so, we need to rerun the downward propagation
102 algorithm for those ancestors.
104 4. When the dir attribute is changed from auto to something else (including
105 the case where it gets removed) on a textarea or an input element with
106 @type=text/search/tel/url/email, we unset the NodeHasDirAuto flag and
107 resolve the directionality based on the directionality of the value of the
108 @dir attribute on element itself or its parent element.
110 5. When the dir attribute is changed from auto to something else (including
111 the case where it gets removed) on any element except case 4 above and the bdi
112 element, we run the following algorithm:
113 * We unset the NodeHasDirAuto flag.
114 * If the element does not have the NodeAncestorHasDirAuto flag, we unset
115 the NodeAncestorHasDirAuto flag on all of its child nodes, except those
116 who are a descendant of another element with NodeHasDirAuto. (Note that if
117 the element has the NodeAncestorHasDirAuto flag, all of its child nodes
118 should still retain the same flag.)
119 * We resolve the directionality of the element based on the value of the @dir
120 attribute on the element itself or its parent element.
121 TextNodeDirectionalityMap needs to be updated as appropriate.
123 5a. When the dir attribute is removed or set to an invalid value on any
124 element (except a bdi element) with the NodeAncestorHasDirAuto flag which
125 previously had a valid dir attribute, it might have a text node descendant
126 that did not previously affect the directionality of any of its ancestors but
127 should now begin to affect them. We run the following algorithm:
128 * Walk up the parent chain from the element.
129 * For any element that appears in the TextNodeDirectionalityMap, remove the
130 element from the map and rerun the downward propagation algorithm
131 (see section 3).
132 * If we reach an element without either of the NodeHasDirAuto or
133 NodeAncestorHasDirAuto flags, abort the parent chain walk.
135 6. When an element with @dir=auto is added to the document, we should handle
136 it similar to the case 2/3 above.
138 7. When an element with NodeHasDirAuto or NodeAncestorHasDirAuto is
139 removed from the document, we should handle it similar to the case 4/5 above,
140 except that we don't need to handle anything in the child subtree. We should
141 also remove all of the occurrences of that node and its descendants from
142 TextNodeDirectionalityMap. (This is the conceptual description of what needs
143 to happen but in the implementation UnbindFromTree is going to be called on
144 all of the descendants so we don't need to descend into the child subtree).
146 8. When the contents of a text node is changed either from script or by the
147 user, we need to run the following algorithm:
148 * If the change has happened after the first character with strong
149 directionality in the text node, do nothing.
150 * If the text node is a child of a bdi, script or style element, do nothing.
151 * If the text node belongs to a textarea with NodeHasDirAuto, we need to
152 update the directionality of the textarea.
153 * Grab a list of elements affected by this text node from
154 TextNodeDirectionalityMap and re-resolve the directionality of each one of
155 them based on the new contents of the text node.
156 * If the text node does not exist in TextNodeDirectionalityMap, and it has the
157 NodeAncestorHasDirAuto flag set, this could potentially be a text node
158 which is going to start affecting the directionality of its parent @dir=auto
159 elements. In this case, we need to fall back to the (potentially expensive)
160 "upward propagation algorithm". The TextNodeDirectionalityMap data structure
161 needs to be update during this algorithm.
162 * If the new contents of the text node do not have any strong characters, and
163 the old contents used to, and the text node used to exist in
164 TextNodeDirectionalityMap and it has the NodeAncestorHasDirAuto flag set,
165 the elements associated with this text node inside TextNodeDirectionalityMap
166 will now get their directionality from another text node. In this case, for
167 each element in the list retrieved from TextNodeDirectionalityMap, run the
168 downward propagation algorithm (section 3), and remove the text node from
169 TextNodeDirectionalityMap.
171 9. When a new text node is injected into a document, we need to run the
172 following algorithm:
173 * If the contents of the text node do not have any characters with strong
174 direction, do nothing.
175 * If the text node is a child of a bdi, script or style element, do nothing.
176 * If the text node is appended to a textarea element with NodeHasDirAuto, we
177 need to update the directionality of the textarea.
178 * If the text node has NodeAncestorHasDirAuto, we need to run the "upward
179 propagation algorithm". The TextNodeDirectionalityMap data structure needs to
180 be update during this algorithm.
182 10. When a text node is removed from a document, we need to run the following
183 algorithm:
184 * If the contents of the text node do not have any characters with strong
185 direction, do nothing.
186 * If the text node is a child of a bdi, script or style element, do nothing.
187 * If the text node is removed from a textarea element with NodeHasDirAuto,
188 set the directionality to "ltr". (This is what the spec currently says, but
189 I'm filing a spec bug to get it fixed -- the directionality should depend on
190 the parent element here.)
191 * If the text node has NodeAncestorHasDirAuto, we need to look at the list
192 of elements being affected by this text node from TextNodeDirectionalityMap,
193 run the "downward propagation algorithm" (section 3) for each one of them,
194 while updating TextNodeDirectionalityMap along the way.
196 11. If the value of the @dir attribute on a bdi element is changed to an
197 invalid value (or if it's removed), determine the new directionality similar
198 to the case 3 above.
200 == Implemention Notes ==
201 When a new node gets bound to the tree, the BindToTree function gets called.
202 The reverse case is UnbindFromTree.
203 When the contents of a text node change, CharacterData::SetTextInternal
204 gets called.
207 #include "mozilla/dom/DirectionalityUtils.h"
209 #include "nsINode.h"
210 #include "nsIContent.h"
211 #include "nsIContentInlines.h"
212 #include "mozilla/dom/Document.h"
213 #include "mozilla/AutoRestore.h"
214 #include "mozilla/DebugOnly.h"
215 #include "mozilla/dom/Element.h"
216 #include "mozilla/dom/HTMLSlotElement.h"
217 #include "mozilla/dom/ShadowRoot.h"
218 #include "nsUnicodeProperties.h"
219 #include "nsTextFragment.h"
220 #include "nsAttrValue.h"
221 #include "nsTextNode.h"
222 #include "nsCheapSets.h"
224 namespace mozilla {
226 using mozilla::dom::Element;
227 using mozilla::dom::HTMLInputElement;
228 using mozilla::dom::HTMLSlotElement;
229 using mozilla::dom::ShadowRoot;
231 static nsIContent* GetParentOrHostOrSlot(
232 nsIContent* aContent, bool* aCrossedShadowBoundary = nullptr) {
233 if (HTMLSlotElement* slot = aContent->GetAssignedSlot()) {
234 if (aCrossedShadowBoundary) {
235 *aCrossedShadowBoundary = true;
237 return slot;
240 nsIContent* parent = aContent->GetParent();
241 if (parent) {
242 return parent;
245 ShadowRoot* sr = ShadowRoot::FromNode(aContent);
246 if (sr) {
247 if (aCrossedShadowBoundary) {
248 *aCrossedShadowBoundary = true;
250 return sr->Host();
253 return nullptr;
256 static bool AncestorChainCrossesShadowBoundary(nsIContent* aDescendant,
257 nsIContent* aAncestor) {
258 bool crossedShadowBoundary = false;
259 nsIContent* content = aDescendant;
260 while (content && content != aAncestor) {
261 content = GetParentOrHostOrSlot(content, &crossedShadowBoundary);
262 if (crossedShadowBoundary) {
263 return true;
267 return false;
271 * Returns true if aElement is one of the elements whose text content should not
272 * affect its own direction, nor the direction of ancestors with dir=auto.
274 * Note that this does not include <bdi>, whose content does affect its own
275 * direction when it has dir=auto (which it has by default), so one needs to
276 * test for it separately, e.g. with DoesNotAffectDirectionOfAncestors.
277 * It *does* include textarea, because even if a textarea has dir=auto, it has
278 * unicode-bidi: plaintext and is handled automatically in bidi resolution.
279 * It also includes `input`, because it takes the `dir` value from its value
280 * attribute, instead of the child nodes.
282 static bool DoesNotParticipateInAutoDirection(const nsIContent* aContent) {
283 mozilla::dom::NodeInfo* nodeInfo = aContent->NodeInfo();
284 return ((!aContent->IsHTMLElement() || nodeInfo->Equals(nsGkAtoms::script) ||
285 nodeInfo->Equals(nsGkAtoms::style) ||
286 nodeInfo->Equals(nsGkAtoms::input) ||
287 nodeInfo->Equals(nsGkAtoms::textarea) ||
288 aContent->IsInNativeAnonymousSubtree())) &&
289 !aContent->IsShadowRoot();
293 * Returns true if aElement is one of the element whose text content should not
294 * affect the direction of ancestors with dir=auto (though it may affect its own
295 * direction, e.g. <bdi>)
297 static bool DoesNotAffectDirectionOfAncestors(const Element* aElement) {
298 return (DoesNotParticipateInAutoDirection(aElement) ||
299 aElement->IsHTMLElement(nsGkAtoms::bdi) || aElement->HasFixedDir());
303 * Returns the directionality of a Unicode character
305 static Directionality GetDirectionFromChar(uint32_t ch) {
306 switch (mozilla::unicode::GetBidiCat(ch)) {
307 case eCharType_RightToLeft:
308 case eCharType_RightToLeftArabic:
309 return eDir_RTL;
311 case eCharType_LeftToRight:
312 return eDir_LTR;
314 default:
315 return eDir_NotSet;
319 inline static bool NodeAffectsDirAutoAncestor(nsIContent* aTextNode) {
320 nsIContent* parent = GetParentOrHostOrSlot(aTextNode);
321 return (parent && !DoesNotParticipateInAutoDirection(parent) &&
322 parent->NodeOrAncestorHasDirAuto() &&
323 !aTextNode->IsInNativeAnonymousSubtree());
326 Directionality GetDirectionFromText(const char16_t* aText,
327 const uint32_t aLength,
328 uint32_t* aFirstStrong) {
329 const char16_t* start = aText;
330 const char16_t* end = aText + aLength;
332 while (start < end) {
333 uint32_t current = start - aText;
334 uint32_t ch = *start++;
336 if (start < end && NS_IS_SURROGATE_PAIR(ch, *start)) {
337 ch = SURROGATE_TO_UCS4(ch, *start++);
338 current++;
341 // Just ignore lone surrogates
342 if (!IS_SURROGATE(ch)) {
343 Directionality dir = GetDirectionFromChar(ch);
344 if (dir != eDir_NotSet) {
345 if (aFirstStrong) {
346 *aFirstStrong = current;
348 return dir;
353 if (aFirstStrong) {
354 *aFirstStrong = UINT32_MAX;
356 return eDir_NotSet;
359 static Directionality GetDirectionFromText(const char* aText,
360 const uint32_t aLength,
361 uint32_t* aFirstStrong = nullptr) {
362 const char* start = aText;
363 const char* end = aText + aLength;
365 while (start < end) {
366 uint32_t current = start - aText;
367 unsigned char ch = (unsigned char)*start++;
369 Directionality dir = GetDirectionFromChar(ch);
370 if (dir != eDir_NotSet) {
371 if (aFirstStrong) {
372 *aFirstStrong = current;
374 return dir;
378 if (aFirstStrong) {
379 *aFirstStrong = UINT32_MAX;
381 return eDir_NotSet;
384 static Directionality GetDirectionFromText(const mozilla::dom::Text* aTextNode,
385 uint32_t* aFirstStrong = nullptr) {
386 const nsTextFragment* frag = &aTextNode->TextFragment();
387 if (frag->Is2b()) {
388 return GetDirectionFromText(frag->Get2b(), frag->GetLength(), aFirstStrong);
391 return GetDirectionFromText(frag->Get1b(), frag->GetLength(), aFirstStrong);
394 static nsTextNode* WalkDescendantsAndGetDirectionFromText(
395 nsINode* aRoot, nsINode* aSkip, Directionality* aDirectionality) {
396 nsIContent* child = aRoot->GetFirstChild();
397 while (child) {
398 if ((child->IsElement() &&
399 DoesNotAffectDirectionOfAncestors(child->AsElement())) ||
400 child->GetAssignedSlot()) {
401 child = child->GetNextNonChildNode(aRoot);
402 continue;
405 if (auto* slot = HTMLSlotElement::FromNode(child)) {
406 const nsTArray<RefPtr<nsINode>>& assignedNodes = slot->AssignedNodes();
407 for (uint32_t i = 0; i < assignedNodes.Length(); ++i) {
408 nsIContent* assignedNode = assignedNodes[i]->AsContent();
409 if (assignedNode->NodeType() == nsINode::TEXT_NODE) {
410 auto text = static_cast<nsTextNode*>(assignedNode);
411 if (assignedNode != aSkip) {
412 Directionality textNodeDir = GetDirectionFromText(text);
413 if (textNodeDir != eDir_NotSet) {
414 *aDirectionality = textNodeDir;
415 return text;
418 } else if (assignedNode->IsElement() &&
419 !DoesNotAffectDirectionOfAncestors(
420 assignedNode->AsElement())) {
421 nsTextNode* text = WalkDescendantsAndGetDirectionFromText(
422 assignedNode, aSkip, aDirectionality);
423 if (text) {
424 return text;
430 if (child->NodeType() == nsINode::TEXT_NODE && child != aSkip) {
431 auto text = static_cast<nsTextNode*>(child);
432 Directionality textNodeDir = GetDirectionFromText(text);
433 if (textNodeDir != eDir_NotSet) {
434 *aDirectionality = textNodeDir;
435 return text;
438 child = child->GetNextNode(aRoot);
441 return nullptr;
445 * Set the directionality of a node with dir=auto as defined in
446 * http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#the-directionality
448 * @param[in] changedNode If we call this method because the content of a text
449 * node is about to change, pass in the changed node, so that we
450 * know not to return it
451 * @return the text node containing the character that determined the direction
453 static nsTextNode* WalkDescendantsSetDirectionFromText(
454 Element* aElement, bool aNotify, nsINode* aChangedNode = nullptr) {
455 MOZ_ASSERT(aElement, "Must have an element");
456 MOZ_ASSERT(aElement->HasDirAuto(), "Element must have dir=auto");
458 if (DoesNotParticipateInAutoDirection(aElement)) {
459 return nullptr;
462 Directionality textNodeDir = eDir_NotSet;
464 // Check the text in Shadow DOM.
465 if (ShadowRoot* shadowRoot = aElement->GetShadowRoot()) {
466 nsTextNode* text = WalkDescendantsAndGetDirectionFromText(
467 shadowRoot, aChangedNode, &textNodeDir);
468 if (text) {
469 aElement->SetDirectionality(textNodeDir, aNotify);
470 return text;
474 // Check the text in light DOM.
475 nsTextNode* text = WalkDescendantsAndGetDirectionFromText(
476 aElement, aChangedNode, &textNodeDir);
477 if (text) {
478 aElement->SetDirectionality(textNodeDir, aNotify);
479 return text;
482 // We walked all the descendants without finding a text node with strong
483 // directional characters. Set the directionality to LTR
484 aElement->SetDirectionality(eDir_LTR, aNotify);
485 return nullptr;
488 class nsTextNodeDirectionalityMap {
489 static void nsTextNodeDirectionalityMapDtor(void* aObject,
490 nsAtom* aPropertyName,
491 void* aPropertyValue,
492 void* aData) {
493 nsINode* textNode = static_cast<nsINode*>(aObject);
494 textNode->ClearHasTextNodeDirectionalityMap();
496 nsTextNodeDirectionalityMap* map =
497 reinterpret_cast<nsTextNodeDirectionalityMap*>(aPropertyValue);
498 map->EnsureMapIsClear();
499 delete map;
502 public:
503 explicit nsTextNodeDirectionalityMap(nsINode* aTextNode)
504 : mElementToBeRemoved(nullptr) {
505 MOZ_ASSERT(aTextNode, "Null text node");
506 MOZ_COUNT_CTOR(nsTextNodeDirectionalityMap);
507 aTextNode->SetProperty(nsGkAtoms::textNodeDirectionalityMap, this,
508 nsTextNodeDirectionalityMapDtor);
509 aTextNode->SetHasTextNodeDirectionalityMap();
512 MOZ_COUNTED_DTOR(nsTextNodeDirectionalityMap)
514 static void nsTextNodeDirectionalityMapPropertyDestructor(
515 void* aObject, nsAtom* aProperty, void* aPropertyValue, void* aData) {
516 nsTextNode* textNode = static_cast<nsTextNode*>(aPropertyValue);
517 nsTextNodeDirectionalityMap* map = GetDirectionalityMap(textNode);
518 if (map) {
519 map->RemoveEntryForProperty(static_cast<Element*>(aObject));
521 NS_RELEASE(textNode);
524 void AddEntry(nsTextNode* aTextNode, Element* aElement) {
525 if (!mElements.Contains(aElement)) {
526 mElements.Put(aElement);
527 NS_ADDREF(aTextNode);
528 aElement->SetProperty(nsGkAtoms::dirAutoSetBy, aTextNode,
529 nsTextNodeDirectionalityMapPropertyDestructor);
530 aElement->SetHasDirAutoSet();
534 void RemoveEntry(nsTextNode* aTextNode, Element* aElement) {
535 NS_ASSERTION(mElements.Contains(aElement),
536 "element already removed from map");
538 mElements.Remove(aElement);
539 aElement->ClearHasDirAutoSet();
540 aElement->RemoveProperty(nsGkAtoms::dirAutoSetBy);
543 void RemoveEntryForProperty(Element* aElement) {
544 if (mElementToBeRemoved != aElement) {
545 mElements.Remove(aElement);
547 aElement->ClearHasDirAutoSet();
550 private:
551 nsCheapSet<nsPtrHashKey<Element>> mElements;
552 // Only used for comparison.
553 Element* mElementToBeRemoved;
555 static nsTextNodeDirectionalityMap* GetDirectionalityMap(nsINode* aTextNode) {
556 MOZ_ASSERT(aTextNode->NodeType() == nsINode::TEXT_NODE,
557 "Must be a text node");
558 nsTextNodeDirectionalityMap* map = nullptr;
560 if (aTextNode->HasTextNodeDirectionalityMap()) {
561 map = static_cast<nsTextNodeDirectionalityMap*>(
562 aTextNode->GetProperty(nsGkAtoms::textNodeDirectionalityMap));
565 return map;
568 static nsCheapSetOperator SetNodeDirection(nsPtrHashKey<Element>* aEntry,
569 void* aDir) {
570 aEntry->GetKey()->SetDirectionality(
571 *reinterpret_cast<Directionality*>(aDir), true);
572 return OpNext;
575 struct nsTextNodeDirectionalityMapAndElement {
576 nsTextNodeDirectionalityMap* mMap;
577 nsCOMPtr<nsINode> mNode;
580 static nsCheapSetOperator ResetNodeDirection(nsPtrHashKey<Element>* aEntry,
581 void* aData) {
582 // run the downward propagation algorithm
583 // and remove the text node from the map
584 nsTextNodeDirectionalityMapAndElement* data =
585 static_cast<nsTextNodeDirectionalityMapAndElement*>(aData);
586 nsINode* oldTextNode = data->mNode;
587 Element* rootNode = aEntry->GetKey();
588 nsTextNode* newTextNode = nullptr;
589 if (rootNode->GetParentNode() && rootNode->HasDirAuto()) {
590 newTextNode =
591 WalkDescendantsSetDirectionFromText(rootNode, true, oldTextNode);
594 AutoRestore<Element*> restore(data->mMap->mElementToBeRemoved);
595 data->mMap->mElementToBeRemoved = rootNode;
596 if (newTextNode) {
597 nsINode* oldDirAutoSetBy = static_cast<nsTextNode*>(
598 rootNode->GetProperty(nsGkAtoms::dirAutoSetBy));
599 if (oldDirAutoSetBy == newTextNode) {
600 // We're already registered.
601 return OpNext;
603 nsTextNodeDirectionalityMap::AddEntryToMap(newTextNode, rootNode);
604 } else {
605 rootNode->ClearHasDirAutoSet();
606 rootNode->RemoveProperty(nsGkAtoms::dirAutoSetBy);
608 return OpRemove;
611 static nsCheapSetOperator TakeEntries(nsPtrHashKey<Element>* aEntry,
612 void* aData) {
613 AutoTArray<Element*, 8>* entries =
614 static_cast<AutoTArray<Element*, 8>*>(aData);
615 entries->AppendElement(aEntry->GetKey());
616 return OpRemove;
619 public:
620 uint32_t UpdateAutoDirection(Directionality aDir) {
621 return mElements.EnumerateEntries(SetNodeDirection, &aDir);
624 void ResetAutoDirection(nsINode* aTextNode) {
625 nsTextNodeDirectionalityMapAndElement data = {this, aTextNode};
626 mElements.EnumerateEntries(ResetNodeDirection, &data);
629 void EnsureMapIsClear() {
630 AutoRestore<Element*> restore(mElementToBeRemoved);
631 AutoTArray<Element*, 8> entries;
632 mElements.EnumerateEntries(TakeEntries, &entries);
633 for (Element* el : entries) {
634 el->ClearHasDirAutoSet();
635 el->RemoveProperty(nsGkAtoms::dirAutoSetBy);
639 static void RemoveElementFromMap(nsTextNode* aTextNode, Element* aElement) {
640 if (aTextNode->HasTextNodeDirectionalityMap()) {
641 GetDirectionalityMap(aTextNode)->RemoveEntry(aTextNode, aElement);
645 static void AddEntryToMap(nsTextNode* aTextNode, Element* aElement) {
646 nsTextNodeDirectionalityMap* map = GetDirectionalityMap(aTextNode);
647 if (!map) {
648 map = new nsTextNodeDirectionalityMap(aTextNode);
651 map->AddEntry(aTextNode, aElement);
654 static uint32_t UpdateTextNodeDirection(nsINode* aTextNode,
655 Directionality aDir) {
656 MOZ_ASSERT(aTextNode->HasTextNodeDirectionalityMap(),
657 "Map missing in UpdateTextNodeDirection");
658 return GetDirectionalityMap(aTextNode)->UpdateAutoDirection(aDir);
661 static void ResetTextNodeDirection(nsTextNode* aTextNode,
662 nsTextNode* aChangedTextNode) {
663 MOZ_ASSERT(aTextNode->HasTextNodeDirectionalityMap(),
664 "Map missing in ResetTextNodeDirection");
665 RefPtr<nsTextNode> textNode = aTextNode;
666 GetDirectionalityMap(textNode)->ResetAutoDirection(aChangedTextNode);
669 static void EnsureMapIsClearFor(nsINode* aTextNode) {
670 if (aTextNode->HasTextNodeDirectionalityMap()) {
671 GetDirectionalityMap(aTextNode)->EnsureMapIsClear();
676 Directionality RecomputeDirectionality(Element* aElement, bool aNotify) {
677 MOZ_ASSERT(!aElement->HasDirAuto(),
678 "RecomputeDirectionality called with dir=auto");
680 if (aElement->HasValidDir()) {
681 return aElement->GetDirectionality();
684 Directionality dir = eDir_LTR;
685 if (nsIContent* parent = GetParentOrHostOrSlot(aElement)) {
686 if (ShadowRoot* shadow = ShadowRoot::FromNode(parent)) {
687 parent = shadow->GetHost();
690 if (parent && parent->IsElement()) {
691 // If the node doesn't have an explicit dir attribute with a valid value,
692 // the directionality is the same as the parent element (but don't
693 // propagate the parent directionality if it isn't set yet).
694 Directionality parentDir = parent->AsElement()->GetDirectionality();
695 if (parentDir != eDir_NotSet) {
696 dir = parentDir;
701 aElement->SetDirectionality(dir, aNotify);
702 return dir;
705 static void SetDirectionalityOnDescendantsInternal(nsINode* aNode,
706 Directionality aDir,
707 bool aNotify) {
708 if (Element* element = Element::FromNode(aNode)) {
709 if (ShadowRoot* shadow = element->GetShadowRoot()) {
710 SetDirectionalityOnDescendantsInternal(shadow, aDir, aNotify);
714 for (nsIContent* child = aNode->GetFirstChild(); child;) {
715 if (!child->IsElement()) {
716 child = child->GetNextNode(aNode);
717 continue;
720 Element* element = child->AsElement();
721 if (element->HasValidDir() || element->HasDirAuto() ||
722 element->GetAssignedSlot()) {
723 child = child->GetNextNonChildNode(aNode);
724 continue;
726 if (ShadowRoot* shadow = element->GetShadowRoot()) {
727 SetDirectionalityOnDescendantsInternal(shadow, aDir, aNotify);
730 if (auto* slot = HTMLSlotElement::FromNode(child)) {
731 const nsTArray<RefPtr<nsINode>>& assignedNodes = slot->AssignedNodes();
732 for (uint32_t i = 0; i < assignedNodes.Length(); ++i) {
733 nsINode* node = assignedNodes[i];
734 Element* assignedElement =
735 node->IsElement() ? node->AsElement() : nullptr;
736 if (assignedElement && !assignedElement->HasValidDir() &&
737 !assignedElement->HasDirAuto()) {
738 assignedElement->SetDirectionality(aDir, aNotify);
739 SetDirectionalityOnDescendantsInternal(assignedElement, aDir,
740 aNotify);
745 element->SetDirectionality(aDir, aNotify);
747 child = child->GetNextNode(aNode);
751 // We want the public version of this only to acc
752 void SetDirectionalityOnDescendants(Element* aElement, Directionality aDir,
753 bool aNotify) {
754 return SetDirectionalityOnDescendantsInternal(aElement, aDir, aNotify);
757 static void ResetAutoDirection(Element* aElement, bool aNotify) {
758 if (aElement->HasDirAutoSet()) {
759 // If the parent has the DirAutoSet flag, its direction is determined by
760 // some text node descendant.
761 // Remove it from the map and reset its direction by the downward
762 // propagation algorithm
763 nsTextNode* setByNode = static_cast<nsTextNode*>(
764 aElement->GetProperty(nsGkAtoms::dirAutoSetBy));
765 if (setByNode) {
766 nsTextNodeDirectionalityMap::RemoveElementFromMap(setByNode, aElement);
770 if (aElement->HasDirAuto()) {
771 nsTextNode* setByNode =
772 WalkDescendantsSetDirectionFromText(aElement, aNotify);
773 if (setByNode) {
774 nsTextNodeDirectionalityMap::AddEntryToMap(setByNode, aElement);
776 SetDirectionalityOnDescendants(aElement, aElement->GetDirectionality(),
777 aNotify);
782 * Walk the parent chain of a text node whose dir attribute has been removed and
783 * reset the direction of any of its ancestors which have dir=auto and whose
784 * directionality is determined by a text node descendant.
786 void WalkAncestorsResetAutoDirection(Element* aElement, bool aNotify) {
787 nsTextNode* setByNode;
788 nsIContent* parent = GetParentOrHostOrSlot(aElement);
789 while (parent && parent->NodeOrAncestorHasDirAuto()) {
790 if (!parent->IsElement()) {
791 parent = GetParentOrHostOrSlot(parent);
792 continue;
795 Element* parentElement = parent->AsElement();
796 if (parent->HasDirAutoSet()) {
797 // If the parent has the DirAutoSet flag, its direction is determined by
798 // some text node descendant.
799 // Remove it from the map and reset its direction by the downward
800 // propagation algorithm
801 setByNode = static_cast<nsTextNode*>(
802 parent->GetProperty(nsGkAtoms::dirAutoSetBy));
803 if (setByNode) {
804 nsTextNodeDirectionalityMap::RemoveElementFromMap(setByNode,
805 parentElement);
808 if (parentElement->HasDirAuto()) {
809 setByNode = WalkDescendantsSetDirectionFromText(parentElement, aNotify);
810 if (setByNode) {
811 nsTextNodeDirectionalityMap::AddEntryToMap(setByNode, parentElement);
813 SetDirectionalityOnDescendants(
814 parentElement, parentElement->GetDirectionality(), aNotify);
815 break;
817 parent = GetParentOrHostOrSlot(parent);
821 static void RecomputeSlottedNodeDirection(HTMLSlotElement& aSlot,
822 nsINode& aNode) {
823 auto* assignedElement = Element::FromNode(aNode);
824 if (!assignedElement) {
825 return;
828 if (assignedElement->HasValidDir() || assignedElement->HasDirAuto()) {
829 return;
832 // Try to optimize out state changes when possible.
833 if (assignedElement->GetDirectionality() == aSlot.GetDirectionality()) {
834 return;
837 assignedElement->SetDirectionality(aSlot.GetDirectionality(), true);
838 SetDirectionalityOnDescendantsInternal(assignedElement,
839 aSlot.GetDirectionality(), true);
842 void SlotAssignedNodeChanged(HTMLSlotElement* aSlot,
843 nsIContent& aAssignedNode) {
844 if (!aSlot) {
845 return;
848 if (aSlot->NodeOrAncestorHasDirAuto()) {
849 // The directionality of the assigned node may impact the directionality of
850 // the slot. So recompute everything.
851 SlotStateChanged(aSlot, /* aAllAssignedNodesChanged = */ false);
854 if (aAssignedNode.GetAssignedSlot() == aSlot) {
855 RecomputeSlottedNodeDirection(*aSlot, aAssignedNode);
859 void SlotStateChanged(HTMLSlotElement* aSlot, bool aAllAssignedNodesChanged) {
860 if (!aSlot) {
861 return;
864 Directionality oldDir = aSlot->GetDirectionality();
866 if (aSlot->HasDirAuto()) {
867 ResetAutoDirection(aSlot, true);
870 if (aSlot->NodeOrAncestorHasDirAuto()) {
871 WalkAncestorsResetAutoDirection(aSlot, true);
874 if (aAllAssignedNodesChanged || oldDir != aSlot->GetDirectionality()) {
875 for (nsINode* node : aSlot->AssignedNodes()) {
876 RecomputeSlottedNodeDirection(*aSlot, *node);
881 void WalkDescendantsResetAutoDirection(Element* aElement) {
882 nsIContent* child = aElement->GetFirstChild();
883 while (child) {
884 if (child->IsElement() && child->AsElement()->HasDirAuto()) {
885 child = child->GetNextNonChildNode(aElement);
886 continue;
889 if (child->NodeType() == nsINode::TEXT_NODE &&
890 child->HasTextNodeDirectionalityMap()) {
891 nsTextNodeDirectionalityMap::ResetTextNodeDirection(
892 static_cast<nsTextNode*>(child), nullptr);
893 // Don't call nsTextNodeDirectionalityMap::EnsureMapIsClearFor(child)
894 // since ResetTextNodeDirection may have kept elements in child's
895 // DirectionalityMap.
897 child = child->GetNextNode(aElement);
901 static void SetAncestorHasDirAutoOnDescendants(nsINode* aRoot);
903 static void MaybeSetAncestorHasDirAutoOnShadowDOM(nsINode* aNode) {
904 if (aNode->IsElement()) {
905 if (ShadowRoot* sr = aNode->AsElement()->GetShadowRoot()) {
906 sr->SetAncestorHasDirAuto();
907 SetAncestorHasDirAutoOnDescendants(sr);
912 static void SetAncestorHasDirAutoOnDescendants(nsINode* aRoot) {
913 MaybeSetAncestorHasDirAutoOnShadowDOM(aRoot);
915 nsIContent* child = aRoot->GetFirstChild();
916 while (child) {
917 if (child->IsElement() &&
918 DoesNotAffectDirectionOfAncestors(child->AsElement())) {
919 child = child->GetNextNonChildNode(aRoot);
920 continue;
923 // If the child is assigned to a slot, it should inherit the state from
924 // that.
925 if (!child->GetAssignedSlot()) {
926 MaybeSetAncestorHasDirAutoOnShadowDOM(child);
927 child->SetAncestorHasDirAuto();
928 if (auto* slot = HTMLSlotElement::FromNode(child)) {
929 const nsTArray<RefPtr<nsINode>>& assignedNodes = slot->AssignedNodes();
930 for (uint32_t i = 0; i < assignedNodes.Length(); ++i) {
931 assignedNodes[i]->SetAncestorHasDirAuto();
932 SetAncestorHasDirAutoOnDescendants(assignedNodes[i]);
936 child = child->GetNextNode(aRoot);
940 void WalkDescendantsSetDirAuto(Element* aElement, bool aNotify) {
941 // Only test for DoesNotParticipateInAutoDirection -- in other words, if
942 // aElement is a <bdi> which is having its dir attribute set to auto (or
943 // removed or set to an invalid value, which are equivalent to dir=auto for
944 // <bdi>, we *do* want to set AncestorHasDirAuto on its descendants, unlike
945 // in SetDirOnBind where we don't propagate AncestorHasDirAuto to a <bdi>
946 // being bound to an existing node with dir=auto.
947 if (!DoesNotParticipateInAutoDirection(aElement) &&
948 !aElement->AncestorHasDirAuto()) {
949 SetAncestorHasDirAutoOnDescendants(aElement);
952 nsTextNode* textNode = WalkDescendantsSetDirectionFromText(aElement, aNotify);
953 if (textNode) {
954 nsTextNodeDirectionalityMap::AddEntryToMap(textNode, aElement);
958 void WalkDescendantsClearAncestorDirAuto(nsIContent* aContent) {
959 if (aContent->IsElement()) {
960 if (ShadowRoot* shadowRoot = aContent->AsElement()->GetShadowRoot()) {
961 shadowRoot->ClearAncestorHasDirAuto();
962 WalkDescendantsClearAncestorDirAuto(shadowRoot);
966 nsIContent* child = aContent->GetFirstChild();
967 while (child) {
968 if (child->GetAssignedSlot()) {
969 // If the child node is assigned to a slot, nodes state is inherited from
970 // the slot, not from element's parent.
971 child = child->GetNextNonChildNode(aContent);
972 continue;
974 if (child->IsElement()) {
975 if (child->AsElement()->HasDirAuto()) {
976 child = child->GetNextNonChildNode(aContent);
977 continue;
980 if (auto* slot = HTMLSlotElement::FromNode(child)) {
981 const nsTArray<RefPtr<nsINode>>& assignedNodes = slot->AssignedNodes();
982 for (uint32_t i = 0; i < assignedNodes.Length(); ++i) {
983 if (assignedNodes[i]->IsElement()) {
984 Element* slottedElement = assignedNodes[i]->AsElement();
985 if (slottedElement->HasDirAuto()) {
986 continue;
990 nsIContent* content = assignedNodes[i]->AsContent();
991 content->ClearAncestorHasDirAuto();
992 WalkDescendantsClearAncestorDirAuto(content);
997 child->ClearAncestorHasDirAuto();
998 child = child->GetNextNode(aContent);
1002 void SetAncestorDirectionIfAuto(nsTextNode* aTextNode, Directionality aDir,
1003 bool aNotify = true) {
1004 MOZ_ASSERT(aTextNode->NodeType() == nsINode::TEXT_NODE,
1005 "Must be a text node");
1007 bool crossedShadowBoundary = false;
1008 nsIContent* parent = GetParentOrHostOrSlot(aTextNode, &crossedShadowBoundary);
1009 while (parent && parent->NodeOrAncestorHasDirAuto()) {
1010 if (!parent->IsElement()) {
1011 parent = GetParentOrHostOrSlot(parent, &crossedShadowBoundary);
1012 continue;
1015 Element* parentElement = parent->AsElement();
1016 if (DoesNotParticipateInAutoDirection(parentElement) ||
1017 parentElement->HasFixedDir()) {
1018 break;
1021 if (parentElement->HasDirAuto()) {
1022 bool resetDirection = false;
1023 nsTextNode* directionWasSetByTextNode = static_cast<nsTextNode*>(
1024 parent->GetProperty(nsGkAtoms::dirAutoSetBy));
1026 if (!parent->HasDirAutoSet()) {
1027 // Fast path if parent's direction is not yet set by any descendant
1028 MOZ_ASSERT(!directionWasSetByTextNode,
1029 "dirAutoSetBy property should be null");
1030 resetDirection = true;
1031 } else {
1032 // If parent's direction is already set, we need to know if
1033 // aTextNode is before or after the text node that had set it.
1034 // We will walk parent's descendants in tree order starting from
1035 // aTextNode to optimize for the most common case where text nodes are
1036 // being appended to tree.
1037 if (!directionWasSetByTextNode) {
1038 resetDirection = true;
1039 } else if (directionWasSetByTextNode != aTextNode) {
1040 if (crossedShadowBoundary || AncestorChainCrossesShadowBoundary(
1041 directionWasSetByTextNode, parent)) {
1042 // Need to take the slow path when the path from either the old or
1043 // new text node to the dir=auto element crosses shadow boundary.
1044 ResetAutoDirection(parentElement, aNotify);
1045 return;
1048 nsIContent* child = aTextNode->GetNextNode(parent);
1049 while (child) {
1050 if (child->IsElement() &&
1051 DoesNotAffectDirectionOfAncestors(child->AsElement())) {
1052 child = child->GetNextNonChildNode(parent);
1053 continue;
1056 if (child == directionWasSetByTextNode) {
1057 // we found the node that set the element's direction after our
1058 // text node, so we need to reset the direction
1059 resetDirection = true;
1060 break;
1063 child = child->GetNextNode(parent);
1068 if (resetDirection) {
1069 if (directionWasSetByTextNode) {
1070 nsTextNodeDirectionalityMap::RemoveElementFromMap(
1071 directionWasSetByTextNode, parentElement);
1073 parentElement->SetDirectionality(aDir, aNotify);
1074 nsTextNodeDirectionalityMap::AddEntryToMap(aTextNode, parentElement);
1075 SetDirectionalityOnDescendants(parentElement, aDir, aNotify);
1078 // Since we found an element with dir=auto, we can stop walking the
1079 // parent chain: none of its ancestors will have their direction set by
1080 // any of its descendants.
1081 return;
1083 parent = GetParentOrHostOrSlot(parent, &crossedShadowBoundary);
1087 bool TextNodeWillChangeDirection(nsTextNode* aTextNode, Directionality* aOldDir,
1088 uint32_t aOffset) {
1089 if (!NodeAffectsDirAutoAncestor(aTextNode)) {
1090 nsTextNodeDirectionalityMap::EnsureMapIsClearFor(aTextNode);
1091 return false;
1094 uint32_t firstStrong;
1095 *aOldDir = GetDirectionFromText(aTextNode, &firstStrong);
1096 return (aOffset <= firstStrong);
1099 void TextNodeChangedDirection(nsTextNode* aTextNode, Directionality aOldDir,
1100 bool aNotify) {
1101 Directionality newDir = GetDirectionFromText(aTextNode);
1102 if (newDir == eDir_NotSet) {
1103 if (aOldDir != eDir_NotSet && aTextNode->HasTextNodeDirectionalityMap()) {
1104 // This node used to have a strong directional character but no
1105 // longer does. ResetTextNodeDirection() will re-resolve the
1106 // directionality of any elements whose directionality was
1107 // determined by this node.
1108 nsTextNodeDirectionalityMap::ResetTextNodeDirection(aTextNode, aTextNode);
1110 } else {
1111 // This node has a strong directional character. If it has a
1112 // TextNodeDirectionalityMap property, it already determines the
1113 // directionality of some element(s), so call UpdateTextNodeDirection to
1114 // reresolve their directionality. If it has no map, or if
1115 // UpdateTextNodeDirection returns zero, indicating that the map is
1116 // empty, call SetAncestorDirectionIfAuto to find ancestor elements
1117 // which should have their directionality determined by this node.
1118 if (aTextNode->HasTextNodeDirectionalityMap() &&
1119 nsTextNodeDirectionalityMap::UpdateTextNodeDirection(aTextNode,
1120 newDir)) {
1121 return;
1123 SetAncestorDirectionIfAuto(aTextNode, newDir, aNotify);
1127 void SetDirectionFromNewTextNode(nsTextNode* aTextNode) {
1128 if (!NodeAffectsDirAutoAncestor(aTextNode)) {
1129 return;
1132 nsIContent* parent = GetParentOrHostOrSlot(aTextNode);
1133 if (parent && parent->NodeOrAncestorHasDirAuto()) {
1134 aTextNode->SetAncestorHasDirAuto();
1137 Directionality dir = GetDirectionFromText(aTextNode);
1138 if (dir != eDir_NotSet) {
1139 SetAncestorDirectionIfAuto(aTextNode, dir);
1143 void ResetDirectionSetByTextNode(nsTextNode* aTextNode) {
1144 if (!NodeAffectsDirAutoAncestor(aTextNode)) {
1145 nsTextNodeDirectionalityMap::EnsureMapIsClearFor(aTextNode);
1146 return;
1149 Directionality dir = GetDirectionFromText(aTextNode);
1150 if (dir != eDir_NotSet && aTextNode->HasTextNodeDirectionalityMap()) {
1151 nsTextNodeDirectionalityMap::ResetTextNodeDirection(aTextNode, aTextNode);
1155 void SetDirectionalityFromValue(Element* aElement, const nsAString& value,
1156 bool aNotify) {
1157 Directionality dir =
1158 GetDirectionFromText(value.BeginReading(), value.Length());
1159 if (dir == eDir_NotSet) {
1160 dir = eDir_LTR;
1163 aElement->SetDirectionality(dir, aNotify);
1166 void OnSetDirAttr(Element* aElement, const nsAttrValue* aNewValue,
1167 bool hadValidDir, bool hadDirAuto, bool aNotify) {
1168 if (aElement->IsHTMLElement(nsGkAtoms::input)) {
1169 return;
1172 if (aElement->AncestorHasDirAuto()) {
1173 if (!hadValidDir) {
1174 // The element is a descendant of an element with dir = auto, is
1175 // having its dir attribute set, and previously didn't have a valid dir
1176 // attribute.
1177 // Check whether any of its text node descendants determine the
1178 // direction of any of its ancestors, and redetermine their direction
1179 WalkDescendantsResetAutoDirection(aElement);
1180 } else if (!aElement->HasValidDir()) {
1181 // The element is a descendant of an element with dir = auto and is
1182 // having its dir attribute removed or set to an invalid value.
1183 // Reset the direction of any of its ancestors whose direction is
1184 // determined by a text node descendant
1185 WalkAncestorsResetAutoDirection(aElement, aNotify);
1187 } else if (hadDirAuto && !aElement->HasDirAuto()) {
1188 // The element isn't a descendant of an element with dir = auto, and is
1189 // having its dir attribute set to something other than auto.
1190 // Walk the descendant tree and clear the AncestorHasDirAuto flag.
1192 // N.B: For elements other than <bdi> it would be enough to test that the
1193 // current value of dir was "auto" in BeforeSetAttr to know that we
1194 // were unsetting dir="auto". For <bdi> things are more complicated,
1195 // since it behaves like dir="auto" whenever the dir attribute is
1196 // empty or invalid, so we would have to check whether the old value
1197 // was not either "ltr" or "rtl", and the new value was either "ltr"
1198 // or "rtl". Element::HasDirAuto() encapsulates all that, so doing it
1199 // here is simpler.
1200 WalkDescendantsClearAncestorDirAuto(aElement);
1203 if (aElement->HasDirAuto()) {
1204 WalkDescendantsSetDirAuto(aElement, aNotify);
1205 } else {
1206 if (aElement->HasDirAutoSet()) {
1207 nsTextNode* setByNode = static_cast<nsTextNode*>(
1208 aElement->GetProperty(nsGkAtoms::dirAutoSetBy));
1209 nsTextNodeDirectionalityMap::RemoveElementFromMap(setByNode, aElement);
1211 SetDirectionalityOnDescendants(
1212 aElement, RecomputeDirectionality(aElement, aNotify), aNotify);
1216 void SetDirOnBind(Element* aElement, nsIContent* aParent) {
1217 // Set the AncestorHasDirAuto flag, unless this element shouldn't affect
1218 // ancestors that have dir=auto
1219 if (!DoesNotParticipateInAutoDirection(aElement) &&
1220 !aElement->IsHTMLElement(nsGkAtoms::bdi) && aParent &&
1221 aParent->NodeOrAncestorHasDirAuto()) {
1222 aElement->SetAncestorHasDirAuto();
1224 SetAncestorHasDirAutoOnDescendants(aElement);
1226 if (aElement->GetFirstChild() || aElement->GetShadowRoot()) {
1227 // We may also need to reset the direction of an ancestor with dir=auto
1228 WalkAncestorsResetAutoDirection(aElement, true);
1232 if (!aElement->HasDirAuto()) {
1233 // if the element doesn't have dir=auto, set its own directionality from
1234 // the dir attribute or by inheriting from its ancestors.
1235 RecomputeDirectionality(aElement, false);
1239 void ResetDir(Element* aElement) {
1240 if (aElement->HasDirAutoSet()) {
1241 nsTextNode* setByNode = static_cast<nsTextNode*>(
1242 aElement->GetProperty(nsGkAtoms::dirAutoSetBy));
1243 nsTextNodeDirectionalityMap::RemoveElementFromMap(setByNode, aElement);
1246 if (!aElement->HasDirAuto()) {
1247 RecomputeDirectionality(aElement, false);
1251 } // end namespace mozilla