|
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 |