nu.validator.checker.table.Table Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of validator Show documentation
Show all versions of validator Show documentation
An HTML-checking library (used by https://html5.validator.nu and the HTML5 facet of the W3C Validator)
/*
* Copyright (c) 2006 Henri Sivonen
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package nu.validator.checker.table;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import nu.validator.checker.AttributeUtil;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.LocatorImpl;
/**
* Represents an XHTML table for table integrity checking. Handles
* table-significant parse events and keeps track of columns.
*
* @version $Id$
* @author hsivonen
*/
final class Table {
/**
* An enumeration for keeping track of the parsing state of a table.
*/
private enum State {
/**
* The table element start has been seen. No child elements have been seen.
* A start of a column, a column group, a row or a row group or the end of
* the table is expected.
*/
IN_TABLE_AT_START,
/**
* The table element is the open element and rows have been seen. A row in
* an implicit group, a row group or the end of the table is expected.
*/
IN_TABLE_AT_POTENTIAL_ROW_GROUP_START,
/**
* A column group is open. It can end or a column can start.
*/
IN_COLGROUP,
/**
* A column inside a column group is open. It can end.
*/
IN_COL_IN_COLGROUP,
/**
* A column that is a child of table is open. It can end.
*/
IN_COL_IN_IMPLICIT_GROUP,
/**
* The open element is an explicit row group. It may end or a row may start.
*/
IN_ROW_GROUP,
/**
* A row in a an explicit row group is open. It may end or a cell may start.
*/
IN_ROW_IN_ROW_GROUP,
/**
* A cell inside a row inside an explicit row group is open. It can end.
*/
IN_CELL_IN_ROW_GROUP,
/**
* A row in an implicit row group is open. It may end or a cell may start.
*/
IN_ROW_IN_IMPLICIT_ROW_GROUP,
/**
* The table itself is the currently open element, but an implicit row group
* been started by previous rows. A row may start, an explicit row group may
* start or the table may end.
*/
IN_IMPLICIT_ROW_GROUP,
/**
* A cell inside an implicit row group is open. It can close.
*/
IN_CELL_IN_IMPLICIT_ROW_GROUP,
/**
* The table itself is the currently open element. Columns and/or column groups
* have been seen but rows or row groups have not been seen yet. A column, a
* column group, a row or a row group can start. The table can end.
*/
IN_TABLE_COLS_SEEN
}
/**
* Keeps track of the handler state between SAX events.
*/
private State state = State.IN_TABLE_AT_START;
/**
* The number of suppressed element starts.
*/
private int suppressedStarts = 0;
/**
* Indicates whether the width of the table was established by column markup.
*/
private boolean hardWidth = false;
/**
* The column count established by column markup or by the first row.
*/
private int columnCount = -1;
/**
* The actual column count as stretched by the widest row.
*/
private int realColumnCount = 0;
/**
* A colgroup span that hasn't been actuated yet in case the element has
* col children. The absolute value counts. The negative sign means that
* the value was implied.
*/
private int pendingColGroupSpan = 0;
/**
* A set of the IDs of header cells.
*/
private final Set headerIds = new HashSet<>();
/**
* A list of cells that refer to headers (in the document order).
*/
private final List cellsReferringToHeaders = new LinkedList<>();
/**
* The owning checker.
*/
private final TableChecker owner;
/**
* The current row group (also implicit groups have an explicit object).
*/
private RowGroup current;
/**
* The head of the column range list.
*/
private ColumnRange first = null;
/**
* The tail of the column range list.
*/
private ColumnRange last = null;
/**
* The range under inspection.
*/
private ColumnRange currentColRange = null;
/**
* The previous range that was inspected.
*/
private ColumnRange previousColRange = null;
/**
* Constructor.
* @param owner reference back to the checker
*/
public Table(TableChecker owner) {
super();
this.owner = owner;
}
private boolean needSuppressStart() {
if (suppressedStarts > 0) {
suppressedStarts++;
return true;
} else {
return false;
}
}
private boolean needSuppressEnd() {
if (suppressedStarts > 0) {
suppressedStarts--;
return true;
} else {
return false;
}
}
void startRowGroup(String type) throws SAXException {
if (needSuppressStart()) {
return;
}
switch (state) {
case IN_IMPLICIT_ROW_GROUP:
current.end();
// fall through
case IN_TABLE_AT_START:
case IN_TABLE_COLS_SEEN:
case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START:
current = new RowGroup(this, type);
state = State.IN_ROW_GROUP;
break;
default:
suppressedStarts = 1;
break;
}
}
void endRowGroup() throws SAXException {
if (needSuppressEnd()) {
return;
}
switch (state) {
case IN_ROW_GROUP:
current.end();
current = null;
state = State.IN_TABLE_AT_POTENTIAL_ROW_GROUP_START;
break;
default:
throw new IllegalStateException("Bug!");
}
}
void startRow() {
if (needSuppressStart()) {
return;
}
switch (state) {
case IN_TABLE_AT_START:
case IN_TABLE_COLS_SEEN:
case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START:
current = new RowGroup(this, null);
// fall through
case IN_IMPLICIT_ROW_GROUP:
state = State.IN_ROW_IN_IMPLICIT_ROW_GROUP;
break;
case IN_ROW_GROUP:
state = State.IN_ROW_IN_ROW_GROUP;
break;
default:
suppressedStarts = 1;
return;
}
currentColRange = first;
previousColRange = null;
current.startRow();
}
void endRow() throws SAXException {
if (needSuppressEnd()) {
return;
}
switch (state) {
case IN_ROW_IN_ROW_GROUP:
state = State.IN_ROW_GROUP;
break;
case IN_ROW_IN_IMPLICIT_ROW_GROUP:
state = State.IN_IMPLICIT_ROW_GROUP;
break;
default:
throw new IllegalStateException("Bug!");
}
current.endRow();
}
void startCell(boolean header, Attributes attributes) throws SAXException {
if (needSuppressStart()) {
return;
}
switch (state) {
case IN_ROW_IN_ROW_GROUP:
state = State.IN_CELL_IN_ROW_GROUP;
break;
case IN_ROW_IN_IMPLICIT_ROW_GROUP:
state = State.IN_CELL_IN_IMPLICIT_ROW_GROUP;
break;
default:
suppressedStarts = 1;
return;
}
if (header) {
int len = attributes.getLength();
for (int i = 0; i < len; i++) {
if ("ID".equals(attributes.getType(i))) {
String val = attributes.getValue(i);
if (!"".equals(val)) {
headerIds.add(val);
}
}
}
}
String[] headers = AttributeUtil.split(attributes.getValue("",
"headers"));
Cell cell = new Cell(
Math.abs(AttributeUtil.parsePositiveInteger(attributes.getValue(
"", "colspan"))),
Math.abs(AttributeUtil.parseNonNegativeInteger(attributes.getValue(
"", "rowspan"))), headers, header,
owner.getDocumentLocator(), owner.getErrorHandler());
if (headers.length > 0) {
cellsReferringToHeaders.add(cell);
}
current.cell(cell);
}
void endCell() {
if (needSuppressEnd()) {
return;
}
switch (state) {
case IN_CELL_IN_ROW_GROUP:
state = State.IN_ROW_IN_ROW_GROUP;
break;
case IN_CELL_IN_IMPLICIT_ROW_GROUP:
state = State.IN_ROW_IN_IMPLICIT_ROW_GROUP;
break;
default:
throw new IllegalStateException("Bug!");
}
}
void startColGroup(int span) {
if (needSuppressStart()) {
return;
}
switch (state) {
case IN_TABLE_AT_START:
hardWidth = true;
columnCount = 0;
// fall through
case IN_TABLE_COLS_SEEN:
pendingColGroupSpan = span;
state = State.IN_COLGROUP;
break;
default:
suppressedStarts = 1;
break;
}
}
void endColGroup() {
if (needSuppressEnd()) {
return;
}
switch (state) {
case IN_COLGROUP:
if (pendingColGroupSpan != 0) {
int right = columnCount + Math.abs(pendingColGroupSpan);
Locator locator = new LocatorImpl(
owner.getDocumentLocator());
ColumnRange colRange = new ColumnRange("colgroup", locator,
columnCount, right);
appendColumnRange(colRange);
columnCount = right;
}
realColumnCount = columnCount;
state = State.IN_TABLE_COLS_SEEN;
break;
default:
throw new IllegalStateException("Bug!");
}
}
void startCol(int span) throws SAXException {
if (needSuppressStart()) {
return;
}
switch (state) {
case IN_TABLE_AT_START:
hardWidth = true;
columnCount = 0;
// fall through
case IN_TABLE_COLS_SEEN:
state = State.IN_COL_IN_IMPLICIT_GROUP;
break;
case IN_COLGROUP:
if (pendingColGroupSpan > 0) {
warn("A col element causes a span attribute with value "
+ pendingColGroupSpan
+ " to be ignored on the parent colgroup.");
}
pendingColGroupSpan = 0;
state = State.IN_COL_IN_COLGROUP;
break;
default:
suppressedStarts = 1;
return;
}
int right = columnCount + Math.abs(span);
Locator locator = new LocatorImpl(owner.getDocumentLocator());
ColumnRange colRange = new ColumnRange("col", locator,
columnCount, right);
appendColumnRange(colRange);
columnCount = right;
realColumnCount = columnCount;
}
/**
* Appends a column range to the linked list of column ranges.
*
* @param colRange the range to append
*/
private void appendColumnRange(ColumnRange colRange) {
if (last == null) {
first = colRange;
last = colRange;
} else {
last.setNext(colRange);
last = colRange;
}
}
void warn(String message) throws SAXException {
owner.warn(message);
}
void err(String message) throws SAXException {
owner.err(message);
}
void endCol() {
if (needSuppressEnd()) {
return;
}
switch (state) {
case IN_COL_IN_IMPLICIT_GROUP:
state = State.IN_TABLE_COLS_SEEN;
break;
case IN_COL_IN_COLGROUP:
state = State.IN_COLGROUP;
break;
default:
throw new IllegalStateException("Bug!");
}
}
void end() throws SAXException {
switch (state) {
case IN_IMPLICIT_ROW_GROUP:
current.end();
current = null;
break;
case IN_TABLE_AT_START:
case IN_TABLE_AT_POTENTIAL_ROW_GROUP_START:
case IN_TABLE_COLS_SEEN:
break;
default:
throw new IllegalStateException("Bug!");
}
// Check referential integrity
for (Cell cell : cellsReferringToHeaders) {
for (String heading : cell.getHeadings()) {
if (!headerIds.contains(heading)) {
cell.err("The \u201Cheaders\u201D attribute on the element \u201C"
+ cell.elementName()
+ "\u201D refers to the ID \u201C"
+ heading
+ "\u201D, but there is no \u201Cth\u201D element with that ID in the same table.");
}
}
}
// Check that each column has non-extended cells
ColumnRange colRange = first;
while (colRange != null) {
if (colRange.isSingleCol()) {
owner.getErrorHandler().error(
new SAXParseException("Table column " + colRange
+ " established by element \u201C"
+ colRange.getElement()
+ "\u201D has no cells beginning in it.",
colRange.getLocator()));
} else {
owner.getErrorHandler().error(
new SAXParseException("Table columns in range "
+ colRange + " established by element \u201C"
+ colRange.getElement()
+ "\u201D have no cells beginning in them.",
colRange.getLocator()));
}
colRange = colRange.getNext();
}
}
/**
* Returns the columnCount.
*
* @return the columnCount
*/
int getColumnCount() {
return columnCount;
}
/**
* Sets the columnCount.
*
* @param columnCount
* the columnCount to set
*/
void setColumnCount(int columnCount) {
this.columnCount = columnCount;
}
/**
* Returns the hardWidth.
*
* @return the hardWidth
*/
boolean isHardWidth() {
return hardWidth;
}
/**
* Reports a cell whose positioning has been decided back to the table
* so that column bookkeeping can be done. (Called from
* RowGroup --not TableChecker .)
*
* @param cell a cell whose position has been calculated
*/
void cell(Cell cell) {
int left = cell.getLeft();
int right = cell.getRight();
// first see if we've got a cell past the last col
if (right > realColumnCount) {
// are we past last col entirely?
if (left == realColumnCount) {
// single col?
if (left + 1 != right) {
appendColumnRange(new ColumnRange(cell.elementName(), cell, left + 1, right));
}
realColumnCount = right;
return;
} else {
// not past entirely
appendColumnRange(new ColumnRange(cell.elementName(), cell, realColumnCount, right));
realColumnCount = right;
}
}
while (currentColRange != null) {
int hit = currentColRange.hits(left);
if (hit == 0) {
ColumnRange newRange = currentColRange.removeColumn(left);
if (newRange == null) {
// zap a list item
if (previousColRange != null) {
previousColRange.setNext(currentColRange.getNext());
}
if (first == currentColRange) {
first = currentColRange.getNext();
}
if (last == currentColRange) {
last = previousColRange;
}
currentColRange = currentColRange.getNext();
} else {
if (last == currentColRange) {
last = newRange;
}
currentColRange = newRange;
}
return;
} else if (hit == -1) {
return;
} else if (hit == 1) {
previousColRange = currentColRange;
currentColRange = currentColRange.getNext();
}
}
}
}
|