1   package org.daisy.pipeline.braille.css.impl;
2   
3   import java.io.File;
4   import java.net.URI;
5   import java.net.URISyntaxException;
6   import java.util.ArrayList;
7   import java.util.Arrays;
8   import java.util.Collection;
9   import java.util.Collections;
10  import java.util.Comparator;
11  import java.util.HashMap;
12  import java.util.HashSet;
13  import java.util.Iterator;
14  import java.util.List;
15  import java.util.Map;
16  import java.util.regex.Matcher;
17  import java.util.regex.Pattern;
18  import java.util.Set;
19  
20  import javax.xml.namespace.QName;
21  import javax.xml.transform.URIResolver;
22  
23  import com.google.common.base.MoreObjects;
24  import com.google.common.collect.ImmutableList;
25  import com.google.common.collect.ImmutableMap;
26  import com.google.common.collect.Iterables;
27  
28  import cz.vutbr.web.css.CSSProperty;
29  import cz.vutbr.web.css.Declaration;
30  import cz.vutbr.web.css.NodeData;
31  import cz.vutbr.web.css.Rule;
32  import cz.vutbr.web.css.RuleFactory;
33  import cz.vutbr.web.css.RuleMargin;
34  import cz.vutbr.web.css.RulePage;
35  import cz.vutbr.web.css.Selector.PseudoElement;
36  import cz.vutbr.web.css.StyleSheet;
37  import cz.vutbr.web.css.SupportedCSS;
38  import cz.vutbr.web.css.Term;
39  import cz.vutbr.web.css.TermIdent;
40  import cz.vutbr.web.css.TermURI;
41  import cz.vutbr.web.csskit.antlr.CSSParserFactory;
42  import cz.vutbr.web.csskit.RuleFactoryImpl;
43  import cz.vutbr.web.domassign.DeclarationTransformer;
44  
45  import org.daisy.braille.css.AnyAtRule;
46  import org.daisy.braille.css.BrailleCSSDeclarationTransformer;
47  import org.daisy.braille.css.BrailleCSSParserFactory;
48  import org.daisy.braille.css.BrailleCSSProperty;
49  import org.daisy.braille.css.BrailleCSSRuleFactory;
50  import org.daisy.braille.css.RuleCounterStyle;
51  import org.daisy.braille.css.RuleHyphenationResource;
52  import org.daisy.braille.css.RuleTextTransform;
53  import org.daisy.braille.css.RuleVolume;
54  import org.daisy.braille.css.RuleVolumeArea;
55  import org.daisy.braille.css.SelectorImpl.PseudoElementImpl;
56  import org.daisy.braille.css.SimpleInlineStyle;
57  import org.daisy.braille.css.SupportedBrailleCSS;
58  import org.daisy.common.file.URLs;
59  import org.daisy.common.transform.XMLTransformer;
60  import org.daisy.pipeline.braille.css.SupportedPrintCSS;
61  import org.daisy.pipeline.braille.css.impl.BrailleCssSerializer;
62  import org.daisy.pipeline.css.CssCascader;
63  import org.daisy.pipeline.css.CssPreProcessor;
64  import org.daisy.pipeline.css.JStyleParserCssCascader;
65  import org.daisy.pipeline.css.Medium;
66  import org.daisy.pipeline.css.XsltProcessor;
67  
68  import org.osgi.service.component.annotations.Component;
69  
70  import org.w3c.dom.Element;
71  import org.w3c.dom.Node;
72  
73  @Component(
74  	name = "BrailleCssCascader",
75  	service = { CssCascader.class }
76  )
77  public class BrailleCssCascader implements CssCascader {
78  
79  	/**
80  	 * Note that this implementation only supports a very small subset of medium "print", namely the
81  	 * properties color, font-style, font-weight, text-decoration.
82  	 */
83  	public boolean supportsMedium(Medium medium) {
84  		switch (medium.getType()) {
85  		case EMBOSSED:
86  		case BRAILLE:
87  		case PRINT:
88  			return true;
89  		default:
90  			return false;
91  		}
92  	}
93  
94  	public XMLTransformer newInstance(Medium medium,
95  	                                  String userStylesheet,
96  	                                  URIResolver uriResolver,
97  	                                  CssPreProcessor preProcessor,
98  	                                  XsltProcessor xsltProcessor,
99  	                                  QName attributeName,
100 	                                  boolean multipleAttrs) {
101 		if (multipleAttrs)
102 			throw new UnsupportedOperationException("Cascading to multiple attributes per element not supported");
103 		if (attributeName == null)
104 			throw new UnsupportedOperationException("A style attribute must be specified");
105 		switch (medium.getType()) {
106 		case EMBOSSED:
107 		case BRAILLE:
108 			return new Transformer(uriResolver, preProcessor, xsltProcessor, userStylesheet, medium, attributeName,
109 			                       brailleParserFactory, brailleRuleFactory, brailleCSS, brailleDeclarationTransformer);
110 		case PRINT:
111 			return new Transformer(uriResolver, preProcessor, xsltProcessor, userStylesheet, medium, attributeName,
112 			                       printParserFactory, printRuleFactory, printCSS, printDeclarationTransformer);
113 		default:
114 			throw new IllegalArgumentException("medium not supported: " + medium);
115 		}
116 	}
117 
118 	// medium print
119 	private static final SupportedCSS printCSS = SupportedPrintCSS.getInstance();
120 	private static DeclarationTransformer printDeclarationTransformer = new DeclarationTransformer(printCSS);
121 	private static final RuleFactory printRuleFactory = RuleFactoryImpl.getInstance();
122 	private static final CSSParserFactory printParserFactory = CSSParserFactory.getInstance();
123 
124 	// medium braille/embossed
125 	private static final SupportedCSS brailleCSS = new SupportedBrailleCSS(false, true);
126 	private static final DeclarationTransformer brailleDeclarationTransformer
127 		= new BrailleCSSDeclarationTransformer(brailleCSS);
128 	private static final RuleFactory brailleRuleFactory = new BrailleCSSRuleFactory();
129 	private static final CSSParserFactory brailleParserFactory = new BrailleCSSParserFactory();
130 
131 	private static class Transformer extends JStyleParserCssCascader {
132 
133 		private final QName attributeName;
134 		private final boolean isBrailleCss;
135 
136 		private Transformer(URIResolver resolver, CssPreProcessor preProcessor, XsltProcessor xsltProcessor,
137 		                    String userStyleSheet, Medium medium, QName attributeName,
138 		                    CSSParserFactory parserFactory, RuleFactory ruleFactory,
139 		                    SupportedCSS supportedCss, DeclarationTransformer declarationTransformer) {
140 			super(resolver, preProcessor, xsltProcessor, userStyleSheet, medium, attributeName,
141 			      parserFactory, ruleFactory, supportedCss, declarationTransformer);
142 			this.attributeName = attributeName;
143 			this.isBrailleCss = medium.getType() == Medium.Type.EMBOSSED || medium.getType() == Medium.Type.BRAILLE;
144 		}
145 
146 		private Map<String,Map<String,RulePage>> pageRules = null;
147 		private Map<String,Map<String,RuleVolume>> volumeRules = null;
148 		private Iterable<RuleTextTransform> textTransformRules = null;
149 		private Iterable<RuleHyphenationResource> hyphenationResourceRules = null;
150 		private Iterable<RuleCounterStyle> counterStyleRules = null;
151 		private Iterable<AnyAtRule> otherAtRules = null;
152 
153 		protected Map<QName,String> serializeStyle(NodeData mainStyle, Map<PseudoElement,NodeData> pseudoStyles, Element context) {
154 			if (isBrailleCss && pageRules == null) {
155 				StyleSheet styleSheet = getParsedStyleSheet();
156 				pageRules = new HashMap<String,Map<String,RulePage>>(); {
157 					for (RulePage r : Iterables.filter(styleSheet, RulePage.class)) {
158 						String name = MoreObjects.firstNonNull(r.getName(), "auto");
159 						String pseudo = MoreObjects.firstNonNull(r.getPseudo(), "");
160 						Map<String,RulePage> pageRule = pageRules.get(name);
161 						if (pageRule == null) {
162 							pageRule = new HashMap<String,RulePage>();
163 							pageRules.put(name, pageRule); }
164 						if (pageRule.containsKey(pseudo))
165 							pageRule.put(pseudo, makePageRule(name, "".equals(pseudo) ? null : pseudo,
166 							                                  ImmutableList.of(r, pageRule.get(pseudo))));
167 						else
168 							pageRule.put(pseudo, r);
169 					}
170 				}
171 				volumeRules = new HashMap<String,Map<String,RuleVolume>>(); {
172 					for (RuleVolume r : Iterables.filter(styleSheet, RuleVolume.class)) {
173 						String name = "auto";
174 						String pseudo = MoreObjects.firstNonNull(r.getPseudo(), "");
175 						if (pseudo.equals("nth(1)")) pseudo = "first";
176 						else if (pseudo.equals("nth-last(1)")) pseudo = "last";
177 						Map<String,RuleVolume> volumeRule = volumeRules.get(name);
178 						if (volumeRule == null) {
179 							volumeRule = new HashMap<String,RuleVolume>();
180 							volumeRules.put(name, volumeRule); }
181 						if (volumeRule.containsKey(pseudo))
182 							volumeRule.put(pseudo, makeVolumeRule("".equals(pseudo) ? null : pseudo,
183 							                                      ImmutableList.of(r, volumeRule.get(pseudo))));
184 						else
185 							volumeRule.put(pseudo, r);
186 					}
187 				}
188 				textTransformRules = Iterables.filter(styleSheet, RuleTextTransform.class);
189 				hyphenationResourceRules = Iterables.filter(styleSheet, RuleHyphenationResource.class);
190 				counterStyleRules = Iterables.filter(styleSheet, RuleCounterStyle.class);
191 				otherAtRules = Iterables.filter(styleSheet, AnyAtRule.class);
192 			}
193 			StringBuilder style = new StringBuilder();
194 			if (mainStyle != null)
195 				insertStyle(style, mainStyle);
196 			for (PseudoElement pseudo : sort(pseudoStyles.keySet(), pseudoElementComparator)) {
197 				NodeData nd = pseudoStyles.get(pseudo);
198 				if (nd != null)
199 					insertPseudoStyle(style, nd, pseudo, pageRules);
200 			}
201 			if (isBrailleCss) {
202 				boolean isRoot = (context.getParentNode().getNodeType() != Node.ELEMENT_NODE);
203 				Map<String,RulePage> pageRule = getPageRule(mainStyle, pageRules);
204 				if (pageRule != null) {
205 					insertPageStyle(style, pageRule, true); }
206 				else if (isRoot) {
207 					pageRule = getPageRule("auto", pageRules);
208 					if (pageRule != null)
209 						insertPageStyle(style, pageRule, true); }
210 				if (isRoot) {
211 					Map<String,RuleVolume> volumeRule = getVolumeRule("auto", volumeRules);
212 					if (volumeRule != null)
213 						insertVolumeStyle(style, volumeRule, pageRules);
214 					for (RuleTextTransform r : textTransformRules)
215 						insertTextTransformDefinition(style, r);
216 					for (RuleHyphenationResource r : hyphenationResourceRules)
217 						insertHyphenationResourceDefinition(style, r);
218 					for (RuleCounterStyle r : counterStyleRules)
219 						insertCounterStyleDefinition(style, r);
220 					for (AnyAtRule r : otherAtRules) {
221 						if (style.length() > 0 && !style.toString().endsWith("} ")) {
222 							style.insert(0, "{ ");
223 							style.append("} "); }
224 						insertAtRule(style, r); }}}
225 			if (!style.toString().replaceAll("\\s+", "").isEmpty())
226 				return ImmutableMap.of(attributeName, style.toString().trim());
227 			else
228 				return null;
229 		}
230 
231 		protected String serializeValue(Term<?> value) {
232 			return BrailleCssSerializer.toString(value);
233 		}
234 	}
235 
236 	@SuppressWarnings("unused")
237 	private static <T extends Comparable<? super T>> Iterable<T> sort(Iterable<T> iterable) {
238 		List<T> list = new ArrayList<T>();
239 		for (T x : iterable)
240 			list.add(x);
241 		Collections.<T>sort(list);
242 		return list;
243 	}
244 
245 	private static <T> Iterable<T> sort(Iterable<T> iterable, Comparator<? super T> comparator) {
246 		List<T> list = new ArrayList<T>();
247 		for (T x : iterable)
248 			list.add(x);
249 		Collections.<T>sort(list, comparator);
250 		return list;
251 	}
252 
253 	private static Comparator<PseudoElement> pseudoElementComparator = new Comparator<PseudoElement>() {
254 		public int compare(PseudoElement e1, PseudoElement e2) {
255 			return e1.toString().compareTo(e2.toString());
256 		}
257 	};
258 
259 	// FIXME: move parts of this to BrailleCssSerializer
260 
261 	private static void insertStyle(StringBuilder builder, NodeData nodeData) {
262 		List<String> keys = new ArrayList<String>(nodeData.getPropertyNames());
263 		keys.remove("page");
264 		Collections.sort(keys);
265 		for (String key : keys) {
266 			Term<?> value = nodeData.getValue(key, false);
267 			if (value != null)
268 				builder.append(key).append(": ").append(BrailleCssSerializer.toString(value)).append("; ");
269 			else {
270 				CSSProperty prop = nodeData.getProperty(key, false);
271 				if (prop != null) // can be null for unspecified inherited properties
272 					builder.append(key).append(": ").append(prop).append("; "); }}
273 	}
274 
275 	private static void pseudoElementToString(StringBuilder builder, PseudoElement elem) {
276 		if (elem instanceof PseudoElementImpl) {
277 			builder.append("&").append(elem);
278 			return; }
279 		else {
280 			builder.append("&::").append(elem.getName());
281 			String[] args = elem.getArguments();
282 			if (args.length > 0) {
283 				StringBuilder s = new StringBuilder();
284 				Iterator<String> it = Arrays.asList(args).iterator();
285 				while (it.hasNext()) {
286 					s.append(it.next());
287 					if (it.hasNext()) s.append(", "); }
288 				builder.append("(").append(s).append(")"); }}
289 	}
290 
291 	private static void insertPseudoStyle(StringBuilder builder, NodeData nodeData, PseudoElement elem,
292 	                                      Map<String,Map<String,RulePage>> pageRules) {
293 		pseudoElementToString(builder, elem);
294 		builder.append(" { ");
295 		insertStyle(builder, nodeData);
296 		Map<String,RulePage> pageRule = getPageRule(nodeData, pageRules);
297 		if (pageRule != null)
298 			insertPageStyle(builder, pageRule, false);
299 		builder.append("} ");
300 	}
301 
302 	private static void insertPageStyle(StringBuilder builder, Map<String,RulePage> pageRule, boolean topLevel) {
303 		for (RulePage r : pageRule.values())
304 			insertPageStyle(builder, r, topLevel);
305 	}
306 
307 	private static void insertPageStyle(StringBuilder builder, RulePage pageRule, boolean topLevel) {
308 		builder.append("@page");
309 		String pseudo = pageRule.getPseudo();
310 		if (pseudo != null && !"".equals(pseudo))
311 			builder.append(":").append(pseudo);
312 		builder.append(" { ");
313 		for (Declaration decl : Iterables.filter(pageRule, Declaration.class))
314 			insertDeclaration(builder, decl);
315 		for (RuleMargin margin : Iterables.filter(pageRule, RuleMargin.class))
316 			insertMarginStyle(builder, margin);
317 		builder.append("} ");
318 	}
319 
320 	private static void insertMarginStyle(StringBuilder builder, RuleMargin ruleMargin) {
321 		builder.append("@").append(ruleMargin.getMarginArea()).append(" { ");
322 		insertStyle(builder, new SimpleInlineStyle(ruleMargin, null, brailleDeclarationTransformer, brailleCSS));
323 		builder.append("} ");
324 	}
325 
326 	private static void insertDeclaration(StringBuilder builder, Declaration decl) {
327 		StringBuilder s = new StringBuilder();
328 		Iterator<Term<?>> it = decl.iterator();
329 		while (it.hasNext()) {
330 			s.append(BrailleCssSerializer.toString(it.next()));
331 			if (it.hasNext()) s.append(" "); }
332 		builder.append(decl.getProperty()).append(": ").append(s).append("; ");
333 	}
334 
335 	private static Map<String,RulePage> getPageRule(NodeData nodeData, Map<String,Map<String,RulePage>> pageRules) {
336 		BrailleCSSProperty.Page pageProperty; {
337 			if (nodeData != null)
338 				pageProperty = nodeData.<BrailleCSSProperty.Page>getProperty("page", false);
339 			else
340 				pageProperty = null;
341 		}
342 		String name; {
343 			if (pageProperty != null) {
344 				if (pageProperty == BrailleCSSProperty.Page.identifier)
345 					name = nodeData.<TermIdent>getValue(TermIdent.class, "page", false).getValue();
346 				else
347 					name = pageProperty.toString(); }
348 			else
349 				name = null;
350 		}
351 		if (name != null)
352 			return getPageRule(name, pageRules);
353 		else
354 			return null;
355 	}
356 
357 	private static Map<String,RulePage> getPageRule(String name, Map<String,Map<String,RulePage>> pageRules) {
358 		Map<String,RulePage> auto = pageRules == null ? null : pageRules.get("auto");
359 		Map<String,RulePage> named = null;
360 		if (!name.equals("auto"))
361 			named = pageRules.get(name);
362 		Map<String,RulePage> result = new HashMap<String,RulePage>();
363 		List<RulePage> from;
364 		RulePage r;
365 		Set<String> pseudos = new HashSet<String>();
366 		if (named != null)
367 			pseudos.addAll(named.keySet());
368 		if (auto != null)
369 			pseudos.addAll(auto.keySet());
370 		for (String pseudo : pseudos) {
371 			boolean noPseudo = "".equals(pseudo);
372 			from = new ArrayList<RulePage>();
373 			if (named != null) {
374 				r = named.get(pseudo);
375 				if (r != null) from.add(r);
376 				if (!noPseudo) {
377 					r = named.get("");
378 					if (r != null) from.add(r); }}
379 			if (auto != null) {
380 				r = auto.get(pseudo);
381 				if (r != null) from.add(r);
382 				if (!noPseudo) {
383 					r = auto.get("");
384 					if (r != null) from.add(r); }}
385 			result.put(pseudo, makePageRule(name, noPseudo ? null : pseudo, from)); }
386 		return result;
387 	}
388 
389 	private static RulePage makePageRule(String name, String pseudo, List<RulePage> from) {
390 		RulePage pageRule = brailleRuleFactory.createPage().setName(name).setPseudo(pseudo);
391 		for (RulePage f : from)
392 			for (Rule<?> r : f)
393 				if (r instanceof Declaration) {
394 					Declaration d = (Declaration)r;
395 					String property = d.getProperty();
396 					if (getDeclaration(pageRule, property) == null)
397 						pageRule.add(r); }
398 				else if (r instanceof RuleMargin) {
399 					RuleMargin m = (RuleMargin)r;
400 					String marginArea = m.getMarginArea();
401 					RuleMargin marginRule = getRuleMargin(pageRule, marginArea);
402 					if (marginRule == null) {
403 						marginRule = brailleRuleFactory.createMargin(marginArea);
404 						pageRule.add(marginRule);
405 						marginRule.replaceAll(m); }
406 					else
407 						for (Declaration d : m)
408 							if (getDeclaration(marginRule, d.getProperty()) == null)
409 								marginRule.add(d); }
410 		return pageRule;
411 	}
412 
413 	private static Declaration getDeclaration(Collection<? extends Rule<?>> rule, String property) {
414 		for (Declaration d : Iterables.filter(rule, Declaration.class))
415 			if (d.getProperty().equals(property))
416 				return d;
417 		return null;
418 	}
419 
420 	private static RuleMargin getRuleMargin(Collection<? extends Rule<?>> rule, String marginArea) {
421 		for (RuleMargin m : Iterables.filter(rule, RuleMargin.class))
422 			if (m.getMarginArea().equals(marginArea))
423 				return m;
424 		return null;
425 	}
426 
427 	private static void insertVolumeStyle(StringBuilder builder, Map<String,RuleVolume> volumeRule, Map<String,Map<String,RulePage>> pageRules) {
428 		for (Map.Entry<String,RuleVolume> r : volumeRule.entrySet())
429 			insertVolumeStyle(builder, r, pageRules);
430 	}
431 
432 	private static void insertVolumeStyle(StringBuilder builder, Map.Entry<String,RuleVolume> volumeRule, Map<String,Map<String,RulePage>> pageRules) {
433 		builder.append("@volume");
434 		String pseudo = volumeRule.getKey();
435 		if (pseudo != null && !"".equals(pseudo))
436 			builder.append(":").append(pseudo);
437 		builder.append(" { ");
438 		for (Declaration decl : Iterables.filter(volumeRule.getValue(), Declaration.class))
439 			insertDeclaration(builder, decl);
440 		for (RuleVolumeArea volumeArea : Iterables.filter(volumeRule.getValue(), RuleVolumeArea.class))
441 			insertVolumeAreaStyle(builder, volumeArea, pageRules);
442 		builder.append("} ");
443 	}
444 
445 	private static void insertVolumeAreaStyle(StringBuilder builder, RuleVolumeArea ruleVolumeArea, Map<String,Map<String,RulePage>> pageRules) {
446 		builder.append("@").append(ruleVolumeArea.getVolumeArea().value).append(" { ");
447 		StringBuilder innerStyle = new StringBuilder();
448 		Map<String,RulePage> pageRule = null;
449 		for (Declaration decl : Iterables.filter(ruleVolumeArea, Declaration.class))
450 			if ("page".equals(decl.getProperty())) {
451 				StringBuilder s = new StringBuilder();
452 				Iterator<Term<?>> it = decl.iterator();
453 				while (it.hasNext()) {
454 					s.append(BrailleCssSerializer.toString(it.next()));
455 					if (it.hasNext()) s.append(" "); }
456 				pageRule = getPageRule(s.toString(), pageRules); }
457 			else
458 				insertDeclaration(innerStyle, decl);
459 		if (pageRule != null)
460 			insertPageStyle(innerStyle, pageRule, false);
461 		builder.append(innerStyle).append("} ");
462 	}
463 
464 	private static void insertTextTransformDefinition(StringBuilder builder, RuleTextTransform rule) {
465 		builder.append("@text-transform");
466 		String name = rule.getName();
467 		if (name != null) builder.append(' ').append(name);
468 		builder.append(" { ");
469 		for (Declaration decl : rule) {
470 			if (decl.size() == 1 && decl.get(0) instanceof TermURI) {
471 				TermURI term = (TermURI)decl.get(0);
472 				URI uri = URLs.asURI(term.getValue());
473 				if (!uri.isAbsolute() && !uri.getSchemeSpecificPart().startsWith("/")) {
474 					// relative resource: make absolute and convert to "volatile-file" URI to bypass
475 					// caching in AbstractTransformProvider
476 					if (term.getBase() != null)
477 						uri = URLs.resolve(URLs.asURI(term.getBase()), uri);
478 					try {
479 						new File(uri);
480 						try {
481 							uri = new URI("volatile-file", uri.getSchemeSpecificPart(), uri.getFragment());
482 						} catch (URISyntaxException e) {
483 							throw new IllegalStateException(e); // should not happen
484 						}
485 					} catch (IllegalArgumentException e) {
486 						// not a file URI
487 					}
488 				}
489 				builder.append(decl.getProperty()).append(": ").append("url(\"" + uri + "\")").append("; ");
490 				continue;
491 			}
492 			insertDeclaration(builder, decl);
493 		}
494 		builder.append("} ");
495 	}
496 
497 	private static void insertHyphenationResourceDefinition(StringBuilder builder, RuleHyphenationResource rule) {
498 		builder.append("@hyphenation-resource");
499 		builder.append(":lang(").append(BrailleCssSerializer.serializeLanguageRanges(rule.getLanguageRanges())).append(")");
500 		builder.append(" { ");
501 		for (Declaration decl : rule) {
502 			if (decl.size() == 1 && decl.get(0) instanceof TermURI) {
503 				TermURI term = (TermURI)decl.get(0);
504 				URI uri = URLs.asURI(term.getValue());
505 				if (!uri.isAbsolute() && !uri.getSchemeSpecificPart().startsWith("/")) {
506 					// relative resource: make absolute and convert to "volatile-file" URI to bypass
507 					// caching in AbstractTransformProvider
508 					if (term.getBase() != null)
509 						uri = URLs.resolve(URLs.asURI(term.getBase()), uri);
510 					try {
511 						new File(uri);
512 						try {
513 							uri = new URI("volatile-file", uri.getSchemeSpecificPart(), uri.getFragment());
514 						} catch (URISyntaxException e) {
515 							throw new IllegalStateException(e); // should not happen
516 						}
517 					} catch (IllegalArgumentException e) {
518 						// not a file URI
519 					}
520 				}
521 				builder.append(decl.getProperty()).append(": ").append("url(\"" + uri + "\")").append("; ");
522 				continue;
523 			}
524 			insertDeclaration(builder, decl);
525 		}
526 		builder.append("} ");
527 	}
528 
529 	private static void insertCounterStyleDefinition(StringBuilder builder, RuleCounterStyle rule) {
530 		String name = rule.getName();
531 		builder.append("@counter-style ").append(name).append(" { ");
532 		for (Declaration decl : rule)
533 			insertDeclaration(builder, decl);
534 		builder.append("} ");
535 	}
536 
537 	private static void insertAtRule(StringBuilder builder, AnyAtRule rule) {
538 		builder.append("@").append(rule.getName()).append(" { ");
539 		for (Declaration decl : Iterables.filter(rule, Declaration.class))
540 			insertDeclaration(builder, decl);
541 		for (AnyAtRule r : Iterables.filter(rule, AnyAtRule.class))
542 			insertAtRule(builder, r);
543 		builder.append("} ");
544 	}
545 
546 	private static Map<String,RuleVolume> getVolumeRule(String name, Map<String,Map<String,RuleVolume>> volumeRules) {
547 		Map<String,RuleVolume> auto = volumeRules.get("auto");
548 		Map<String,RuleVolume> named = null;
549 		if (!name.equals("auto"))
550 			named = volumeRules.get(name);
551 		Map<String,RuleVolume> result = new HashMap<String,RuleVolume>();
552 		List<RuleVolume> from;
553 		RuleVolume r;
554 		Set<String> pseudos = new HashSet<String>();
555 		if (named != null)
556 			pseudos.addAll(named.keySet());
557 		if (auto != null)
558 			pseudos.addAll(auto.keySet());
559 		// Create a special rule for volumes that match both :first and :last (i.e. for the case
560 		// there is only a single volume)
561 		// FIXME: The order in which rules are defined currently does not affect the
562 		// precedence. ':first' rules always override ':last' rules.
563 		if (pseudos.contains("first") && pseudos.contains("last"))
564 			pseudos.add("only");
565 		for (String pseudo : pseudos) {
566 			from = new ArrayList<RuleVolume>();
567 			if (named != null) {
568 				r = named.get(pseudo);
569 				if (r != null) from.add(r);
570 				if ("only".equals(pseudo)) {
571 					r = named.get("first");
572 					if (r != null) from.add(r);
573 					r = named.get("last");
574 					if (r != null) from.add(r); }
575 				if (!"".equals(pseudo)) {
576 					r = named.get("");
577 					if (r != null) from.add(r); }}
578 			if (auto != null) {
579 				r = auto.get(pseudo);
580 				if (r != null) from.add(r);
581 				if ("only".equals(pseudo)) {
582 					r = auto.get("first");
583 					if (r != null) from.add(r);
584 					r = auto.get("last");
585 					if (r != null) from.add(r); }
586 				if (!"".equals(pseudo)) {
587 					r = auto.get("");
588 					if (r != null) from.add(r); }}
589 			result.put(pseudo,
590 			           makeVolumeRule(
591 			               // "only" is not a valid pseudo name so we drop it. The value we pass to
592 			               // makeVolumeRule() does not matter anyway as long as we use the
593 			               // corresponding key of the map where we store the volume rule to
594 			               // serialize the rule.
595 			               ("".equals(pseudo) || "only".equals(pseudo)) ? null : pseudo,
596 			               from)); }
597 		return result;
598 	}
599 
600 	private static final Pattern FUNCTION = Pattern.compile("(nth|nth-last)\\(([1-9][0-9]*)\\)");
601 
602 	private static RuleVolume makeVolumeRule(String pseudo, List<RuleVolume> from) {
603 		String arg = null;
604 		if (pseudo != null) {
605 			Matcher m = FUNCTION.matcher(pseudo);
606 			if (m.matches()) {
607 				pseudo = m.group(1);
608 				arg = m.group(2); }}
609 		RuleVolume volumeRule = new RuleVolume(pseudo, arg);
610 		for (RuleVolume f : from)
611 			for (Rule<?> r : f)
612 				if (r instanceof Declaration) {
613 					Declaration d = (Declaration)r;
614 					String property = d.getProperty();
615 					if (getDeclaration(volumeRule, property) == null)
616 						volumeRule.add(r); }
617 				else if (r instanceof RuleVolumeArea) {
618 					RuleVolumeArea a = (RuleVolumeArea)r;
619 					String volumeArea = a.getVolumeArea().value;
620 					RuleVolumeArea volumeAreaRule = getRuleVolumeArea(volumeRule, volumeArea);
621 					if (volumeAreaRule == null) {
622 						volumeAreaRule = new RuleVolumeArea(volumeArea);
623 						volumeRule.add(volumeAreaRule);
624 						volumeAreaRule.replaceAll(a); }
625 					else
626 						for (Declaration d : Iterables.filter(a, Declaration.class))
627 							if (getDeclaration(volumeAreaRule, d.getProperty()) == null)
628 								volumeAreaRule.add(d); }
629 		return volumeRule;
630 	}
631 
632 	private static RuleVolumeArea getRuleVolumeArea(Collection<? extends Rule<?>> rule, String volumeArea) {
633 		for (RuleVolumeArea m : Iterables.filter(rule, RuleVolumeArea.class))
634 			if (m.getVolumeArea().value.equals(volumeArea))
635 				return m;
636 		return null;
637 	}
638 }