Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / content / renderer / accessibility / accessibility_node_serializer.cc
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "content/renderer/accessibility/accessibility_node_serializer.h"
6
7 #include <set>
8
9 #include "base/strings/string_number_conversions.h"
10 #include "base/strings/string_util.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "content/renderer/accessibility/blink_ax_enum_conversion.h"
13 #include "third_party/WebKit/public/platform/WebRect.h"
14 #include "third_party/WebKit/public/platform/WebSize.h"
15 #include "third_party/WebKit/public/platform/WebString.h"
16 #include "third_party/WebKit/public/platform/WebVector.h"
17 #include "third_party/WebKit/public/web/WebAXEnums.h"
18 #include "third_party/WebKit/public/web/WebAXObject.h"
19 #include "third_party/WebKit/public/web/WebDocument.h"
20 #include "third_party/WebKit/public/web/WebDocumentType.h"
21 #include "third_party/WebKit/public/web/WebElement.h"
22 #include "third_party/WebKit/public/web/WebFormControlElement.h"
23 #include "third_party/WebKit/public/web/WebFrame.h"
24 #include "third_party/WebKit/public/web/WebInputElement.h"
25 #include "third_party/WebKit/public/web/WebNode.h"
26
27 using base::UTF16ToUTF8;
28 using blink::WebAXObject;
29 using blink::WebDocument;
30 using blink::WebDocumentType;
31 using blink::WebElement;
32 using blink::WebNode;
33 using blink::WebVector;
34
35 namespace content {
36 namespace {
37
38 // Returns true if |ancestor| is the first unignored parent of |child|,
39 // which means that when walking up the parent chain from |child|,
40 // |ancestor| is the *first* ancestor that isn't marked as
41 // accessibilityIsIgnored().
42 bool IsParentUnignoredOf(const WebAXObject& ancestor,
43                          const WebAXObject& child) {
44   WebAXObject parent = child.parentObject();
45   while (!parent.isDetached() && parent.accessibilityIsIgnored())
46     parent = parent.parentObject();
47   return parent.equals(ancestor);
48 }
49
50   bool IsTrue(std::string html_value) {
51   return LowerCaseEqualsASCII(html_value, "true");
52 }
53
54 std::string GetEquivalentAriaRoleString(const ui::AXRole role) {
55   switch (role) {
56     case ui::AX_ROLE_ARTICLE:
57       return "article";
58     case ui::AX_ROLE_BANNER:
59       return "banner";
60     case ui::AX_ROLE_COMPLEMENTARY:
61       return "complementary";
62     case ui::AX_ROLE_CONTENT_INFO:
63     case ui::AX_ROLE_FOOTER:
64       return "contentinfo";
65     case ui::AX_ROLE_MAIN:
66       return "main";
67     case ui::AX_ROLE_NAVIGATION:
68       return "navigation";
69     case ui::AX_ROLE_REGION:
70       return "region";
71     default:
72       break;
73   }
74
75   return std::string();
76 }
77
78 }  // Anonymous namespace
79
80 void SerializeAccessibilityNode(
81     const WebAXObject& src,
82     ui::AXNodeData* dst) {
83   dst->role = AXRoleFromBlink(src.role());
84   dst->state = AXStateFromBlink(src);
85   dst->location = src.boundingBoxRect();
86   dst->id = src.axID();
87   std::string name = base::UTF16ToUTF8(src.title());
88
89   std::string value;
90   if (src.valueDescription().length()) {
91     dst->AddStringAttribute(ui::AX_ATTR_VALUE,
92                             UTF16ToUTF8(src.valueDescription()));
93   } else {
94     dst->AddStringAttribute(ui::AX_ATTR_VALUE, UTF16ToUTF8(src.stringValue()));
95   }
96
97   if (dst->role == ui::AX_ROLE_COLOR_WELL) {
98     int r, g, b;
99     src.colorValue(r, g, b);
100     dst->AddIntAttribute(ui::AX_ATTR_COLOR_VALUE_RED, r);
101     dst->AddIntAttribute(ui::AX_ATTR_COLOR_VALUE_GREEN, g);
102     dst->AddIntAttribute(ui::AX_ATTR_COLOR_VALUE_BLUE, b);
103   }
104
105   if (dst->role == ui::AX_ROLE_INLINE_TEXT_BOX) {
106     dst->AddIntAttribute(ui::AX_ATTR_TEXT_DIRECTION,
107                          AXTextDirectionFromBlink(src.textDirection()));
108
109     WebVector<int> src_character_offsets;
110     src.characterOffsets(src_character_offsets);
111     std::vector<int32> character_offsets;
112     character_offsets.reserve(src_character_offsets.size());
113     for (size_t i = 0; i < src_character_offsets.size(); ++i)
114       character_offsets.push_back(src_character_offsets[i]);
115     dst->AddIntListAttribute(ui::AX_ATTR_CHARACTER_OFFSETS, character_offsets);
116
117     WebVector<int> src_word_starts;
118     WebVector<int> src_word_ends;
119     src.wordBoundaries(src_word_starts, src_word_ends);
120     std::vector<int32> word_starts;
121     std::vector<int32> word_ends;
122     word_starts.reserve(src_word_starts.size());
123     word_ends.reserve(src_word_starts.size());
124     for (size_t i = 0; i < src_word_starts.size(); ++i) {
125       word_starts.push_back(src_word_starts[i]);
126       word_ends.push_back(src_word_ends[i]);
127     }
128     dst->AddIntListAttribute(ui::AX_ATTR_WORD_STARTS, word_starts);
129     dst->AddIntListAttribute(ui::AX_ATTR_WORD_ENDS, word_ends);
130   }
131
132   if (src.accessKey().length())
133     dst->AddStringAttribute(ui::AX_ATTR_ACCESS_KEY,
134         UTF16ToUTF8(src.accessKey()));
135   if (src.actionVerb().length())
136     dst->AddStringAttribute(ui::AX_ATTR_ACTION, UTF16ToUTF8(src.actionVerb()));
137   if (src.isAriaReadOnly())
138     dst->AddBoolAttribute(ui::AX_ATTR_ARIA_READONLY, true);
139   if (src.isButtonStateMixed())
140     dst->AddBoolAttribute(ui::AX_ATTR_BUTTON_MIXED, true);
141   if (src.canSetValueAttribute())
142     dst->AddBoolAttribute(ui::AX_ATTR_CAN_SET_VALUE, true);
143   if (src.accessibilityDescription().length()) {
144     dst->AddStringAttribute(ui::AX_ATTR_DESCRIPTION,
145                             UTF16ToUTF8(src.accessibilityDescription()));
146   }
147   if (src.hasComputedStyle()) {
148     dst->AddStringAttribute(ui::AX_ATTR_DISPLAY,
149                             UTF16ToUTF8(src.computedStyleDisplay()));
150   }
151   if (src.helpText().length())
152     dst->AddStringAttribute(ui::AX_ATTR_HELP, UTF16ToUTF8(src.helpText()));
153   if (src.keyboardShortcut().length()) {
154     dst->AddStringAttribute(ui::AX_ATTR_SHORTCUT,
155                             UTF16ToUTF8(src.keyboardShortcut()));
156   }
157   if (!src.titleUIElement().isDetached()) {
158     dst->AddIntAttribute(ui::AX_ATTR_TITLE_UI_ELEMENT,
159                          src.titleUIElement().axID());
160   }
161   if (!src.url().isEmpty())
162     dst->AddStringAttribute(ui::AX_ATTR_URL, src.url().spec());
163
164   if (dst->role == ui::AX_ROLE_HEADING)
165     dst->AddIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL, src.headingLevel());
166   else if ((dst->role == ui::AX_ROLE_TREE_ITEM ||
167             dst->role == ui::AX_ROLE_ROW) &&
168            src.hierarchicalLevel() > 0) {
169     dst->AddIntAttribute(ui::AX_ATTR_HIERARCHICAL_LEVEL,
170         src.hierarchicalLevel());
171   }
172
173   // Treat the active list box item as focused.
174   if (dst->role == ui::AX_ROLE_LIST_BOX_OPTION &&
175       src.isSelectedOptionActive()) {
176     dst->state |= (1 << ui::AX_STATE_FOCUSED);
177   }
178
179   if (src.canvasHasFallbackContent())
180     dst->AddBoolAttribute(ui::AX_ATTR_CANVAS_HAS_FALLBACK, true);
181
182   WebNode node = src.node();
183   bool is_iframe = false;
184   std::string live_atomic;
185   std::string live_busy;
186   std::string live_status;
187   std::string live_relevant;
188
189   if (!node.isNull() && node.isElementNode()) {
190     WebElement element = node.to<WebElement>();
191     is_iframe = (element.tagName() == base::ASCIIToUTF16("IFRAME"));
192
193     if (LowerCaseEqualsASCII(element.getAttribute("aria-expanded"), "true"))
194       dst->state |= (1 << ui::AX_STATE_EXPANDED);
195
196     // TODO(ctguil): The tagName in WebKit is lower cased but
197     // HTMLElement::nodeName calls localNameUpper. Consider adding
198     // a WebElement method that returns the original lower cased tagName.
199     dst->AddStringAttribute(
200         ui::AX_ATTR_HTML_TAG,
201         StringToLowerASCII(UTF16ToUTF8(element.tagName())));
202     for (unsigned i = 0; i < element.attributeCount(); ++i) {
203       std::string name = StringToLowerASCII(base::UTF16ToUTF8(
204           element.attributeLocalName(i)));
205       std::string value = base::UTF16ToUTF8(element.attributeValue(i));
206       dst->html_attributes.push_back(std::make_pair(name, value));
207     }
208
209     if (dst->role == ui::AX_ROLE_EDITABLE_TEXT ||
210         dst->role == ui::AX_ROLE_TEXT_AREA ||
211         dst->role == ui::AX_ROLE_TEXT_FIELD) {
212       dst->AddIntAttribute(ui::AX_ATTR_TEXT_SEL_START, src.selectionStart());
213       dst->AddIntAttribute(ui::AX_ATTR_TEXT_SEL_END, src.selectionEnd());
214
215       WebVector<int> src_line_breaks;
216       src.lineBreaks(src_line_breaks);
217       if (src_line_breaks.size() > 0) {
218         std::vector<int32> line_breaks;
219         line_breaks.reserve(src_line_breaks.size());
220         for (size_t i = 0; i < src_line_breaks.size(); ++i)
221           line_breaks.push_back(src_line_breaks[i]);
222         dst->AddIntListAttribute(ui::AX_ATTR_LINE_BREAKS, line_breaks);
223       }
224     }
225
226     // ARIA role.
227     if (element.hasAttribute("role")) {
228       dst->AddStringAttribute(ui::AX_ATTR_ROLE,
229                               UTF16ToUTF8(element.getAttribute("role")));
230     } else {
231       std::string role = GetEquivalentAriaRoleString(dst->role);
232       if (!role.empty())
233         dst->AddStringAttribute(ui::AX_ATTR_ROLE, role);
234     }
235
236     // Live region attributes
237     live_atomic = base::UTF16ToUTF8(element.getAttribute("aria-atomic"));
238     live_busy = base::UTF16ToUTF8(element.getAttribute("aria-busy"));
239     live_status = base::UTF16ToUTF8(element.getAttribute("aria-live"));
240     live_relevant = base::UTF16ToUTF8(element.getAttribute("aria-relevant"));
241   }
242
243   // Walk up the parent chain to set live region attributes of containers
244   std::string container_live_atomic;
245   std::string container_live_busy;
246   std::string container_live_status;
247   std::string container_live_relevant;
248   WebAXObject container_accessible = src;
249   while (!container_accessible.isDetached()) {
250     WebNode container_node = container_accessible.node();
251     if (!container_node.isNull() && container_node.isElementNode()) {
252       WebElement container_elem = container_node.to<WebElement>();
253       if (container_elem.hasAttribute("aria-atomic") &&
254           container_live_atomic.empty()) {
255         container_live_atomic =
256             base::UTF16ToUTF8(container_elem.getAttribute("aria-atomic"));
257       }
258       if (container_elem.hasAttribute("aria-busy") &&
259           container_live_busy.empty()) {
260         container_live_busy =
261             base::UTF16ToUTF8(container_elem.getAttribute("aria-busy"));
262       }
263       if (container_elem.hasAttribute("aria-live") &&
264           container_live_status.empty()) {
265         container_live_status =
266             base::UTF16ToUTF8(container_elem.getAttribute("aria-live"));
267       }
268       if (container_elem.hasAttribute("aria-relevant") &&
269           container_live_relevant.empty()) {
270         container_live_relevant =
271             base::UTF16ToUTF8(container_elem.getAttribute("aria-relevant"));
272       }
273     }
274     container_accessible = container_accessible.parentObject();
275   }
276
277   if (!live_atomic.empty())
278     dst->AddBoolAttribute(ui::AX_ATTR_LIVE_ATOMIC, IsTrue(live_atomic));
279   if (!live_busy.empty())
280     dst->AddBoolAttribute(ui::AX_ATTR_LIVE_BUSY, IsTrue(live_busy));
281   if (!live_status.empty())
282     dst->AddStringAttribute(ui::AX_ATTR_LIVE_STATUS, live_status);
283   if (!live_relevant.empty())
284     dst->AddStringAttribute(ui::AX_ATTR_LIVE_RELEVANT, live_relevant);
285
286   if (!container_live_atomic.empty()) {
287     dst->AddBoolAttribute(ui::AX_ATTR_CONTAINER_LIVE_ATOMIC,
288                           IsTrue(container_live_atomic));
289   }
290   if (!container_live_busy.empty()) {
291     dst->AddBoolAttribute(ui::AX_ATTR_CONTAINER_LIVE_BUSY,
292                           IsTrue(container_live_busy));
293   }
294   if (!container_live_status.empty()) {
295     dst->AddStringAttribute(ui::AX_ATTR_CONTAINER_LIVE_STATUS,
296                             container_live_status);
297   }
298   if (!container_live_relevant.empty()) {
299     dst->AddStringAttribute(ui::AX_ATTR_CONTAINER_LIVE_RELEVANT,
300                             container_live_relevant);
301   }
302
303   if (dst->role == ui::AX_ROLE_PROGRESS_INDICATOR ||
304       dst->role == ui::AX_ROLE_SCROLL_BAR ||
305       dst->role == ui::AX_ROLE_SLIDER ||
306       dst->role == ui::AX_ROLE_SPIN_BUTTON) {
307     dst->AddFloatAttribute(ui::AX_ATTR_VALUE_FOR_RANGE, src.valueForRange());
308     dst->AddFloatAttribute(ui::AX_ATTR_MAX_VALUE_FOR_RANGE,
309                            src.maxValueForRange());
310     dst->AddFloatAttribute(ui::AX_ATTR_MIN_VALUE_FOR_RANGE,
311                            src.minValueForRange());
312   }
313
314   if (dst->role == ui::AX_ROLE_DOCUMENT ||
315       dst->role == ui::AX_ROLE_WEB_AREA) {
316     dst->AddStringAttribute(ui::AX_ATTR_HTML_TAG, "#document");
317     const WebDocument& document = src.document();
318     if (name.empty())
319       name = UTF16ToUTF8(document.title());
320     dst->AddStringAttribute(ui::AX_ATTR_DOC_TITLE,
321         UTF16ToUTF8(document.title()));
322     dst->AddStringAttribute(ui::AX_ATTR_DOC_URL, document.url().spec());
323     dst->AddStringAttribute(
324         ui::AX_ATTR_DOC_MIMETYPE,
325         document.isXHTMLDocument() ? "text/xhtml" : "text/html");
326     dst->AddBoolAttribute(ui::AX_ATTR_DOC_LOADED, src.isLoaded());
327     dst->AddFloatAttribute(ui::AX_ATTR_DOC_LOADING_PROGRESS,
328                            src.estimatedLoadingProgress());
329
330     const WebDocumentType& doctype = document.doctype();
331     if (!doctype.isNull()) {
332       dst->AddStringAttribute(ui::AX_ATTR_DOC_DOCTYPE,
333                               UTF16ToUTF8(doctype.name()));
334     }
335
336     const gfx::Size& scroll_offset = document.frame()->scrollOffset();
337     dst->AddIntAttribute(ui::AX_ATTR_SCROLL_X, scroll_offset.width());
338     dst->AddIntAttribute(ui::AX_ATTR_SCROLL_Y, scroll_offset.height());
339
340     const gfx::Size& min_offset = document.frame()->minimumScrollOffset();
341     dst->AddIntAttribute(ui::AX_ATTR_SCROLL_X_MIN, min_offset.width());
342     dst->AddIntAttribute(ui::AX_ATTR_SCROLL_Y_MIN, min_offset.height());
343
344     const gfx::Size& max_offset = document.frame()->maximumScrollOffset();
345     dst->AddIntAttribute(ui::AX_ATTR_SCROLL_X_MAX, max_offset.width());
346     dst->AddIntAttribute(ui::AX_ATTR_SCROLL_Y_MAX, max_offset.height());
347   }
348
349   if (dst->role == ui::AX_ROLE_TABLE) {
350     int column_count = src.columnCount();
351     int row_count = src.rowCount();
352     if (column_count > 0 && row_count > 0) {
353       std::set<int32> unique_cell_id_set;
354       std::vector<int32> cell_ids;
355       std::vector<int32> unique_cell_ids;
356       dst->AddIntAttribute(ui::AX_ATTR_TABLE_COLUMN_COUNT, column_count);
357       dst->AddIntAttribute(ui::AX_ATTR_TABLE_ROW_COUNT, row_count);
358       WebAXObject header = src.headerContainerObject();
359       if (!header.isDetached())
360         dst->AddIntAttribute(ui::AX_ATTR_TABLE_HEADER_ID, header.axID());
361       for (int i = 0; i < column_count * row_count; ++i) {
362         WebAXObject cell = src.cellForColumnAndRow(
363             i % column_count, i / column_count);
364         int cell_id = -1;
365         if (!cell.isDetached()) {
366           cell_id = cell.axID();
367           if (unique_cell_id_set.find(cell_id) == unique_cell_id_set.end()) {
368             unique_cell_id_set.insert(cell_id);
369             unique_cell_ids.push_back(cell_id);
370           }
371         }
372         cell_ids.push_back(cell_id);
373       }
374       dst->AddIntListAttribute(ui::AX_ATTR_CELL_IDS, cell_ids);
375       dst->AddIntListAttribute(ui::AX_ATTR_UNIQUE_CELL_IDS, unique_cell_ids);
376     }
377   }
378
379   if (dst->role == ui::AX_ROLE_ROW) {
380     dst->AddIntAttribute(ui::AX_ATTR_TABLE_ROW_INDEX, src.rowIndex());
381     WebAXObject header = src.rowHeader();
382     if (!header.isDetached())
383       dst->AddIntAttribute(ui::AX_ATTR_TABLE_ROW_HEADER_ID, header.axID());
384   }
385
386   if (dst->role == ui::AX_ROLE_COLUMN) {
387     dst->AddIntAttribute(ui::AX_ATTR_TABLE_COLUMN_INDEX, src.columnIndex());
388     WebAXObject header = src.columnHeader();
389     if (!header.isDetached())
390       dst->AddIntAttribute(ui::AX_ATTR_TABLE_COLUMN_HEADER_ID, header.axID());
391   }
392
393   if (dst->role == ui::AX_ROLE_CELL ||
394       dst->role == ui::AX_ROLE_ROW_HEADER ||
395       dst->role == ui::AX_ROLE_COLUMN_HEADER) {
396     dst->AddIntAttribute(ui::AX_ATTR_TABLE_CELL_COLUMN_INDEX,
397                          src.cellColumnIndex());
398     dst->AddIntAttribute(ui::AX_ATTR_TABLE_CELL_COLUMN_SPAN,
399                          src.cellColumnSpan());
400     dst->AddIntAttribute(ui::AX_ATTR_TABLE_CELL_ROW_INDEX, src.cellRowIndex());
401     dst->AddIntAttribute(ui::AX_ATTR_TABLE_CELL_ROW_SPAN, src.cellRowSpan());
402   }
403
404   dst->AddStringAttribute(ui::AX_ATTR_NAME, name);
405
406   // Add the ids of *indirect* children - those who are children of this node,
407   // but whose parent is *not* this node. One example is a table
408   // cell, which is a child of both a row and a column. Because the cell's
409   // parent is the row, the row adds it as a child, and the column adds it
410   // as an indirect child.
411   int child_count = src.childCount();
412   for (int i = 0; i < child_count; ++i) {
413     WebAXObject child = src.childAt(i);
414     std::vector<int32> indirect_child_ids;
415     if (!is_iframe && !child.isDetached() && !IsParentUnignoredOf(src, child))
416       indirect_child_ids.push_back(child.axID());
417     if (indirect_child_ids.size() > 0) {
418       dst->AddIntListAttribute(
419           ui::AX_ATTR_INDIRECT_CHILD_IDS, indirect_child_ids);
420     }
421   }
422 }
423
424 bool ShouldIncludeChildNode(
425     const WebAXObject& parent,
426     const WebAXObject& child) {
427   // The child may be invalid due to issues in webkit accessibility code.
428   // Don't add children that are invalid thus preventing a crash.
429   // https://bugs.webkit.org/show_bug.cgi?id=44149
430   // TODO(ctguil): We may want to remove this check as webkit stabilizes.
431   if (child.isDetached())
432     return false;
433
434   // Skip children whose parent isn't this - see indirect_child_ids, above.
435   // As an exception, include children of an iframe element.
436   bool is_iframe = false;
437   WebNode node = parent.node();
438   if (!node.isNull() && node.isElementNode()) {
439     WebElement element = node.to<WebElement>();
440     is_iframe = (element.tagName() == base::ASCIIToUTF16("IFRAME"));
441   }
442
443   return (is_iframe || IsParentUnignoredOf(parent, child));
444 }
445
446 }  // namespace content