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