layout cells according to CALS colspec elements

Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestTableColumnLayout.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestTableColumnLayout.java
new file mode 100644
index 0000000..e800a77
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/boxes/TestTableColumnLayout.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Florian Thienel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.boxes;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestTableColumnLayout {
+
+	private TableColumnLayout layout;
+
+	@Before
+	public void initLayout() {
+		layout = new TableColumnLayout();
+	}
+
+	@Test
+	public void shouldCountGivenColumns() throws Exception {
+		layout.addColumn(0, null, null);
+		layout.addColumn(0, null, null);
+		layout.addColumn(0, null, null);
+		assertThat(layout.getLastIndex(), is(equalTo(3)));
+	}
+
+	@Test
+	public void givenColumnIndex_whenColumnIndexIsZero_shouldReturnActualColumnIndex() throws Exception {
+		assertThat(layout.addColumn(0, null, null), is(equalTo(1)));
+	}
+
+	@Test
+	public void givenColumnIndex_whenColumnIndexGreaterThanLastColumnIndex_shouldReturnGivenColumnIndex() throws Exception {
+		assertThat(layout.addColumn(2, null, null), is(equalTo(2)));
+	}
+
+	@Test
+	public void givenColumnIndex_whenColumnIndexLessThanLastColumnIndex_shouldReturnGivenColumnIndex() throws Exception {
+		layout.addColumn(2, null, null);
+		assertThat(layout.addColumn(1, null, null), is(equalTo(3)));
+	}
+
+	@Test
+	public void givenColumnName_whenColumnIsDefined_shouldReturnColumnIndex() throws Exception {
+		layout.addColumn(4, "myColumn", null);
+		assertThat(layout.getIndex("myColumn"), is(equalTo(4)));
+	}
+
+	@Test
+	public void givenColumnName_whenColumnIsNotDefined_shoudlReturnZero() throws Exception {
+		assertThat(layout.getIndex("someUndefinedColumn"), is(equalTo(0)));
+	}
+
+	@Test
+	public void givenSpan_shouldProvideStartIndexByName() throws Exception {
+		layout.addSpan(2, 4, "span2To4");
+		assertThat(layout.getStartIndex("span2To4"), is(equalTo(2)));
+	}
+
+	@Test
+	public void givenSpan_shouldProvideEndIndexByName() throws Exception {
+		layout.addSpan(3, 8, "span3To8");
+		assertThat(layout.getEndIndex("span3To8"), is(equalTo(8)));
+	}
+
+	@Test
+	public void givenChildLayout_whenColumnIsOnlyDefinedInParent_shouldProvideIndexFromParent() throws Exception {
+		layout.addColumn(1, "columnInParent", null);
+		assertThat(new TableColumnLayout(layout).getIndex("columnInParent"), is(equalTo(1)));
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
index 6d7da5c..36d18c2 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/BoxFactory.java
@@ -142,24 +142,27 @@
 		return listItem;
 	}
 
-	public static Table table(final IStructuralBox... children) {
+	public static Table table(final TableColumnLayout columnLayout, final IStructuralBox... children) {
 		final Table table = new Table();
+		table.setColumnLayout(columnLayout);
 		for (final IStructuralBox child : children) {
 			table.appendChild(child);
 		}
 		return table;
 	}
 
-	public static TableRowGroup tableRowGroup(final IStructuralBox... children) {
+	public static TableRowGroup tableRowGroup(final TableColumnLayout columnLayout, final IStructuralBox... children) {
 		final TableRowGroup tableRowGroup = new TableRowGroup();
+		tableRowGroup.setColumnLayout(columnLayout);
 		for (final IStructuralBox child : children) {
 			tableRowGroup.appendChild(child);
 		}
 		return tableRowGroup;
 	}
 
-	public static TableRow tableRow(final IStructuralBox... children) {
+	public static TableRow tableRow(final TableColumnLayout columnLayout, final IStructuralBox... children) {
 		final TableRow tableRow = new TableRow();
+		tableRow.setColumnLayout(columnLayout);
 		for (final IStructuralBox child : children) {
 			tableRow.appendChild(child);
 		}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Table.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Table.java
index d09c7b7..54c5832 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Table.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/Table.java
@@ -29,6 +29,8 @@
 	private int height;
 	private final ArrayList<IStructuralBox> children = new ArrayList<IStructuralBox>();
 
+	private TableColumnLayout columnLayout = new TableColumnLayout();
+
 	@Override
 	public void setParent(final IBox parent) {
 		this.parent = parent;
@@ -138,6 +140,14 @@
 		return children;
 	}
 
+	public TableColumnLayout getColumnLayout() {
+		return columnLayout;
+	}
+
+	public void setColumnLayout(final TableColumnLayout columnLayout) {
+		this.columnLayout = columnLayout;
+	}
+
 	public void layout(final Graphics graphics) {
 		height = 0;
 		for (int i = 0; i < children.size(); i += 1) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableColumnLayout.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableColumnLayout.java
new file mode 100644
index 0000000..f3c2a8a
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableColumnLayout.java
@@ -0,0 +1,147 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Florian Thienel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.boxes;
+
+import java.util.HashMap;
+
+/**
+ * @author Florian Thienel
+ */
+public class TableColumnLayout {
+
+	private final TableColumnLayout parentLayout;
+	private int width;
+
+	private int lastIndex;
+	private final HashMap<String, Span> indexByName = new HashMap<String, Span>();
+
+	public TableColumnLayout() {
+		this(null);
+	}
+
+	public TableColumnLayout(final TableColumnLayout parentLayout) {
+		this.parentLayout = parentLayout;
+	}
+
+	public TableColumnLayout getParentLayout() {
+		return parentLayout;
+	}
+
+	public int addColumn(final int index, final String name, final String width) {
+		final int columnIndex;
+		if (index <= lastIndex) {
+			columnIndex = lastIndex += 1;
+		} else {
+			columnIndex = lastIndex = index;
+		}
+
+		if (name != null) {
+			indexByName.put(name, new Span(columnIndex));
+		}
+
+		return columnIndex;
+	}
+
+	public void addSpan(final int startIndex, final int endIndex, final String name) {
+		indexByName.put(name, new Span(startIndex, endIndex));
+	}
+
+	public int getWidth() {
+		return width;
+	}
+
+	public void setWidth(final int width) {
+		this.width = width;
+	}
+
+	public int getLastIndex() {
+		if (parentLayout != null) {
+			return Math.max(lastIndex, parentLayout.lastIndex);
+		}
+		return lastIndex;
+	}
+
+	public int getIndex(final String name) {
+		return getStartIndex(name);
+	}
+
+	public int getStartIndex(final String name) {
+		return getSpan(name).start;
+	}
+
+	public int getEndIndex(final String name) {
+		return getSpan(name).end;
+	}
+
+	private Span getSpan(final String name) {
+		if (indexByName.containsKey(name)) {
+			return indexByName.get(name);
+		}
+		if (parentLayout != null) {
+			return parentLayout.getSpan(name);
+		}
+		return Span.NULL;
+	}
+
+	public int getWidth(final int startIndex, final int endIndex) {
+		return 0;
+	}
+
+	public int getWidth(final int index) {
+		return 0;
+	}
+
+	private static class Span {
+		public static final Span NULL = new Span(0, 0);
+
+		public final int start;
+		public final int end;
+
+		public Span(final int index) {
+			this(index, index);
+		}
+
+		public Span(final int start, final int end) {
+			this.start = start;
+			this.end = end;
+		}
+
+		@Override
+		public int hashCode() {
+			final int prime = 31;
+			int result = 1;
+			result = prime * result + end;
+			result = prime * result + start;
+			return result;
+		}
+
+		@Override
+		public boolean equals(final Object obj) {
+			if (this == obj) {
+				return true;
+			}
+			if (obj == null) {
+				return false;
+			}
+			if (getClass() != obj.getClass()) {
+				return false;
+			}
+			final Span other = (Span) obj;
+			if (end != other.end) {
+				return false;
+			}
+			if (start != other.start) {
+				return false;
+			}
+			return true;
+		}
+	}
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableRow.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableRow.java
index 2660eef..7a418f6 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableRow.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableRow.java
@@ -29,6 +29,8 @@
 	private int height;
 	private final ArrayList<IStructuralBox> children = new ArrayList<IStructuralBox>();
 
+	private TableColumnLayout columnLayout = new TableColumnLayout();
+
 	@Override
 	public void setParent(final IBox parent) {
 		this.parent = parent;
@@ -138,6 +140,18 @@
 		return children;
 	}
 
+	public TableColumnLayout getColumnLayout() {
+		return columnLayout;
+	}
+
+	public void setColumnLayout(final TableColumnLayout columnLayout) {
+		if (columnLayout == null) {
+			this.columnLayout = new TableColumnLayout();
+		} else {
+			this.columnLayout = columnLayout;
+		}
+	}
+
 	public void layout(final Graphics graphics) {
 		height = 0;
 		int columnIndex = 1;
@@ -167,10 +181,10 @@
 	}
 
 	private int getColumnWidth(final int index) {
-		if (index < 1 || index > children.size()) {
+		if (index < 1 || index > columnLayout.getLastIndex()) {
 			return 0;
 		}
-		return Math.round(width / children.size());
+		return Math.round(width / columnLayout.getLastIndex());
 	}
 
 	private static int getStartColumn(final IStructuralBox box, final int defaultColumn) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableRowGroup.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableRowGroup.java
index 71f7f40..11071d7 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableRowGroup.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/boxes/TableRowGroup.java
@@ -29,6 +29,8 @@
 	private int height;
 	private final ArrayList<IStructuralBox> children = new ArrayList<IStructuralBox>();
 
+	private TableColumnLayout columnLayout = new TableColumnLayout();
+
 	@Override
 	public void setParent(final IBox parent) {
 		this.parent = parent;
@@ -138,6 +140,14 @@
 		return children;
 	}
 
+	public TableColumnLayout getColumnLayout() {
+		return columnLayout;
+	}
+
+	public void setColumnLayout(final TableColumnLayout columnLayout) {
+		this.columnLayout = columnLayout;
+	}
+
 	public void layout(final Graphics graphics) {
 		height = 0;
 		for (int i = 0; i < children.size(); i += 1) {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBasedBoxModelBuilder.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBasedBoxModelBuilder.java
index 8d05098..5aa83c5 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBasedBoxModelBuilder.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/visualization/CssBasedBoxModelBuilder.java
@@ -50,6 +50,11 @@
 import org.eclipse.vex.core.internal.boxes.LineWrappingRule;
 import org.eclipse.vex.core.internal.boxes.Paragraph;
 import org.eclipse.vex.core.internal.boxes.RootBox;
+import org.eclipse.vex.core.internal.boxes.Table;
+import org.eclipse.vex.core.internal.boxes.TableCell;
+import org.eclipse.vex.core.internal.boxes.TableColumnLayout;
+import org.eclipse.vex.core.internal.boxes.TableRow;
+import org.eclipse.vex.core.internal.boxes.TableRowGroup;
 import org.eclipse.vex.core.internal.boxes.TextContent;
 import org.eclipse.vex.core.internal.css.AttributeDependendContent;
 import org.eclipse.vex.core.internal.css.BulletStyle;
@@ -66,6 +71,7 @@
 import org.eclipse.vex.core.internal.dom.CollectingNodeTraversal;
 import org.eclipse.vex.core.provisional.dom.BaseNodeVisitorWithResult;
 import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.IAttribute;
 import org.eclipse.vex.core.provisional.dom.IComment;
 import org.eclipse.vex.core.provisional.dom.IContent;
 import org.eclipse.vex.core.provisional.dom.IDocument;
@@ -126,6 +132,8 @@
 
 	private VisualizeResult visualize(final INode node) {
 		return node.accept(new CollectingNodeTraversal<VisualizeResult>() {
+			private TableColumnLayout currentColumnLayout = null;
+
 			@Override
 			public VisualizeResult visit(final IDocument document) {
 				final Styles styles = styleSheet.getStyles(document);
@@ -143,22 +151,40 @@
 			@Override
 			public VisualizeResult visit(final IElement element) {
 				final Styles styles = styleSheet.getStyles(element);
-				final Collection<VisualizeResult> childrenResults = traverseChildren(element);
 				if (isTable(styles)) {
-					return new VisualizeResult(element, styles, childrenResults, visualizeAsTable(element, styles, childrenResults));
+					pushColumnLayout();
+					final Collection<VisualizeResult> childrenResults = traverseChildren(element);
+					final Table table = visualizeAsTable(element, styles, childrenResults, currentColumnLayout);
+					popColumnLayout();
+
+					return new VisualizeResult(element, styles, childrenResults, table);
 				} else if (isTableRowGroup(styles)) {
-					return new VisualizeResult(element, styles, childrenResults, visualizeAsTableRowGroup(element, styles, childrenResults));
+					pushColumnLayout();
+					final Collection<VisualizeResult> childrenResults = traverseChildren(element);
+					final TableRowGroup rowGroup = visualizeAsTableRowGroup(element, styles, childrenResults, currentColumnLayout);
+					popColumnLayout();
+
+					return new VisualizeResult(element, styles, childrenResults, rowGroup);
+				} else if (isTableColSpec(styles, element)) {
+					final Collection<VisualizeResult> childrenResults = traverseChildren(element);
+					return new VisualizeResult(element, styles, childrenResults, visualizeAsTableColSpec(element, styles, childrenResults, currentColumnLayout));
 				} else if (isTableRow(styles)) {
-					return new VisualizeResult(element, styles, childrenResults, visualizeAsTableRow(element, styles, childrenResults));
+					final Collection<VisualizeResult> childrenResults = traverseChildren(element);
+					return new VisualizeResult(element, styles, childrenResults, visualizeAsTableRow(element, styles, childrenResults, currentColumnLayout));
 				} else if (isTableCell(styles)) {
-					return new VisualizeResult(element, styles, childrenResults, visualizeAsTableCell(element, styles, childrenResults));
+					final Collection<VisualizeResult> childrenResults = traverseChildren(element);
+					return new VisualizeResult(element, styles, childrenResults, visualizeAsTableCell(element, styles, childrenResults, currentColumnLayout));
 				} else if (isListRoot(styles)) {
+					final Collection<VisualizeResult> childrenResults = traverseChildren(element);
 					return new VisualizeResult(element, styles, childrenResults, visualizeAsList(element, styles, childrenResults));
 				} else if (isListItem(styles)) {
+					final Collection<VisualizeResult> childrenResults = traverseChildren(element);
 					return new VisualizeResult(element, styles, childrenResults, visualizeAsListItem(element, styles, childrenResults));
 				} else if (isDisplayedAsBlock(styles)) {
+					final Collection<VisualizeResult> childrenResults = traverseChildren(element);
 					return new VisualizeResult(element, styles, childrenResults, visualizeAsBlock(element, styles, childrenResults));
 				} else {
+					final Collection<VisualizeResult> childrenResults = traverseChildren(element);
 					return new VisualizeResult(element, styles, childrenResults, visualizeInline(element, styles, childrenResults));
 				}
 			}
@@ -191,6 +217,18 @@
 				final List<VisualizeResult> childrenResults = Collections.<VisualizeResult> emptyList();
 				return new VisualizeResult(text, styles, childrenResults, visualizeInline(text, styles, childrenResults));
 			}
+
+			private void pushColumnLayout() {
+				currentColumnLayout = new TableColumnLayout(currentColumnLayout);
+			}
+
+			private TableColumnLayout popColumnLayout() {
+				final TableColumnLayout columnLayout = currentColumnLayout;
+				if (currentColumnLayout != null) {
+					currentColumnLayout = currentColumnLayout.getParentLayout();
+				}
+				return columnLayout;
+			}
 		});
 	}
 
@@ -221,6 +259,13 @@
 		return CSS.TABLE_CELL.equals(styles.getDisplay());
 	}
 
+	private static boolean isTableColSpec(final Styles styles, final IElement element) {
+		return CSS.TABLE_COLUMN.equals(styles.getDisplay())
+				|| "colspec".equals(element.getLocalName())
+				|| "spanspec".equals(element.getLocalName())
+				|| "col".equals(element.getLocalName());
+	}
+
 	private static boolean isDisplayedAsBlock(final Styles styles) {
 		// currently we can only render blocks or inline, hence everything that is not inline must be a block
 		return !isDisplayedInline(styles);
@@ -241,21 +286,75 @@
 	/*
 	 * Render as Table
 	 */
-	private IStructuralBox visualizeAsTable(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults) {
-		return table(visualizeAsBlock(element, styles, childrenResults));
+	private Table visualizeAsTable(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults, final TableColumnLayout columnLayout) {
+		return table(columnLayout, visualizeAsBlock(element, styles, childrenResults));
 	}
 
-	private IStructuralBox visualizeAsTableRowGroup(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults) {
-		return tableRowGroup(visualizeAsBlock(element, styles, childrenResults));
+	private TableRowGroup visualizeAsTableRowGroup(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults, final TableColumnLayout columnLayout) {
+		return tableRowGroup(columnLayout, visualizeAsBlock(element, styles, childrenResults));
 	}
 
-	private IStructuralBox visualizeAsTableRow(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults) {
-		final IStructuralBox content = visualizeChildrenAsStructure(element, styles, childrenResults, tableRow());
-		return wrapUpStructuralElementContent(element, styles, childrenResults, content);
+	private IStructuralBox visualizeAsTableColSpec(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults, final TableColumnLayout columnLayout) {
+		if ("colspec".equals(element.getLocalName())) {
+			final int index = toInt(element.getAttribute("colnum"));
+			final String name = toString(element.getAttribute("colname"));
+			columnLayout.addColumn(index, name, null);
+		} else if ("spanspec".equals(element.getLocalName())) {
+			// TODO CALS span
+		} else if ("col".equals(element.getLocalName())) {
+			// TODO HTML table
+		}
+
+		return visualizeAsBlock(element, styles, childrenResults);
 	}
 
-	private IStructuralBox visualizeAsTableCell(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults) {
-		return tableCell(visualizeAsBlock(element, styles, childrenResults));
+	private IStructuralBox visualizeAsTableRow(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults, final TableColumnLayout columnLayout) {
+		final TableRow row = visualizeChildrenAsStructure(element, styles, childrenResults, tableRow(columnLayout));
+		return wrapUpStructuralElementContent(element, styles, childrenResults, row);
+	}
+
+	private TableCell visualizeAsTableCell(final IElement element, final Styles styles, final Collection<VisualizeResult> childrenResults, final TableColumnLayout columnLayout) {
+		final TableCell cell = tableCell(visualizeAsBlock(element, styles, childrenResults));
+
+		if ("entry".equals(element.getLocalName())) {
+			final IAttribute colName = element.getAttribute("colname");
+			final IAttribute spanName = element.getAttribute("spanname");
+			final IAttribute nameStart = element.getAttribute("namest");
+			final IAttribute nameEnd = element.getAttribute("nameend");
+			if (colName != null) {
+				final int columnIndex = columnLayout.getIndex(colName.getValue());
+				cell.setStartColumn(columnIndex);
+				cell.setEndColumn(columnIndex);
+			} else if (spanName != null) {
+				cell.setStartColumn(columnLayout.getStartIndex(spanName.getValue()));
+				cell.setEndColumn(columnLayout.getEndIndex(spanName.getValue()));
+			} else if (nameStart != null && nameEnd != null) {
+				cell.setStartColumn(columnLayout.getStartIndex(nameStart.getValue()));
+				cell.setEndColumn(columnLayout.getEndIndex(nameEnd.getValue()));
+			}
+		} else if ("th".equals(element.getLocalName()) || "td".equals(element.getLocalName())) {
+			// TODO HTML table
+		}
+
+		return cell;
+	}
+
+	private static int toInt(final IAttribute attribute) {
+		if (attribute == null) {
+			return 0;
+		}
+		try {
+			return Integer.parseInt(attribute.getValue());
+		} catch (final NumberFormatException e) {
+			return 0;
+		}
+	}
+
+	private static String toString(final IAttribute attribute) {
+		if (attribute == null) {
+			return null;
+		}
+		return attribute.getValue();
 	}
 
 	/*