1 package org.daisy.pipeline.css.sass.impl;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.io.InputStreamReader;
6 import java.io.Reader;
7 import java.io.UncheckedIOException;
8 import java.net.URI;
9 import java.net.URL;
10 import java.nio.charset.Charset;
11 import java.nio.charset.StandardCharsets;
12 import java.util.ArrayList;
13 import java.util.Collection;
14 import java.util.Comparator;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Set;
18 import java.util.TreeSet;
19
20 import javax.xml.parsers.DocumentBuilderFactory;
21 import javax.xml.parsers.ParserConfigurationException;
22 import javax.xml.transform.dom.DOMSource;
23 import javax.xml.transform.Source;
24 import javax.xml.transform.TransformerException;
25 import javax.xml.transform.URIResolver;
26 import javax.xml.transform.sax.SAXSource;
27 import javax.xml.transform.stream.StreamSource;
28
29 import com.google.common.collect.Iterables;
30 import com.google.common.io.CharStreams;
31
32 import cz.vutbr.web.css.NetworkProcessor;
33 import cz.vutbr.web.csskit.DefaultNetworkProcessor;
34 import cz.vutbr.web.csskit.antlr.CSSSource;
35 import cz.vutbr.web.csskit.antlr.CSSSourceReader;
36 import cz.vutbr.web.csskit.antlr.DefaultCSSSourceReader;
37 import cz.vutbr.web.domassign.GenericTreeWalker;
38 import cz.vutbr.web.csskit.antlr.CSSSourceReader.CSSInputStream;
39
40 import org.antlr.runtime.ANTLRReaderStream;
41 import org.antlr.runtime.CharStream;
42 import org.antlr.runtime.CommonTokenStream;
43 import org.antlr.runtime.RecognitionException;
44
45 import org.daisy.common.file.URLs;
46 import org.daisy.pipeline.css.Medium;
47 import org.daisy.pipeline.datatypes.DatatypeRegistry;
48
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 import org.w3c.dom.Attr;
53 import org.w3c.dom.CharacterData;
54 import org.w3c.dom.Document;
55 import org.w3c.dom.Element;
56 import org.w3c.dom.Node;
57 import org.w3c.dom.NodeList;
58 import org.w3c.dom.traversal.DocumentTraversal;
59 import org.w3c.dom.traversal.NodeFilter;
60 import org.w3c.dom.traversal.TreeWalker;
61
62 import org.xml.sax.InputSource;
63 import org.xml.sax.SAXException;
64
65 public class SassAnalyzer {
66
67 private static final Logger logger = LoggerFactory.getLogger(SassAnalyzer.class.getName());
68
69 private final Collection<Medium> media;
70 private final CSSSourceReader cssReader;
71 final DatatypeRegistry datatypes;
72
73 public SassAnalyzer(Collection<Medium> media, URIResolver uriResolver, DatatypeRegistry datatypes) {
74 this.media = media;
75 this.datatypes = datatypes;
76 NetworkProcessor network = new DefaultNetworkProcessor() {
77 @Override
78 public Reader fetch(URL url, Charset encoding, boolean forceEncoding, boolean assertEncoding)
79 throws IOException {
80
81 logger.debug("Fetching style sheet: " + url);
82 if (uriResolver != null) {
83 Source resolved; {
84 try {
85 resolved = uriResolver.resolve(URLs.asURI(url).toASCIIString(), ""); }
86 catch (TransformerException e) {
87 throw new IOException(e); }}
88 if (resolved != null) {
89 if (resolved instanceof StreamSource) {
90 InputStreamReader r = detectEncodingAndSkipBOM(
91 ((StreamSource)resolved).getInputStream(), null, encoding, forceEncoding);
92 if (assertEncoding) {
93 if (encoding == null)
94 throw new IllegalArgumentException("encoding must not be null");
95 if (!encoding.equals(getEncoding(r)))
96 throw new IOException("Failed to read URL as " + encoding + ": " + url);
97 }
98 return r;
99 } else {
100 url = new URL(resolved.getSystemId());
101 }
102 }
103 }
104 return super.fetch(url, encoding, forceEncoding, assertEncoding);
105 }
106 };
107 cssReader = new DefaultCSSSourceReader(network) {
108 @Override
109 public boolean supportsMediaType(String mediaType, URL url) {
110 if ("text/css".equals(mediaType) || "text/x-scss".equals(mediaType))
111 return true;
112 else if (mediaType == null
113 && (url == null || url.toString().endsWith(".css") || url.toString().endsWith(".scss")))
114 return true;
115 else
116 return false;
117 }
118
119 @Override
120 public CSSInputStream read(CSSSource source) throws IOException {
121 if (source.type == CSSSource.SourceType.URL) {
122 URL url = (URL)source.source;
123 if (url != null && !url.toString().endsWith(".css") && !url.toString().endsWith(".scss"))
124
125 try {
126 return read(new CSSSource(new URL(url.toString() + ".scss"), source.encoding, source.mediaType));
127 } catch (IOException e) {
128
129 try {
130 return read(new CSSSource(new URL(url.toString() + ".css"), source.encoding, source.mediaType));
131 } catch (IOException ee) {
132 }
133
134 try {
135 String fileName = url.toString();
136 fileName = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.length());
137 return read(new CSSSource(URLs.asURL(URLs.asURI(url).resolve("_" + fileName + ".scss")),
138 source.encoding,
139 source.mediaType));
140 } catch (IOException ee) {
141 }
142
143 try {
144 return read(new CSSSource(new URL(url.toString() + "/_index.scss"), source.encoding, source.mediaType));
145 } catch (IOException ee) {
146 }
147
148 throw e;
149 }
150 if (uriResolver != null) {
151 try {
152
153
154 Source resolved = uriResolver.resolve(URLs.asURI(url).toASCIIString(), "");
155 if (resolved != null)
156 source = new CSSSource(new URL(resolved.getSystemId()), source.encoding, source.mediaType);
157 } catch (TransformerException e) {
158 throw new IOException(e);
159 }
160 }
161 }
162 return super.read(source);
163 }
164 };
165 }
166
167 public Collection<SassVariable> getVariableDeclarations(Iterable<Source> userAndUserAgentStylesheets,
168 Source sourceDocument)
169 throws IOException {
170
171 List<CSSSource> stylesheets = new ArrayList<>();
172 for (Source s : userAndUserAgentStylesheets) {
173 URL base; {
174 String systemId = s.getSystemId();
175 if (systemId == null || "".equals(systemId))
176 base = null;
177 else {
178 URI baseURI = URLs.asURI(systemId);
179 if (isOpaque(baseURI) || !baseURI.isAbsolute())
180 throw new IllegalArgumentException("not an absolute hierarchical base URI: " + baseURI);
181 base = URLs.asURL(baseURI);
182 }
183 }
184 if (s instanceof StreamSource) {
185 StreamSource ss = (StreamSource)s;
186 Reader r = ss.getReader();
187 if (r == null)
188 r = new InputStreamReader(ss.getInputStream(), StandardCharsets.UTF_8);
189 stylesheets.add(new CSSSource(CharStreams.toString(r), (String)null, base, 0, 0));
190 } else {
191 if (base == null) {
192 InputSource is = SAXSource.sourceToInputSource(s);
193 if (is == null)
194 throw new IllegalArgumentException("unexpected source");
195 Reader r = is.getCharacterStream();
196 if (r == null) {
197 InputStream bs = is.getByteStream();
198 if (bs != null)
199 r = new InputStreamReader(bs, StandardCharsets.UTF_8);
200 else
201 throw new IllegalArgumentException("unexpected source: no content and no base URI");
202 }
203 stylesheets.add(new CSSSource(CharStreams.toString(r), (String)null, base, 0, 0));
204 } else
205 stylesheets.add(new CSSSource(base, StandardCharsets.UTF_8, null));
206 }
207 }
208 if (sourceDocument != null) {
209 try {
210 Document doc;
211 URL base; {
212 if (sourceDocument instanceof DOMSource && ((DOMSource)sourceDocument).getNode() instanceof Document) {
213 doc = (Document)((DOMSource)sourceDocument).getNode();
214 URI baseURI = URLs.asURI(doc.getBaseURI());
215 if (isOpaque(baseURI) || !baseURI.isAbsolute())
216 throw new IllegalArgumentException("not an absolute hierarchical base URI: " + baseURI);
217 base = URLs.asURL(baseURI);
218 } else {
219 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
220 factory.setNamespaceAware(true);
221 doc = factory.newDocumentBuilder().parse(SAXSource.sourceToInputSource(sourceDocument));
222 String systemId = sourceDocument.getSystemId();
223 if (systemId == null || "".equals(systemId))
224 base = null;
225 else {
226 URI baseURI = URLs.asURI(systemId);
227 if (isOpaque(baseURI) || !baseURI.isAbsolute())
228 throw new IllegalArgumentException("not an absolute hierarchical base URI: " + baseURI);
229 base = URLs.asURL(baseURI);
230 }
231 }
232 }
233 new Traversal(doc) {
234 @Override
235 protected void processElement(Element e) {
236 if ("style".equalsIgnoreCase(e.getNodeName())) {
237 Attr q = e.getAttributeNode("media");
238 if (Iterables.any(media, m -> m.matches(q != null ? q.getValue() : null))) {
239 Attr type = e.getAttributeNode("type");
240 String mediaType = type != null ? type.getValue().toLowerCase() : null;
241 if (cssReader.supportsMediaType(mediaType, null))
242 stylesheets.add(new CSSSource(extractElementText(e), mediaType, base, -1, -1));
243 }
244 } else if ("link".equalsIgnoreCase(e.getNodeName())) {
245 Attr rel = e.getAttributeNode("rel");
246 if (rel != null && rel.getValue().toLowerCase().contains("stylesheet")) {
247 Attr href = e.getAttributeNode("href");
248 if (href != null) {
249 Attr q = e.getAttributeNode("media");
250 if (Iterables.any(media, m -> m.matches(q != null ? q.getValue() : null))) {
251 URL url; {
252 try {
253 URI uri = URLs.asURI(href.getValue());
254 if (base != null)
255 uri = URLs.resolve(URLs.asURI(base), uri);
256 url = URLs.asURL(uri);
257 } catch (RuntimeException ex) {
258 throw new UncheckedIOException(new IOException(ex));
259 }
260 }
261 Attr type = e.getAttributeNode("type");
262 String mediaType = type != null ? type.getValue().toLowerCase() : null;
263 if (cssReader.supportsMediaType(mediaType, url))
264 stylesheets.add(new CSSSource(url, null, mediaType));
265 }
266 }
267 }
268 }
269 }
270 }.traverse();
271 } catch (UncheckedIOException e) {
272 throw e.getCause();
273 } catch (SAXException|ParserConfigurationException e) {
274 throw new IOException(e);
275 }
276 }
277 return getVariableDeclarations(stylesheets.toArray(new CSSSource[stylesheets.size()]));
278 }
279
280 Collection<SassVariable> getVariableDeclarations(CSSSource... stylesheets) throws IOException {
281 List<SassVariable> vars = new ArrayList<>();
282 try {
283 for (CSSSource source : stylesheets) {
284 CSSInputStream s = cssReader.read(source);
285 CharStream input = new ANTLRReaderStream(s.stream);
286 SassDocumentationLexer lexer = new SassDocumentationLexer(input);
287 CommonTokenStream tokens = new CommonTokenStream(lexer);
288 SassDocumentationParser parser = new SassDocumentationParser(tokens).init(
289 "text/x-scss".equals(source.mediaType) || (s.base != null && s.base.toString().endsWith(".scss")),
290 s.base != null ? URLs.asURI(s.base) : null,
291 media,
292 this);
293 vars.addAll(parser.variables());
294 }
295 } catch (RecognitionException e) {
296 throw new RuntimeException("Error happened while parsing the SCSS", e);
297 }
298
299 Set<SassVariable> unique = new TreeSet<>(Comparator.comparing(SassVariable::getName));
300 Iterator<SassVariable> i = vars.iterator();
301 while (i.hasNext()) {
302 SassVariable v = i.next();
303 if (!v.isDefault())
304 if (!unique.add(v))
305 i.remove(); }
306 i = vars.iterator();
307 while (i.hasNext()) {
308 SassVariable v = i.next();
309 if (v.isDefault())
310 if (!unique.add(v))
311 i.remove(); }
312 return vars;
313 }
314
315 private static abstract class Traversal {
316 private final TreeWalker walker;
317 protected Traversal(Document doc) {
318 if (doc instanceof DocumentTraversal)
319 walker = ((DocumentTraversal)doc).createTreeWalker(doc.getDocumentElement(), NodeFilter.SHOW_ELEMENT, null, false);
320 else
321 walker = new GenericTreeWalker(doc.getDocumentElement(), NodeFilter.SHOW_ELEMENT);
322 }
323 protected abstract void processElement(Element element);
324 public void traverse() {
325 Node current, checkpoint = null;
326 current = walker.getCurrentNode();
327 while (current != null) {
328 checkpoint = walker.getCurrentNode();
329 processElement((Element)current);
330 walker.setCurrentNode(checkpoint);
331 current = walker.nextNode();
332 }
333 }
334 }
335
336 private static String extractElementText(Element e) {
337 StringBuilder s = new StringBuilder();
338 NodeList children = e.getChildNodes();
339 for (int i = 0; i < children.getLength(); i++) {
340 Node child = children.item(i);
341 if (child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE)
342 s.append(((CharacterData)child).getData());
343 else if (child.getNodeType() == Node.COMMENT_NODE)
344 s.append("<!--").append(((CharacterData)child).getData()).append("-->");
345 }
346 return s.toString();
347 }
348
349 private static boolean isOpaque(URI uri) {
350 if (uri.toString().startsWith("jar:file:"))
351 return uri.isAbsolute() && !uri.getSchemeSpecificPart().substring(5).startsWith("/");
352 else
353 return uri.isOpaque();
354 }
355 }