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