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/. */
8 Implementation description from https://etherpad.mozilla.org/dir-auto
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:
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
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
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
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
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
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
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
207 #include "mozilla/dom/DirectionalityUtils.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"
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;
240 nsIContent
* parent
= aContent
->GetParent();
245 ShadowRoot
* sr
= ShadowRoot::FromNode(aContent
);
247 if (aCrossedShadowBoundary
) {
248 *aCrossedShadowBoundary
= true;
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
) {
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
:
311 case eCharType_LeftToRight
:
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
++);
341 // Just ignore lone surrogates
342 if (!IS_SURROGATE(ch
)) {
343 Directionality dir
= GetDirectionFromChar(ch
);
344 if (dir
!= eDir_NotSet
) {
346 *aFirstStrong
= current
;
354 *aFirstStrong
= UINT32_MAX
;
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
) {
372 *aFirstStrong
= current
;
379 *aFirstStrong
= UINT32_MAX
;
384 static Directionality
GetDirectionFromText(const mozilla::dom::Text
* aTextNode
,
385 uint32_t* aFirstStrong
= nullptr) {
386 const nsTextFragment
* frag
= &aTextNode
->TextFragment();
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();
398 if ((child
->IsElement() &&
399 DoesNotAffectDirectionOfAncestors(child
->AsElement())) ||
400 child
->GetAssignedSlot()) {
401 child
= child
->GetNextNonChildNode(aRoot
);
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
;
418 } else if (assignedNode
->IsElement() &&
419 !DoesNotAffectDirectionOfAncestors(
420 assignedNode
->AsElement())) {
421 nsTextNode
* text
= WalkDescendantsAndGetDirectionFromText(
422 assignedNode
, aSkip
, aDirectionality
);
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
;
438 child
= child
->GetNextNode(aRoot
);
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
)) {
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
);
469 aElement
->SetDirectionality(textNodeDir
, aNotify
);
474 // Check the text in light DOM.
475 nsTextNode
* text
= WalkDescendantsAndGetDirectionFromText(
476 aElement
, aChangedNode
, &textNodeDir
);
478 aElement
->SetDirectionality(textNodeDir
, aNotify
);
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
);
488 class nsTextNodeDirectionalityMap
{
489 static void nsTextNodeDirectionalityMapDtor(void* aObject
,
490 nsAtom
* aPropertyName
,
491 void* aPropertyValue
,
493 nsINode
* textNode
= static_cast<nsINode
*>(aObject
);
494 textNode
->ClearHasTextNodeDirectionalityMap();
496 nsTextNodeDirectionalityMap
* map
=
497 reinterpret_cast<nsTextNodeDirectionalityMap
*>(aPropertyValue
);
498 map
->EnsureMapIsClear();
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
);
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();
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
));
568 static nsCheapSetOperator
SetNodeDirection(nsPtrHashKey
<Element
>* aEntry
,
570 aEntry
->GetKey()->SetDirectionality(
571 *reinterpret_cast<Directionality
*>(aDir
), true);
575 struct nsTextNodeDirectionalityMapAndElement
{
576 nsTextNodeDirectionalityMap
* mMap
;
577 nsCOMPtr
<nsINode
> mNode
;
580 static nsCheapSetOperator
ResetNodeDirection(nsPtrHashKey
<Element
>* aEntry
,
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()) {
591 WalkDescendantsSetDirectionFromText(rootNode
, true, oldTextNode
);
594 AutoRestore
<Element
*> restore(data
->mMap
->mElementToBeRemoved
);
595 data
->mMap
->mElementToBeRemoved
= rootNode
;
597 nsINode
* oldDirAutoSetBy
= static_cast<nsTextNode
*>(
598 rootNode
->GetProperty(nsGkAtoms::dirAutoSetBy
));
599 if (oldDirAutoSetBy
== newTextNode
) {
600 // We're already registered.
603 nsTextNodeDirectionalityMap::AddEntryToMap(newTextNode
, rootNode
);
605 rootNode
->ClearHasDirAutoSet();
606 rootNode
->RemoveProperty(nsGkAtoms::dirAutoSetBy
);
611 static nsCheapSetOperator
TakeEntries(nsPtrHashKey
<Element
>* aEntry
,
613 AutoTArray
<Element
*, 8>* entries
=
614 static_cast<AutoTArray
<Element
*, 8>*>(aData
);
615 entries
->AppendElement(aEntry
->GetKey());
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
);
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
) {
701 aElement
->SetDirectionality(dir
, aNotify
);
705 static void SetDirectionalityOnDescendantsInternal(nsINode
* aNode
,
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
);
720 Element
* element
= child
->AsElement();
721 if (element
->HasValidDir() || element
->HasDirAuto() ||
722 element
->GetAssignedSlot()) {
723 child
= child
->GetNextNonChildNode(aNode
);
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
,
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
,
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
));
766 nsTextNodeDirectionalityMap::RemoveElementFromMap(setByNode
, aElement
);
770 if (aElement
->HasDirAuto()) {
771 nsTextNode
* setByNode
=
772 WalkDescendantsSetDirectionFromText(aElement
, aNotify
);
774 nsTextNodeDirectionalityMap::AddEntryToMap(setByNode
, aElement
);
776 SetDirectionalityOnDescendants(aElement
, aElement
->GetDirectionality(),
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
);
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
));
804 nsTextNodeDirectionalityMap::RemoveElementFromMap(setByNode
,
808 if (parentElement
->HasDirAuto()) {
809 setByNode
= WalkDescendantsSetDirectionFromText(parentElement
, aNotify
);
811 nsTextNodeDirectionalityMap::AddEntryToMap(setByNode
, parentElement
);
813 SetDirectionalityOnDescendants(
814 parentElement
, parentElement
->GetDirectionality(), aNotify
);
817 parent
= GetParentOrHostOrSlot(parent
);
821 static void RecomputeSlottedNodeDirection(HTMLSlotElement
& aSlot
,
823 auto* assignedElement
= Element::FromNode(aNode
);
824 if (!assignedElement
) {
828 if (assignedElement
->HasValidDir() || assignedElement
->HasDirAuto()) {
832 // Try to optimize out state changes when possible.
833 if (assignedElement
->GetDirectionality() == aSlot
.GetDirectionality()) {
837 assignedElement
->SetDirectionality(aSlot
.GetDirectionality(), true);
838 SetDirectionalityOnDescendantsInternal(assignedElement
,
839 aSlot
.GetDirectionality(), true);
842 void SlotAssignedNodeChanged(HTMLSlotElement
* aSlot
,
843 nsIContent
& aAssignedNode
) {
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
) {
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();
884 if (child
->IsElement() && child
->AsElement()->HasDirAuto()) {
885 child
= child
->GetNextNonChildNode(aElement
);
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();
917 if (child
->IsElement() &&
918 DoesNotAffectDirectionOfAncestors(child
->AsElement())) {
919 child
= child
->GetNextNonChildNode(aRoot
);
923 // If the child is assigned to a slot, it should inherit the state from
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
);
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();
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
);
974 if (child
->IsElement()) {
975 if (child
->AsElement()->HasDirAuto()) {
976 child
= child
->GetNextNonChildNode(aContent
);
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()) {
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
);
1015 Element
* parentElement
= parent
->AsElement();
1016 if (DoesNotParticipateInAutoDirection(parentElement
) ||
1017 parentElement
->HasFixedDir()) {
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;
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
);
1048 nsIContent
* child
= aTextNode
->GetNextNode(parent
);
1050 if (child
->IsElement() &&
1051 DoesNotAffectDirectionOfAncestors(child
->AsElement())) {
1052 child
= child
->GetNextNonChildNode(parent
);
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;
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.
1083 parent
= GetParentOrHostOrSlot(parent
, &crossedShadowBoundary
);
1087 bool TextNodeWillChangeDirection(nsTextNode
* aTextNode
, Directionality
* aOldDir
,
1089 if (!NodeAffectsDirAutoAncestor(aTextNode
)) {
1090 nsTextNodeDirectionalityMap::EnsureMapIsClearFor(aTextNode
);
1094 uint32_t firstStrong
;
1095 *aOldDir
= GetDirectionFromText(aTextNode
, &firstStrong
);
1096 return (aOffset
<= firstStrong
);
1099 void TextNodeChangedDirection(nsTextNode
* aTextNode
, Directionality aOldDir
,
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
);
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
,
1123 SetAncestorDirectionIfAuto(aTextNode
, newDir
, aNotify
);
1127 void SetDirectionFromNewTextNode(nsTextNode
* aTextNode
) {
1128 if (!NodeAffectsDirAutoAncestor(aTextNode
)) {
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
);
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
,
1157 Directionality dir
=
1158 GetDirectionFromText(value
.BeginReading(), value
.Length());
1159 if (dir
== eDir_NotSet
) {
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
)) {
1172 if (aElement
->AncestorHasDirAuto()) {
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
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
1200 WalkDescendantsClearAncestorDirAuto(aElement
);
1203 if (aElement
->HasDirAuto()) {
1204 WalkDescendantsSetDirAuto(aElement
, aNotify
);
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