1 package org.daisy.pipeline.css;
2
3 import java.io.InputStreamReader;
4 import java.io.IOException;
5 import java.io.Reader;
6 import java.io.StringReader;
7 import java.net.URL;
8 import java.net.URI;
9 import java.nio.charset.Charset;
10 import java.util.ArrayList;
11 import java.util.HashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Optional;
15 import java.util.StringTokenizer;
16 import java.util.function.Function;
17
18 import javax.xml.namespace.QName;
19 import javax.xml.stream.XMLStreamException;
20 import javax.xml.transform.Source;
21 import javax.xml.transform.stream.StreamSource;
22 import javax.xml.transform.URIResolver;
23
24 import com.google.common.collect.Iterables;
25 import com.google.common.io.CharStreams;
26
27 import cz.vutbr.web.css.CombinedSelector;
28 import cz.vutbr.web.css.CSSFactory;
29 import cz.vutbr.web.css.CSSProperty;
30 import cz.vutbr.web.css.Declaration;
31 import cz.vutbr.web.css.MediaSpec;
32 import cz.vutbr.web.css.NetworkProcessor;
33 import cz.vutbr.web.css.NodeData;
34 import cz.vutbr.web.css.RuleFactory;
35 import cz.vutbr.web.css.Selector;
36 import cz.vutbr.web.css.Selector.Combinator;
37 import cz.vutbr.web.css.Selector.PseudoElement;
38 import cz.vutbr.web.css.SourceLocator;
39 import cz.vutbr.web.css.StyleSheet;
40 import cz.vutbr.web.css.SupportedCSS;
41 import cz.vutbr.web.css.Term;
42 import cz.vutbr.web.css.TermIdent;
43 import cz.vutbr.web.css.TermInteger;
44 import cz.vutbr.web.css.TermString;
45 import cz.vutbr.web.csskit.antlr.CSSParserFactory;
46 import cz.vutbr.web.csskit.antlr.CSSSource;
47 import cz.vutbr.web.csskit.antlr.CSSSourceReader;
48 import cz.vutbr.web.csskit.antlr.DefaultCSSSourceReader;
49 import cz.vutbr.web.csskit.antlr.SourceMap;
50 import cz.vutbr.web.csskit.DefaultNetworkProcessor;
51 import cz.vutbr.web.csskit.RuleXslt;
52 import cz.vutbr.web.domassign.Analyzer;
53 import cz.vutbr.web.domassign.DeclarationTransformer;
54 import cz.vutbr.web.domassign.StyleMap;
55 import net.sf.saxon.dom.NodeOverNodeInfo;
56 import net.sf.saxon.om.NodeInfo;
57
58 import org.daisy.common.file.URLs;
59 import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
60 import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
61 import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttributes;
62 import static org.daisy.common.stax.XMLStreamWriterHelper.writeCharacters;
63 import static org.daisy.common.stax.XMLStreamWriterHelper.writeComment;
64 import static org.daisy.common.stax.XMLStreamWriterHelper.writeProcessingInstruction;
65 import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
66 import org.daisy.common.transform.InputValue;
67 import org.daisy.common.transform.Mult;
68 import org.daisy.common.transform.SingleInSingleOutXMLTransformer;
69 import org.daisy.common.transform.TransformerException;
70 import org.daisy.common.transform.XMLInputValue;
71 import org.daisy.common.transform.XMLOutputValue;
72
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
75
76 import org.w3c.dom.Document;
77 import org.w3c.dom.Element;
78 import org.w3c.dom.NamedNodeMap;
79 import org.w3c.dom.Node;
80 import org.w3c.dom.ProcessingInstruction;
81
82 public abstract class JStyleParserCssCascader extends SingleInSingleOutXMLTransformer {
83
84 private final String userAndUserAgentStylesheets;
85 private final MediaSpec medium;
86 private final QName removeInlineStyleAttribute;
87 private final CSSParserFactory parserFactory;
88 private final RuleFactory ruleFactory;
89 private final SupportedCSS supportedCSS;
90 private final DeclarationTransformer declarationTransformer;
91 private final CSSSourceReader cssReader;
92 private final XsltProcessor xsltProcessor;
93
94 private static final Logger logger = LoggerFactory.getLogger(JStyleParserCssCascader.class);
95
96
97
98
99 public JStyleParserCssCascader(URIResolver uriResolver,
100 CssPreProcessor preProcessor,
101 XsltProcessor xsltProcessor,
102 String userAndUserAgentStylesheets,
103 Medium medium,
104 QName removeInlineStyleAttribute,
105 CSSParserFactory parserFactory,
106 RuleFactory ruleFactory,
107 SupportedCSS supportedCSS,
108 DeclarationTransformer declarationTransformer) {
109 this.userAndUserAgentStylesheets = userAndUserAgentStylesheets;
110 this.medium = medium.asMediaSpec();
111 this.removeInlineStyleAttribute = removeInlineStyleAttribute;
112 this.parserFactory = parserFactory;
113 this.ruleFactory = ruleFactory;
114 this.supportedCSS = supportedCSS;
115 this.declarationTransformer = declarationTransformer;
116 this.xsltProcessor = xsltProcessor;
117 NetworkProcessor network = new DefaultNetworkProcessor() {
118 @Override
119 public Reader fetch(URL url, Charset encoding, boolean forceEncoding, boolean assertEncoding) throws IOException {
120 logger.debug("Fetching style sheet: " + url);
121 if (uriResolver != null) {
122 Source resolved; {
123 try {
124 resolved = uriResolver.resolve(URLs.asURI(url).toASCIIString(), ""); }
125 catch (javax.xml.transform.TransformerException e) {
126 throw new IOException(e); }}
127 if (resolved != null) {
128 if (resolved instanceof StreamSource) {
129 InputStreamReader r = detectEncodingAndSkipBOM(
130 ((StreamSource)resolved).getInputStream(), null, encoding, forceEncoding);
131 if (assertEncoding) {
132 if (encoding == null)
133 throw new IllegalArgumentException("encoding must not be null");
134 if (!encoding.equals(getEncoding(r)))
135 throw new IOException("Failed to read URL as " + encoding + ": " + url);
136 }
137 return r;
138 } else {
139 url = new URL(resolved.getSystemId());
140 }
141 }
142 }
143 return super.fetch(url, encoding, forceEncoding, assertEncoding);
144 }
145 };
146
147
148
149
150
151 this.cssReader = new DefaultCSSSourceReader(network) {
152 @Override
153 public boolean supportsMediaType(String mediaType, URL url) {
154 if ("text/css".equals(mediaType))
155 return true;
156 else if (mediaType == null && (url == null || url.toString().endsWith(".css")))
157 return true;
158 else if (preProcessor == null)
159 return false;
160 else
161 return preProcessor.supportsMediaType(mediaType, url);
162 }
163 @Override
164 public CSSInputStream read(CSSSource source) throws IOException {
165 if (source.type == CSSSource.SourceType.URL && uriResolver != null) {
166 try {
167
168
169 Source resolved = uriResolver.resolve(URLs.asURI((URL)source.source).toASCIIString(), "");
170 if (resolved != null)
171 source = new CSSSource(new URL(resolved.getSystemId()), source.encoding, source.mediaType);
172 } catch (javax.xml.transform.TransformerException e) {
173 throw new IOException(e);
174 }
175 }
176 CSSInputStream cssStream = super.read(source);
177 if (!("text/css".equals(source.mediaType)
178 || source.mediaType == null && (source.type != CSSSource.SourceType.URL
179 || ((URL)source.source).toString().endsWith(".css")))) {
180
181 try {
182 CssPreProcessor.PreProcessingResult result = preProcessor.compile(
183 new CssPreProcessor.PreProcessingSource(cssStream.stream, URLs.asURI(cssStream.base)) {
184 @Override
185 public Reader reread(Charset encoding) throws IOException {
186 Reader r = cssStream.reread(encoding);
187 stream.close();
188 return r;
189 }
190 }
191 );
192 SourceMap sourceMap; {
193 if (result.sourceMap != null) {
194 SourceMap m = SourceMapReader.read(result.sourceMap, result.base);
195 if (cssStream.sourceMap != null) {
196 sourceMap = new SourceMap() {
197 public SourceLocator get(int line, int column) {
198 SourceLocator loc = m.get(line, column);
199 if (loc != null && loc.getURL().equals(cssStream.base))
200 loc = cssStream.sourceMap.get(loc.getLineNumber(), loc.getColumnNumber());
201 return loc;
202 }
203 public SourceLocator floor(int line, int column) {
204 SourceLocator loc = m.floor(line, column);
205 if (loc != null && loc.getURL().equals(cssStream.base))
206 loc = cssStream.sourceMap.floor(loc.getLineNumber(), loc.getColumnNumber());
207 return loc;
208 }
209 public SourceLocator ceiling(int line, int column) {
210 SourceLocator loc = m.ceiling(line, column);
211 if (loc != null && loc.getURL().equals(cssStream.base))
212 loc = cssStream.sourceMap.ceiling(loc.getLineNumber(), loc.getColumnNumber());
213 return loc;
214 }
215 };
216 } else
217 sourceMap = m;
218 } else
219 sourceMap = cssStream.sourceMap;
220 }
221 String resultString = CharStreams.toString(result.stream);
222 return new CSSInputStream(new StringReader(resultString), cssStream.base, sourceMap) {
223 @Override
224 public Reader reread(Charset encoding) throws IOException {
225
226
227 result.stream.close();
228 return new StringReader(resultString);
229 }
230 };
231 } catch (RuntimeException e) {
232 throw new IOException(
233 (source.mediaType != null ? (source.mediaType + " p") : "P")
234 + "re-processing failed: " + e.getMessage(), e);
235 }
236 } else
237 return cssStream;
238 }
239 };
240 }
241
242 private StyleSheet styleSheet = null;
243
244 public Runnable transform(XMLInputValue<?> source, XMLOutputValue<?> result, InputValue<?> params) throws TransformerException {
245 if (source == null || result == null)
246 throw new TransformerException(new IllegalArgumentException());
247 return () -> transform(source.ensureSingleItem().mult(2), result.asXMLStreamWriter());
248 }
249
250 private void transform(Mult<? extends XMLInputValue<?>> source, BaseURIAwareXMLStreamWriter output) throws TransformerException {
251 Node node = source.get().asNodeIterator().next();
252 if (!(node instanceof Document))
253 throw new TransformerException(new IllegalArgumentException());
254 Document document = (Document)node;
255 BaseURIAwareXMLStreamWriter writer = output;
256 try {
257 URI baseURI = new URI(document.getBaseURI());
258 Function<Node,SourceLocator> nodeLocator = n -> {
259 if (n instanceof NodeOverNodeInfo) {
260 NodeInfo info = ((NodeOverNodeInfo)n).getUnderlyingNodeInfo();
261 return new SourceLocator() {
262 public URL getURL() {
263 return URLs.asURL(URI.create(info.getBaseURI()));
264 }
265 public int getLineNumber() {
266 int line = info.getLineNumber();
267 if (line > 0)
268 return line - 1;
269 else
270 return -1;
271 }
272 public int getColumnNumber() {
273 return info.getColumnNumber();
274 }
275 };
276 } else {
277
278 return new SourceLocator() {
279 public URL getURL() {
280 return URLs.asURL(URI.create(n.getBaseURI()));
281 }
282 public int getLineNumber() {
283 return -1;
284 }
285 public int getColumnNumber() {
286 return -1;
287 }
288 };
289 }
290 };
291 StyleMap styleMap;
292 StyleSheet userAndUserAgentStyleSheet; {
293 StyleSheet s = (StyleSheet)ruleFactory.createStyleSheet().unlock();
294 if (userAndUserAgentStylesheets != null) {
295 StringTokenizer t = new StringTokenizer(userAndUserAgentStylesheets);
296 while (t.hasMoreTokens()) {
297 URL u = URLs.asURL(URLs.resolve(baseURI, URLs.asURI(t.nextToken())));
298 if (!cssReader.supportsMediaType(null, u))
299 logger.warn("Style sheet type not supported: " + u);
300 else
301 try {
302 s = parserFactory.append(new CSSSource(u, (Charset)null, (String)null), cssReader, s);
303 } catch (IOException e) {
304 logger.warn("Style sheet could not be parsed: " + u, e);
305 }
306 }
307 }
308 userAndUserAgentStyleSheet = s;
309 }
310 styleSheet = (StyleSheet)ruleFactory.createStyleSheet().unlock();
311 styleSheet.addAll(userAndUserAgentStyleSheet);
312 synchronized(JStyleParserCssCascader.class) {
313
314 CSSFactory.registerCSSParserFactory(parserFactory);
315 styleSheet = CSSFactory.getUsedStyles(document, nodeLocator, medium, cssReader, styleSheet);
316 }
317
318
319 XMLInputValue<?> transformed = null;
320 for (RuleXslt r : Iterables.filter(styleSheet, RuleXslt.class)) {
321 Map<QName,InputValue<?>> params = new HashMap<>();
322 for (Declaration d : r) {
323 List<Object> val = new ArrayList<>();
324 boolean invalid = false;
325 for (Term<?> t : d) {
326 if (t instanceof TermIdent || t instanceof TermString) {
327 String v = ((Term<String>)t).getValue();
328 if (t instanceof TermIdent && ("true".equals(v) || "false".equals(v)))
329 val.add(Boolean.valueOf(v));
330 else
331 val.add(v);
332 } else if (t instanceof TermInteger)
333 val.add(((TermInteger)t).getIntValue());
334 else {
335 logger.warn("@xslt parameter value must be a sequence of string, ident or integer, but got " + d);
336 invalid = true;
337 break;
338 }
339 }
340 if (!invalid)
341 params.put(new QName(d.getProperty()), new InputValue<>(val));
342 }
343 Document doc; {
344 if (transformed != null) {
345 Mult<? extends XMLInputValue<?>> m = transformed.mult(2);
346 doc = (Document)m.get().ensureSingleItem().asNodeIterator().next();
347 transformed = m.get();
348 } else
349 doc = document;
350 }
351 params.put(
352 new QName("style"),
353 new InputValue<>(
354 new StyleAccessor() {
355 StyleMap style = null;
356 public Optional<String> get(Element element, String property) {
357 if (style == null) {
358 StyleSheet s = (StyleSheet)ruleFactory.createStyleSheet().unlock();
359 s.addAll(userAndUserAgentStyleSheet);
360 synchronized(JStyleParserCssCascader.class) {
361
362 CSSFactory.registerCSSParserFactory(parserFactory);
363
364 s = CSSFactory.getUsedStyles(doc, nodeLocator, medium, cssReader, s);
365 }
366 style = new Analyzer(s, declarationTransformer, supportedCSS)
367 .evaluateDOM(doc, medium, true); }
368 NodeData data = style.get(element);
369 if (data != null) {
370 Term<?> value = data.getValue(property, true);
371 if (value != null)
372 return Optional.of(serializeValue(value));
373 else {
374 CSSProperty p = data.getProperty(property);
375 if (p != null)
376 return Optional.of(p.toString()); }}
377 return Optional.empty();
378 }
379
380 Map<String,List<CombinedSelector>> selectorCache = null;
381 public boolean matches(Element element, String selector) {
382 List<CombinedSelector> compiledSelector;
383 if (selectorCache == null)
384 selectorCache = new HashMap<>();
385 if (selectorCache.containsKey(selector))
386 compiledSelector = selectorCache.get(selector);
387 else {
388 compiledSelector = parserFactory.parseSelector(selector, r.namespaces);
389 if (compiledSelector != null)
390 selectorCache.put(selector, compiledSelector); }
391 if (compiledSelector == null)
392 return false;
393 for (CombinedSelector sel : compiledSelector) {
394 boolean match = false;
395 Combinator combinator = null;
396 for (int i = sel.size() - 1; i >= 0; i--) {
397 Selector s = sel.get(i);
398 if (combinator == null)
399 match = s.matches(element);
400 else if (combinator == Combinator.ADJACENT) {
401 Node adjacent = element.getPreviousSibling();
402 while (adjacent != null && !(adjacent instanceof Element))
403 adjacent = adjacent.getPreviousSibling();
404 match = adjacent != null && s.matches((Element)adjacent); }
405 else if (combinator == Combinator.PRECEDING) {
406 match = false;
407 Node preceding = element.getPreviousSibling();
408 while (!match) {
409 while (preceding != null && !(preceding instanceof Element))
410 preceding = preceding.getPreviousSibling();
411 if (preceding == null)
412 break;
413 match = s.matches((Element)preceding);
414 preceding = preceding.getPreviousSibling(); }}
415 else if (combinator == Combinator.DESCENDANT) {
416 match = false;
417 Node ancestor = element.getParentNode();
418 while (!match && ancestor != null) {
419 match = s.matches((Element)ancestor);
420 ancestor = ancestor.getParentNode(); }}
421 else if (combinator == Combinator.CHILD) {
422 Element parent = (Element)element.getParentNode();
423 match = parent != null && s.matches(parent); }
424 combinator = s.getCombinator();
425 if (!match)
426 return false;
427 }
428 if (match == true)
429 return true;
430 }
431 return false;
432 }
433 }
434 )
435 );
436 transformed = xsltProcessor.transform(
437 URLs.resolve(URLs.asURI(r.base), URLs.asURI(r.uri)),
438 transformed != null ? transformed : source.get(),
439 params);
440 }
441 if (transformed != null) {
442 node = transformed.ensureSingleItem().asNodeIterator().next();
443 if (!(node instanceof Document))
444 throw new TransformerException(
445 new RuntimeException("XsltProcessor must return a (single) document"));
446 document = (Document)node;
447
448
449
450 styleSheet = (StyleSheet)ruleFactory.createStyleSheet().unlock();
451 styleSheet.addAll(userAndUserAgentStyleSheet);
452 synchronized(JStyleParserCssCascader.class) {
453
454 CSSFactory.registerCSSParserFactory(parserFactory);
455 styleSheet = CSSFactory.getUsedStyles(document, nodeLocator, medium, cssReader, styleSheet);
456 }
457 }
458 styleMap = new Analyzer(styleSheet, declarationTransformer, supportedCSS).evaluateDOM(document, medium, true);
459 writer.setBaseURI(baseURI);
460 traverse(document, styleMap, writer);
461 } catch (TransformerException e) {
462 throw e;
463 } catch (Exception e) {
464 throw new TransformerException(e);
465 } finally {
466 styleSheet = null;
467 }
468 }
469
470 protected abstract Map<QName,String> serializeStyle(NodeData mainStyle, Map<PseudoElement,NodeData> pseudoStyles, Element context);
471
472 protected abstract String serializeValue(Term<?> value);
473
474 protected StyleSheet getParsedStyleSheet() {
475 if (styleSheet == null)
476 throw new UnsupportedOperationException();
477 return styleSheet;
478 }
479
480 protected void traverse(Node node, StyleMap styleMap, BaseURIAwareXMLStreamWriter writer) throws XMLStreamException {
481 if (node.getNodeType() == Node.DOCUMENT_NODE) {
482 writer.writeStartDocument();
483 for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling())
484 traverse(child, styleMap, writer);
485 writer.writeEndDocument(); }
486 else if (node.getNodeType() == Node.ELEMENT_NODE)
487 processElement((Element)node, styleMap, writer);
488 else if (node.getNodeType() == Node.COMMENT_NODE)
489 writeComment(writer, node);
490 else if (node.getNodeType() == Node.TEXT_NODE)
491 writeCharacters(writer, node);
492 else if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE)
493
494 writer.writeProcessingInstruction(((ProcessingInstruction)node).getTarget(), node.getNodeValue());
495 else
496 throw new UnsupportedOperationException("Unexpected node type");
497 }
498
499 protected void processElement(Element element, StyleMap styleMap, BaseURIAwareXMLStreamWriter writer) throws XMLStreamException {
500 writeStartElement(writer, element);
501 copyAttributes(element, writer);
502 Map<PseudoElement,NodeData> pseudoStyles = new HashMap<>(); {
503 for (PseudoElement pseudo : styleMap.pseudoSet(element))
504 pseudoStyles.put(pseudo, styleMap.get(element, pseudo)); }
505 Map<QName,String> style = serializeStyle(
506 styleMap.get(element),
507 pseudoStyles,
508 element);
509 if (style != null)
510 writeAttributes(writer, style);
511 for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling())
512 traverse(child, styleMap, writer);
513 writer.writeEndElement();
514 }
515
516 protected void copyAttributes(Element element, BaseURIAwareXMLStreamWriter writer) throws XMLStreamException {
517 NamedNodeMap attributes = element.getAttributes();
518 for (int i = 0; i < attributes.getLength(); i++) {
519 Node attr = attributes.item(i);
520 if (removeInlineStyleAttribute == null || !nodeNameEquals(attr, removeInlineStyleAttribute))
521 writeAttribute(writer, attr); }
522 }
523
524 private static boolean nodeNameEquals(Node node, QName name) {
525 if (name == null)
526 return false;
527 return new QName(node.getNamespaceURI(), node.getLocalName()).equals(name);
528 }
529 }