| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.freemarker.docgen; |
| |
| import java.util.ArrayList; |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.SAXException; |
| |
| import static org.freemarker.docgen.DocBook5Constants.*; |
| |
| /** |
| * Resolves (removes) <tt>colgroup</tt> and <tt>col</tt> elements, because |
| * they are not known by many important browsers. This resolution is possible as |
| * <tt>colgroup</tt> and <tt>col</tt> are just convenience elements to avoid |
| * typing the aligns for each cells of the same column. |
| */ |
| final class TableSimplifier { |
| |
| private final Element table; |
| |
| /** |
| * Alignment defaults (possibly {@code null}) with the column as their |
| * index. |
| */ |
| private final List<Alignment> colGroupAligns |
| = new ArrayList<Alignment>(); |
| private final List<VAlignment> colGroupVAligns |
| = new ArrayList<VAlignment>(); |
| |
| /** |
| * Used for tracking the various cell spans. This is a 2D matrix, where |
| * each entry symbolizes an imaginary table cell that is either visually |
| * overlapped by an actual table cell ({@code true}) or not ({@code false}). |
| */ |
| private boolean[] cellMatrix; |
| private int cellMatrixWidth; |
| private int cellMatrixHeight; |
| private int cellMatrixCurRow; |
| private int cellMatrixCurCol; |
| |
| static void simplify(Element table) throws SAXException, DocgenException { |
| new TableSimplifier(table).processTable(); |
| } |
| |
| private TableSimplifier(Element table) { |
| this.table = table; |
| } |
| |
| private void processTable() throws SAXException, DocgenException { |
| processAndRemoveColGroupsAndCols(); |
| decorateCells(); |
| } |
| |
| /** |
| * Fills {@link #colGroupAligns} and {@link #colGroupVAligns}, while |
| * removes the related nodes. |
| * @throws SAXException if something violates the DocBook rules |
| * @throws DocgenException if something violates the Docgen restrictions |
| * (or the DocBook rules, if there was no prior Relax NG validation). |
| */ |
| private void processAndRemoveColGroupsAndCols() |
| throws SAXException, DocgenException { |
| LinkedList<Element> toRemove = new LinkedList<Element>(); |
| |
| NodeList children = table.getChildNodes(); |
| int childCnt = children.getLength(); |
| fetchChildren: for (int childIdx = 0; childIdx < childCnt; childIdx++) { |
| Node child = children.item(childIdx); |
| if (child instanceof Element) { |
| Element elem = (Element) child; |
| if (!elem.getNamespaceURI().equals(XMLNS_DOCBOOK5)) { |
| continue fetchChildren; |
| } |
| String elemName = elem.getLocalName(); |
| |
| if (elemName.equals(E_COLGROUP)) { |
| processColGroup(elem); |
| toRemove.add(elem); |
| } else if (elemName.equals(E_COL)) { |
| processCol(elem, null, null); |
| toRemove.add(elem); |
| } |
| // Ignore other elements, "tbody" and like. |
| } |
| // Ignore non-elements |
| } |
| |
| for (Element child : toRemove) { |
| table.removeChild(child); |
| } |
| } |
| |
| private void processColGroup(Element colGroup) |
| throws SAXException, DocgenException { |
| boolean usesAtts = false; |
| |
| int span = 1; // Default from the HTML spec. |
| Alignment align = null; |
| VAlignment valign = null; |
| |
| NamedNodeMap atts = colGroup.getAttributes(); |
| int attCnt = atts.getLength(); |
| fetchAtts: for (int attIdx = 0; attIdx < attCnt; attIdx++) { |
| Attr att = (Attr) atts.item(attIdx); |
| String attNS = att.getNamespaceURI(); |
| if (attNS != null && attNS.length() != 0) { |
| continue fetchAtts; |
| } |
| String attName = att.getLocalName(); |
| String attValue = att.getValue().trim(); |
| |
| if (attName.equals(A_SPAN)) { |
| usesAtts = true; |
| span = parseSpanAttribute(attValue, A_SPAN, E_COLGROUP); |
| } else if (attName.equals(A_ALIGN)) { |
| align = parseAlignAttribute(attValue, E_COLGROUP); |
| } else if (attName.equals(A_VALIGN)) { |
| valign = parseVAlignAttribute(attValue, E_COLGROUP); |
| } else { |
| throw new DocgenException("The \"" + attName |
| + "\" attribute of the \"" + E_COLGROUP |
| + "\" element is not supported by Docgen."); |
| } |
| } // fetchAtts |
| |
| if (usesAtts) { |
| for (int i = 0; i < span; i++) { |
| colGroupAligns.add(align); |
| colGroupVAligns.add(valign); |
| } |
| } |
| |
| NodeList children = colGroup.getChildNodes(); |
| int childCnt = children.getLength(); |
| fetchChildren: for (int childIdx = 0; childIdx < childCnt; childIdx++) { |
| Node child = children.item(childIdx); |
| if (child instanceof Element) { |
| Element elem = (Element) child; |
| if (!elem.getNamespaceURI().equals(XMLNS_DOCBOOK5)) { |
| continue fetchChildren; |
| } |
| String elemName = elem.getLocalName(); |
| |
| if (elemName.equals(E_COL)) { |
| if (usesAtts) { |
| throw new SAXException("The \"" + E_COLGROUP |
| + "\" already used attributes, so it can't " |
| + "have \"" + E_COL + "\" elements in it."); |
| } |
| processCol(elem, align, valign); |
| } else { |
| throw new SAXException("The \"" + elemName + "\" element " |
| + "is unexpected inside \"" + E_COLGROUP + "\"."); |
| } |
| } |
| // Ignore non-elements |
| } |
| |
| } |
| |
| private void processCol(Element col, |
| Alignment colGroupDefaultAlign, VAlignment colGroupDefaultVAlign) |
| throws SAXException, DocgenException { |
| int span = 1; // Default from the HTML spec. |
| Alignment align = colGroupDefaultAlign; |
| VAlignment valign = colGroupDefaultVAlign; |
| |
| NamedNodeMap atts = col.getAttributes(); |
| int attCnt = atts.getLength(); |
| fetchAtts: for (int attIdx = 0; attIdx < attCnt; attIdx++) { |
| Attr att = (Attr) atts.item(attIdx); |
| String attNS = att.getNamespaceURI(); |
| if (attNS != null && attNS.length() != 0) { |
| continue fetchAtts; |
| } |
| String attName = att.getLocalName(); |
| String attValue = att.getValue().trim(); |
| |
| if (attName.equals(A_SPAN)) { |
| span = parseSpanAttribute(attValue, A_SPAN, E_COL); |
| } else if (attName.equals(A_ALIGN)) { |
| align = parseAlignAttribute(attValue, E_COL); |
| } else if (attName.equals(A_VALIGN)) { |
| valign = parseVAlignAttribute(attValue, E_COL); |
| } else { |
| throw new DocgenException("The \"" + attName |
| + "\" attribute of the \"" + E_COL |
| + "\" element is not supported by Docgen."); |
| } |
| } // fetchAtts |
| |
| for (int i = 0; i < span; i++) { |
| colGroupAligns.add(align); |
| colGroupVAligns.add(valign); |
| } |
| } |
| |
| private Alignment parseAlignAttribute( |
| String attValue, String elemName) |
| throws SAXException { |
| String lAttValue = attValue.toLowerCase(); |
| if (lAttValue.equals("left")) { |
| return Alignment.LEFT; |
| } else if (lAttValue.equals("center")) { |
| return Alignment.CENTER; |
| } else if (lAttValue.equals("right")) { |
| return Alignment.RIGHT; |
| } else { |
| throw new SAXException("Illegal \"" + A_ALIGN |
| + "\" attribute value for a(n) \"" + elemName |
| + "\" element: " + attValue); |
| } |
| } |
| |
| private VAlignment parseVAlignAttribute( |
| String attValue, String elemName) |
| throws SAXException, DocgenException { |
| String lAttValue = attValue.toLowerCase(); |
| if (lAttValue.equals("top")) { |
| return VAlignment.TOP; |
| } else if (lAttValue.equals("bottom")) { |
| return VAlignment.BOTTOM; |
| } else if (lAttValue.equals("middle")) { |
| return VAlignment.MIDDLE; |
| } else if (lAttValue.equals("baseline")) { |
| throw new DocgenException("The \"baseline\" value for \"" + A_VALIGN |
| + "\" attribute is not implemented by Docgen."); |
| } else { |
| throw new SAXException("Illegal \"" + A_VALIGN |
| + "\" attribute value for a(n) \"" + elemName |
| + "\" element: " + attValue); |
| } |
| } |
| |
| @SuppressWarnings("boxing") |
| private int parseSpanAttribute( |
| String attValue, String attName, String elemName) |
| throws SAXException { |
| int span; |
| try { |
| span = Integer.valueOf(attValue); |
| } catch (NumberFormatException e) { |
| throw new SAXException("Illegal \"" + attName |
| + "\" attribute value for a(n) \"" + elemName |
| + "\" element: " + attValue); |
| } |
| if (span < 1) { |
| throw new SAXException("Illegal \"" + attName |
| + "\" attribute value for a(n) \"" + elemName |
| + "\" element: " + attValue); |
| } |
| return span; |
| } |
| |
| /** |
| * This will add the required {@code align} and {@code valign} attributes. |
| */ |
| private void decorateCells() throws SAXException, DocgenException { |
| initCellMatrix(); |
| |
| NodeList children = table.getChildNodes(); |
| int childCnt = children.getLength(); |
| fetchChildren: for (int childIdx = 0; childIdx < childCnt; childIdx++) { |
| Node child = children.item(childIdx); |
| if (child instanceof Element) { |
| Element elem = (Element) child; |
| if (!elem.getNamespaceURI().equals(XMLNS_DOCBOOK5)) { |
| continue fetchChildren; |
| } |
| String elemName = elem.getLocalName(); |
| |
| if (elemName.equals(E_THEAD) |
| || elemName.equals(E_TBODY) |
| || elemName.equals(E_TFOOT)) { |
| decorateTDiv(elem); |
| } else if (elemName.equals(E_TR)) { |
| decorateRow(elem, null, null); |
| } |
| // Ignore other elements... "caption", etc. |
| } |
| // Ignore non-elements |
| } |
| |
| checkFinishedCellMatrix(); |
| } |
| |
| private void decorateTDiv(Element tDiv) |
| throws SAXException, DocgenException { |
| initCellMatrix(); |
| |
| Alignment tDivAlign = null; // "div" refers to tbody, thead or tfoot |
| VAlignment tDivVAlign = null; |
| |
| NamedNodeMap atts = tDiv.getAttributes(); |
| int attCnt = atts.getLength(); |
| fetchAtts: for (int attIdx = 0; attIdx < attCnt; attIdx++) { |
| Attr att = (Attr) atts.item(attIdx); |
| String attNS = att.getNamespaceURI(); |
| if (attNS != null && attNS.length() != 0) { |
| continue fetchAtts; |
| } |
| String attName = att.getLocalName(); |
| String attValue = att.getValue().trim(); |
| |
| if (attName.equals(A_ALIGN)) { |
| tDivAlign = parseAlignAttribute( |
| attValue, tDiv.getLocalName()); |
| } else if (attName.equals(A_VALIGN)) { |
| tDivVAlign = parseVAlignAttribute( |
| attValue, tDiv.getLocalName()); |
| } else { |
| throw new DocgenException("The \"" + attName |
| + "\" attribute of the \"" + tDiv.getLocalName() |
| + "\" element is not supported by Docgen."); |
| } |
| } // fetchAtts |
| |
| NodeList children = tDiv.getChildNodes(); |
| int childCnt = children.getLength(); |
| fetchChildren: for (int childIdx = 0; childIdx < childCnt; childIdx++) { |
| Node child = children.item(childIdx); |
| if (child instanceof Element) { |
| Element elem = (Element) child; |
| if (!elem.getNamespaceURI().equals(XMLNS_DOCBOOK5)) { |
| continue fetchChildren; |
| } |
| String elemName = elem.getLocalName(); |
| |
| if (elemName.equals(E_TR)) { |
| decorateRow(elem, tDivAlign, tDivVAlign); |
| } else { |
| throw new SAXException("The \"" + elemName + "\" should " |
| + "not occur inside \"" + tDiv.getLocalName() |
| + "\"."); |
| } |
| } |
| // Ignore non-elements |
| } |
| |
| checkFinishedCellMatrix(); |
| } |
| |
| private void decorateRow( |
| Element tr, Alignment tDivAlign, VAlignment tDivVAlign) |
| throws SAXException, DocgenException { |
| addRowToCellMatrix(); |
| |
| Alignment trAlign = null; |
| VAlignment trVAlign = null; |
| |
| NamedNodeMap atts = tr.getAttributes(); |
| int attCnt = atts.getLength(); |
| fetchAtts: for (int attIdx = 0; attIdx < attCnt; attIdx++) { |
| Attr att = (Attr) atts.item(attIdx); |
| String attNS = att.getNamespaceURI(); |
| if (attNS != null && attNS.length() != 0) { |
| continue fetchAtts; |
| } |
| String attName = att.getLocalName(); |
| String attValue = att.getValue().trim(); |
| |
| if (attName.equals(A_ALIGN)) { |
| trAlign = parseAlignAttribute(attValue, E_TR); |
| } else if (attName.equals(A_VALIGN)) { |
| trVAlign = parseVAlignAttribute(attValue, E_TR); |
| } else { |
| throw new DocgenException("The \"" + attName |
| + "\" attribute of the \"" + E_TR |
| + "\" element is not supported by Docgen."); |
| } |
| } // fetchAtts |
| |
| if (trAlign == null) { |
| trAlign = tDivAlign; |
| } |
| if (trVAlign == null) { |
| trVAlign = tDivVAlign; |
| } |
| |
| NodeList children = tr.getChildNodes(); |
| int childCnt = children.getLength(); |
| fetchChildren: for (int childIdx = 0; childIdx < childCnt; childIdx++) { |
| Node child = children.item(childIdx); |
| if (child instanceof Element) { |
| Element elem = (Element) child; |
| if (!elem.getNamespaceURI().equals(XMLNS_DOCBOOK5)) { |
| continue fetchChildren; |
| } |
| String elemName = elem.getLocalName(); |
| |
| if (elemName.equals(E_TD) || elemName.equals(E_TH)) { |
| decorateCell(elem, trAlign, trVAlign); |
| } else { |
| throw new SAXException("The \"" + elemName + "\" should " |
| + "not occur inside \"" + E_TR + "\"."); |
| } |
| } |
| // Ignore non-elements |
| } |
| } |
| |
| private void decorateCell( |
| Element cell, Alignment trAlign, VAlignment trVAlign) |
| throws SAXException, DocgenException { |
| Alignment cellAlign = null; |
| VAlignment cellVAlign = null; |
| int rowSpan = 1; |
| int colSpan = 1; |
| |
| String elemName = cell.getLocalName(); |
| |
| NamedNodeMap atts = cell.getAttributes(); |
| int attCnt = atts.getLength(); |
| fetchAtts: for (int attIdx = 0; attIdx < attCnt; attIdx++) { |
| Attr att = (Attr) atts.item(attIdx); |
| String attNS = att.getNamespaceURI(); |
| if (attNS != null && attNS.length() != 0) { |
| continue fetchAtts; |
| } |
| String attName = att.getLocalName(); |
| String attValue = att.getValue().trim(); |
| |
| if (attName.equals(A_ALIGN)) { |
| cellAlign = parseAlignAttribute(attValue, elemName); |
| } else if (attName.equals(A_VALIGN)) { |
| cellVAlign = parseVAlignAttribute(attValue, elemName); |
| } else if (attName.equals(A_ROWSPAN)) { |
| rowSpan = parseSpanAttribute(attValue, A_ROWSPAN, elemName); |
| } else if (attName.equals(A_COLSPAN)) { |
| colSpan = parseSpanAttribute(attValue, A_COLSPAN, elemName); |
| } else { |
| throw new DocgenException("The \"" + attName |
| + "\" attribute of the \"" + elemName |
| + "\" element is not supported by Docgen."); |
| } |
| } // fetchAtts |
| |
| int visualCol = addCellToCellMatrix(rowSpan, colSpan); |
| |
| if (cellAlign == null) { |
| Alignment colGroupAlign = colGroupAligns.size() > visualCol |
| ? colGroupAligns.get(visualCol) |
| : null; |
| // Column-scope horizontal alignment has precedence over row-scope |
| if (colGroupAlign != null && trAlign != colGroupAlign) { |
| cell.setAttribute(A_ALIGN, colGroupAlign.toString()); |
| } |
| } |
| |
| // Row-scope vertical alignment has precedence over column-scope |
| if (cellVAlign == null && trVAlign == null) { |
| VAlignment colGroupVAlign = colGroupVAligns.size() > visualCol |
| ? colGroupVAligns.get(visualCol) |
| : null; |
| if (colGroupVAlign != null) { |
| cell.setAttribute(A_VALIGN, colGroupVAlign.toString()); |
| } |
| } |
| |
| } |
| |
| private void addRowToCellMatrix() { |
| cellMatrixCurRow++; |
| cellMatrixCurCol = 0; |
| } |
| |
| private int addCellToCellMatrix(int rowSpan, int colSpan) |
| throws DocgenException { |
| // Find the coordinates of the first free cell in the current row: |
| int curIdx = cellMatrixCurRow * cellMatrixWidth + cellMatrixCurCol; |
| if (cellMatrixCurRow < cellMatrixHeight) { |
| int curIdxLim = (cellMatrixCurRow + 1) * cellMatrixWidth; |
| while (curIdx < curIdxLim && cellMatrix[curIdx]) { |
| curIdx++; |
| cellMatrixCurCol++; |
| } |
| } |
| |
| // Draw a rectangle of rowSpan*colSpan there: |
| |
| // - Ensure that the backing array has the required capacity: |
| int newCellMatrixHeight = cellMatrixHeight; |
| int newCellMatrixWidth = cellMatrixWidth; |
| if (cellMatrixHeight <= cellMatrixCurRow + rowSpan - 1) { |
| newCellMatrixHeight = (cellMatrixCurRow + rowSpan) * 2; |
| } |
| if (cellMatrixWidth <= cellMatrixCurCol + colSpan - 1) { |
| newCellMatrixWidth = (cellMatrixCurCol + colSpan) * 2; |
| } |
| if (newCellMatrixHeight != cellMatrixHeight |
| || newCellMatrixWidth != cellMatrixWidth) { |
| // Resize the backing array... |
| boolean[] newCellMatrix = new boolean[ |
| newCellMatrixHeight * newCellMatrixWidth]; |
| for (int row = 0; row < cellMatrixHeight; row++) { |
| System.arraycopy( |
| cellMatrix, row * cellMatrixWidth, |
| newCellMatrix, row * newCellMatrixWidth, |
| cellMatrixWidth); |
| } |
| cellMatrix = newCellMatrix; |
| cellMatrixWidth = newCellMatrixWidth; |
| cellMatrixHeight = newCellMatrixHeight; |
| |
| // Re-calculation needed as width maybe changed |
| curIdx = cellMatrixCurRow * cellMatrixWidth + cellMatrixCurCol; |
| } |
| |
| // - "Pain" the rectangle: |
| for (int relRow = 0; relRow < rowSpan; relRow++) { |
| int brushIdx = curIdx + relRow * cellMatrixWidth; |
| for (int relCol = 0; relCol < colSpan; relCol++) { |
| if (cellMatrix[brushIdx]) { |
| throw new DocgenException( |
| XMLUtil.theSomethingElement(table, true) |
| + " has overlapping cells; check if \"" |
| + A_ROWSPAN + "\"-s and/or \"" + A_COLSPAN |
| + "\"-s are correct."); |
| } |
| cellMatrix[brushIdx++] = true; |
| } |
| } |
| |
| /* |
| // For debugging: Prints the current cell layout to the stdout |
| System.out.println( |
| "(" + cellMatrixWidth + "x" + cellMatrixHeight + ")"); |
| for (int row = 0; row < cellMatrixHeight; row++) { |
| for (int col = 0; col < cellMatrixWidth; col++) { |
| System.out.print( |
| cellMatrix[row * cellMatrixWidth + col] ? "X" : "."); |
| } |
| System.out.println(); |
| } |
| */ |
| |
| return cellMatrixCurCol; |
| } |
| |
| private void initCellMatrix() throws DocgenException { |
| cellMatrix = null; |
| cellMatrixWidth = 0; |
| cellMatrixHeight = 0; |
| cellMatrixCurRow = -1; |
| cellMatrixCurCol = 0; |
| } |
| |
| private void checkFinishedCellMatrix() throws DocgenException { |
| int firstRowUtilizedWidth = -1; |
| for (int row = 0; row < cellMatrixHeight; row++) { |
| int currentRowUtilizedWidth = 0; |
| // We will check that there is no false -> true change, and we |
| // expect the first cell will be true, or that all cells will be |
| // false. So: |
| boolean lastCell = true; |
| for (int col = 0; col < cellMatrixWidth; col++) { |
| boolean cell = cellMatrix[row * cellMatrixWidth + col]; |
| if (cell) { |
| // Is this false -> true change? |
| if (!lastCell) { |
| throw newNonRectangularTableException(); |
| } |
| currentRowUtilizedWidth++; |
| } |
| lastCell = cell; |
| } |
| if (firstRowUtilizedWidth == -1) { |
| firstRowUtilizedWidth = currentRowUtilizedWidth; |
| } else if (currentRowUtilizedWidth != firstRowUtilizedWidth |
| && currentRowUtilizedWidth != 0) { |
| throw newNonRectangularTableException(); |
| } |
| } |
| } |
| |
| private DocgenException newNonRectangularTableException() { |
| return new DocgenException("Wrong cell layout in " |
| + XMLUtil.theSomethingElement(table) |
| + ". Due to the bad number of cells in" |
| + " some rows (or due to wrong \"" + A_COLSPAN |
| + "\" and/or \"" + A_ROWSPAN + "\" values) the contour of the " |
| + "table, or the contour of some of its row-groups (\"" |
| + E_THEAD + "\" or \"" + E_TBODY + "\" or \"" + E_TFOOT |
| + "\") is not rectangular. That is, some cells protrude " |
| + "out at the edge. (Visual editors may hide this error by " |
| + "chopping protruding cells or by adding blind cells.)"); |
| } |
| |
| private enum Alignment { |
| LEFT { |
| @Override |
| public String toString() { |
| return "left"; |
| } |
| }, |
| |
| CENTER { |
| @Override |
| public String toString() { |
| return "center"; |
| } |
| }, |
| |
| RIGHT { |
| @Override |
| public String toString() { |
| return "right"; |
| } |
| } |
| } |
| |
| private enum VAlignment { |
| TOP { |
| @Override |
| public String toString() { |
| return "top"; |
| } |
| }, |
| |
| MIDDLE { |
| @Override |
| public String toString() { |
| return "middle"; |
| } |
| }, |
| |
| BOTTOM { |
| @Override |
| public String toString() { |
| return "bottom"; |
| } |
| } |
| } |
| |
| } |