content/base/src/DirectionalityUtils.cpp

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:4e40960f2693
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 sw=2 et tw=78: */
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 Implementation description from https://etherpad.mozilla.org/dir-auto
9
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.
15
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.
19
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.
22
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.
27
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:
37
38 <div dir=auto>
39 <span>foo</span>
40 <span>بار</span>
41 </div>
42
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.
45
46 I will call this algorithm "upward propagation".
47
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.
55
56 *IMPLEMENTATION NOTE*
57 In practice, the implementation uses two per-node properties:
58
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.
62
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.
65
66 Handling dynamic changes
67 ========================
68
69 We need to handle the following cases:
70
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.
73
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.
78
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
88 in http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#the-dir-attribute
89 (I'll call this the "downward propagation algorithm".) by walking the child
90 subtree in tree order. Note that an element with @dir=auto should not affect
91 other elements in its document with @dir=auto. So there is no need to walk up
92 the parent chain in this case. TextNodeDirectionalityMap needs to be updated
93 as appropriate.
94
95 3a. When the dir attribute is set to any valid value on an element that didn't
96 have a valid dir attribute before, this means that any descendant of that
97 element will not affect the directionality of any of its ancestors. So we need
98 to check whether any text node descendants of the element are listed in
99 TextNodeDirectionalityMap, and whether the elements whose direction they set
100 are ancestors of the element. If so, we need to rerun the downward propagation
101 algorithm for those ancestors.
102
103 4. When the dir attribute is changed from auto to something else (including
104 the case where it gets removed) on a textarea or an input element with
105 @type=text/search/tel/url/email, we unset the NodeHasDirAuto flag and
106 resolve the directionality based on the directionality of the value of the @dir
107 attribute on element itself or its parent element.
108
109 5. When the dir attribute is changed from auto to something else (including the
110 case where it gets removed) on any element except case 4 above and the bdi
111 element, we run the following algorithm:
112 * We unset the NodeHasDirAuto flag.
113 * If the element does not have the NodeAncestorHasDirAuto flag, we unset
114 the NodeAncestorHasDirAuto flag on all of its child nodes, except those
115 who are a descendant of another element with NodeHasDirAuto. (Note that if
116 the element has the NodeAncestorHasDirAuto flag, all of its child nodes
117 should still retain the same flag.)
118 * We resolve the directionality of the element based on the value of the @dir
119 attribute on the element itself or its parent element.
120 TextNodeDirectionalityMap needs to be updated as appropriate.
121
122 5a. When the dir attribute is removed or set to an invalid value on any
123 element (except a bdi element) with the NodeAncestorHasDirAuto flag which
124 previously had a valid dir attribute, it might have a text node descendant that
125 did not previously affect the directionality of any of its ancestors but should
126 now begin to affect them.
127 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.
134
135 6. When an element with @dir=auto is added to the document, we should handle it
136 similar to the case 2/3 above.
137
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 to
143 happen but in the implementation UnbindFromTree is going to be called on all of
144 the descendants so we don't need to descend into the child subtree).
145
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 them
155 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.
170
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.
181
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 I'm
189 filing a spec bug to get it fixed -- the directionality should depend on the
190 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.
195
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.
199
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, nsGenericDOMDataNode::SetTextInternal
204 gets called.
205 */
206
207 #include "mozilla/dom/DirectionalityUtils.h"
208
209 #include "nsINode.h"
210 #include "nsIContent.h"
211 #include "nsIDocument.h"
212 #include "mozilla/DebugOnly.h"
213 #include "mozilla/dom/Element.h"
214 #include "nsIDOMHTMLDocument.h"
215 #include "nsUnicodeProperties.h"
216 #include "nsTextFragment.h"
217 #include "nsAttrValue.h"
218 #include "nsTextNode.h"
219 #include "nsCheapSets.h"
220
221 namespace mozilla {
222
223 using mozilla::dom::Element;
224
225 /**
226 * Returns true if aElement is one of the elements whose text content should not
227 * affect its own direction, nor the direction of ancestors with dir=auto.
228 *
229 * Note that this does not include <bdi>, whose content does affect its own
230 * direction when it has dir=auto (which it has by default), so one needs to
231 * test for it separately, e.g. with DoesNotAffectDirectionOfAncestors.
232 * It *does* include textarea, because even if a textarea has dir=auto, it has
233 * unicode-bidi: plaintext and is handled automatically in bidi resolution.
234 */
235 static bool
236 DoesNotParticipateInAutoDirection(const Element* aElement)
237 {
238 nsINodeInfo* nodeInfo = aElement->NodeInfo();
239 return (!aElement->IsHTML() ||
240 nodeInfo->Equals(nsGkAtoms::script) ||
241 nodeInfo->Equals(nsGkAtoms::style) ||
242 nodeInfo->Equals(nsGkAtoms::textarea) ||
243 aElement->IsInAnonymousSubtree());
244 }
245
246 static inline bool
247 IsBdiWithoutDirAuto(const Element* aElement)
248 {
249 // We are testing for bdi elements without explicit dir="auto", so we can't
250 // use the HasDirAuto() flag, since that will return true for bdi element with
251 // no dir attribute or an invalid dir attribute
252 return (aElement->IsHTML(nsGkAtoms::bdi) &&
253 (!aElement->HasValidDir() || aElement->HasFixedDir()));
254 }
255
256 /**
257 * Returns true if aElement is one of the element whose text content should not
258 * affect the direction of ancestors with dir=auto (though it may affect its own
259 * direction, e.g. <bdi>)
260 */
261 static bool
262 DoesNotAffectDirectionOfAncestors(const Element* aElement)
263 {
264 return (DoesNotParticipateInAutoDirection(aElement) ||
265 IsBdiWithoutDirAuto(aElement) ||
266 aElement->HasFixedDir());
267 }
268
269 /**
270 * Returns the directionality of a Unicode character
271 */
272 static Directionality
273 GetDirectionFromChar(uint32_t ch)
274 {
275 switch(mozilla::unicode::GetBidiCat(ch)) {
276 case eCharType_RightToLeft:
277 case eCharType_RightToLeftArabic:
278 return eDir_RTL;
279
280 case eCharType_LeftToRight:
281 return eDir_LTR;
282
283 default:
284 return eDir_NotSet;
285 }
286 }
287
288 inline static bool NodeAffectsDirAutoAncestor(nsINode* aTextNode)
289 {
290 Element* parent = aTextNode->GetParentElement();
291 return (parent &&
292 !DoesNotParticipateInAutoDirection(parent) &&
293 parent->NodeOrAncestorHasDirAuto());
294 }
295
296 /**
297 * Various methods for returning the directionality of a string using the
298 * first-strong algorithm defined in http://unicode.org/reports/tr9/#P2
299 *
300 * @param[out] aFirstStrong the offset to the first character in the string with
301 * strong directionality, or UINT32_MAX if there is none (return
302 value is eDir_NotSet).
303 * @return the directionality of the string
304 */
305 static Directionality
306 GetDirectionFromText(const char16_t* aText, const uint32_t aLength,
307 uint32_t* aFirstStrong = nullptr)
308 {
309 const char16_t* start = aText;
310 const char16_t* end = aText + aLength;
311
312 while (start < end) {
313 uint32_t current = start - aText;
314 uint32_t ch = *start++;
315
316 if (NS_IS_HIGH_SURROGATE(ch) &&
317 start < end &&
318 NS_IS_LOW_SURROGATE(*start)) {
319 ch = SURROGATE_TO_UCS4(ch, *start++);
320 current++;
321 }
322
323 // Just ignore lone surrogates
324 if (!IS_SURROGATE(ch)) {
325 Directionality dir = GetDirectionFromChar(ch);
326 if (dir != eDir_NotSet) {
327 if (aFirstStrong) {
328 *aFirstStrong = current;
329 }
330 return dir;
331 }
332 }
333 }
334
335 if (aFirstStrong) {
336 *aFirstStrong = UINT32_MAX;
337 }
338 return eDir_NotSet;
339 }
340
341 static Directionality
342 GetDirectionFromText(const char* aText, const uint32_t aLength,
343 uint32_t* aFirstStrong = nullptr)
344 {
345 const char* start = aText;
346 const char* end = aText + aLength;
347
348 while (start < end) {
349 uint32_t current = start - aText;
350 unsigned char ch = (unsigned char)*start++;
351
352 Directionality dir = GetDirectionFromChar(ch);
353 if (dir != eDir_NotSet) {
354 if (aFirstStrong) {
355 *aFirstStrong = current;
356 }
357 return dir;
358 }
359 }
360
361 if (aFirstStrong) {
362 *aFirstStrong = UINT32_MAX;
363 }
364 return eDir_NotSet;
365 }
366
367 static Directionality
368 GetDirectionFromText(const nsTextFragment* aFrag,
369 uint32_t* aFirstStrong = nullptr)
370 {
371 if (aFrag->Is2b()) {
372 return GetDirectionFromText(aFrag->Get2b(), aFrag->GetLength(),
373 aFirstStrong);
374 }
375
376 return GetDirectionFromText(aFrag->Get1b(), aFrag->GetLength(),
377 aFirstStrong);
378 }
379
380 /**
381 * Set the directionality of a node with dir=auto as defined in
382 * http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#the-directionality
383 *
384 * @param[in] changedNode If we call this method because the content of a text
385 * node is about to change, pass in the changed node, so that we
386 * know not to return it
387 * @return the text node containing the character that determined the direction
388 */
389 static nsINode*
390 WalkDescendantsSetDirectionFromText(Element* aElement, bool aNotify = true,
391 nsINode* aChangedNode = nullptr)
392 {
393 MOZ_ASSERT(aElement, "Must have an element");
394 MOZ_ASSERT(aElement->HasDirAuto(), "Element must have dir=auto");
395
396 if (DoesNotParticipateInAutoDirection(aElement)) {
397 return nullptr;
398 }
399
400 nsIContent* child = aElement->GetFirstChild();
401 while (child) {
402 if (child->IsElement() &&
403 DoesNotAffectDirectionOfAncestors(child->AsElement())) {
404 child = child->GetNextNonChildNode(aElement);
405 continue;
406 }
407
408 if (child->NodeType() == nsIDOMNode::TEXT_NODE &&
409 child != aChangedNode) {
410 Directionality textNodeDir = GetDirectionFromText(child->GetText());
411 if (textNodeDir != eDir_NotSet) {
412 // We found a descendant text node with strong directional characters.
413 // Set the directionality of aElement to the corresponding value.
414 aElement->SetDirectionality(textNodeDir, aNotify);
415 return child;
416 }
417 }
418 child = child->GetNextNode(aElement);
419 }
420
421 // We walked all the descendants without finding a text node with strong
422 // directional characters. Set the directionality to LTR
423 aElement->SetDirectionality(eDir_LTR, aNotify);
424 return nullptr;
425 }
426
427 class nsTextNodeDirectionalityMap
428 {
429 static void
430 nsTextNodeDirectionalityMapDtor(void *aObject, nsIAtom* aPropertyName,
431 void *aPropertyValue, void* aData)
432 {
433 nsINode* textNode = static_cast<nsINode * >(aObject);
434 textNode->ClearHasTextNodeDirectionalityMap();
435
436 nsTextNodeDirectionalityMap* map =
437 reinterpret_cast<nsTextNodeDirectionalityMap * >(aPropertyValue);
438 map->EnsureMapIsClear(textNode);
439 delete map;
440 }
441
442 public:
443 nsTextNodeDirectionalityMap(nsINode* aTextNode)
444 {
445 MOZ_ASSERT(aTextNode, "Null text node");
446 MOZ_COUNT_CTOR(nsTextNodeDirectionalityMap);
447 aTextNode->SetProperty(nsGkAtoms::textNodeDirectionalityMap, this,
448 nsTextNodeDirectionalityMapDtor);
449 aTextNode->SetHasTextNodeDirectionalityMap();
450 }
451
452 ~nsTextNodeDirectionalityMap()
453 {
454 MOZ_COUNT_DTOR(nsTextNodeDirectionalityMap);
455 }
456
457 void AddEntry(nsINode* aTextNode, Element* aElement)
458 {
459 if (!mElements.Contains(aElement)) {
460 mElements.Put(aElement);
461 aElement->SetProperty(nsGkAtoms::dirAutoSetBy, aTextNode);
462 aElement->SetHasDirAutoSet();
463 }
464 }
465
466 void RemoveEntry(nsINode* aTextNode, Element* aElement)
467 {
468 NS_ASSERTION(mElements.Contains(aElement),
469 "element already removed from map");
470
471 mElements.Remove(aElement);
472 aElement->ClearHasDirAutoSet();
473 aElement->UnsetProperty(nsGkAtoms::dirAutoSetBy);
474 }
475
476 private:
477 nsCheapSet<nsPtrHashKey<Element> > mElements;
478
479 static nsTextNodeDirectionalityMap* GetDirectionalityMap(nsINode* aTextNode)
480 {
481 MOZ_ASSERT(aTextNode->NodeType() == nsIDOMNode::TEXT_NODE,
482 "Must be a text node");
483 nsTextNodeDirectionalityMap* map = nullptr;
484
485 if (aTextNode->HasTextNodeDirectionalityMap()) {
486 map = static_cast<nsTextNodeDirectionalityMap * >
487 (aTextNode->GetProperty(nsGkAtoms::textNodeDirectionalityMap));
488 }
489
490 return map;
491 }
492
493 static PLDHashOperator SetNodeDirection(nsPtrHashKey<Element>* aEntry, void* aDir)
494 {
495 MOZ_ASSERT(aEntry->GetKey()->IsElement(), "Must be an Element");
496 aEntry->GetKey()->SetDirectionality(*reinterpret_cast<Directionality*>(aDir),
497 true);
498 return PL_DHASH_NEXT;
499 }
500
501 static PLDHashOperator ResetNodeDirection(nsPtrHashKey<Element>* aEntry, void* aData)
502 {
503 MOZ_ASSERT(aEntry->GetKey()->IsElement(), "Must be an Element");
504 // run the downward propagation algorithm
505 // and remove the text node from the map
506 nsINode* oldTextNode = static_cast<Element*>(aData);
507 Element* rootNode = aEntry->GetKey();
508 nsINode* newTextNode = nullptr;
509 if (oldTextNode && rootNode->HasDirAuto()) {
510 newTextNode = WalkDescendantsSetDirectionFromText(rootNode, true,
511 oldTextNode);
512 }
513 if (newTextNode) {
514 nsTextNodeDirectionalityMap::AddEntryToMap(newTextNode, rootNode);
515 } else {
516 rootNode->ClearHasDirAutoSet();
517 rootNode->UnsetProperty(nsGkAtoms::dirAutoSetBy);
518 }
519 return PL_DHASH_REMOVE;
520 }
521
522 static PLDHashOperator ClearEntry(nsPtrHashKey<Element>* aEntry, void* aData)
523 {
524 Element* rootNode = aEntry->GetKey();
525 rootNode->ClearHasDirAutoSet();
526 rootNode->UnsetProperty(nsGkAtoms::dirAutoSetBy);
527 return PL_DHASH_REMOVE;
528 }
529
530 public:
531 void UpdateAutoDirection(Directionality aDir)
532 {
533 mElements.EnumerateEntries(SetNodeDirection, &aDir);
534 }
535
536 void ClearAutoDirection()
537 {
538 mElements.EnumerateEntries(ResetNodeDirection, nullptr);
539 }
540
541 void ResetAutoDirection(nsINode* aTextNode)
542 {
543 mElements.EnumerateEntries(ResetNodeDirection, aTextNode);
544 }
545
546 void EnsureMapIsClear(nsINode* aTextNode)
547 {
548 DebugOnly<uint32_t> clearedEntries =
549 mElements.EnumerateEntries(ClearEntry, aTextNode);
550 MOZ_ASSERT(clearedEntries == 0, "Map should be empty already");
551 }
552
553 static void RemoveElementFromMap(nsINode* aTextNode, Element* aElement)
554 {
555 if (aTextNode->HasTextNodeDirectionalityMap()) {
556 GetDirectionalityMap(aTextNode)->RemoveEntry(aTextNode, aElement);
557 }
558 }
559
560 static void AddEntryToMap(nsINode* aTextNode, Element* aElement)
561 {
562 nsTextNodeDirectionalityMap* map = GetDirectionalityMap(aTextNode);
563 if (!map) {
564 map = new nsTextNodeDirectionalityMap(aTextNode);
565 }
566
567 map->AddEntry(aTextNode, aElement);
568 }
569
570 static void UpdateTextNodeDirection(nsINode* aTextNode, Directionality aDir)
571 {
572 MOZ_ASSERT(aTextNode->HasTextNodeDirectionalityMap(),
573 "Map missing in UpdateTextNodeDirection");
574 GetDirectionalityMap(aTextNode)->UpdateAutoDirection(aDir);
575 }
576
577 static void ClearTextNodeDirection(nsINode* aTextNode)
578 {
579 MOZ_ASSERT(aTextNode->HasTextNodeDirectionalityMap(),
580 "Map missing in ResetTextNodeDirection");
581 GetDirectionalityMap(aTextNode)->ClearAutoDirection();
582 }
583
584 static void ResetTextNodeDirection(nsINode* aTextNode)
585 {
586 MOZ_ASSERT(aTextNode->HasTextNodeDirectionalityMap(),
587 "Map missing in ResetTextNodeDirection");
588 GetDirectionalityMap(aTextNode)->ResetAutoDirection(aTextNode);
589 }
590
591 static void EnsureMapIsClearFor(nsINode* aTextNode)
592 {
593 if (aTextNode->HasTextNodeDirectionalityMap()) {
594 GetDirectionalityMap(aTextNode)->EnsureMapIsClear(aTextNode);
595 }
596 }
597 };
598
599 Directionality
600 RecomputeDirectionality(Element* aElement, bool aNotify)
601 {
602 MOZ_ASSERT(!aElement->HasDirAuto(),
603 "RecomputeDirectionality called with dir=auto");
604
605 Directionality dir = eDir_LTR;
606
607 if (aElement->HasValidDir()) {
608 dir = aElement->GetDirectionality();
609 } else {
610 Element* parent = aElement->GetParentElement();
611 if (parent) {
612 // If the element doesn't have an explicit dir attribute with a valid
613 // value, the directionality is the same as the parent element (but
614 // don't propagate the parent directionality if it isn't set yet).
615 Directionality parentDir = parent->GetDirectionality();
616 if (parentDir != eDir_NotSet) {
617 dir = parentDir;
618 }
619 } else {
620 // If there is no parent element and no dir attribute, the directionality
621 // is LTR.
622 dir = eDir_LTR;
623 }
624
625 aElement->SetDirectionality(dir, aNotify);
626 }
627 return dir;
628 }
629
630 void
631 SetDirectionalityOnDescendants(Element* aElement, Directionality aDir,
632 bool aNotify)
633 {
634 for (nsIContent* child = aElement->GetFirstChild(); child; ) {
635 if (!child->IsElement()) {
636 child = child->GetNextNode(aElement);
637 continue;
638 }
639
640 Element* element = child->AsElement();
641 if (element->HasValidDir() || element->HasDirAuto()) {
642 child = child->GetNextNonChildNode(aElement);
643 continue;
644 }
645 element->SetDirectionality(aDir, aNotify);
646 child = child->GetNextNode(aElement);
647 }
648 }
649
650 /**
651 * Walk the parent chain of a text node whose dir attribute has been removed and
652 * reset the direction of any of its ancestors which have dir=auto and whose
653 * directionality is determined by a text node descendant.
654 */
655 void
656 WalkAncestorsResetAutoDirection(Element* aElement, bool aNotify)
657 {
658 nsINode* setByNode;
659 Element* parent = aElement->GetParentElement();
660
661 while (parent && parent->NodeOrAncestorHasDirAuto()) {
662 if (parent->HasDirAutoSet()) {
663 // If the parent has the DirAutoSet flag, its direction is determined by
664 // some text node descendant.
665 // Remove it from the map and reset its direction by the downward
666 // propagation algorithm
667 setByNode =
668 static_cast<nsINode*>(parent->GetProperty(nsGkAtoms::dirAutoSetBy));
669 if (setByNode) {
670 nsTextNodeDirectionalityMap::RemoveElementFromMap(setByNode, parent);
671 }
672 }
673 if (parent->HasDirAuto()) {
674 setByNode = WalkDescendantsSetDirectionFromText(parent, aNotify);
675 if (setByNode) {
676 nsTextNodeDirectionalityMap::AddEntryToMap(setByNode, parent);
677 }
678 break;
679 }
680 parent = parent->GetParentElement();
681 }
682 }
683
684 void
685 WalkDescendantsResetAutoDirection(Element* aElement)
686 {
687 nsIContent* child = aElement->GetFirstChild();
688 while (child) {
689 if (child->HasDirAuto()) {
690 child = child->GetNextNonChildNode(aElement);
691 continue;
692 }
693
694 if (child->HasTextNodeDirectionalityMap()) {
695 nsTextNodeDirectionalityMap::ResetTextNodeDirection(child);
696 nsTextNodeDirectionalityMap::EnsureMapIsClearFor(child);
697 }
698 child = child->GetNextNode(aElement);
699 }
700 }
701
702 void
703 WalkDescendantsSetDirAuto(Element* aElement, bool aNotify)
704 {
705 // Only test for DoesNotParticipateInAutoDirection -- in other words, if
706 // aElement is a <bdi> which is having its dir attribute set to auto (or
707 // removed or set to an invalid value, which are equivalent to dir=auto for
708 // <bdi>, we *do* want to set AncestorHasDirAuto on its descendants, unlike
709 // in SetDirOnBind where we don't propagate AncestorHasDirAuto to a <bdi>
710 // being bound to an existing node with dir=auto.
711 if (!DoesNotParticipateInAutoDirection(aElement)) {
712
713 bool setAncestorDirAutoFlag =
714 #ifdef DEBUG
715 true;
716 #else
717 !aElement->AncestorHasDirAuto();
718 #endif
719
720 if (setAncestorDirAutoFlag) {
721 nsIContent* child = aElement->GetFirstChild();
722 while (child) {
723 if (child->IsElement() &&
724 DoesNotAffectDirectionOfAncestors(child->AsElement())) {
725 child = child->GetNextNonChildNode(aElement);
726 continue;
727 }
728
729 MOZ_ASSERT(!aElement->AncestorHasDirAuto() ||
730 child->AncestorHasDirAuto(),
731 "AncestorHasDirAuto set on node but not its children");
732 child->SetAncestorHasDirAuto();
733 child = child->GetNextNode(aElement);
734 }
735 }
736 }
737
738 nsINode* textNode = WalkDescendantsSetDirectionFromText(aElement, aNotify);
739 if (textNode) {
740 nsTextNodeDirectionalityMap::AddEntryToMap(textNode, aElement);
741 }
742 }
743
744 void
745 WalkDescendantsClearAncestorDirAuto(Element* aElement)
746 {
747 nsIContent* child = aElement->GetFirstChild();
748 while (child) {
749 if (child->HasDirAuto()) {
750 child = child->GetNextNonChildNode(aElement);
751 continue;
752 }
753
754 child->ClearAncestorHasDirAuto();
755 child = child->GetNextNode(aElement);
756 }
757 }
758
759 void SetAncestorDirectionIfAuto(nsINode* aTextNode, Directionality aDir,
760 bool aNotify = true)
761 {
762 MOZ_ASSERT(aTextNode->NodeType() == nsIDOMNode::TEXT_NODE,
763 "Must be a text node");
764
765 Element* parent = aTextNode->GetParentElement();
766 while (parent && parent->NodeOrAncestorHasDirAuto()) {
767 if (DoesNotParticipateInAutoDirection(parent) || parent->HasFixedDir()) {
768 break;
769 }
770
771 if (parent->HasDirAuto()) {
772 bool resetDirection = false;
773 nsINode* directionWasSetByTextNode =
774 static_cast<nsINode*>(parent->GetProperty(nsGkAtoms::dirAutoSetBy));
775
776 if (!parent->HasDirAutoSet()) {
777 // Fast path if parent's direction is not yet set by any descendant
778 MOZ_ASSERT(!directionWasSetByTextNode,
779 "dirAutoSetBy property should be null");
780 resetDirection = true;
781 } else {
782 // If parent's direction is already set, we need to know if
783 // aTextNode is before or after the text node that had set it.
784 // We will walk parent's descendants in tree order starting from
785 // aTextNode to optimize for the most common case where text nodes are
786 // being appended to tree.
787 if (!directionWasSetByTextNode) {
788 resetDirection = true;
789 } else if (directionWasSetByTextNode != aTextNode) {
790 nsIContent* child = aTextNode->GetNextNode(parent);
791 while (child) {
792 if (child->IsElement() &&
793 DoesNotAffectDirectionOfAncestors(child->AsElement())) {
794 child = child->GetNextNonChildNode(parent);
795 continue;
796 }
797
798 if (child == directionWasSetByTextNode) {
799 // we found the node that set the element's direction after our
800 // text node, so we need to reset the direction
801 resetDirection = true;
802 break;
803 }
804
805 child = child->GetNextNode(parent);
806 }
807 }
808 }
809
810 if (resetDirection) {
811 if (directionWasSetByTextNode) {
812 nsTextNodeDirectionalityMap::RemoveElementFromMap(
813 directionWasSetByTextNode, parent
814 );
815 }
816 parent->SetDirectionality(aDir, aNotify);
817 nsTextNodeDirectionalityMap::AddEntryToMap(aTextNode, parent);
818 SetDirectionalityOnDescendants(parent, aDir, aNotify);
819 }
820
821 // Since we found an element with dir=auto, we can stop walking the
822 // parent chain: none of its ancestors will have their direction set by
823 // any of its descendants.
824 return;
825 }
826 parent = parent->GetParentElement();
827 }
828 }
829
830 bool
831 TextNodeWillChangeDirection(nsIContent* aTextNode, Directionality* aOldDir,
832 uint32_t aOffset)
833 {
834 if (!NodeAffectsDirAutoAncestor(aTextNode)) {
835 nsTextNodeDirectionalityMap::EnsureMapIsClearFor(aTextNode);
836 return false;
837 }
838
839 uint32_t firstStrong;
840 *aOldDir = GetDirectionFromText(aTextNode->GetText(), &firstStrong);
841 return (aOffset <= firstStrong);
842 }
843
844 void
845 TextNodeChangedDirection(nsIContent* aTextNode, Directionality aOldDir,
846 bool aNotify)
847 {
848 Directionality newDir = GetDirectionFromText(aTextNode->GetText());
849 if (newDir == eDir_NotSet) {
850 if (aOldDir != eDir_NotSet && aTextNode->HasTextNodeDirectionalityMap()) {
851 // This node used to have a strong directional character but no
852 // longer does. ResetTextNodeDirection() will re-resolve the
853 // directionality of any elements whose directionality was
854 // determined by this node.
855 nsTextNodeDirectionalityMap::ResetTextNodeDirection(aTextNode);
856 }
857 } else {
858 // This node has a strong directional character. If it has a
859 // TextNodeDirectionalityMap property, it already determines the
860 // directionality of some element(s), so call UpdateTextNodeDirection to
861 // reresolve their directionality. Otherwise call
862 // SetAncestorDirectionIfAuto to find ancestor elements which should
863 // have their directionality determined by this node.
864 if (aTextNode->HasTextNodeDirectionalityMap()) {
865 nsTextNodeDirectionalityMap::UpdateTextNodeDirection(aTextNode, newDir);
866 } else {
867 SetAncestorDirectionIfAuto(aTextNode, newDir, aNotify);
868 }
869 }
870 }
871
872 void
873 SetDirectionFromNewTextNode(nsIContent* aTextNode)
874 {
875 if (!NodeAffectsDirAutoAncestor(aTextNode)) {
876 return;
877 }
878
879 Element* parent = aTextNode->GetParentElement();
880 if (parent && parent->NodeOrAncestorHasDirAuto()) {
881 aTextNode->SetAncestorHasDirAuto();
882 }
883
884 Directionality dir = GetDirectionFromText(aTextNode->GetText());
885 if (dir != eDir_NotSet) {
886 SetAncestorDirectionIfAuto(aTextNode, dir);
887 }
888 }
889
890 void
891 ResetDirectionSetByTextNode(nsTextNode* aTextNode, bool aNullParent)
892 {
893 if (!NodeAffectsDirAutoAncestor(aTextNode)) {
894 nsTextNodeDirectionalityMap::EnsureMapIsClearFor(aTextNode);
895 return;
896 }
897
898 Directionality dir = GetDirectionFromText(aTextNode->GetText());
899 if (dir != eDir_NotSet && aTextNode->HasTextNodeDirectionalityMap()) {
900 if (aNullParent) {
901 nsTextNodeDirectionalityMap::ClearTextNodeDirection(aTextNode);
902 } else {
903 nsTextNodeDirectionalityMap::ResetTextNodeDirection(aTextNode);
904 }
905 }
906 }
907
908 void
909 SetDirectionalityFromValue(Element* aElement, const nsAString& value,
910 bool aNotify)
911 {
912 Directionality dir = GetDirectionFromText(PromiseFlatString(value).get(),
913 value.Length());
914 if (dir == eDir_NotSet) {
915 dir = eDir_LTR;
916 }
917
918 aElement->SetDirectionality(dir, aNotify);
919 }
920
921 void
922 OnSetDirAttr(Element* aElement, const nsAttrValue* aNewValue,
923 bool hadValidDir, bool hadDirAuto, bool aNotify)
924 {
925 if (aElement->IsHTML(nsGkAtoms::input)) {
926 return;
927 }
928
929 if (aElement->AncestorHasDirAuto()) {
930 if (!hadValidDir) {
931 // The element is a descendant of an element with dir = auto, is
932 // having its dir attribute set, and previously didn't have a valid dir
933 // attribute.
934 // Check whether any of its text node descendants determine the
935 // direction of any of its ancestors, and redetermine their direction
936 WalkDescendantsResetAutoDirection(aElement);
937 } else if (!aElement->HasValidDir()) {
938 // The element is a descendant of an element with dir = auto and is
939 // having its dir attribute removed or set to an invalid value.
940 // Reset the direction of any of its ancestors whose direction is
941 // determined by a text node descendant
942 WalkAncestorsResetAutoDirection(aElement, aNotify);
943 }
944 } else if (hadDirAuto && !aElement->HasDirAuto()) {
945 // The element isn't a descendant of an element with dir = auto, and is
946 // having its dir attribute set to something other than auto.
947 // Walk the descendant tree and clear the AncestorHasDirAuto flag.
948 //
949 // N.B: For elements other than <bdi> it would be enough to test that the
950 // current value of dir was "auto" in BeforeSetAttr to know that we
951 // were unsetting dir="auto". For <bdi> things are more complicated,
952 // since it behaves like dir="auto" whenever the dir attribute is
953 // empty or invalid, so we would have to check whether the old value
954 // was not either "ltr" or "rtl", and the new value was either "ltr"
955 // or "rtl". Element::HasDirAuto() encapsulates all that, so doing it
956 // here is simpler.
957 WalkDescendantsClearAncestorDirAuto(aElement);
958 }
959
960 if (aElement->HasDirAuto()) {
961 WalkDescendantsSetDirAuto(aElement, aNotify);
962 } else {
963 if (aElement->HasDirAutoSet()) {
964 nsINode* setByNode =
965 static_cast<nsINode*>(aElement->GetProperty(nsGkAtoms::dirAutoSetBy));
966 nsTextNodeDirectionalityMap::RemoveElementFromMap(setByNode, aElement);
967 }
968 SetDirectionalityOnDescendants(aElement,
969 RecomputeDirectionality(aElement, aNotify),
970 aNotify);
971 }
972 }
973
974 void
975 SetDirOnBind(mozilla::dom::Element* aElement, nsIContent* aParent)
976 {
977 // Set the AncestorHasDirAuto flag, unless this element shouldn't affect
978 // ancestors that have dir=auto
979 if (!DoesNotParticipateInAutoDirection(aElement) &&
980 !aElement->IsHTML(nsGkAtoms::bdi) &&
981 aParent && aParent->NodeOrAncestorHasDirAuto()) {
982 aElement->SetAncestorHasDirAuto();
983
984 nsIContent* child = aElement->GetFirstChild();
985 if (child) {
986 // If we are binding an element to the tree that already has descendants,
987 // and the parent has NodeHasDirAuto or NodeAncestorHasDirAuto, we need
988 // to set NodeAncestorHasDirAuto on all the element's descendants, except
989 // for nodes that don't affect the direction of their ancestors.
990 do {
991 if (child->IsElement() &&
992 DoesNotAffectDirectionOfAncestors(child->AsElement())) {
993 child = child->GetNextNonChildNode(aElement);
994 continue;
995 }
996
997 child->SetAncestorHasDirAuto();
998 child = child->GetNextNode(aElement);
999 } while (child);
1000
1001 // We may also need to reset the direction of an ancestor with dir=auto
1002 WalkAncestorsResetAutoDirection(aElement, true);
1003 }
1004 }
1005
1006 if (!aElement->HasDirAuto()) {
1007 // if the element doesn't have dir=auto, set its own directionality from
1008 // the dir attribute or by inheriting from its ancestors.
1009 RecomputeDirectionality(aElement, false);
1010 }
1011 }
1012
1013 void ResetDir(mozilla::dom::Element* aElement)
1014 {
1015 if (aElement->HasDirAutoSet()) {
1016 nsINode* setByNode =
1017 static_cast<nsINode*>(aElement->GetProperty(nsGkAtoms::dirAutoSetBy));
1018 nsTextNodeDirectionalityMap::RemoveElementFromMap(setByNode, aElement);
1019 }
1020
1021 if (!aElement->HasDirAuto()) {
1022 RecomputeDirectionality(aElement, false);
1023 }
1024 }
1025
1026 } // end namespace mozilla
1027

mercurial