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 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 "AXObjectCache.h"
33#include "AccessibilityTableCell.h"
34#include "AccessibilityTableColumn.h"
35#include "AccessibilityTableHeaderContainer.h"
36#include "AccessibilityTableRow.h"
37#include "ElementIterator.h"
38#include "HTMLNames.h"
39#include "HTMLTableCaptionElement.h"
40#include "HTMLTableCellElement.h"
41#include "HTMLTableElement.h"
42#include "RenderObject.h"
43#include "RenderTable.h"
44#include "RenderTableCell.h"
45#include "RenderTableSection.h"
46
47namespace WebCore {
48
49using namespace HTMLNames;
50
51AccessibilityTable::AccessibilityTable(RenderObject* renderer)
52    : AccessibilityRenderObject(renderer)
53    , m_headerContainer(0)
54    , m_isAccessibilityTable(true)
55{
56}
57
58AccessibilityTable::~AccessibilityTable()
59{
60}
61
62void AccessibilityTable::init()
63{
64    AccessibilityRenderObject::init();
65    m_isAccessibilityTable = isTableExposableThroughAccessibility();
66}
67
68PassRefPtr<AccessibilityTable> AccessibilityTable::create(RenderObject* renderer)
69{
70    return adoptRef(new AccessibilityTable(renderer));
71}
72
73bool AccessibilityTable::hasARIARole() const
74{
75    if (!m_renderer)
76        return false;
77
78    AccessibilityRole ariaRole = ariaRoleAttribute();
79    if (ariaRole != UnknownRole)
80        return true;
81
82    return false;
83}
84
85bool AccessibilityTable::isAccessibilityTable() const
86{
87    if (!m_renderer)
88        return false;
89
90    return m_isAccessibilityTable;
91}
92
93HTMLTableElement* AccessibilityTable::tableElement() const
94{
95    if (!m_renderer->isTable())
96        return nullptr;
97
98    RenderTable* table = toRenderTable(m_renderer);
99    if (table->element() && isHTMLTableElement(table->element()))
100        return toHTMLTableElement(table->element());
101
102    // If the table has a display:table-row-group, then the RenderTable does not have a pointer to it's HTMLTableElement.
103    // We can instead find it by asking the firstSection for its parent.
104    RenderTableSection* firstBody = table->firstBody();
105    if (!firstBody || !firstBody->element())
106        return nullptr;
107
108    Element* actualTable = firstBody->element()->parentElement();
109    if (!actualTable || !isHTMLTableElement(actualTable))
110        return nullptr;
111
112    return toHTMLTableElement(actualTable);
113}
114
115bool AccessibilityTable::isDataTable() const
116{
117    if (!m_renderer)
118        return false;
119
120    // Do not consider it a data table is it has an ARIA role.
121    if (hasARIARole())
122        return false;
123
124    // When a section of the document is contentEditable, all tables should be
125    // treated as data tables, otherwise users may not be able to work with rich
126    // text editors that allow creating and editing tables.
127    if (node() && node()->hasEditableStyle())
128        return true;
129
130    if (!m_renderer->isTable())
131        return false;
132
133    // This employs a heuristic to determine if this table should appear.
134    // Only "data" tables should be exposed as tables.
135    // Unfortunately, there is no good way to determine the difference
136    // between a "layout" table and a "data" table.
137    RenderTable* table = toRenderTable(m_renderer);
138    HTMLTableElement* tableElement = this->tableElement();
139    if (tableElement) {
140        // If there is a caption element, summary, THEAD, or TFOOT section, it's most certainly a data table.
141        if (!tableElement->summary().isEmpty() || tableElement->tHead() || tableElement->tFoot() || tableElement->caption())
142            return true;
143
144        // If someone used "rules" attribute than the table should appear.
145        if (!tableElement->rules().isEmpty())
146            return true;
147
148        // If there's a colgroup or col element, it's probably a data table.
149        for (const auto& child : childrenOfType<Element>(*tableElement)) {
150            if (child.hasTagName(colTag) || child.hasTagName(colgroupTag))
151                return true;
152        }
153    }
154
155    // go through the cell's and check for tell-tale signs of "data" table status
156    // cells have borders, or use attributes like headers, abbr, scope or axis
157    table->recalcSectionsIfNeeded();
158    RenderTableSection* firstBody = table->firstBody();
159    if (!firstBody)
160        return false;
161
162    int numCols = firstBody->numColumns();
163    int numRows = firstBody->numRows();
164
165    // If there's only one cell, it's not a good AXTable candidate.
166    if (numRows == 1 && numCols == 1)
167        return false;
168
169    // If there are at least 20 rows, we'll call it a data table.
170    if (numRows >= 20)
171        return true;
172
173    // Store the background color of the table to check against cell's background colors.
174    const RenderStyle& tableStyle = table->style();
175    Color tableBGColor = tableStyle.visitedDependentColor(CSSPropertyBackgroundColor);
176
177    // check enough of the cells to find if the table matches our criteria
178    // Criteria:
179    //   1) must have at least one valid cell (and)
180    //   2) at least half of cells have borders (or)
181    //   3) at least half of cells have different bg colors than the table, and there is cell spacing
182    unsigned validCellCount = 0;
183    unsigned borderedCellCount = 0;
184    unsigned backgroundDifferenceCellCount = 0;
185    unsigned cellsWithTopBorder = 0;
186    unsigned cellsWithBottomBorder = 0;
187    unsigned cellsWithLeftBorder = 0;
188    unsigned cellsWithRightBorder = 0;
189
190    Color alternatingRowColors[5];
191    int alternatingRowColorCount = 0;
192
193    int headersInFirstColumnCount = 0;
194    for (int row = 0; row < numRows; ++row) {
195
196        int headersInFirstRowCount = 0;
197        for (int col = 0; col < numCols; ++col) {
198            RenderTableCell* cell = firstBody->primaryCellAt(row, col);
199            if (!cell)
200                continue;
201
202            Element* cellElement = cell->element();
203            if (!cellElement)
204                continue;
205
206            if (cell->width() < 1 || cell->height() < 1)
207                continue;
208
209            validCellCount++;
210
211            bool isTHCell = cellElement->hasTagName(thTag);
212            // If the first row is comprised of all <th> tags, assume it is a data table.
213            if (!row && isTHCell)
214                headersInFirstRowCount++;
215
216            // If the first column is comprised of all <th> tags, assume it is a data table.
217            if (!col && isTHCell)
218                headersInFirstColumnCount++;
219
220            // In this case, the developer explicitly assigned a "data" table attribute.
221            if (cellElement->hasTagName(tdTag) || cellElement->hasTagName(thTag)) {
222                HTMLTableCellElement* tableCellElement = toHTMLTableCellElement(cellElement);
223                if (!tableCellElement->headers().isEmpty() || !tableCellElement->abbr().isEmpty()
224                    || !tableCellElement->axis().isEmpty() || !tableCellElement->scope().isEmpty())
225                    return true;
226            }
227            const RenderStyle& renderStyle = cell->style();
228
229            // If the empty-cells style is set, we'll call it a data table.
230            if (renderStyle.emptyCells() == HIDE)
231                return true;
232
233            // If a cell has matching bordered sides, call it a (fully) bordered cell.
234            if ((cell->borderTop() > 0 && cell->borderBottom() > 0)
235                || (cell->borderLeft() > 0 && cell->borderRight() > 0))
236                borderedCellCount++;
237
238            // Also keep track of each individual border, so we can catch tables where most
239            // cells have a bottom border, for example.
240            if (cell->borderTop() > 0)
241                cellsWithTopBorder++;
242            if (cell->borderBottom() > 0)
243                cellsWithBottomBorder++;
244            if (cell->borderLeft() > 0)
245                cellsWithLeftBorder++;
246            if (cell->borderRight() > 0)
247                cellsWithRightBorder++;
248
249            // If the cell has a different color from the table and there is cell spacing,
250            // then it is probably a data table cell (spacing and colors take the place of borders).
251            Color cellColor = renderStyle.visitedDependentColor(CSSPropertyBackgroundColor);
252            if (table->hBorderSpacing() > 0 && table->vBorderSpacing() > 0
253                && tableBGColor != cellColor && cellColor.alpha() != 1)
254                backgroundDifferenceCellCount++;
255
256            // If we've found 10 "good" cells, we don't need to keep searching.
257            if (borderedCellCount >= 10 || backgroundDifferenceCellCount >= 10)
258                return true;
259
260            // For the first 5 rows, cache the background color so we can check if this table has zebra-striped rows.
261            if (row < 5 && row == alternatingRowColorCount) {
262                RenderObject* renderRow = cell->parent();
263                if (!renderRow || !renderRow->isBoxModelObject() || !toRenderBoxModelObject(renderRow)->isTableRow())
264                    continue;
265                const RenderStyle& rowRenderStyle = renderRow->style();
266                Color rowColor = rowRenderStyle.visitedDependentColor(CSSPropertyBackgroundColor);
267                alternatingRowColors[alternatingRowColorCount] = rowColor;
268                alternatingRowColorCount++;
269            }
270        }
271
272        if (!row && headersInFirstRowCount == numCols && numCols > 1)
273            return true;
274    }
275
276    if (headersInFirstColumnCount == numRows && numRows > 1)
277        return true;
278
279    // if there is less than two valid cells, it's not a data table
280    if (validCellCount <= 1)
281        return false;
282
283    // half of the cells had borders, it's a data table
284    unsigned neededCellCount = validCellCount / 2;
285    if (borderedCellCount >= neededCellCount
286        || cellsWithTopBorder >= neededCellCount
287        || cellsWithBottomBorder >= neededCellCount
288        || cellsWithLeftBorder >= neededCellCount
289        || cellsWithRightBorder >= neededCellCount)
290        return true;
291
292    // half had different background colors, it's a data table
293    if (backgroundDifferenceCellCount >= neededCellCount)
294        return true;
295
296    // Check if there is an alternating row background color indicating a zebra striped style pattern.
297    if (alternatingRowColorCount > 2) {
298        Color firstColor = alternatingRowColors[0];
299        for (int k = 1; k < alternatingRowColorCount; k++) {
300            // If an odd row was the same color as the first row, its not alternating.
301            if (k % 2 == 1 && alternatingRowColors[k] == firstColor)
302                return false;
303            // If an even row is not the same as the first row, its not alternating.
304            if (!(k % 2) && alternatingRowColors[k] != firstColor)
305                return false;
306        }
307        return true;
308    }
309
310    return false;
311}
312
313bool AccessibilityTable::isTableExposableThroughAccessibility() const
314{
315    // The following is a heuristic used to determine if a
316    // <table> should be exposed as an AXTable. The goal
317    // is to only show "data" tables.
318
319    if (!m_renderer)
320        return false;
321
322    // If the developer assigned an aria role to this, then we
323    // shouldn't expose it as a table, unless, of course, the aria
324    // role is a table.
325    if (hasARIARole())
326        return false;
327
328    // Gtk+ ATs expect all tables to be exposed as tables.
329#if PLATFORM(GTK) || PLATFORM(EFL)
330    Element* tableNode = toRenderTable(m_renderer)->element();
331    return tableNode && isHTMLTableElement(tableNode);
332#endif
333
334    return isDataTable();
335}
336
337void AccessibilityTable::clearChildren()
338{
339    AccessibilityRenderObject::clearChildren();
340    m_rows.clear();
341    m_columns.clear();
342
343    if (m_headerContainer) {
344        m_headerContainer->detachFromParent();
345        m_headerContainer = 0;
346    }
347}
348
349void AccessibilityTable::addChildren()
350{
351    if (!isAccessibilityTable()) {
352        AccessibilityRenderObject::addChildren();
353        return;
354    }
355
356    ASSERT(!m_haveChildren);
357
358    m_haveChildren = true;
359    if (!m_renderer || !m_renderer->isTable())
360        return;
361
362    RenderTable* table = toRenderTable(m_renderer);
363    // Go through all the available sections to pull out the rows and add them as children.
364    table->recalcSectionsIfNeeded();
365
366    unsigned maxColumnCount = 0;
367    RenderTableSection* footer = table->footer();
368
369    for (RenderTableSection* tableSection = table->topSection(); tableSection; tableSection = table->sectionBelow(tableSection, SkipEmptySections)) {
370        if (tableSection == footer)
371            continue;
372        addChildrenFromSection(tableSection, maxColumnCount);
373    }
374
375    // Process the footer last, in case it was ordered earlier in the DOM.
376    if (footer)
377        addChildrenFromSection(footer, maxColumnCount);
378
379    AXObjectCache* axCache = m_renderer->document().axObjectCache();
380    // make the columns based on the number of columns in the first body
381    unsigned length = maxColumnCount;
382    for (unsigned i = 0; i < length; ++i) {
383        AccessibilityTableColumn* column = toAccessibilityTableColumn(axCache->getOrCreate(ColumnRole));
384        column->setColumnIndex((int)i);
385        column->setParent(this);
386        m_columns.append(column);
387        if (!column->accessibilityIsIgnored())
388            m_children.append(column);
389    }
390
391    AccessibilityObject* headerContainerObject = headerContainer();
392    if (headerContainerObject && !headerContainerObject->accessibilityIsIgnored())
393        m_children.append(headerContainerObject);
394}
395
396void AccessibilityTable::addChildrenFromSection(RenderTableSection* tableSection, unsigned& maxColumnCount)
397{
398    ASSERT(tableSection);
399    if (!tableSection)
400        return;
401
402    AXObjectCache* axCache = m_renderer->document().axObjectCache();
403    HashSet<AccessibilityObject*> appendedRows;
404    unsigned numRows = tableSection->numRows();
405    for (unsigned rowIndex = 0; rowIndex < numRows; ++rowIndex) {
406
407        RenderTableRow* renderRow = tableSection->rowRendererAt(rowIndex);
408        if (!renderRow)
409            continue;
410
411        AccessibilityObject* rowObject = axCache->getOrCreate(renderRow);
412        if (!rowObject->isTableRow())
413            continue;
414
415        AccessibilityTableRow* row = toAccessibilityTableRow(rowObject);
416        // We need to check every cell for a new row, because cell spans
417        // can cause us to miss rows if we just check the first column.
418        if (appendedRows.contains(row))
419            continue;
420
421        row->setRowIndex(static_cast<int>(m_rows.size()));
422        m_rows.append(row);
423        if (!row->accessibilityIsIgnored())
424            m_children.append(row);
425#if PLATFORM(GTK) || PLATFORM(EFL)
426        else
427            m_children.appendVector(row->children());
428#endif
429        appendedRows.add(row);
430    }
431
432    maxColumnCount = std::max(tableSection->numColumns(), maxColumnCount);
433}
434
435AccessibilityObject* AccessibilityTable::headerContainer()
436{
437    if (m_headerContainer)
438        return m_headerContainer.get();
439
440    AccessibilityMockObject* tableHeader = toAccessibilityMockObject(axObjectCache()->getOrCreate(TableHeaderContainerRole));
441    tableHeader->setParent(this);
442
443    m_headerContainer = tableHeader;
444    return m_headerContainer.get();
445}
446
447const AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::columns()
448{
449    updateChildrenIfNecessary();
450
451    return m_columns;
452}
453
454const AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::rows()
455{
456    updateChildrenIfNecessary();
457
458    return m_rows;
459}
460
461void AccessibilityTable::columnHeaders(AccessibilityChildrenVector& headers)
462{
463    if (!m_renderer)
464        return;
465
466    updateChildrenIfNecessary();
467
468    for (const auto& column : m_columns) {
469        if (AccessibilityObject* header = toAccessibilityTableColumn(column.get())->headerObject())
470            headers.append(header);
471    }
472}
473
474void AccessibilityTable::rowHeaders(AccessibilityChildrenVector& headers)
475{
476    if (!m_renderer)
477        return;
478
479    updateChildrenIfNecessary();
480
481    for (const auto& row : m_rows) {
482        if (AccessibilityObject* header = toAccessibilityTableRow(row.get())->headerObject())
483            headers.append(header);
484    }
485}
486
487void AccessibilityTable::visibleRows(AccessibilityChildrenVector& rows)
488{
489    if (!m_renderer)
490        return;
491
492    updateChildrenIfNecessary();
493
494    for (const auto& row : m_rows) {
495        if (row && !row->isOffScreen())
496            rows.append(row);
497    }
498}
499
500void AccessibilityTable::cells(AccessibilityObject::AccessibilityChildrenVector& cells)
501{
502    if (!m_renderer)
503        return;
504
505    updateChildrenIfNecessary();
506
507    for (const auto& row : m_rows)
508        cells.appendVector(row->children());
509}
510
511unsigned AccessibilityTable::columnCount()
512{
513    updateChildrenIfNecessary();
514
515    return m_columns.size();
516}
517
518unsigned AccessibilityTable::rowCount()
519{
520    updateChildrenIfNecessary();
521
522    return m_rows.size();
523}
524
525int AccessibilityTable::tableLevel() const
526{
527    int level = 0;
528    for (AccessibilityObject* obj = static_cast<AccessibilityObject*>(const_cast<AccessibilityTable*>(this)); obj; obj = obj->parentObject()) {
529        if (obj->isAccessibilityTable())
530            ++level;
531    }
532
533    return level;
534}
535
536AccessibilityTableCell* AccessibilityTable::cellForColumnAndRow(unsigned column, unsigned row)
537{
538    updateChildrenIfNecessary();
539    if (column >= columnCount() || row >= rowCount())
540        return 0;
541
542    // Iterate backwards through the rows in case the desired cell has a rowspan and exists in a previous row.
543    for (unsigned rowIndexCounter = row + 1; rowIndexCounter > 0; --rowIndexCounter) {
544        unsigned rowIndex = rowIndexCounter - 1;
545        const auto& children = m_rows[rowIndex]->children();
546        // Since some cells may have colspans, we have to check the actual range of each
547        // cell to determine which is the right one.
548        for (unsigned colIndexCounter = std::min(static_cast<unsigned>(children.size()), column + 1); colIndexCounter > 0; --colIndexCounter) {
549            unsigned colIndex = colIndexCounter - 1;
550            AccessibilityObject* child = children[colIndex].get();
551            ASSERT(child->isTableCell());
552            if (!child->isTableCell())
553                continue;
554
555            std::pair<unsigned, unsigned> columnRange;
556            std::pair<unsigned, unsigned> rowRange;
557            AccessibilityTableCell* tableCellChild = toAccessibilityTableCell(child);
558            tableCellChild->columnIndexRange(columnRange);
559            tableCellChild->rowIndexRange(rowRange);
560
561            if ((column >= columnRange.first && column < (columnRange.first + columnRange.second))
562                && (row >= rowRange.first && row < (rowRange.first + rowRange.second)))
563                return tableCellChild;
564        }
565    }
566
567    return 0;
568}
569
570AccessibilityRole AccessibilityTable::roleValue() const
571{
572    if (!isAccessibilityTable())
573        return AccessibilityRenderObject::roleValue();
574
575    return TableRole;
576}
577
578bool AccessibilityTable::computeAccessibilityIsIgnored() const
579{
580    AccessibilityObjectInclusion decision = defaultObjectInclusion();
581    if (decision == IncludeObject)
582        return false;
583    if (decision == IgnoreObject)
584        return true;
585
586    if (!isAccessibilityTable())
587        return AccessibilityRenderObject::computeAccessibilityIsIgnored();
588
589    return false;
590}
591
592void AccessibilityTable::titleElementText(Vector<AccessibilityText>& textOrder) const
593{
594    String title = this->title();
595    if (!title.isEmpty())
596        textOrder.append(AccessibilityText(title, LabelByElementText));
597}
598
599String AccessibilityTable::title() const
600{
601    if (!isAccessibilityTable())
602        return AccessibilityRenderObject::title();
603
604    String title;
605    if (!m_renderer)
606        return title;
607
608    // see if there is a caption
609    Node* tableElement = m_renderer->node();
610    if (tableElement && isHTMLTableElement(tableElement)) {
611        HTMLTableCaptionElement* caption = toHTMLTableElement(tableElement)->caption();
612        if (caption)
613            title = caption->innerText();
614    }
615
616    // try the standard
617    if (title.isEmpty())
618        title = AccessibilityRenderObject::title();
619
620    return title;
621}
622
623} // namespace WebCore
624