org.daisy.pipeline.css.JStyleParserCssCascader Maven / Gradle / Ivy
The newest version!
package org.daisy.pipeline.css;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.URL;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.function.Function;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.URIResolver;
import com.google.common.collect.Iterables;
import com.google.common.io.CharStreams;
import cz.vutbr.web.css.CombinedSelector;
import cz.vutbr.web.css.CSSFactory;
import cz.vutbr.web.css.CSSProperty;
import cz.vutbr.web.css.Declaration;
import cz.vutbr.web.css.MediaSpec;
import cz.vutbr.web.css.NetworkProcessor;
import cz.vutbr.web.css.NodeData;
import cz.vutbr.web.css.RuleFactory;
import cz.vutbr.web.css.Selector;
import cz.vutbr.web.css.Selector.Combinator;
import cz.vutbr.web.css.Selector.PseudoElement;
import cz.vutbr.web.css.SourceLocator;
import cz.vutbr.web.css.StyleSheet;
import cz.vutbr.web.css.SupportedCSS;
import cz.vutbr.web.css.Term;
import cz.vutbr.web.css.TermIdent;
import cz.vutbr.web.css.TermInteger;
import cz.vutbr.web.css.TermString;
import cz.vutbr.web.csskit.antlr.CSSParserFactory;
import cz.vutbr.web.csskit.antlr.CSSSource;
import cz.vutbr.web.csskit.antlr.CSSSourceReader;
import cz.vutbr.web.csskit.antlr.DefaultCSSSourceReader;
import cz.vutbr.web.csskit.antlr.SourceMap;
import cz.vutbr.web.csskit.DefaultNetworkProcessor;
import cz.vutbr.web.csskit.RuleXslt;
import cz.vutbr.web.domassign.Analyzer;
import cz.vutbr.web.domassign.DeclarationTransformer;
import cz.vutbr.web.domassign.StyleMap;
import net.sf.saxon.dom.NodeOverNodeInfo;
import net.sf.saxon.om.NodeInfo;
import org.daisy.common.file.URLs;
import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttributes;
import static org.daisy.common.stax.XMLStreamWriterHelper.writeCharacters;
import static org.daisy.common.stax.XMLStreamWriterHelper.writeComment;
import static org.daisy.common.stax.XMLStreamWriterHelper.writeProcessingInstruction;
import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
import org.daisy.common.transform.InputValue;
import org.daisy.common.transform.Mult;
import org.daisy.common.transform.SingleInSingleOutXMLTransformer;
import org.daisy.common.transform.TransformerException;
import org.daisy.common.transform.XMLInputValue;
import org.daisy.common.transform.XMLOutputValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
public abstract class JStyleParserCssCascader extends SingleInSingleOutXMLTransformer {
private final String userAndUserAgentStylesheets;
private final MediaSpec medium;
private final QName removeInlineStyleAttribute;
private final CSSParserFactory parserFactory;
private final RuleFactory ruleFactory;
private final SupportedCSS supportedCSS;
private final DeclarationTransformer declarationTransformer;
private final CSSSourceReader cssReader;
private final XsltProcessor xsltProcessor;
private static final Logger logger = LoggerFactory.getLogger(JStyleParserCssCascader.class);
/**
* @param ruleFactory to create StyleSheet objects
*/
public JStyleParserCssCascader(URIResolver uriResolver,
CssPreProcessor preProcessor,
XsltProcessor xsltProcessor,
String userAndUserAgentStylesheets,
Medium medium,
QName removeInlineStyleAttribute,
CSSParserFactory parserFactory,
RuleFactory ruleFactory,
SupportedCSS supportedCSS,
DeclarationTransformer declarationTransformer) {
this.userAndUserAgentStylesheets = userAndUserAgentStylesheets;
this.medium = medium.asMediaSpec();
this.removeInlineStyleAttribute = removeInlineStyleAttribute;
this.parserFactory = parserFactory;
this.ruleFactory = ruleFactory;
this.supportedCSS = supportedCSS;
this.declarationTransformer = declarationTransformer;
this.xsltProcessor = xsltProcessor;
NetworkProcessor network = new DefaultNetworkProcessor() {
@Override
public Reader fetch(URL url, Charset encoding, boolean forceEncoding, boolean assertEncoding) throws IOException {
logger.debug("Fetching style sheet: " + url);
if (uriResolver != null) {
Source resolved; {
try {
resolved = uriResolver.resolve(URLs.asURI(url).toASCIIString(), ""); }
catch (javax.xml.transform.TransformerException e) {
throw new IOException(e); }}
if (resolved != null) {
if (resolved instanceof StreamSource) {
InputStreamReader r = detectEncodingAndSkipBOM(
((StreamSource)resolved).getInputStream(), null, encoding, forceEncoding);
if (assertEncoding) {
if (encoding == null)
throw new IllegalArgumentException("encoding must not be null");
if (!encoding.equals(getEncoding(r)))
throw new IOException("Failed to read URL as " + encoding + ": " + url);
}
return r;
} else {
url = new URL(resolved.getSystemId());
}
}
}
return super.fetch(url, encoding, forceEncoding, assertEncoding);
}
};
/*
* CSSSourceReader that handles media types supported by preProcessor. Throws a
* IOException if something goes wrong when resolving the source or if the
* pre-processing fails.
*/
this.cssReader = new DefaultCSSSourceReader(network) {
@Override
public boolean supportsMediaType(String mediaType, URL url) {
if ("text/css".equals(mediaType))
return true;
else if (mediaType == null && (url == null || url.toString().endsWith(".css")))
return true;
else if (preProcessor == null)
return false;
else
return preProcessor.supportsMediaType(mediaType, url);
}
@Override
public CSSInputStream read(CSSSource source) throws IOException {
if (source.type == CSSSource.SourceType.URL && uriResolver != null) {
try {
// NetworkProcessor.fetch() also does resolve() but we need an additional resolve() to give
// CSSInputStream the correct base URL
Source resolved = uriResolver.resolve(URLs.asURI((URL)source.source).toASCIIString(), "");
if (resolved != null)
source = new CSSSource(new URL(resolved.getSystemId()), source.encoding, source.mediaType);
} catch (javax.xml.transform.TransformerException e) {
throw new IOException(e);
}
}
CSSInputStream cssStream = super.read(source);
if (!("text/css".equals(source.mediaType)
|| source.mediaType == null && (source.type != CSSSource.SourceType.URL
|| ((URL)source.source).toString().endsWith(".css")))) {
// preProcessor must be non-null
try {
CssPreProcessor.PreProcessingResult result = preProcessor.compile(
new CssPreProcessor.PreProcessingSource(cssStream.stream, URLs.asURI(cssStream.base)) {
@Override
public Reader reread(Charset encoding) throws IOException {
Reader r = cssStream.reread(encoding);
stream.close();
return r;
}
}
);
SourceMap sourceMap; {
if (result.sourceMap != null) {
SourceMap m = SourceMapReader.read(result.sourceMap, result.base);
if (cssStream.sourceMap != null) {
sourceMap = new SourceMap() {
public SourceLocator get(int line, int column) {
SourceLocator loc = m.get(line, column);
if (loc != null && loc.getURL().equals(cssStream.base))
loc = cssStream.sourceMap.get(loc.getLineNumber(), loc.getColumnNumber());
return loc;
}
public SourceLocator floor(int line, int column) {
SourceLocator loc = m.floor(line, column);
if (loc != null && loc.getURL().equals(cssStream.base))
loc = cssStream.sourceMap.floor(loc.getLineNumber(), loc.getColumnNumber());
return loc;
}
public SourceLocator ceiling(int line, int column) {
SourceLocator loc = m.ceiling(line, column);
if (loc != null && loc.getURL().equals(cssStream.base))
loc = cssStream.sourceMap.ceiling(loc.getLineNumber(), loc.getColumnNumber());
return loc;
}
};
} else
sourceMap = m;
} else
sourceMap = cssStream.sourceMap;
}
String resultString = CharStreams.toString(result.stream);
return new CSSInputStream(new StringReader(resultString), cssStream.base, sourceMap) {
@Override
public Reader reread(Charset encoding) throws IOException {
// assuming that the preprocessor has already handled @charset rules
// simply return the remainder of the stream
result.stream.close();
return new StringReader(resultString);
}
};
} catch (RuntimeException e) {
throw new IOException(
(source.mediaType != null ? (source.mediaType + " p") : "P")
+ "re-processing failed: " + e.getMessage(), e);
}
} else
return cssStream;
}
};
}
private StyleSheet styleSheet = null;
public Runnable transform(XMLInputValue> source, XMLOutputValue> result, InputValue> params) throws TransformerException {
if (source == null || result == null)
throw new TransformerException(new IllegalArgumentException());
return () -> transform(source.ensureSingleItem().mult(2), result.asXMLStreamWriter());
}
private void transform(Mult extends XMLInputValue>> source, BaseURIAwareXMLStreamWriter output) throws TransformerException {
Node node = source.get().asNodeIterator().next();
if (!(node instanceof Document))
throw new TransformerException(new IllegalArgumentException());
Document document = (Document)node;
BaseURIAwareXMLStreamWriter writer = output;
try {
URI baseURI = new URI(document.getBaseURI());
Function nodeLocator = n -> {
if (n instanceof NodeOverNodeInfo) {
NodeInfo info = ((NodeOverNodeInfo)n).getUnderlyingNodeInfo();
return new SourceLocator() {
public URL getURL() {
return URLs.asURL(URI.create(info.getBaseURI()));
}
public int getLineNumber() {
return info.getLineNumber();
}
public int getColumnNumber() {
return info.getColumnNumber();
}
};
} else {
return new SourceLocator() {
public URL getURL() {
return URLs.asURL(URI.create(n.getBaseURI()));
}
public int getLineNumber() {
return 0;
}
public int getColumnNumber() {
return 0;
}
};
}
};
StyleMap styleMap;
StyleSheet userAndUserAgentStyleSheet; {
StyleSheet s = (StyleSheet)ruleFactory.createStyleSheet().unlock();
if (userAndUserAgentStylesheets != null) {
StringTokenizer t = new StringTokenizer(userAndUserAgentStylesheets);
while (t.hasMoreTokens()) {
URL u = URLs.asURL(URLs.resolve(baseURI, URLs.asURI(t.nextToken())));
if (!cssReader.supportsMediaType(null, u))
logger.warn("Style sheet type not supported: " + u);
else
try {
s = parserFactory.append(new CSSSource(u, (Charset)null, (String)null), cssReader, s);
} catch (IOException e) {
logger.warn("Style sheet could not be parsed: " + u);
}
}
}
userAndUserAgentStyleSheet = s;
}
styleSheet = (StyleSheet)ruleFactory.createStyleSheet().unlock();
styleSheet.addAll(userAndUserAgentStyleSheet);
synchronized(JStyleParserCssCascader.class) {
// FIXME: CSSParserFactory injected in CSSAssignTraversal. in CSSFactory.getUsedStyles
CSSFactory.registerCSSParserFactory(parserFactory);
styleSheet = CSSFactory.getUsedStyles(document, nodeLocator, medium, cssReader, styleSheet);
}
// FIXME: use a dedicated parser to parse @xslt rules (and ignore all the rest)
XMLInputValue> transformed = null;
for (RuleXslt r : Iterables.filter(styleSheet, RuleXslt.class)) {
Map> params = new HashMap<>();
for (Declaration d : r) {
List