1 /*
2 * Copyright (C) 2008 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14 * its contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29 #include "config.h"
30 #include "AccessibilityTable.h"
31
32 #include "AccessibilityTableCell.h"
33 #include "AccessibilityTableColumn.h"
34 #include "AccessibilityTableHeaderContainer.h"
35 #include "AccessibilityTableRow.h"
36 #include "AXObjectCache.h"
37 #include "HTMLNames.h"
38 #include "HTMLTableElement.h"
39 #include "HTMLTableCaptionElement.h"
40 #include "HTMLTableCellElement.h"
41 #include "RenderObject.h"
42 #include "RenderTable.h"
43 #include "RenderTableCell.h"
44 #include "RenderTableSection.h"
45
46 using namespace std;
47
48 namespace WebCore {
49
50 using namespace HTMLNames;
51
AccessibilityTable(RenderObject * renderer)52 AccessibilityTable::AccessibilityTable(RenderObject* renderer)
53 : AccessibilityRenderObject(renderer),
54 m_headerContainer(0)
55 {
56 #if ACCESSIBILITY_TABLES
57 m_isAccessibilityTable = isTableExposableThroughAccessibility();
58 #else
59 m_isAccessibilityTable = false;
60 #endif
61 }
62
~AccessibilityTable()63 AccessibilityTable::~AccessibilityTable()
64 {
65 }
66
create(RenderObject * renderer)67 PassRefPtr<AccessibilityTable> AccessibilityTable::create(RenderObject* renderer)
68 {
69 return adoptRef(new AccessibilityTable(renderer));
70 }
71
isTableExposableThroughAccessibility()72 bool AccessibilityTable::isTableExposableThroughAccessibility()
73 {
74 // the following is a heuristic used to determine if a
75 // <table> should be exposed as an AXTable. The goal
76 // is to only show "data" tables
77
78 if (!m_renderer || !m_renderer->isTable())
79 return false;
80
81 // if the developer assigned an aria role to this, then we shouldn't
82 // expose it as a table, unless, of course, the aria role is a table
83 AccessibilityRole ariaRole = ariaRoleAttribute();
84 if (ariaRole != UnknownRole)
85 return false;
86
87 RenderTable* table = toRenderTable(m_renderer);
88
89 // this employs a heuristic to determine if this table should appear.
90 // Only "data" tables should be exposed as tables.
91 // Unfortunately, there is no good way to determine the difference
92 // between a "layout" table and a "data" table
93
94 Node* tableNode = table->node();
95 if (!tableNode || !tableNode->hasTagName(tableTag))
96 return false;
97
98 // if there is a caption element, summary, THEAD, or TFOOT section, it's most certainly a data table
99 HTMLTableElement* tableElement = static_cast<HTMLTableElement*>(tableNode);
100 if (!tableElement->summary().isEmpty() || tableElement->tHead() || tableElement->tFoot() || tableElement->caption())
101 return true;
102
103 // if someone used "rules" attribute than the table should appear
104 if (!tableElement->rules().isEmpty())
105 return true;
106
107 // go through the cell's and check for tell-tale signs of "data" table status
108 // cells have borders, or use attributes like headers, abbr, scope or axis
109 RenderTableSection* firstBody = table->firstBody();
110 if (!firstBody)
111 return false;
112
113 int numCols = firstBody->numColumns();
114 int numRows = firstBody->numRows();
115
116 // if there's only one cell, it's not a good AXTable candidate
117 if (numRows == 1 && numCols == 1)
118 return false;
119
120 // store the background color of the table to check against cell's background colors
121 RenderStyle* tableStyle = table->style();
122 if (!tableStyle)
123 return false;
124 Color tableBGColor = tableStyle->backgroundColor();
125
126 // check enough of the cells to find if the table matches our criteria
127 // Criteria:
128 // 1) must have at least one valid cell (and)
129 // 2) at least half of cells have borders (or)
130 // 3) at least half of cells have different bg colors than the table, and there is cell spacing
131 unsigned validCellCount = 0;
132 unsigned borderedCellCount = 0;
133 unsigned backgroundDifferenceCellCount = 0;
134
135 for (int row = 0; row < numRows; ++row) {
136 for (int col = 0; col < numCols; ++col) {
137 RenderTableCell* cell = firstBody->cellAt(row, col).cell;
138 if (!cell)
139 continue;
140 Node* cellNode = cell->node();
141 if (!cellNode)
142 continue;
143
144 if (cell->width() < 1 || cell->height() < 1)
145 continue;
146
147 validCellCount++;
148
149 HTMLTableCellElement* cellElement = static_cast<HTMLTableCellElement*>(cellNode);
150
151 // in this case, the developer explicitly assigned a "data" table attribute
152 if (!cellElement->headers().isEmpty() || !cellElement->abbr().isEmpty() ||
153 !cellElement->axis().isEmpty() || !cellElement->scope().isEmpty())
154 return true;
155
156 RenderStyle* renderStyle = cell->style();
157 if (!renderStyle)
158 continue;
159
160 // a cell needs to have matching bordered sides, before it can be considered a bordered cell.
161 if ((cell->borderTop() > 0 && cell->borderBottom() > 0) ||
162 (cell->borderLeft() > 0 && cell->borderRight() > 0))
163 borderedCellCount++;
164
165 // if the cell has a different color from the table and there is cell spacing,
166 // then it is probably a data table cell (spacing and colors take the place of borders)
167 Color cellColor = renderStyle->backgroundColor();
168 if (table->hBorderSpacing() > 0 && table->vBorderSpacing() > 0 &&
169 tableBGColor != cellColor && cellColor.alpha() != 1)
170 backgroundDifferenceCellCount++;
171
172 // if we've found 10 "good" cells, we don't need to keep searching
173 if (borderedCellCount >= 10 || backgroundDifferenceCellCount >= 10)
174 return true;
175 }
176 }
177
178 // if there is less than two valid cells, it's not a data table
179 if (validCellCount <= 1)
180 return false;
181
182 // half of the cells had borders, it's a data table
183 unsigned neededCellCount = validCellCount / 2;
184 if (borderedCellCount >= neededCellCount)
185 return true;
186
187 // half had different background colors, it's a data table
188 if (backgroundDifferenceCellCount >= neededCellCount)
189 return true;
190
191 return false;
192 }
193
clearChildren()194 void AccessibilityTable::clearChildren()
195 {
196 m_children.clear();
197 m_rows.clear();
198 m_columns.clear();
199 m_haveChildren = false;
200 }
201
addChildren()202 void AccessibilityTable::addChildren()
203 {
204 if (!isDataTable()) {
205 AccessibilityRenderObject::addChildren();
206 return;
207 }
208
209 ASSERT(!m_haveChildren);
210
211 m_haveChildren = true;
212 if (!m_renderer || !m_renderer->isTable())
213 return;
214
215 RenderTable* table = toRenderTable(m_renderer);
216 AXObjectCache* axCache = m_renderer->document()->axObjectCache();
217
218 // go through all the available sections to pull out the rows
219 // and add them as children
220 RenderTableSection* tableSection = table->header();
221 if (!tableSection)
222 tableSection = table->firstBody();
223
224 if (!tableSection)
225 return;
226
227 RenderTableSection* initialTableSection = tableSection;
228
229 while (tableSection) {
230
231 HashSet<AccessibilityObject*> appendedRows;
232
233 unsigned numRows = tableSection->numRows();
234 unsigned numCols = tableSection->numColumns();
235 for (unsigned rowIndex = 0; rowIndex < numRows; ++rowIndex) {
236 for (unsigned colIndex = 0; colIndex < numCols; ++colIndex) {
237
238 RenderTableCell* cell = tableSection->cellAt(rowIndex, colIndex).cell;
239 if (!cell)
240 continue;
241
242 AccessibilityObject* rowObject = axCache->getOrCreate(cell->parent());
243 if (!rowObject->isTableRow())
244 continue;
245
246 AccessibilityTableRow* row = static_cast<AccessibilityTableRow*>(rowObject);
247 // we need to check every cell for a new row, because cell spans
248 // can cause us to mess rows if we just check the first column
249 if (appendedRows.contains(row))
250 continue;
251
252 row->setRowIndex((int)m_rows.size());
253 m_rows.append(row);
254 m_children.append(row);
255 appendedRows.add(row);
256 }
257 }
258
259 tableSection = table->sectionBelow(tableSection, true);
260 }
261
262 // make the columns based on the number of columns in the first body
263 unsigned length = initialTableSection->numColumns();
264 for (unsigned i = 0; i < length; ++i) {
265 AccessibilityTableColumn* column = static_cast<AccessibilityTableColumn*>(axCache->getOrCreate(ColumnRole));
266 column->setColumnIndex((int)i);
267 column->setParentTable(this);
268 m_columns.append(column);
269 m_children.append(column);
270 }
271
272 AccessibilityObject* headerContainerObject = headerContainer();
273 if (headerContainerObject)
274 m_children.append(headerContainerObject);
275 }
276
headerContainer()277 AccessibilityObject* AccessibilityTable::headerContainer()
278 {
279 if (m_headerContainer)
280 return m_headerContainer;
281
282 m_headerContainer = static_cast<AccessibilityTableHeaderContainer*>(axObjectCache()->getOrCreate(TableHeaderContainerRole));
283 m_headerContainer->setParentTable(this);
284
285 return m_headerContainer;
286 }
287
columns()288 AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::columns()
289 {
290 if (!hasChildren())
291 addChildren();
292
293 return m_columns;
294 }
295
rows()296 AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::rows()
297 {
298 if (!hasChildren())
299 addChildren();
300
301 return m_rows;
302 }
303
rowHeaders(AccessibilityChildrenVector & headers)304 void AccessibilityTable::rowHeaders(AccessibilityChildrenVector& headers)
305 {
306 if (!m_renderer)
307 return;
308
309 if (!hasChildren())
310 addChildren();
311
312 unsigned rowCount = m_rows.size();
313 for (unsigned k = 0; k < rowCount; ++k) {
314 AccessibilityObject* header = static_cast<AccessibilityTableRow*>(m_rows[k].get())->headerObject();
315 if (!header)
316 continue;
317 headers.append(header);
318 }
319 }
320
columnHeaders(AccessibilityChildrenVector & headers)321 void AccessibilityTable::columnHeaders(AccessibilityChildrenVector& headers)
322 {
323 if (!m_renderer)
324 return;
325
326 if (!hasChildren())
327 addChildren();
328
329 unsigned colCount = m_columns.size();
330 for (unsigned k = 0; k < colCount; ++k) {
331 AccessibilityObject* header = static_cast<AccessibilityTableColumn*>(m_columns[k].get())->headerObject();
332 if (!header)
333 continue;
334 headers.append(header);
335 }
336 }
337
cells(AccessibilityObject::AccessibilityChildrenVector & cells)338 void AccessibilityTable::cells(AccessibilityObject::AccessibilityChildrenVector& cells)
339 {
340 if (!m_renderer)
341 return;
342
343 if (!hasChildren())
344 addChildren();
345
346 int numRows = m_rows.size();
347 for (int row = 0; row < numRows; ++row) {
348 AccessibilityChildrenVector rowChildren = m_rows[row]->children();
349 cells.append(rowChildren);
350 }
351 }
352
columnCount()353 unsigned AccessibilityTable::columnCount()
354 {
355 if (!hasChildren())
356 addChildren();
357
358 return m_columns.size();
359 }
360
rowCount()361 unsigned AccessibilityTable::rowCount()
362 {
363 if (!hasChildren())
364 addChildren();
365
366 return m_rows.size();
367 }
368
cellForColumnAndRow(unsigned column,unsigned row)369 AccessibilityTableCell* AccessibilityTable::cellForColumnAndRow(unsigned column, unsigned row)
370 {
371 if (!m_renderer || !m_renderer->isTable())
372 return 0;
373
374 if (!hasChildren())
375 addChildren();
376
377 RenderTable* table = toRenderTable(m_renderer);
378 RenderTableSection* tableSection = table->header();
379 if (!tableSection)
380 tableSection = table->firstBody();
381
382 RenderTableCell* cell = 0;
383 unsigned rowCount = 0;
384 unsigned rowOffset = 0;
385 while (tableSection) {
386
387 unsigned numRows = tableSection->numRows();
388 unsigned numCols = tableSection->numColumns();
389
390 rowCount += numRows;
391
392 unsigned sectionSpecificRow = row - rowOffset;
393 if (row < rowCount && column < numCols && sectionSpecificRow < numRows) {
394 cell = tableSection->cellAt(sectionSpecificRow, column).cell;
395
396 // we didn't find the cell, which means there's spanning happening
397 // search backwards to find the spanning cell
398 if (!cell) {
399
400 // first try rows
401 for (int testRow = sectionSpecificRow-1; testRow >= 0; --testRow) {
402 cell = tableSection->cellAt(testRow, column).cell;
403 // cell overlapped. use this one
404 if (cell && ((cell->row() + (cell->rowSpan()-1)) >= (int)sectionSpecificRow))
405 break;
406 cell = 0;
407 }
408
409 if (!cell) {
410 // try cols
411 for (int testCol = column-1; testCol >= 0; --testCol) {
412 cell = tableSection->cellAt(sectionSpecificRow, testCol).cell;
413 // cell overlapped. use this one
414 if (cell && ((cell->col() + (cell->colSpan()-1)) >= (int)column))
415 break;
416 cell = 0;
417 }
418 }
419 }
420 }
421
422 if (cell)
423 break;
424
425 rowOffset += numRows;
426 // we didn't find anything between the rows we should have
427 if (row < rowCount)
428 break;
429 tableSection = table->sectionBelow(tableSection, true);
430 }
431
432 if (!cell)
433 return 0;
434
435 AccessibilityObject* cellObject = axObjectCache()->getOrCreate(cell);
436 ASSERT(cellObject->isTableCell());
437
438 return static_cast<AccessibilityTableCell*>(cellObject);
439 }
440
roleValue() const441 AccessibilityRole AccessibilityTable::roleValue() const
442 {
443 if (!isDataTable())
444 return AccessibilityRenderObject::roleValue();
445
446 return TableRole;
447 }
448
accessibilityIsIgnored() const449 bool AccessibilityTable::accessibilityIsIgnored() const
450 {
451 if (!isDataTable())
452 return AccessibilityRenderObject::accessibilityIsIgnored();
453
454 return false;
455 }
456
title() const457 String AccessibilityTable::title() const
458 {
459 if (!isDataTable())
460 return AccessibilityRenderObject::title();
461
462 String title;
463 if (!m_renderer)
464 return title;
465
466 // see if there is a caption
467 Node* tableElement = m_renderer->node();
468 if (tableElement && tableElement->hasTagName(tableTag)) {
469 HTMLTableCaptionElement* caption = static_cast<HTMLTableElement*>(tableElement)->caption();
470 if (caption)
471 title = caption->innerText();
472 }
473
474 // try the standard
475 if (title.isEmpty())
476 title = AccessibilityRenderObject::title();
477
478 return title;
479 }
480
isDataTable() const481 bool AccessibilityTable::isDataTable() const
482 {
483 if (!m_renderer)
484 return false;
485
486 return m_isAccessibilityTable;
487 }
488
489 } // namespace WebCore
490