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 "nsIDOMXULLabeledControlEl.h"
14 
15 using namespace mozilla::a11y;
16 
17 /**
18  * The accessible for which we are computing a text equivalent. It is useful
19  * for bailing out during recursive text computation, or for special cases
20  * like step f. of the ARIA implementation guide.
21  */
22 static Accessible* sInitiatorAcc = nullptr;
23 
24 ////////////////////////////////////////////////////////////////////////////////
25 // nsTextEquivUtils. Public.
26 
GetNameFromSubtree(Accessible * aAccessible,nsAString & aName)27 nsresult nsTextEquivUtils::GetNameFromSubtree(Accessible* aAccessible,
28                                               nsAString& aName) {
29   aName.Truncate();
30 
31   if (sInitiatorAcc) return NS_OK;
32 
33   sInitiatorAcc = aAccessible;
34   if (GetRoleRule(aAccessible->Role()) == eNameFromSubtreeRule) {
35     // XXX: is it necessary to care the accessible is not a document?
36     if (aAccessible->IsContent()) {
37       nsAutoString name;
38       AppendFromAccessibleChildren(aAccessible, &name);
39       name.CompressWhitespace();
40       if (!nsCoreUtils::IsWhitespaceString(name)) aName = name;
41     }
42   }
43 
44   sInitiatorAcc = nullptr;
45 
46   return NS_OK;
47 }
48 
GetTextEquivFromIDRefs(Accessible * aAccessible,nsAtom * aIDRefsAttr,nsAString & aTextEquiv)49 nsresult nsTextEquivUtils::GetTextEquivFromIDRefs(Accessible* aAccessible,
50                                                   nsAtom* aIDRefsAttr,
51                                                   nsAString& aTextEquiv) {
52   aTextEquiv.Truncate();
53 
54   nsIContent* content = aAccessible->GetContent();
55   if (!content) return NS_OK;
56 
57   nsIContent* refContent = nullptr;
58   IDRefsIterator iter(aAccessible->Document(), content, aIDRefsAttr);
59   while ((refContent = iter.NextElem())) {
60     if (!aTextEquiv.IsEmpty()) aTextEquiv += ' ';
61 
62     nsresult rv =
63         AppendTextEquivFromContent(aAccessible, refContent, &aTextEquiv);
64     NS_ENSURE_SUCCESS(rv, rv);
65   }
66 
67   return NS_OK;
68 }
69 
AppendTextEquivFromContent(Accessible * aInitiatorAcc,nsIContent * aContent,nsAString * aString)70 nsresult nsTextEquivUtils::AppendTextEquivFromContent(Accessible* aInitiatorAcc,
71                                                       nsIContent* aContent,
72                                                       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->IsNodeOfType(nsINode::eTEXT)) {
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::OFFSETS_IN_CONTENT_TEXT,
129             nsIFrame::TrailingWhitespace::DONT_TRIM_TRAILING_WHITESPACE);
130         aString->Append(text.mString);
131       } else {
132         // If aContent is an object that is display: none, we have no a frame.
133         aContent->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(Accessible * aAccessible,nsAString * aString)155 nsresult nsTextEquivUtils::AppendFromAccessibleChildren(Accessible* aAccessible,
156                                                         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     uint32_t nameRule = GetRoleRule(aAccessible->Role());
197     if (nameRule & eNameFromSubtreeIfReqRule) {
198       rv = AppendFromAccessibleChildren(aAccessible, aString);
199       NS_ENSURE_SUCCESS(rv, rv);
200 
201       if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) isEmptyTextEquiv = false;
202     }
203   }
204 
205   // Implementation of h. step
206   if (isEmptyTextEquiv && !text.IsEmpty()) {
207     AppendString(aString, text);
208     return NS_OK;
209   }
210 
211   return rv;
212 }
213 
AppendFromValue(Accessible * aAccessible,nsAString * aString)214 nsresult nsTextEquivUtils::AppendFromValue(Accessible* aAccessible,
215                                            nsAString* aString) {
216   if (GetRoleRule(aAccessible->Role()) != eNameFromValueRule)
217     return NS_OK_NO_NAME_CLAUSE_HANDLED;
218 
219   // Implementation of step f. of text equivalent computation. If the given
220   // accessible is not root accessible (the accessible the text equivalent is
221   // computed for in the end) then append accessible value. Otherwise append
222   // value if and only if the given accessible is in the middle of its parent.
223 
224   nsAutoString text;
225   if (aAccessible != sInitiatorAcc) {
226     aAccessible->Value(text);
227 
228     return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED;
229   }
230 
231   // XXX: is it necessary to care the accessible is not a document?
232   if (aAccessible->IsDoc()) return NS_ERROR_UNEXPECTED;
233 
234   nsIContent* content = aAccessible->GetContent();
235 
236   for (nsIContent* childContent = content->GetPreviousSibling(); childContent;
237        childContent = childContent->GetPreviousSibling()) {
238     // check for preceding text...
239     if (!childContent->TextIsOnlyWhitespace()) {
240       for (nsIContent* siblingContent = content->GetNextSibling();
241            siblingContent; siblingContent = siblingContent->GetNextSibling()) {
242         // .. and subsequent text
243         if (!siblingContent->TextIsOnlyWhitespace()) {
244           aAccessible->Value(text);
245 
246           return AppendString(aString, text) ? NS_OK
247                                              : NS_OK_NO_NAME_CLAUSE_HANDLED;
248           break;
249         }
250       }
251       break;
252     }
253   }
254 
255   return NS_OK_NO_NAME_CLAUSE_HANDLED;
256 }
257 
AppendFromDOMChildren(nsIContent * aContent,nsAString * aString)258 nsresult nsTextEquivUtils::AppendFromDOMChildren(nsIContent* aContent,
259                                                  nsAString* aString) {
260   for (nsIContent* childContent = aContent->GetFirstChild(); childContent;
261        childContent = childContent->GetNextSibling()) {
262     nsresult rv = AppendFromDOMNode(childContent, aString);
263     NS_ENSURE_SUCCESS(rv, rv);
264   }
265 
266   return NS_OK;
267 }
268 
AppendFromDOMNode(nsIContent * aContent,nsAString * aString)269 nsresult nsTextEquivUtils::AppendFromDOMNode(nsIContent* aContent,
270                                              nsAString* aString) {
271   nsresult rv = AppendTextEquivFromTextContent(aContent, aString);
272   NS_ENSURE_SUCCESS(rv, rv);
273 
274   if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) return NS_OK;
275 
276   if (aContent->IsXULElement()) {
277     nsAutoString textEquivalent;
278     nsCOMPtr<nsIDOMXULLabeledControlElement> labeledEl =
279         do_QueryInterface(aContent);
280 
281     if (labeledEl) {
282       labeledEl->GetLabel(textEquivalent);
283     } else {
284       if (aContent->NodeInfo()->Equals(nsGkAtoms::label, kNameSpaceID_XUL))
285         aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::value,
286                                        textEquivalent);
287 
288       if (textEquivalent.IsEmpty())
289         aContent->AsElement()->GetAttr(kNameSpaceID_None,
290                                        nsGkAtoms::tooltiptext, textEquivalent);
291     }
292 
293     AppendString(aString, textEquivalent);
294   }
295 
296   return AppendFromDOMChildren(aContent, aString);
297 }
298 
AppendString(nsAString * aString,const nsAString & aTextEquivalent)299 bool nsTextEquivUtils::AppendString(nsAString* aString,
300                                     const nsAString& aTextEquivalent) {
301   if (aTextEquivalent.IsEmpty()) return false;
302 
303   // Insert spaces to insure that words from controls aren't jammed together.
304   if (!aString->IsEmpty() && !nsCoreUtils::IsWhitespace(aString->Last()))
305     aString->Append(char16_t(' '));
306 
307   aString->Append(aTextEquivalent);
308 
309   if (!nsCoreUtils::IsWhitespace(aString->Last()))
310     aString->Append(char16_t(' '));
311 
312   return true;
313 }
314 
GetRoleRule(role aRole)315 uint32_t nsTextEquivUtils::GetRoleRule(role aRole) {
316 #define ROLE(geckoRole, stringRole, atkRole, macRole, msaaRole, ia2Role, \
317              nameRule)                                                   \
318   case roles::geckoRole:                                                 \
319     return nameRule;
320 
321   switch (aRole) {
322 #include "RoleMap.h"
323     default:
324       MOZ_CRASH("Unknown role.");
325   }
326 
327 #undef ROLE
328 }
329