1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:expandtab:shiftwidth=2:tabstop=2:
3  */
4 /* This Source Code Form is subject to the terms of the Mozilla Public
5  * License, v. 2.0. If a copy of the MPL was not distributed with this
6  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 
8 #include "nsTextEquivUtils.h"
9 
10 #include "Accessible-inl.h"
11 #include "AccIterator.h"
12 #include "nsCoreUtils.h"
13 #include "mozilla/dom/Text.h"
14 
15 using namespace mozilla;
16 using namespace mozilla::a11y;
17 
18 /**
19  * The accessible for which we are computing a text equivalent. It is useful
20  * for bailing out during recursive text computation, or for special cases
21  * like step f. of the ARIA implementation guide.
22  */
23 static const Accessible* sInitiatorAcc = nullptr;
24 
25 ////////////////////////////////////////////////////////////////////////////////
26 // nsTextEquivUtils. Public.
27 
GetNameFromSubtree(const Accessible * aAccessible,nsAString & aName)28 nsresult nsTextEquivUtils::GetNameFromSubtree(const Accessible* aAccessible,
29                                               nsAString& aName) {
30   aName.Truncate();
31 
32   if (sInitiatorAcc) return NS_OK;
33 
34   sInitiatorAcc = aAccessible;
35   if (GetRoleRule(aAccessible->Role()) == eNameFromSubtreeRule) {
36     // XXX: is it necessary to care the accessible is not a document?
37     if (aAccessible->IsContent()) {
38       nsAutoString name;
39       AppendFromAccessibleChildren(aAccessible, &name);
40       name.CompressWhitespace();
41       if (!nsCoreUtils::IsWhitespaceString(name)) aName = name;
42     }
43   }
44 
45   sInitiatorAcc = nullptr;
46 
47   return NS_OK;
48 }
49 
GetTextEquivFromIDRefs(const Accessible * aAccessible,nsAtom * aIDRefsAttr,nsAString & aTextEquiv)50 nsresult nsTextEquivUtils::GetTextEquivFromIDRefs(const Accessible* aAccessible,
51                                                   nsAtom* aIDRefsAttr,
52                                                   nsAString& aTextEquiv) {
53   aTextEquiv.Truncate();
54 
55   nsIContent* content = aAccessible->GetContent();
56   if (!content) return NS_OK;
57 
58   nsIContent* refContent = nullptr;
59   IDRefsIterator iter(aAccessible->Document(), content, aIDRefsAttr);
60   while ((refContent = iter.NextElem())) {
61     if (!aTextEquiv.IsEmpty()) aTextEquiv += ' ';
62 
63     nsresult rv =
64         AppendTextEquivFromContent(aAccessible, refContent, &aTextEquiv);
65     NS_ENSURE_SUCCESS(rv, rv);
66   }
67 
68   return NS_OK;
69 }
70 
AppendTextEquivFromContent(const Accessible * aInitiatorAcc,nsIContent * aContent,nsAString * aString)71 nsresult nsTextEquivUtils::AppendTextEquivFromContent(
72     const Accessible* aInitiatorAcc, nsIContent* aContent, nsAString* aString) {
73   // Prevent recursion which can cause infinite loops.
74   if (sInitiatorAcc) return NS_OK;
75 
76   sInitiatorAcc = aInitiatorAcc;
77 
78   // If the given content is not visible or isn't accessible then go down
79   // through the DOM subtree otherwise go down through accessible subtree and
80   // calculate the flat string.
81   nsIFrame* frame = aContent->GetPrimaryFrame();
82   bool isVisible = frame && frame->StyleVisibility()->IsVisible();
83 
84   nsresult rv = NS_ERROR_FAILURE;
85   bool goThroughDOMSubtree = true;
86 
87   if (isVisible) {
88     Accessible* accessible = sInitiatorAcc->Document()->GetAccessible(aContent);
89     if (accessible) {
90       rv = AppendFromAccessible(accessible, aString);
91       goThroughDOMSubtree = false;
92     }
93   }
94 
95   if (goThroughDOMSubtree) rv = AppendFromDOMNode(aContent, aString);
96 
97   sInitiatorAcc = nullptr;
98   return rv;
99 }
100 
AppendTextEquivFromTextContent(nsIContent * aContent,nsAString * aString)101 nsresult nsTextEquivUtils::AppendTextEquivFromTextContent(nsIContent* aContent,
102                                                           nsAString* aString) {
103   if (aContent->IsText()) {
104     bool isHTMLBlock = false;
105 
106     nsIContent* parentContent = aContent->GetFlattenedTreeParent();
107     if (parentContent) {
108       nsIFrame* frame = parentContent->GetPrimaryFrame();
109       if (frame) {
110         // If this text is inside a block level frame (as opposed to span
111         // level), we need to add spaces around that block's text, so we don't
112         // get words jammed together in final name.
113         const nsStyleDisplay* display = frame->StyleDisplay();
114         if (display->IsBlockOutsideStyle() ||
115             display->mDisplay == StyleDisplay::TableCell) {
116           isHTMLBlock = true;
117           if (!aString->IsEmpty()) {
118             aString->Append(char16_t(' '));
119           }
120         }
121       }
122     }
123 
124     if (aContent->TextLength() > 0) {
125       nsIFrame* frame = aContent->GetPrimaryFrame();
126       if (frame) {
127         nsIFrame::RenderedText text = frame->GetRenderedText(
128             0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText,
129             nsIFrame::TrailingWhitespace::DontTrim);
130         aString->Append(text.mString);
131       } else {
132         // If aContent is an object that is display: none, we have no a frame.
133         aContent->GetAsText()->AppendTextTo(*aString);
134       }
135       if (isHTMLBlock && !aString->IsEmpty()) {
136         aString->Append(char16_t(' '));
137       }
138     }
139 
140     return NS_OK;
141   }
142 
143   if (aContent->IsHTMLElement() &&
144       aContent->NodeInfo()->Equals(nsGkAtoms::br)) {
145     aString->AppendLiteral("\r\n");
146     return NS_OK;
147   }
148 
149   return NS_OK_NO_NAME_CLAUSE_HANDLED;
150 }
151 
152 ////////////////////////////////////////////////////////////////////////////////
153 // nsTextEquivUtils. Private.
154 
AppendFromAccessibleChildren(const Accessible * aAccessible,nsAString * aString)155 nsresult nsTextEquivUtils::AppendFromAccessibleChildren(
156     const Accessible* aAccessible, nsAString* aString) {
157   nsresult rv = NS_OK_NO_NAME_CLAUSE_HANDLED;
158 
159   uint32_t childCount = aAccessible->ChildCount();
160   for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
161     Accessible* child = aAccessible->GetChildAt(childIdx);
162     rv = AppendFromAccessible(child, aString);
163     NS_ENSURE_SUCCESS(rv, rv);
164   }
165 
166   return rv;
167 }
168 
AppendFromAccessible(Accessible * aAccessible,nsAString * aString)169 nsresult nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible,
170                                                 nsAString* aString) {
171   // XXX: is it necessary to care the accessible is not a document?
172   if (aAccessible->IsContent()) {
173     nsresult rv =
174         AppendTextEquivFromTextContent(aAccessible->GetContent(), aString);
175     if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) return rv;
176   }
177 
178   bool isEmptyTextEquiv = true;
179 
180   // If the name is from tooltip then append it to result string in the end
181   // (see h. step of name computation guide).
182   nsAutoString text;
183   if (aAccessible->Name(text) != eNameFromTooltip)
184     isEmptyTextEquiv = !AppendString(aString, text);
185 
186   // Implementation of f. step.
187   nsresult rv = AppendFromValue(aAccessible, aString);
188   NS_ENSURE_SUCCESS(rv, rv);
189 
190   if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) isEmptyTextEquiv = false;
191 
192   // Implementation of g) step of text equivalent computation guide. Go down
193   // into subtree if accessible allows "text equivalent from subtree rule" or
194   // it's not root and not control.
195   if (isEmptyTextEquiv) {
196     if (ShouldIncludeInSubtreeCalculation(aAccessible)) {
197       rv = AppendFromAccessibleChildren(aAccessible, aString);
198       NS_ENSURE_SUCCESS(rv, rv);
199 
200       if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) isEmptyTextEquiv = false;
201     }
202   }
203 
204   // Implementation of h. step
205   if (isEmptyTextEquiv && !text.IsEmpty()) {
206     AppendString(aString, text);
207     return NS_OK;
208   }
209 
210   return rv;
211 }
212 
AppendFromValue(Accessible * aAccessible,nsAString * aString)213 nsresult nsTextEquivUtils::AppendFromValue(Accessible* aAccessible,
214                                            nsAString* aString) {
215   if (GetRoleRule(aAccessible->Role()) != eNameFromValueRule)
216     return NS_OK_NO_NAME_CLAUSE_HANDLED;
217 
218   // Implementation of step f. of text equivalent computation. If the given
219   // accessible is not root accessible (the accessible the text equivalent is
220   // computed for in the end) then append accessible value. Otherwise append
221   // value if and only if the given accessible is in the middle of its parent.
222 
223   nsAutoString text;
224   if (aAccessible != sInitiatorAcc) {
225     aAccessible->Value(text);
226 
227     return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED;
228   }
229 
230   // XXX: is it necessary to care the accessible is not a document?
231   if (aAccessible->IsDoc()) return NS_ERROR_UNEXPECTED;
232 
233   nsIContent* content = aAccessible->GetContent();
234 
235   for (nsIContent* childContent = content->GetPreviousSibling(); childContent;
236        childContent = childContent->GetPreviousSibling()) {
237     // check for preceding text...
238     if (!childContent->TextIsOnlyWhitespace()) {
239       for (nsIContent* siblingContent = content->GetNextSibling();
240            siblingContent; siblingContent = siblingContent->GetNextSibling()) {
241         // .. and subsequent text
242         if (!siblingContent->TextIsOnlyWhitespace()) {
243           aAccessible->Value(text);
244 
245           return AppendString(aString, text) ? NS_OK
246                                              : NS_OK_NO_NAME_CLAUSE_HANDLED;
247           break;
248         }
249       }
250       break;
251     }
252   }
253 
254   return NS_OK_NO_NAME_CLAUSE_HANDLED;
255 }
256 
AppendFromDOMChildren(nsIContent * aContent,nsAString * aString)257 nsresult nsTextEquivUtils::AppendFromDOMChildren(nsIContent* aContent,
258                                                  nsAString* aString) {
259   for (nsIContent* childContent = aContent->GetFirstChild(); childContent;
260        childContent = childContent->GetNextSibling()) {
261     nsresult rv = AppendFromDOMNode(childContent, aString);
262     NS_ENSURE_SUCCESS(rv, rv);
263   }
264 
265   return NS_OK;
266 }
267 
AppendFromDOMNode(nsIContent * aContent,nsAString * aString)268 nsresult nsTextEquivUtils::AppendFromDOMNode(nsIContent* aContent,
269                                              nsAString* aString) {
270   nsresult rv = AppendTextEquivFromTextContent(aContent, aString);
271   NS_ENSURE_SUCCESS(rv, rv);
272 
273   if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) return NS_OK;
274 
275   if (aContent->IsXULElement()) {
276     nsAutoString textEquivalent;
277     if (aContent->NodeInfo()->Equals(nsGkAtoms::label, kNameSpaceID_XUL)) {
278       aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::value,
279                                      textEquivalent);
280     } else {
281       aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
282                                      textEquivalent);
283     }
284 
285     if (textEquivalent.IsEmpty()) {
286       aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext,
287                                      textEquivalent);
288     }
289 
290     AppendString(aString, textEquivalent);
291   }
292 
293   return AppendFromDOMChildren(aContent, aString);
294 }
295 
AppendString(nsAString * aString,const nsAString & aTextEquivalent)296 bool nsTextEquivUtils::AppendString(nsAString* aString,
297                                     const nsAString& aTextEquivalent) {
298   if (aTextEquivalent.IsEmpty()) return false;
299 
300   // Insert spaces to insure that words from controls aren't jammed together.
301   if (!aString->IsEmpty() && !nsCoreUtils::IsWhitespace(aString->Last()))
302     aString->Append(char16_t(' '));
303 
304   aString->Append(aTextEquivalent);
305 
306   if (!nsCoreUtils::IsWhitespace(aString->Last()))
307     aString->Append(char16_t(' '));
308 
309   return true;
310 }
311 
GetRoleRule(role aRole)312 uint32_t nsTextEquivUtils::GetRoleRule(role aRole) {
313 #define ROLE(geckoRole, stringRole, atkRole, macRole, msaaRole, ia2Role, \
314              androidClass, nameRule)                                     \
315   case roles::geckoRole:                                                 \
316     return nameRule;
317 
318   switch (aRole) {
319 #include "RoleMap.h"
320     default:
321       MOZ_CRASH("Unknown role.");
322   }
323 
324 #undef ROLE
325 }
326 
ShouldIncludeInSubtreeCalculation(Accessible * aAccessible)327 bool nsTextEquivUtils::ShouldIncludeInSubtreeCalculation(
328     Accessible* aAccessible) {
329   uint32_t nameRule = GetRoleRule(aAccessible->Role());
330   if (nameRule == eNameFromSubtreeRule) {
331     return true;
332   }
333   if (!(nameRule & eNameFromSubtreeIfReqRule)) {
334     return false;
335   }
336 
337   if (aAccessible == sInitiatorAcc) {
338     // We're calculating the text equivalent for this accessible, but this
339     // accessible should only be included when calculating the text equivalent
340     // for something else.
341     return false;
342   }
343 
344   // sInitiatorAcc can be null when, for example, Accessible::Value calls
345   // GetTextEquivFromSubtree.
346   role initiatorRole = sInitiatorAcc ? sInitiatorAcc->Role() : roles::NOTHING;
347   if (initiatorRole == roles::OUTLINEITEM &&
348       aAccessible->Role() == roles::GROUPING) {
349     // Child treeitems are contained in a group. We don't want to include those
350     // in the parent treeitem's text equivalent.
351     return false;
352   }
353 
354   return true;
355 }
356