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