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
170
171
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
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) {
289 if (selector.get(0).size() > 0) {
290
291
292
293
294 if (selector.get(0).get(0) instanceof PseudoElementImpl) {
295
296
297
298
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
324
325
326
327
328
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 ;
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
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
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);
412 } catch (InvalidTableException e) {
413 String message = "Table structure broken: " + e.getMessage();
414 throw new TransformerException(
415 new RuntimeException(
416 message,
417
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
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
463
464
465 public abstract List<TableCell> newlyRenderedHeaders();
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480 public abstract List<TableCell> newlyPromotedHeaders();
481
482
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
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
513
514
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
524 for (TableCell h : parent.deferredHeaders()) {
525 newlyRenderedOrPromotedHeaders.add(h);
526
527 canOmit = false; }
528 for (TableCell h : newlyAppliedHeaders()) {
529
530 if (canOmit
531 && h.headerPolicy != TableCell.HeaderPolicy.ALWAYS
532 && lastAppliedHeaders.hasNext() && lastAppliedHeaders.next().equals(h))
533 continue;
534
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;
609 subGroupingAxis = hasSubGroups ? ((TableCellGroup)children.get(0)).groupingAxis : null;
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
617
618
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
628
629
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
662
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
681
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
700
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
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
744 private List<TableCell> previouslyAppliedHeaders() {
745 if (parent != null)
746 return parent.appliedHeaders();
747 else
748 return emptyList;
749 }
750
751
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
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
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
784 private List<TableCell> previouslyDeferredHeaders() {
785 if (parent != null)
786 return parent.deferredHeaders();
787 else
788 return emptyList;
789 }
790
791
792
793
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
800 while (i >= 0 && newlyAppliedHeaders().get(i).headerPolicy == TableCell.HeaderPolicy.ALWAYS)
801 i--;
802
803 if (i >= 0) {
804 Iterator<TableCell> lastAppliedHeaders = (
805 precedingSibling != null ? precedingSibling.newlyAppliedHeaders() : emptyList
806 ).iterator();
807 boolean canOmit = true;
808
809 for (TableCell h : previouslyDeferredHeaders()) {
810 newlyRenderedOrPromotedHeaders.add(h);
811
812 canOmit = false; }
813 for (int j = 0; j <= i; j++) {
814 TableCell h = newlyAppliedHeaders().get(j);
815
816 if (canOmit
817 && h.headerPolicy != TableCell.HeaderPolicy.ALWAYS
818 && lastAppliedHeaders.hasNext() && lastAppliedHeaders.next().equals(h))
819 continue;
820
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();
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();
896 writer.writeEndElement(); }
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();
908 i++; }
909 if (hasSubGroups)
910 writer.writeEndElement();
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
965
966
967 r = selector != null
968 ? new RuleRelativeBlock(selector)
969 : new AbstractRuleBlock<Rule<?>>();
970 r.unlock();
971 }
972
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
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
1058 if (cell.headers != null) {
1059 for (String id : cell.headers)
1060 index = recurAddHeader(headers, index, getById(id), firstLeftThenUpward);
1061 return index; }
1062
1063
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: {
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: {
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 ;
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
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
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
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);
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);
1471 }
1472 return xml.toString();
1473 }
1474 }