1   package org.daisy.pipeline.braille.css.saxon.impl;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.Comparator;
6   import java.util.concurrent.atomic.AtomicReference;
7   import java.util.Deque;
8   import java.util.HashMap;
9   import java.util.HashSet;
10  import java.util.Iterator;
11  import java.util.LinkedHashMap;
12  import java.util.LinkedList;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.NoSuchElementException;
16  import java.util.Set;
17  
18  import javax.xml.namespace.QName;
19  import static javax.xml.stream.XMLStreamConstants.CHARACTERS;
20  import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
21  import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
22  import javax.xml.stream.XMLStreamException;
23  import javax.xml.stream.XMLStreamReader;
24  import javax.xml.stream.XMLStreamWriter;
25  import javax.xml.XMLConstants;
26  
27  import com.google.common.base.Predicate;
28  import com.google.common.base.Splitter;
29  import com.google.common.collect.ImmutableList;
30  
31  import cz.vutbr.web.css.Rule;
32  import cz.vutbr.web.css.RuleBlock;
33  import cz.vutbr.web.css.Selector;
34  import cz.vutbr.web.css.Selector.PseudoClass;
35  import cz.vutbr.web.css.Selector.SelectorPart;
36  import cz.vutbr.web.csskit.AbstractRuleBlock;
37  
38  import org.daisy.braille.css.InlineStyle;
39  import org.daisy.braille.css.InlineStyle.RuleMainBlock;
40  import org.daisy.braille.css.InlineStyle.RuleRelativeBlock;
41  import org.daisy.braille.css.SelectorImpl;
42  import org.daisy.braille.css.SelectorImpl.PseudoClassImpl;
43  import org.daisy.braille.css.SelectorImpl.PseudoElementImpl;
44  import org.daisy.braille.css.SimpleInlineStyle;
45  
46  import org.daisy.common.stax.XMLStreamWriterHelper.ToStringWriter;
47  import org.daisy.common.stax.XMLStreamWriterHelper.WriterEvent;
48  import org.daisy.common.transform.InputValue;
49  import org.daisy.common.transform.Mult;
50  import org.daisy.common.transform.SingleInSingleOutXMLTransformer;
51  import org.daisy.common.transform.TransformerException;
52  import org.daisy.common.transform.XMLInputValue;
53  import org.daisy.common.transform.XMLOutputValue;
54  import org.daisy.common.stax.BaseURIAwareXMLStreamReader;
55  import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
56  import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
57  import static org.daisy.common.stax.XMLStreamWriterHelper.writeElement;
58  import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
59  import org.daisy.pipeline.braille.css.impl.BrailleCssSerializer;
60  
61  public class TableAsList extends SingleInSingleOutXMLTransformer {
62  
63  	private static final String XMLNS_CSS = "http://www.daisy.org/ns/pipeline/braille-css";
64  
65  	private static final QName _STYLE = new QName("style");
66  	private static final QName _ID = new QName("id");
67  	private static final QName XML_LANG = new QName(XMLConstants.XML_NS_URI, "lang", XMLConstants.XML_NS_PREFIX);
68  
69  	private static final String XMLNS_HTML = "http://www.w3.org/1999/xhtml";
70  	private static final String XMLNS_DTB = "http://www.daisy.org/z3986/2005/dtbook/";
71  
72  	private static final String TABLE = "table";
73  	private static final String THEAD = "thead";
74  	private static final String TFOOT = "tfoot";
75  	private static final String TBODY = "tbody";
76  	private static final String TR = "tr";
77  	private static final String TD = "td";
78  	private static final String TH = "th";
79  	private static final String COLGROUP = "colgroup";
80  	private static final String COL = "col";
81  
82  	private static final QName _HEADERS = new QName("headers");
83  	private static final QName _SCOPE = new QName("scope");
84  	private static final QName _AXIS = new QName("axis");
85  	private static final QName _ROWSPAN = new QName("rowspan");
86  	private static final QName _COLSPAN = new QName("colspan");
87  
88  	private static final QName CSS_TABLE_HEADER_POLICY = new QName(XMLNS_CSS, "table-header-policy", "css");
89  	private static final QName CSS_TABLE_BY = new QName(XMLNS_CSS, "table-by", "css");
90  	private static final QName CSS_LIST_ITEM = new QName(XMLNS_CSS, "list-item", "css");
91  	private static final QName CSS_LIST_HEADER = new QName(XMLNS_CSS, "list-header", "css");
92  	private static final QName CSS_ID = new QName(XMLNS_CSS, "id", "css");
93  	private static final QName CSS_FLOW = new QName(XMLNS_CSS, "flow", "css");
94  
95  	private static final Splitter HEADERS_SPLITTER = Splitter.on(' ').trimResults().omitEmptyStrings();
96  	private static final Splitter AXIS_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings();
97  
98  	final List<String> axes;
99  
100 	TableAsList(String axes) {
101 		this.axes = new ArrayList<String>(AXIS_SPLITTER.splitToList(axes));
102 		if (this.axes.remove("auto"))
103 			if (!this.axes.isEmpty())
104 				throw new RuntimeException();
105 	}
106 
107 	private List<WriterEvent> writeActionsBefore;
108 	private List<WriterEvent> writeActionsAfter;
109 	private List<TableCell> cells;
110 	private Set<CellCoordinates> coveredCoordinates;
111 
112 	@Override
113 	public Runnable transform(XMLInputValue<?> source, XMLOutputValue<?> result, InputValue<?> params)
114 			throws IllegalArgumentException {
115 
116 		if (source == null || result == null)
117 			throw new IllegalArgumentException();
118 		return () -> transform(source.ensureSingleItem().mult(2), result.asXMLStreamWriter());
119 	}
120 
121 	private void transform(Mult<? extends XMLInputValue<?>> source, BaseURIAwareXMLStreamWriter writer)
122 			throws TransformerException {
123 		XMLStreamReader reader = source.get().asXMLStreamReader();
124 		try {
125 			writeActionsBefore = new ArrayList<WriterEvent>();
126 			writeActionsAfter = new ArrayList<WriterEvent>();
127 			cells = new ArrayList<TableCell>();
128 			coveredCoordinates = new HashSet<CellCoordinates>();
129 			List<WriterEvent> writeActions = writeActionsBefore;
130 			int depth = 0;
131 			TableCell withinCell = null;
132 			TableCell.RowType rowType = TableCell.RowType.TBODY;
133 			int rowGroup = 1;
134 			int row = 1;
135 			int col = 1;
136 			String namespace = null;
137 			Deque<SimpleInlineStyle> inheritedStyle = new LinkedList<>();
138 			Deque<String> inheritedLang = new LinkedList<>();
139 			while (reader.hasNext()) {
140 				switch (reader.next()) {
141 					case START_ELEMENT: {
142 						QName name = reader.getName();
143 						depth++;
144 						boolean isCell = false;
145 						if (depth == 1) {
146 							if (!isHTMLorDTBookElement(TABLE, name))
147 								throw new RuntimeException("Expected table element (html|dtb).");
148 							if (XMLNS_HTML.equals(name.getNamespaceURI()))
149 								namespace = XMLNS_HTML;
150 							else if (XMLNS_DTB.equals(name.getNamespaceURI()))
151 								namespace = XMLNS_DTB; }
152 						else if (isHTMLorDTBookElement(THEAD, name) ||
153 						         isHTMLorDTBookElement(TFOOT, name) ||
154 						         isHTMLorDTBookElement(TBODY, name) ||
155 						         isHTMLorDTBookElement(TR, name)) {
156 							rowType = isHTMLorDTBookElement(THEAD, name)
157 								? TableCell.RowType.THEAD
158 								: isHTMLorDTBookElement(TFOOT, name)
159 									? TableCell.RowType.TFOOT
160 									: isHTMLorDTBookElement(TBODY, name)
161 										? TableCell.RowType.TBODY
162 										: rowType;
163 										;
164 							String style = null; {
165 								for (int i = 0; i < reader.getAttributeCount(); i++) {
166 									if (_STYLE.equals(reader.getAttributeName(i))) {
167 										style = reader.getAttributeValue(i);
168 										break; }}}
169 							// FIXME: if (non-default) block-level style present, warn that style is ignored
170 							// FIXME: We assume that not both a tbody/thead/tfoot and a child tr have a text-transform property,
171 							// because two text-transform properties can not always simply be replaced with a single one.
172 							inheritedStyle.push(new SimpleInlineStyle(style, inheritedStyle.isEmpty() ? null : inheritedStyle.peek()));
173 							String lang = null; {
174 								for (int i = 0; i < reader.getAttributeCount(); i++) {
175 									if (XML_LANG.equals(reader.getAttributeName(i))) {
176 										lang = reader.getAttributeValue(i);
177 										break; }}}
178 							inheritedLang.push(lang != null ? lang : inheritedLang.isEmpty() ? null : inheritedLang.peek());
179 							break; }
180 						else if (isHTMLorDTBookElement(COLGROUP, name) || isHTMLorDTBookElement(COL, name))
181 							throw new RuntimeException("Elements colgroup and col not supported yet.");
182 						if (isHTMLorDTBookElement(TD, name) || isHTMLorDTBookElement(TH, name)) {
183 							isCell = true;
184 							withinCell = new TableCell();
185 							withinCell.row = row;
186 							withinCell.col = col;
187 							withinCell.rowGroup = rowGroup;
188 							withinCell.rowType = rowType;
189 							withinCell.ns = namespace;
190 							setCovered(row, col);
191 							cells.add(withinCell);
192 							if (isHTMLorDTBookElement(TH, name))
193 								withinCell.type = TableCell.CellType.TH;
194 							String style = null; {
195 								for (int i = 0; i < reader.getAttributeCount(); i++) {
196 									if (_STYLE.equals(reader.getAttributeName(i))) {
197 										style = reader.getAttributeValue(i);
198 										break; }}}
199 							withinCell.style = new SimpleInlineStyle(style, inheritedStyle.isEmpty() ? null : inheritedStyle.peek());
200 							withinCell.lang = inheritedLang.isEmpty() ? null : inheritedLang.peek();
201 							writeActions = withinCell.content; }
202 						else {
203 							if (withinCell != null) {
204 								String flow = "normal";
205 								for (int i = 0; i < reader.getAttributeCount(); i++)
206 									if (CSS_FLOW.equals(reader.getAttributeName(i))) {
207 										flow = reader.getAttributeValue(i);
208 										break; }
209 								if (!"normal".equals(flow)) {
210 									writeActions.add(writeElementOnce(reader));
211 									break; }}
212 							writeActions.add(w -> writeStartElement(w, name)); }
213 						for (int i = 0; i < reader.getNamespaceCount(); i++) {
214 							String prf = reader.getNamespacePrefix(i);
215 							String ns = reader.getNamespaceURI(i);
216 							writeActions.add(w -> w.writeNamespace(prf, ns)); }
217 						for (int i = 0; i < reader.getAttributeCount(); i++) {
218 							QName attrName = reader.getAttributeName(i);
219 							String attrValue = reader.getAttributeValue(i);
220 							if (CSS_ID.equals(attrName)) {
221 								WriteOnlyOnce writeOnlyOnce = new WriteOnlyOnce();
222 								writeOnlyOnce.add(w -> writeAttribute(w, attrName, attrValue));
223 								writeActions.add(writeOnlyOnce); }
224 							else if (CSS_TABLE_HEADER_POLICY.equals(attrName)) {
225 								if (isCell)
226 									if ("once".equals(attrValue))
227 										withinCell.headerPolicy = TableCell.HeaderPolicy.ONCE;
228 									else if ("always".equals(attrValue))
229 										withinCell.headerPolicy = TableCell.HeaderPolicy.ALWAYS;
230 									else if ("front".equals(attrValue))
231 										withinCell.headerPolicy = TableCell.HeaderPolicy.FRONT;
232 									else
233 										throw new RuntimeException(
234 											"Expected value once|always for table-header-policy property but got " + attrValue); }
235 							else if (isCell && _HEADERS.equals(attrName))
236 								withinCell.headers = HEADERS_SPLITTER.splitToList(attrValue);
237 							else if (isCell && _SCOPE.equals(attrName)) {
238 								if ("row".equals(attrValue))
239 									withinCell.scope = TableCell.Scope.ROW;
240 								else if ("col".equals(attrValue))
241 									withinCell.scope = TableCell.Scope.COL;
242 								else if ("colgroup".equals(attrValue) || "rowgroup".equals(attrValue))
243 									throw new RuntimeException(
244 											"Value " + attrValue + " for scope attribute not supported yet.");
245 								else
246 									throw new RuntimeException(
247 											"Expected value col|row|colgroup|rowgroup for scope attribute but got " + attrValue); }
248 							else if (isCell && _AXIS.equals(attrName))
249 								withinCell.axis = AXIS_SPLITTER.splitToList(attrValue);
250 							else if (isCell && _ROWSPAN.equals(attrName)) {
251 								try {
252 									withinCell.rowspan = Integer.parseInt(attrValue);
253 									if (withinCell.rowspan < 0)
254 										throw new InvalidTableException("Invalid rowspan: " + attrValue);
255 									else if (withinCell.rowspan == 0)
256 										throw new RuntimeException("rowspan 0 not supported yet."); }
257 								catch (NumberFormatException e) {
258 									throw new InvalidTableException("Invalid rowspan: " + attrValue); }
259 								for (int m = 1; m < withinCell.rowspan; m++)
260 									for (int n = 0; n < withinCell.colspan; n++)
261 										setCovered(row + m, col + n); }
262 							else if (isCell && _COLSPAN.equals(attrName)) {
263 								try {
264 									withinCell.colspan = Integer.parseInt(attrValue);
265 									if (withinCell.colspan < 0)
266 										throw new InvalidTableException("Invalid colspan: " + attrValue);
267 									else if (withinCell.colspan == 0)
268 										throw new RuntimeException("colspan 0 not supported yet."); }
269 								catch (NumberFormatException e) {
270 									throw new InvalidTableException("Invalid colspan: " + attrValue); }
271 								for (int m = 0; m < withinCell.rowspan; m++)
272 									for (int n = 1; n < withinCell.colspan; n++)
273 										setCovered(row + m, col + n); }
274 
275 							// TODO: check that there are no duplicate IDs?
276 							else if (isCell && _ID.equals(attrName))
277 								withinCell.id = attrValue;
278 							else if (depth == 1 && _STYLE.equals(attrName)) {
279 								String newStyle; {
280 									InlineStyle style = new InlineStyle(attrValue);
281 									List<RuleBlock<? extends Rule<?>>> builder = new ArrayList<>();
282 									for (RuleBlock<?> block : style) {
283 										if (block instanceof RuleMainBlock)
284 											builder.add(style.getMainStyle());
285 										else if (block instanceof RuleRelativeBlock) {
286 											RuleRelativeBlock ruleblock = (RuleRelativeBlock)block;
287 											List<Selector> selector = ruleblock.getSelector();
288 											if (selector.size() > 0) { // should always be true
289 												if (selector.get(0).size() > 0) { // should always be true
290 													// the first part is normally a PseudoElementImpl, i.e. a pseudo element or a custom
291 													// pseudo class like :-obfl-alternate-scenario
292 													// other parts are possible too, but will in practice already have been processed
293 													// by css:inline
294 													if (selector.get(0).get(0) instanceof PseudoElementImpl) {
295 														// selector.get(0).size() should normally always be 1
296 														// pseudo classes and pseudo elements are stacked onto the first pseudo element,
297 														// and for other parts (classes or attributes) it does not makes sense to come after
298 														// a pseudo element
299 														PseudoElementImpl pseudo = (PseudoElementImpl)selector.get(0).get(0);
300 														if ("list-header".equals(pseudo.getName())) {
301 															if (pseudo.getPseudoClasses().isEmpty())
302 																addListHeaderStyle(
303 																	new ListItemStyle(rest(ruleblock))); }
304 														else if ("table-by".equals(pseudo.getName())) {
305 															String axis = pseudo.getArguments()[0];
306 															if (pseudo.getPseudoClasses().isEmpty()) {
307 																if (pseudo.hasStackedPseudoElement()) {
308 																	pseudo = pseudo.getStackedPseudoElement();
309 																	ruleblock = (RuleRelativeBlock)rest(ruleblock);
310 																	if ("list-item".equals(pseudo.getName()))
311 																		getTableByStyle(axis).addListItemStyle(
312 																			pseudo.getPseudoClasses(),
313 																			new ListItemStyle(rest(ruleblock)));
314 																	else if ("list-header".equals(pseudo.getName())) {
315 																		if (pseudo.getPseudoClasses().isEmpty())
316 																			getTableByStyle(axis).addListHeaderStyle(
317 																				new ListItemStyle(rest(ruleblock))); }
318 																	else
319 																		getTableByStyle(axis).addRuleBlock(ruleblock); }
320 																else
321 																	getTableByStyle(axis).addRuleBlock(rest(ruleblock)); }}
322 														else
323 															// could be some other pseudo element or a custom pseudo class like
324 															// :-obfl-alternate-scenario (yes, it is implemented as a PseudoElementImpl even
325 															// though it is a class)
326 															// in the case a ::list-item, ::list-header or ::table-by follows later in the
327 															// selector, they will be handled later in a subsequent call to css:render-table-by,
328 															// after the pseudo elements/classes have been handled elsewhere
329 															builder.add(ruleblock); }
330 													else
331 														builder.add(ruleblock); }
332 												else
333 													builder.add(ruleblock); }}
334 										else
335 											throw new RuntimeException("Unexpected style " + block); }
336 									newStyle = BrailleCssSerializer.serializeRuleBlockList(builder); }
337 								if (!newStyle.isEmpty())
338 									writeActions.add(w -> writeAttribute(w, attrName, newStyle)); }
339 							else if (isCell && _STYLE.equals(attrName))
340 								; // handled above
341 							else
342 								writeActions.add(w -> writeAttribute(w, attrName, attrValue)); }
343 						break; }
344 					case CHARACTERS:
345 						String chars = reader.getText();
346 						writeActions.add(w -> w.writeCharacters(chars));
347 						break;
348 					case END_ELEMENT: {
349 						QName name = reader.getName();
350 						depth--;
351 						if (isHTMLorDTBookElement(THEAD, name)
352 						    || isHTMLorDTBookElement(TFOOT, name)
353 						    || isHTMLorDTBookElement(TBODY, name)) {
354 							inheritedStyle.pop();
355 							inheritedLang.pop();
356 							rowGroup++;
357 							break; }
358 						else if (isHTMLorDTBookElement(TR, name)) {
359 							inheritedStyle.pop();
360 							inheritedLang.pop();
361 							row++;
362 							col = 1;
363 							while (isCovered(row, col)) col++;
364 							break; }
365 						if (isHTMLorDTBookElement(TD, name) || isHTMLorDTBookElement(TH, name)) {
366 							withinCell = null;
367 							writeActions = writeActionsAfter;
368 							while (isCovered(row, col)) col++; }
369 						else
370 							writeActions.add(w -> w.writeEndElement());
371 						break; }}}
372 
373 			// handle colspan and rowspan on data cells by simply splitting them into several identical ones for now
374 			List<TableCell> moreCells = new ArrayList<TableCell>();
375 			for (TableCell c : cells)
376 				if (!isHeader(c)) {
377 					if (c.rowspan > 1) {
378 						int span = c.rowspan;
379 						c.rowspan = 1;
380 						for (int i = 1; i < span; i++) {
381 							TableCell dup = c.clone();
382 							dup.row = c.row + i;
383 							moreCells.add(dup); }}
384 					if (c.colspan > 1) {
385 						int span = c.colspan;
386 						c.colspan = 1;
387 						for (int i = 1; i < span; i++) {
388 							TableCell dup = c.clone();
389 							dup.col = c.col + i;
390 							moreCells.add(dup); }}}
391 			cells.addAll(moreCells);
392 
393 			// rearrange row groups and order cells by row
394 			Collections.sort(cells, compose(sortByRowType, sortByRow, sortByColumn));
395 			rowGroup = 0;
396 			row = 0;
397 			int newRowGroup = rowGroup = 0;
398 			int newRow = row = 0;
399 			for (TableCell c : cells) {
400 				if (c.rowGroup != rowGroup) {
401 					rowGroup = c.rowGroup;
402 					newRowGroup++; }
403 				if (c.row != row) {
404 					row = c.row;
405 					newRow++; }
406 				c.rowGroup = newRowGroup;
407 				c.row = newRow; }
408 
409 			write(writer);
410 		} catch (XMLStreamException e) {
411 			throw new RuntimeException(e); // should not happen
412 		} catch (InvalidTableException e) {
413 			String message = "Table structure broken: " + e.getMessage();
414 			throw new TransformerException(
415 				new RuntimeException(
416 					message,
417 					// trick to show table XML only in detailed (DEBUG) log
418 					new RuntimeException(message + ":\n" + elementToString(source.get()))));
419 		} catch (RuntimeException e) {
420 			String message = "Error happened while processing table";
421 			throw new TransformerException(
422 				new RuntimeException(
423 					message,
424 					// trick to show table XML only in detailed (DEBUG) log
425 					new RuntimeException(message + ":\n" + elementToString(source.get()), e)));
426 		}
427 	}
428 
429 	private boolean isHTMLorDTBookElement(String element, QName name) {
430 		return ((XMLNS_HTML.equals(name.getNamespaceURI())
431 		        || XMLNS_DTB.equals(name.getNamespaceURI()))
432 		        && name.getLocalPart().equalsIgnoreCase(element));
433 	}
434 
435 	private void setCovered(int row, int col) {
436 		CellCoordinates coords = new CellCoordinates(row, col);
437 		if (coveredCoordinates.contains(coords))
438 			throw new InvalidTableException("cells overlap");
439 		coveredCoordinates.add(coords);
440 	}
441 
442 	private boolean isCovered(int row, int col) {
443 		return coveredCoordinates.contains(new CellCoordinates(row, col));
444 	}
445 
446 	private void write(XMLStreamWriter writer) throws XMLStreamException {
447 		for (WriterEvent action : writeActionsBefore)
448 			action.writeTo(writer);
449 		List<TableCell> dataCells = new ArrayList<TableCell>();
450 		for (TableCell c : cells)
451 			if (!isHeader(c))
452 				dataCells.add(c);
453 		new TableCellGroup(dataCells, axes.iterator()).write(writer);
454 		for (WriterEvent action : writeActionsAfter)
455 			action.writeTo(writer);
456 	}
457 
458 	private static final List<TableCell> emptyList = new ArrayList<TableCell>();
459 
460 	private abstract class TableCellCollection {
461 		/**
462 		 * Headers associated with this cell or group of cells that must be rendered before it, in
463 		 * the order of this list.
464 		 */
465 		public abstract List<TableCell> newlyRenderedHeaders();
466 		/**
467 		 * Headers associated with this cell or group of cells that must be rendered before the
468 		 * first sibling group of the containing group, together with the promoted headers of all
469 		 * the siblings of this cell/group.
470 		 *
471 		 * In the example below ROW_1 is the group header of (a1, b1, c1) and ROW_2 is the group
472 		 * header of (a2, b2, c2). COLUMN_A is the promoted header of a1 and a2, COLUMN_B is the
473 		 * promoted header of b1 and b2 and COLUMN_C is the promoted header of c1 and c2. This
474 		 * results in the promoted header group (COLUMN_A, COLUMN_B, COLUMN_C).
475 		 *
476 		 *           COLUMN_A, COLUMN_B, COLUMN_C
477 		 *    ROW_1: a1,       b1,       c1
478 		 *    ROW_2: a2,       b2,       c2
479 		 */
480 		public abstract List<TableCell> newlyPromotedHeaders();
481 		/**
482 		 * Get XML representation.
483 		 */
484 		public abstract void write(XMLStreamWriter writer) throws XMLStreamException;
485 	}
486 
487 	private class SingleTableCell extends TableCellCollection {
488 
489 		private final TableCell cell;
490 		private final TableCellGroup parent;
491 		private final SingleTableCell precedingSibling;
492 		private final boolean columnAppliedBeforeRow;
493 
494 		private SingleTableCell(TableCell cell, TableCellGroup parent, SingleTableCell precedingSibling, boolean columnAppliedBeforeRow) {
495 			this.cell = cell;
496 			this.parent = parent;
497 			this.precedingSibling = precedingSibling;
498 			this.columnAppliedBeforeRow = columnAppliedBeforeRow;
499 		}
500 
501 		/* Headers associated with this cell but not with the containing group. */
502 		private List<TableCell> newlyAppliedHeaders;
503 		public List<TableCell> newlyAppliedHeaders() {
504 			if (newlyAppliedHeaders == null) {
505 				newlyAppliedHeaders = new ArrayList<TableCell>();
506 				for (TableCell h : findHeaders(cell, !columnAppliedBeforeRow))
507 					if (!parent.appliedHeaders().contains(h))
508 						newlyAppliedHeaders.add(h); }
509 			return newlyAppliedHeaders;
510 		}
511 
512 		/* The first part of this list are the headers to be promoted, the other part are the
513 		 * headers to be rendered. The last header of the first part is the last header with policy
514 		 * FRONT. */
515 		private List<TableCell> newlyRenderedOrPromotedHeaders;
516 		private List<TableCell> newlyRenderedOrPromotedHeaders() {
517 			if (newlyRenderedOrPromotedHeaders == null) {
518 				newlyRenderedOrPromotedHeaders = new ArrayList<TableCell>();
519 				Iterator<TableCell> lastAppliedHeaders = (
520 					precedingSibling != null ? precedingSibling.newlyAppliedHeaders() : emptyList
521 				).iterator();
522 				boolean canOmit = true;
523 				// add headers that were deferred by the containing group
524 				for (TableCell h : parent.deferredHeaders()) {
525 					newlyRenderedOrPromotedHeaders.add(h);
526 					// no headers can be omitted if headers were deferred by the containing group
527 					canOmit = false; }
528 				for (TableCell h : newlyAppliedHeaders()) {
529 					// omit headers that have already been rendered and do not need to be repeated
530 					if (canOmit
531 					    && h.headerPolicy != TableCell.HeaderPolicy.ALWAYS
532 					    && lastAppliedHeaders.hasNext() && lastAppliedHeaders.next().equals(h))
533 						continue;
534 					// no more headers can be omitted as soon as one header could not be omitted
535 					newlyRenderedOrPromotedHeaders.add(h);
536 					canOmit = false; }}
537 			return newlyRenderedOrPromotedHeaders;
538 		}
539 
540 		private List<TableCell> newlyPromotedHeaders;
541 		public List<TableCell> newlyPromotedHeaders() {
542 			if (newlyPromotedHeaders == null) {
543 				newlyPromotedHeaders = new ArrayList<TableCell>();
544 				int i = newlyRenderedOrPromotedHeaders().size() - 1;
545 				while (i >= 0 && newlyRenderedOrPromotedHeaders().get(i).headerPolicy != TableCell.HeaderPolicy.FRONT)
546 					i--;
547 				while (i >= 0)
548 					newlyPromotedHeaders.add(0, newlyRenderedOrPromotedHeaders().get(i--)); }
549 			return newlyPromotedHeaders;
550 		}
551 
552 		private List<TableCell> newlyRenderedHeaders;
553 		public  List<TableCell> newlyRenderedHeaders() {
554 			if (newlyRenderedHeaders == null) {
555 				newlyRenderedHeaders = new ArrayList<TableCell>();
556 				int i = newlyRenderedOrPromotedHeaders().size() - 1;
557 				while (i >= 0 && newlyRenderedOrPromotedHeaders().get(i).headerPolicy != TableCell.HeaderPolicy.FRONT)
558 					newlyRenderedHeaders.add(0, newlyRenderedOrPromotedHeaders().get(i--)); }
559 			return newlyRenderedHeaders;
560 		}
561 
562 		public void write(XMLStreamWriter writer) throws XMLStreamException {
563 			cell.write(writer);
564 		}
565 
566 		@Override
567 		public String toString() {
568 			ToStringWriter xml = new ToStringWriter();
569 			try {
570 				write(xml); }
571 			catch (Exception e) {
572 				throw new RuntimeException("coding error", e); }
573 			StringBuilder s = new StringBuilder();
574 			s.append("SingleTableCell[header: ").append(newlyRenderedHeaders());
575 			s.append(", cell: ").append(cell);
576 			s.append(", xml: ").append(xml).append("]");
577 			return s.toString();
578 		}
579 	}
580 
581 	private class TableCellGroup extends TableCellCollection {
582 
583 		private final TableCellGroup parent;
584 		private final TableCell groupingHeader;
585 		private final String groupingAxis;
586 		private final TableCellGroup precedingSibling;
587 		private final boolean hasSubGroups;
588 		private final String subGroupingAxis;
589 		private final List<TableCellCollection> children;
590 		private final boolean rowApplied;
591 		private final boolean columnAppliedBeforeRow;
592 
593 		private TableCellGroup(List<TableCell> cells, Iterator<String> nextAxes) {
594 			this(cells, nextAxes, null, null, null, null, false, false);
595 		}
596 
597 		private TableCellGroup(List<TableCell> cells, Iterator<String> nextAxes,
598 		                       TableCellGroup parent, TableCell groupingHeader, String groupingAxis,
599 		                       TableCellGroup precedingSibling,
600 		                       boolean rowApplied, boolean columnAppliedBeforeRow) {
601 			this.parent = parent;
602 			this.groupingHeader = groupingHeader;
603 			this.groupingAxis = groupingAxis;
604 			this.precedingSibling = precedingSibling;
605 			this.rowApplied = rowApplied;
606 			this.columnAppliedBeforeRow = columnAppliedBeforeRow;
607 			children = groupCellsBy(cells, nextAxes);
608 			hasSubGroups = !children.isEmpty() && children.get(0) instanceof TableCellGroup; // all children of same type
609 			subGroupingAxis = hasSubGroups ? ((TableCellGroup)children.get(0)).groupingAxis : null; // all children has same groupingAxis
610 		}
611 
612 		private List<TableCellCollection> groupCellsBy(List<TableCell> cells, Iterator<String> axes) {
613 			String firstAxis = axes.hasNext() ? axes.next() : null;
614 			List<String> nextAxes = ImmutableList.copyOf(axes);
615 
616 			// Use the last child of the preceding sibling group as the preceding sibling for the
617 			// first child of this group if no new headers are associated with this group and the
618 			// previous one.
619 			TableCellCollection transferChild
620 				= (precedingSibling != null
621 				   && precedingSibling.newlyAppliedHeaders().isEmpty()
622 				   && newlyAppliedHeaders().isEmpty())
623 				? precedingSibling.children.get(precedingSibling.children.size() - 1)
624 				: null;
625 			if (firstAxis != null) {
626 
627 				// If any of the cells have a header within the given axis, group the cells
628 				// according to the scopes of the headers in the axis. The cells that do not fall
629 				// within the scope of any of the headers in the axis form their own group.
630 				Map<TableCell,List<TableCell>> categories = new LinkedHashMap<TableCell,List<TableCell>>();
631 				List<TableCell> uncategorized = null;
632 				for (TableCell c : cells) {
633 					boolean categorized = false;
634 					for (TableCell h : findHeaders(c, !columnAppliedBeforeRow))
635 						if (h.axis != null && h.axis.contains(firstAxis)) {
636 							List<TableCell> category = categories.get(h);
637 							if (category == null) {
638 								category = new ArrayList<TableCell>();
639 								categories.put(h, category); }
640 							category.add(c);
641 							categorized = true; }
642 					if (!categorized) {
643 						if (uncategorized == null)
644 							uncategorized = new ArrayList<TableCell>();
645 						uncategorized.add(c); }}
646 				if (!categories.isEmpty()) {
647 					List<TableCellCollection> children = new ArrayList<TableCellCollection>();
648 					TableCellGroup child = null;
649 					if (transferChild instanceof TableCellGroup)
650 						child = (TableCellGroup)transferChild;
651 					for (TableCell h : categories.keySet()) {
652 						child = new TableCellGroup(categories.get(h), nextAxes.iterator(), this, h, firstAxis, child,
653 						                           rowApplied, columnAppliedBeforeRow);
654 						children.add(child); }
655 					if (uncategorized != null) {
656 						child = new TableCellGroup(uncategorized, nextAxes.iterator(), this, null, firstAxis, child,
657 						                           rowApplied, columnAppliedBeforeRow);
658 						children.add(child); }
659 					return children; }
660 
661 				// If the axis is "row", group cells according to the row their are in. This is
662 				// regardless of whether they have a header.
663 				else if ("row".equals(firstAxis)) {
664 					List<TableCellCollection> children = new ArrayList<TableCellCollection>();
665 					Map<Integer,List<TableCell>> rows = new LinkedHashMap<Integer,List<TableCell>>();
666 					for (TableCell c : cells) {
667 						List<TableCell> row = rows.get(c.row);
668 						if (row == null) {
669 							row = new ArrayList<TableCell>();
670 							rows.put(c.row, row); }
671 						row.add(c); }
672 					TableCellGroup child = null;
673 					if (transferChild instanceof TableCellGroup)
674 						child = (TableCellGroup)transferChild;
675 					for (List<TableCell> row : rows.values()) {
676 						child = new TableCellGroup(row, nextAxes.iterator(), this, null, firstAxis, child, true, columnAppliedBeforeRow);
677 						children.add(child); }
678 					return children; }
679 
680 				// If the axis is "column", group cells according to the column their are in. This
681 				// is regardless of whether they have a header.
682 				else if ("column".equals(firstAxis) || "col".equals(firstAxis)) {
683 					List<TableCellCollection> children = new ArrayList<TableCellCollection>();
684 					Map<Integer,List<TableCell>> columns = new LinkedHashMap<Integer,List<TableCell>>();
685 					for (TableCell c : cells) {
686 						List<TableCell> column = columns.get(c.col);
687 						if (column == null) {
688 							column = new ArrayList<TableCell>();
689 							columns.put(c.col, column); }
690 						column.add(c); }
691 					TableCellGroup child = null;
692 					if (transferChild instanceof TableCellGroup)
693 						child = (TableCellGroup)transferChild;
694 					for (List<TableCell> column : columns.values()) {
695 						child = new TableCellGroup(column, nextAxes.iterator(), this, null, firstAxis, child, rowApplied, !rowApplied);
696 						children.add(child); }
697 					return children; }
698 
699 				// If the axis is "row-group", group cells according to the row group (thead, tbody,
700 				// tfoot) their are in. This is regardless of whether they have a header.
701 				else if ("row-group".equals(firstAxis)) {
702 					List<TableCellCollection> children = new ArrayList<TableCellCollection>();
703 					Map<Integer,List<TableCell>> rowGroups = new LinkedHashMap<Integer,List<TableCell>>();
704 					for (TableCell c : cells) {
705 						List<TableCell> rowGroup = rowGroups.get(c.rowGroup);
706 						if (rowGroup == null) {
707 							rowGroup = new ArrayList<TableCell>();
708 							rowGroups.put(c.rowGroup, rowGroup); }
709 						rowGroup.add(c); }
710 					TableCellGroup child = null;
711 					if (transferChild instanceof TableCellGroup)
712 						child = (TableCellGroup)transferChild;
713 					for (List<TableCell> rowGroup : rowGroups.values()) {
714 						child = new TableCellGroup(rowGroup, nextAxes.iterator(), this, null, firstAxis, child,
715 						                           rowApplied, columnAppliedBeforeRow);
716 						children.add(child); }
717 					return children; }
718 				else
719 					return groupCellsBy(cells, nextAxes.iterator()); }
720 			else {
721 				List<TableCellCollection> children = new ArrayList<TableCellCollection>();
722 				SingleTableCell child = null;
723 				if (transferChild instanceof SingleTableCell)
724 					child = (SingleTableCell)transferChild;
725 				for (TableCell c : cells) {
726 					child = new SingleTableCell(c, this, child, columnAppliedBeforeRow);
727 					children.add(child); }
728 				return children; }
729 		}
730 
731 		/* Headers associated with this group but not with the containing group. */
732 		private List<TableCell> newlyAppliedHeaders;
733 		public List<TableCell> newlyAppliedHeaders() {
734 			if (newlyAppliedHeaders == null) {
735 				newlyAppliedHeaders = new ArrayList<TableCell>();
736 				if (groupingHeader != null)
737 					for (TableCell h : findHeaders(groupingHeader, !columnAppliedBeforeRow))
738 						if (!previouslyAppliedHeaders().contains(h))
739 							newlyAppliedHeaders.add(h); }
740 			return newlyAppliedHeaders;
741 		}
742 
743 		/* Headers associated with the containing group. */
744 		private List<TableCell> previouslyAppliedHeaders() {
745 			if (parent != null)
746 				return parent.appliedHeaders();
747 			else
748 				return emptyList;
749 		}
750 
751 		/* All headers associated with this group (as a whole). */
752 		private List<TableCell> appliedHeaders;
753 		public List<TableCell> appliedHeaders() {
754 			if (appliedHeaders == null) {
755 				appliedHeaders = new ArrayList<TableCell>();
756 				appliedHeaders.addAll(previouslyAppliedHeaders());
757 				appliedHeaders.addAll(newlyAppliedHeaders()); }
758 			return appliedHeaders;
759 		}
760 
761 		/* Headers associated with this group but not with the containing group and not rendered yet. */
762 		private List<TableCell> newlyDeferredHeaders;
763 		private List<TableCell> newlyDeferredHeaders() {
764 			if (newlyDeferredHeaders == null) {
765 				newlyDeferredHeaders = new ArrayList<TableCell>();
766 				int i = newlyAppliedHeaders().size() - 1;
767 				while (i >= 0 && newlyAppliedHeaders().get(i).headerPolicy == TableCell.HeaderPolicy.ALWAYS)
768 					newlyDeferredHeaders.add(0, newlyAppliedHeaders().get(i--)); }
769 			return newlyDeferredHeaders;
770 		}
771 
772 		/* Headers associated with this group but not rendered yet. */
773 		private List<TableCell> deferredHeaders;
774 		private List<TableCell> deferredHeaders() {
775 			if (deferredHeaders == null) {
776 				deferredHeaders = new ArrayList<TableCell>();
777 				if (newlyRenderedOrPromotedHeaders().isEmpty())
778 					deferredHeaders.addAll(previouslyDeferredHeaders());
779 				deferredHeaders.addAll(newlyDeferredHeaders()); }
780 			return deferredHeaders;
781 		}
782 
783 		/* Headers associated with the containing group but not rendered yet. */
784 		private List<TableCell> previouslyDeferredHeaders() {
785 			if (parent != null)
786 				return parent.deferredHeaders();
787 			else
788 				return emptyList;
789 		}
790 
791 		/* The first part of this list are the headers to be promoted, the other part are the
792 		 * headers to be rendered. The last header of the first part is the last header with policy
793 		 * FRONT. */
794 		private List<TableCell> newlyRenderedOrPromotedHeaders;
795 		private List<TableCell> newlyRenderedOrPromotedHeaders() {
796 			if (newlyRenderedOrPromotedHeaders == null) {
797 				newlyRenderedOrPromotedHeaders = new ArrayList<TableCell>();
798 				int i = newlyAppliedHeaders().size() - 1;
799 				// defer the last headers if they have policy ALWAYS
800 				while (i >= 0 && newlyAppliedHeaders().get(i).headerPolicy == TableCell.HeaderPolicy.ALWAYS)
801 					i--;
802 				// if not all headers are deferred ...
803 				if (i >= 0) {
804 					Iterator<TableCell> lastAppliedHeaders = (
805 						precedingSibling != null ? precedingSibling.newlyAppliedHeaders() : emptyList
806 					).iterator();
807 					boolean canOmit = true;
808 					// add headers that were deferred by the containing group
809 					for (TableCell h : previouslyDeferredHeaders()) {
810 						newlyRenderedOrPromotedHeaders.add(h);
811 						// no headers can be omitted if headers were deferred by the containing group
812 						canOmit = false; }
813 					for (int j = 0; j <= i; j++) {
814 						TableCell h = newlyAppliedHeaders().get(j);
815 						// omit headers that have already been rendered and do not need to be repeated
816 						if (canOmit
817 						    && h.headerPolicy != TableCell.HeaderPolicy.ALWAYS
818 						    && lastAppliedHeaders.hasNext() && lastAppliedHeaders.next().equals(h))
819 							continue;
820 						// no more headers can be omitted as soon as one header could not be omitted
821 						newlyRenderedOrPromotedHeaders.add(h);
822 						canOmit = false; }}}
823 			return newlyRenderedOrPromotedHeaders;
824 		}
825 
826 		private List<TableCell> newlyPromotedHeaders;
827 		public List<TableCell> newlyPromotedHeaders() {
828 			if (newlyPromotedHeaders == null) {
829 				newlyPromotedHeaders = new ArrayList<TableCell>();
830 				int i = newlyRenderedOrPromotedHeaders().size() - 1;
831 				while (i >= 0 && newlyRenderedOrPromotedHeaders().get(i).headerPolicy != TableCell.HeaderPolicy.FRONT)
832 					i--;
833 				while (i >= 0)
834 					newlyPromotedHeaders.add(0, newlyRenderedOrPromotedHeaders().get(i--)); }
835 			return newlyPromotedHeaders;
836 		}
837 
838 		private List<TableCell> newlyRenderedHeaders;
839 		public List<TableCell> newlyRenderedHeaders() {
840 			if (newlyRenderedHeaders == null) {
841 				newlyRenderedHeaders = new ArrayList<TableCell>();
842 				int i = newlyRenderedOrPromotedHeaders().size() - 1;
843 				while (i >= 0 && newlyRenderedOrPromotedHeaders().get(i).headerPolicy != TableCell.HeaderPolicy.FRONT)
844 					newlyRenderedHeaders.add(0, newlyRenderedOrPromotedHeaders().get(i--)); }
845 			return newlyRenderedHeaders;
846 		}
847 
848 		public void write(XMLStreamWriter writer) {
849 			try {
850 				if (hasSubGroups) {
851 					writeStartElement(writer, CSS_TABLE_BY);
852 					writeAttribute(writer, _AXIS, subGroupingAxis);
853 					writeStyleAttribute(writer, getTableByStyle(subGroupingAxis)); }
854 				List<List<TableCell>> promotedHeaders = null;
855 				int i = 0;
856 				for (TableCellCollection c : children) {
857 					if (c instanceof TableCellGroup) {
858 						TableCellGroup g = (TableCellGroup)c;
859 						int j = 0;
860 						for (TableCellCollection cc : g.children) {
861 							if (!cc.newlyPromotedHeaders().isEmpty()) {
862 								if (promotedHeaders == null) {
863 									if (i == 0 && j == 0) {
864 										writeStartElement(writer, CSS_LIST_HEADER);
865 										writeStyleAttribute(writer, getTableByStyle(g.groupingAxis).getListHeaderStyle());
866 										if (g.hasSubGroups) {
867 											writeStartElement(writer, CSS_TABLE_BY);
868 											writeAttribute(writer, _AXIS, g.subGroupingAxis);
869 											writeStyleAttribute(writer, getTableByStyle(g.subGroupingAxis)); }
870 										promotedHeaders = new ArrayList<List<TableCell>>(); }
871 									else
872 										throw new RuntimeException("Some headers of children promoted but not all children have a promoted header."); }
873 								if (i == 0) {
874 									if (g.hasSubGroups) {
875 										writeStartElement(writer, CSS_LIST_ITEM);
876 										Predicate<PseudoClass> matcher = matchesPosition(j + 1, g.children.size());
877 										writeStyleAttribute(writer, getTableByStyle(g.subGroupingAxis).getListItemStyle(matcher)); }
878 									for (TableCell h : cc.newlyPromotedHeaders())
879 										h.write(writer);
880 									if (g.hasSubGroups)
881 										writer.writeEndElement(); // css:list-item
882 									promotedHeaders.add(cc.newlyPromotedHeaders()); }
883 								else if (!promotedHeaders.get(j).equals(cc.newlyPromotedHeaders()))
884 									throw new RuntimeException("Headers of children promoted but not the same as promoted headers of sibling groups."); }
885 							else if (promotedHeaders != null)
886 								throw new RuntimeException("Some headers of children promoted but not all children have a promoted header.");
887 							j++; }
888 						if (promotedHeaders != null && promotedHeaders.size() != j) {
889 							throw new RuntimeException("Headers of children promoted but not the same as promoted headers of sibling groups."); }}
890 					else if (promotedHeaders != null)
891 						throw new RuntimeException("Coding error");
892 					i++; }
893 				if (promotedHeaders != null) {
894 					if (((TableCellGroup)children.get(0)).hasSubGroups)
895 						writer.writeEndElement(); // css:table-by
896 					writer.writeEndElement(); } // css:list-header
897 				i = 0;
898 				for (TableCellCollection c : children) {
899 					if (hasSubGroups) {
900 						writeStartElement(writer, CSS_LIST_ITEM);
901 						Predicate<PseudoClass> matcher = matchesPosition(i + 1, children.size());
902 						writeStyleAttribute(writer, getTableByStyle(subGroupingAxis).getListItemStyle(matcher)); }
903 					for (TableCell h : c.newlyRenderedHeaders())
904 						h.write(writer);
905 					c.write(writer);
906 					if (hasSubGroups)
907 						writer.writeEndElement(); // css:list-item
908 					i++; }
909 				if (hasSubGroups)
910 					writer.writeEndElement(); // css:table-by
911 			} catch (XMLStreamException e) {
912 				throw new RuntimeException(e); }
913 		}
914 
915 		@Override
916 		public String toString() {
917 			ToStringWriter xml = new ToStringWriter();
918 			write(xml);
919 			StringBuilder s = new StringBuilder();
920 			s.append("TableCellGroup[header: ").append(newlyRenderedHeaders());
921 			s.append(", children: ").append(children);
922 			s.append(", xml: ").append(xml).append("]");
923 			return s.toString();
924 		}
925 	}
926 
927 	private static void writeStyleAttribute(XMLStreamWriter writer, PseudoElementStyle style) throws XMLStreamException {
928 		if (!style.isEmpty())
929 			writeAttribute(writer, _STYLE, style.toString());
930 	}
931 
932 	final private Map<String,TableByStyle> tableByStyles = new HashMap<String,TableByStyle>();
933 	private ListItemStyle listHeaderStyle = new ListItemStyle();
934 
935 	public void addListHeaderStyle(ListItemStyle style) {
936 		listHeaderStyle = listHeaderStyle.mergeWith(style);
937 	}
938 
939 	public TableByStyle getTableByStyle(String axis) {
940 		TableByStyle style = tableByStyles.get(axis);
941 		if (style == null) {
942 			style = new TableByStyle();
943 			tableByStyles.put(axis, style); }
944 		return style;
945 	}
946 
947 	public ListItemStyle getListHeaderStyle() {
948 		return listHeaderStyle;
949 	}
950 
951 	private static class PseudoElementStyle {
952 
953 		final protected Map<List<Selector>,RuleBlock<Rule<?>>> ruleBlocks = new HashMap<>();
954 
955 		public void addRuleBlock(RuleBlock<Rule<?>> ruleblock) {
956 			if (!ruleblock.isEmpty()) {
957 				List<Selector> selector = null;
958 				if (ruleblock instanceof RuleRelativeBlock)
959 					selector = ((RuleRelativeBlock)ruleblock).getSelector();
960 				RuleBlock<Rule<?>> r;
961 				if (ruleBlocks.containsKey(selector))
962 					r = ruleBlocks.get(selector);
963 				else {
964 					// we make a copy so that we can modify the rule later without affecting the
965 					// original which might be used in other places (it should be considered
966 					// immutable)
967 					r = selector != null
968 						? new RuleRelativeBlock(selector)
969 						: new AbstractRuleBlock<Rule<?>>();
970 					r.unlock();
971 				}
972 				// we can modify the existing rule because it is mutable and dedicated to this class
973 				r.addAll(ruleblock);
974 				ruleBlocks.put(selector, r);
975 			}
976 		}
977 
978 		public boolean isEmpty() {
979 			return ruleBlocks.isEmpty();
980 		}
981 
982 		@Override
983 		public String toString() {
984 			return BrailleCssSerializer.serializeRuleBlockList(ruleBlocks.values());
985 		}
986 	}
987 
988 	private static class TableByStyle extends PseudoElementStyle {
989 
990 		final private Map<List<PseudoClass>,ListItemStyle> listItemStyles = new LinkedHashMap<List<PseudoClass>,ListItemStyle>();
991 		private ListItemStyle listHeaderStyle = new ListItemStyle();
992 
993 		public TableByStyle() {}
994 
995 		public void addListItemStyle(List<PseudoClass> pseudo, ListItemStyle style) {
996 			if (!listItemStyles.containsKey(pseudo))
997 				listItemStyles.put(pseudo, style);
998 			else
999 				listItemStyles.put(pseudo, listItemStyles.get(pseudo).mergeWith(style));
1000 		}
1001 
1002 		public void addListHeaderStyle(ListItemStyle style) {
1003 			listHeaderStyle = listHeaderStyle.mergeWith(style);
1004 		}
1005 
1006 		public ListItemStyle getListItemStyle(Predicate<PseudoClass> matcher) {
1007 			ListItemStyle style = new ListItemStyle();
1008 		  outer: for (List<PseudoClass> pseudoClasses : listItemStyles.keySet()) {
1009 				for (PseudoClass pseudoClass : pseudoClasses)
1010 					if (!matcher.apply(pseudoClass))
1011 						continue outer;
1012 				style = style.mergeWith(listItemStyles.get(pseudoClasses)); }
1013 			return style;
1014 		}
1015 
1016 		public ListItemStyle getListHeaderStyle() {
1017 			return listHeaderStyle;
1018 		}
1019 	}
1020 
1021 	private static class ListItemStyle extends PseudoElementStyle {
1022 
1023 		public ListItemStyle() {}
1024 
1025 		public ListItemStyle(RuleBlock<Rule<?>> ruleblock) {
1026 			addRuleBlock(ruleblock);
1027 		}
1028 
1029 		public ListItemStyle mergeWith(ListItemStyle style) {
1030 			for (RuleBlock<Rule<?>> r: style.ruleBlocks.values())
1031 				addRuleBlock(r);
1032 			return this;
1033 		}
1034 	}
1035 
1036 	private final static Predicate<PseudoClass> matchesPosition(final int position, final int elementCount) {
1037 		return new Predicate<PseudoClass>() {
1038 			public boolean apply(PseudoClass pseudo) {
1039 				if (pseudo instanceof PseudoClassImpl)
1040 					return ((PseudoClassImpl)pseudo).matchesPosition(position, elementCount);
1041 				return false;
1042 			}
1043 		};
1044 	}
1045 
1046 	// see https://www.w3.org/TR/REC-html40/struct/tables.html#h-11.4.3
1047 	private List<TableCell> findHeaders(TableCell cell, boolean firstLeftThenUpward) {
1048 		List<TableCell> headers = new ArrayList<TableCell>();
1049 		if (isHeader(cell))
1050 			headers.add(cell);
1051 		findHeaders(headers, 0, cell, firstLeftThenUpward);
1052 		return headers;
1053 	}
1054 
1055 	private int findHeaders(List<TableCell> headers, int index, TableCell cell, boolean firstLeftThenUpward) {
1056 
1057 		// headers attribute
1058 		if (cell.headers != null) {
1059 			for (String id : cell.headers)
1060 				index = recurAddHeader(headers, index, getById(id), firstLeftThenUpward);
1061 			return index; }
1062 
1063 		// scope attribute can be used instead of headers (they should not be used in same table)
1064 		List<TableCell> rowHeaders = new ArrayList<TableCell>();
1065 		List<TableCell> colHeaders = new ArrayList<TableCell>();
1066 		for (TableCell h : cells)
1067 			if (h != cell && h.scope != null) {
1068 				switch (h.scope) {
1069 				case ROW:
1070 					if (h.row <= (cell.row + cell.rowspan - 1) && cell.row <= (h.row + h.rowspan - 1))
1071 						rowHeaders.add(h);
1072 					break;
1073 				case COL:
1074 					if (h.col <= (cell.col + cell.colspan - 1) && cell.col <= (h.col + h.colspan - 1))
1075 						colHeaders.add(h);
1076 					break; }}
1077 		Collections.sort(rowHeaders, sortByColumnAndThenRow);
1078 		for (TableCell h : rowHeaders)
1079 			index = recurAddHeader(headers, index, h, firstLeftThenUpward);
1080 		Collections.sort(colHeaders, sortByRowAndThenColumn);
1081 		for (TableCell h : colHeaders)
1082 			index = recurAddHeader(headers, index, h, firstLeftThenUpward);
1083 
1084 		if (!isHeader(cell)) {
1085 			int direction = (firstLeftThenUpward ? 0 : 1);
1086 		  outer: while (true) {
1087 				switch (direction) {
1088 				case 0: { // search left from the cell's position to find row header cells
1089 					int k = 0;
1090 					for (int i = 0; i < cell.rowspan; i++)
1091 						for (int j = cell.col - 1; j > 0;) {
1092 							boolean foundHeader = false;
1093 							TableCell c = getByCoordinates(cell.row + i, j);
1094 							if (c != null && isHeader(c)) {
1095 								foundHeader = true;
1096 								if (c.scope == null) {
1097 									int l = recurAddHeader(headers, index, c, firstLeftThenUpward) - index;
1098 									k += l;
1099 									if (l > 1)
1100 										break; }}
1101 							else if (foundHeader)
1102 								break;
1103 							if (c == null)
1104 								j--;
1105 							else
1106 								j = j - c.colspan; }
1107 					index += k;
1108 					if (!firstLeftThenUpward)
1109 						break outer; }
1110 				case 1: { // search upwards from the cell's position to find column header cells
1111 					int k = 0;
1112 					for (int i = 0; i < cell.colspan; i++)
1113 						for (int j = cell.row - 1; j > 0;) {
1114 							boolean foundHeader = false;
1115 							TableCell c = getByCoordinates(j, cell.col + i);
1116 							if (c != null && isHeader(c)) {
1117 								foundHeader = true;
1118 								if (c.scope == null) {
1119 									int l = recurAddHeader(headers, index, c, firstLeftThenUpward) - index;
1120 									k += l;
1121 									if (l > 1)
1122 										break; }}
1123 							else if (foundHeader)
1124 								break;
1125 							if (c == null)
1126 								j--;
1127 							else
1128 								j = j - c.rowspan; }
1129 					index += k;
1130 					if (!firstLeftThenUpward) {
1131 						direction--;
1132 						continue outer; }
1133 					break outer; }}}}
1134 
1135 		return index;
1136 	}
1137 
1138 	private int recurAddHeader(List<TableCell> headers, int index, TableCell header, boolean firstLeftThenUpward) {
1139 		if (headers.contains(header))
1140 			; // if a cell spans multiple cols/rows we could find the same header via multiple paths
1141 		else {
1142 			headers.add(index, header);
1143 			index = findHeaders(headers, index, header, firstLeftThenUpward);
1144 			index ++; }
1145 		return index;
1146 	}
1147 
1148 	private TableCell getById(String id) {
1149 		for (TableCell c : cells)
1150 			if (id.equals(c.id))
1151 				return c;
1152 		throw new InvalidTableException("No element found with id " + id);
1153 	}
1154 
1155 	private TableCell getByCoordinates(int row, int col) {
1156 		for (TableCell c : cells)
1157 			if (c.row <= row && (c.row + c.rowspan - 1) >= row &&
1158 			    c.col <= col && (c.col + c.colspan - 1) >= col)
1159 				return c;
1160 		return null;
1161 	}
1162 
1163 	private static boolean isHeader(TableCell cell) {
1164 		return (cell.type == TableCell.CellType.TH || (cell.axis != null) || (cell.scope != null));
1165 	}
1166 
1167 	@SafeVarargs
1168 	private static final <T> Comparator<T> compose(final Comparator<T>... comparators) {
1169 		return new Comparator<T>() {
1170 			public int compare(T x1, T x2) {
1171 				for (Comparator<T> comparator : comparators) {
1172 					int result = comparator.compare(x1, x2);
1173 					if (result != 0)
1174 						return result; }
1175 				return 0;
1176 			}
1177 		};
1178 	}
1179 
1180 	private static final Comparator<TableCell> sortByRow = new Comparator<TableCell>() {
1181 		public int compare(TableCell c1, TableCell c2) {
1182 			return new Integer(c1.row).compareTo(c2.row);
1183 		}
1184 	};
1185 
1186 	private static final Comparator<TableCell> sortByColumn = new Comparator<TableCell>() {
1187 		public int compare(TableCell c1, TableCell c2) {
1188 			return new Integer(c1.col).compareTo(c2.col);
1189 		}
1190 	};
1191 
1192 	private static final Comparator<TableCell> sortByRowType = new Comparator<TableCell>() {
1193 		public int compare(TableCell c1, TableCell c2) {
1194 			return c1.rowType.compareTo(c2.rowType);
1195 		}
1196 	};
1197 
1198 	private static final Comparator<TableCell> sortByRowAndThenColumn = compose(sortByRow, sortByColumn);
1199 
1200 	private static final Comparator<TableCell> sortByColumnAndThenRow = compose(sortByColumn, sortByRow);
1201 
1202 	private static class TableCell {
1203 
1204 		enum CellType {
1205 			TD,
1206 			TH
1207 		}
1208 
1209 		enum RowType {
1210 			THEAD,
1211 			TBODY,
1212 			TFOOT
1213 		}
1214 
1215 		enum HeaderPolicy {
1216 			ALWAYS,
1217 			ONCE,
1218 			FRONT
1219 		}
1220 
1221 		// TODO: handle colgroup and rowgroup
1222 		enum Scope {
1223 			COL,
1224 			ROW
1225 		}
1226 
1227 		int rowGroup;
1228 		int row;
1229 		int col;
1230 		CellType type = CellType.TD;
1231 		RowType rowType = RowType.TBODY;
1232 		HeaderPolicy headerPolicy = HeaderPolicy.ONCE;
1233 		String id;
1234 		List<String> headers;
1235 		Scope scope = null;
1236 		List<String> axis;
1237 		int rowspan = 1;
1238 		int colspan = 1;
1239 		String ns;
1240 		SimpleInlineStyle style = null;
1241 		String lang = null;
1242 		List<WriterEvent> content = new ArrayList<WriterEvent>();
1243 
1244 		private AtomicReference<Boolean> written = new AtomicReference<Boolean>(false);
1245 
1246 		public void write(XMLStreamWriter writer) throws XMLStreamException {
1247 			writer.writeStartElement(ns, type == CellType.TD ? "td" : "th");
1248 			if (axis != null) {
1249 				StringBuilder s = new StringBuilder();
1250 				Iterator<String> it = axis.iterator();
1251 				while (it.hasNext()) {
1252 					s.append(it.next());
1253 					if (it.hasNext()) s.append(","); }
1254 				writeAttribute(writer, _AXIS, s.toString()); }
1255 			if (id != null && !written.get()) {
1256 				writeAttribute(writer, _ID, id);
1257 				written.set(true); }
1258 			if (headers != null) {
1259 				StringBuilder s = new StringBuilder();
1260 				Iterator<String> it = headers.iterator();
1261 				while (it.hasNext()) {
1262 					s.append(it.next());
1263 					if (it.hasNext()) s.append(" "); }
1264 				writeAttribute(writer, _HEADERS, s.toString()); }
1265 			if (lang != null)
1266 				writeAttribute(writer, XML_LANG, lang);
1267 			if (style != null) {
1268 				String styleAttr = BrailleCssSerializer.toString(style);
1269 				if (styleAttr != null && !"".equals(styleAttr))
1270 					writeAttribute(writer, _STYLE, styleAttr); }
1271 			for (WriterEvent action : content)
1272 				action.writeTo(writer);
1273 			writer.writeEndElement();
1274 		}
1275 
1276 		public TableCell clone() {
1277 			TableCell clone = new TableCell();
1278 			clone.rowGroup = this.rowGroup;
1279 			clone.row = this.row;
1280 			clone.col = this.col;
1281 			clone.type = this.type;
1282 			clone.rowType = this.rowType;
1283 			clone.headerPolicy = this.headerPolicy;
1284 			clone.id = this.id;
1285 			clone.headers = this.headers;
1286 			clone.scope = this.scope;
1287 			clone.axis = this.axis;
1288 			clone.rowspan = this.rowspan;
1289 			clone.colspan = this.colspan;
1290 			clone.ns = this.ns;
1291 			clone.style = (SimpleInlineStyle)this.style.clone();
1292 			clone.content.addAll(this.content);
1293 			clone.written = this.written;
1294 			return clone;
1295 		}
1296 
1297 		@Override
1298 		public String toString() {
1299 			ToStringWriter xml = new ToStringWriter();
1300 			try {
1301 				write(xml); }
1302 			catch (Exception e) {
1303 				throw new RuntimeException("coding error", e); }
1304 			StringBuilder s = new StringBuilder();
1305 			s.append("TableCell{" + row + "," + col + "}[").append(xml).append("]");
1306 			return s.toString();
1307 		}
1308 	}
1309 
1310 	private static class CellCoordinates {
1311 
1312 		private final int row;
1313 		private final int col;
1314 
1315 		private CellCoordinates(int row, int col) {
1316 			this.row = row;
1317 			this.col = col;
1318 		}
1319 
1320 		public int hashCode() {
1321 			final int prime = 31;
1322 			int result = 1;
1323 			result = prime * result + col;
1324 			result = prime * result + row;
1325 			return result;
1326 		}
1327 
1328 		public boolean equals(Object obj) {
1329 			if (this == obj)
1330 				return true;
1331 			if (obj == null)
1332 				return false;
1333 			if (getClass() != obj.getClass())
1334 				return false;
1335 			CellCoordinates other = (CellCoordinates) obj;
1336 			if (col != other.col)
1337 				return false;
1338 			if (row != other.row)
1339 				return false;
1340 			return true;
1341 		}
1342 	}
1343 
1344 	private static class WriteOnlyOnce extends ArrayList<WriterEvent> implements WriterEvent {
1345 		public void writeTo(XMLStreamWriter writer) throws XMLStreamException {
1346 			Iterator<WriterEvent> i = iterator();
1347 			while (i.hasNext()) {
1348 				i.next().writeTo(writer);
1349 				i.remove();
1350 			}
1351 		}
1352 	}
1353 
1354 	public static WriterEvent writeElementOnce(XMLStreamReader reader) throws XMLStreamException {
1355 		WriteOnlyOnce list = new WriteOnlyOnce();
1356 		int depth = 0;
1357 		element: while (true)
1358 			try {
1359 				switch (reader.getEventType()) {
1360 				case START_ELEMENT: {
1361 					QName name = reader.getName();
1362 					depth++;
1363 					list.add(w -> writeStartElement(w, name));
1364 					for (int i = 0; i < reader.getNamespaceCount(); i++) {
1365 						String prf = reader.getNamespacePrefix(i);
1366 						String ns = reader.getNamespaceURI(i);
1367 						list.add(w -> w.writeNamespace(prf, ns)); }
1368 					for (int i = 0; i < reader.getAttributeCount(); i++) {
1369 						QName attrName = reader.getAttributeName(i);
1370 						String attrValue = reader.getAttributeValue(i);
1371 						list.add(w -> writeAttribute(w, attrName, attrValue)); }
1372 					break; }
1373 				case CHARACTERS:
1374 					String chars = reader.getText();
1375 					list.add(w -> w.writeCharacters(chars));
1376 					break;
1377 				case END_ELEMENT: {
1378 					QName name = reader.getName();
1379 					list.add(w -> w.writeEndElement());
1380 					depth--;
1381 					if (depth == 0)
1382 						break element; }}
1383 				reader.next(); }
1384 			catch (NoSuchElementException e) {
1385 				throw new RuntimeException("coding error"); }
1386 		return list;
1387 	}
1388 
1389 	/* Remove the first part of the selector, or the whole selector if it exists of only one part */
1390 	private static RuleBlock<Rule<?>> rest(RuleRelativeBlock rule) {
1391 		List<Selector> combinedSelector = rule.getSelector();
1392 		if (combinedSelector.size() == 0 || combinedSelector.get(0).size() == 0)
1393 			throw new RuntimeException("coding error");
1394 		SelectorPart first = combinedSelector.get(0).get(0);
1395 		if (first instanceof PseudoElementImpl) {
1396 			// combinedSelector.get(0).size() should normally always be 1
1397 			List<Selector> rest = combinedSelector.subList(1, combinedSelector.size());
1398 			RuleBlock<Rule<?>> newRule;
1399 			PseudoElementImpl pseudo = (PseudoElementImpl)first;
1400 			if (pseudo.hasStackedPseudoElement()) {
1401 				combinedSelector = new ArrayList<>();
1402 				Selector selector = (Selector)new SelectorImpl().unlock();
1403 				selector.add(pseudo.getStackedPseudoElement());
1404 				combinedSelector.add(selector);
1405 				combinedSelector.addAll(rest);
1406 				newRule = new RuleRelativeBlock(combinedSelector);
1407 			} else {
1408 				combinedSelector = pseudo.getCombinedSelectors();
1409 				newRule = combinedSelector.isEmpty()
1410 					? new AbstractRuleBlock<Rule<?>>()
1411 					: new RuleRelativeBlock(combinedSelector);
1412 			}
1413 			newRule.replaceAll(rule); // we can do this because rule is considered immutable
1414 			return newRule;
1415 		} else {
1416 			throw new RuntimeException("not implemented");
1417 		}
1418 	}
1419 	
1420 	private static String elementToString(XMLInputValue<?> element) {
1421 		ToStringWriter xml = new ToStringWriter() {
1422 				int skipElement = 0;
1423 				@Override
1424 				public void writeStartElement(String prefix, String localName, String namespaceURI)
1425 						throws XMLStreamException {
1426 					if (skipElement > 0) {
1427 						skipElement++;
1428 						return;
1429 					}
1430 					if (XMLNS_CSS.equals(namespaceURI)) {
1431 						skipElement++;
1432 						return;
1433 					}
1434 					super.writeStartElement(prefix, localName, namespaceURI);
1435 				}
1436 				@Override
1437 				public void writeEndElement() throws XMLStreamException {
1438 					if (skipElement > 0) {
1439 						skipElement--;
1440 						return;
1441 					}
1442 					super.writeEndElement();
1443 				}
1444 				@Override
1445 				public void writeAttribute(String prefix, String namespaceURI, String localName, String value)
1446 						throws XMLStreamException {
1447 					if (skipElement > 0)
1448 						return;
1449 					if (XMLNS_CSS.equals(namespaceURI))
1450 						return;
1451 					if ("style".equals(localName))
1452 						return;
1453 					super.writeAttribute(prefix, namespaceURI, localName, value);
1454 				}
1455 				@Override
1456 				public void writeCharacters(String text) throws XMLStreamException {
1457 					if (skipElement > 0)
1458 						return;
1459 					super.writeCharacters(text);
1460 				}
1461 			};
1462 		XMLStreamReader r = element.asXMLStreamReader();
1463 		try {
1464 			if (r.next() != START_ELEMENT)
1465 				throw new IllegalArgumentException();
1466 			writeElement(xml, r);
1467 		} catch(NoSuchElementException e) {
1468 			throw new IllegalArgumentException();
1469 		} catch (XMLStreamException e) {
1470 			throw new RuntimeException(e); // should not happen
1471 		}
1472 		return xml.toString();
1473 	}
1474 }