/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jooby.assets;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.jooby.MediaType;
import com.google.common.base.Splitter;
import com.google.common.io.ByteStreams;
import com.typesafe.config.Config;
import io.bit3.jsass.CompilationException;
import io.bit3.jsass.Compiler;
import io.bit3.jsass.Options;
import io.bit3.jsass.Output;
import io.bit3.jsass.OutputStyle;
import io.bit3.jsass.context.StringContext;
import io.bit3.jsass.importer.Import;
import io.bit3.jsass.importer.Importer;
import javaslang.control.Try;
/**
* <h1>sass</h1>
* <p>
* <a href="http://sass-lang.com/">sass-lang</a> implementation from
* <a href="https://github.com/bit3/jsass">Java sass compiler</a>. Sass is the most mature, stable,
* and powerful professional grade CSS extension language in the world.
* </p>
* <p>
* <a href="https://github.com/bit3/jsass">Java sass compiler</a> Feature complete java sass
* compiler using libsass.
* </p>
*
* <h2>usage</h2>
*
* <pre>
* assets {
* fileset {
* home: home.scss
* }
*
* pipeline {
* dev: [sass]
* dist: [sass]
* }
* }
* </pre>
*
* <h2>options</h2>
* <pre>
* assets {
* ...
* sass {
* syntax: scss
* dev {
* sourceMap: inline
* }
* dist {
* style: compressed
* }
* }
* }
* </pre>
*
* @author edgar
* @since 0.11.0
*/
public class Sass extends AssetProcessor {
static enum FileResolver implements Function<String, URI> {
FS {
@Override
public URI apply(final String it) {
File file = new File(it);
return file.exists() ? file.toURI() : null;
}
},
CLASSPATH {
@Override
public URI apply(final String it) {
URL resource = Sass.class.getResource(it);
return resource == null ? null : Try.of(resource::toURI).get();
}
};
}
static class SassImporter implements Importer {
private String ext;
private Function<String, URI> resolver;
public SassImporter(final String ext, final Function<String, URI> resolver) {
this.ext = ext;
this.resolver = resolver;
}
@Override
public Collection<Import> apply(final String url, final Import previous) {
String fname = nameWithExtension(url);
List<String> segments = Splitter.on('/').trimResults().omitEmptyStrings()
.splitToList(previous.getAbsoluteUri().toString());
String relative = segments.subList(0, segments.size() - 1).stream()
.collect(Collectors.joining("/", "/", ""));
return Arrays.asList(relative + fname, fname)
.stream()
.map(resolver::apply)
.filter(it -> it != null)
.findFirst()
.map(it -> {
String content = Try.of(() -> {
try (InputStream in = it.toURL().openStream()) {
return new String(ByteStreams.toByteArray(in), StandardCharsets.UTF_8);
}
}).get();
return Arrays.asList(new Import(it, it, content));
})
.orElse(null);
}
private String nameWithExtension(final String name) {
String filename = name;
if (!filename.endsWith(ext)) {
filename += "." + ext;
}
if (filename.charAt(0) != '/') {
return "/" + filename;
}
return filename;
}
}
static final Pattern LOCATION = Pattern.compile("\"(.+?)\":\\s+(\\d+)");
static final Function<String, URI> FS = it -> {
File file = new File(it);
return file.exists() ? file.toURI() : null;
};
static final Function<String, URI> CP = it -> {
URL resource = Sass.class.getResource(it);
return resource == null ? null : Try.of(resource::toURI).get();
};
public Sass() {
set("syntax", "scss");
set("style", "nested");
set("importer", "classpath");
set("indent", " ");
set("linefeed", "\n");
set("omitSourceMapUrl", false);
set("precision", 8);
set("sourceComments", false);
}
@Override
public boolean matches(final MediaType type) {
return MediaType.css.matches(type);
}
@Override
public String process(final String filename, final String source, final Config conf)
throws Exception {
String syntax = get("syntax");
FileResolver resolver = FileResolver.valueOf(get("importer").toString().toUpperCase());
OutputStyle style = OutputStyle.valueOf(get("style").toString().toUpperCase());
Options options = new Options();
options.setIsIndentedSyntaxSrc("sass".equals(syntax));
options.getImporters().add(new SassImporter(syntax, resolver));
options.setOutputStyle(style);
options.setIndent(get("indent"));
options.setLinefeed(get("linefeed"));
options.setOmitSourceMapUrl(get("omitSourceMapUrl"));
options.setPrecision(get("precision"));
options.setSourceComments(get("sourceComments"));
String sourcemap = get("sourcemap");
if ("inline".equals(sourcemap)) {
options.setSourceMapEmbed(true);
} else if ("file".equals(sourcemap)) {
options.setSourceMapFile(URI.create(filename + ".map"));
}
try {
URI input = URI.create(filename);
StringContext ctx = new StringContext(source, input, null, options);
Output output = new Compiler().compile(ctx);
return filename.endsWith(".map") ? output.getSourceMap() : output.getCss();
} catch (CompilationException x) {
Matcher matcher = LOCATION.matcher(x.getErrorJson());
Map<String, Integer> location = new HashMap<>();
while (matcher.find()) {
location.put(matcher.group(1), Integer.parseInt(matcher.group(2)));
}
int line = location.getOrDefault("line", -1);
int column = location.getOrDefault("column", -1);
AssetException aex = new AssetException(name(),
new AssetProblem(Optional.ofNullable(x.getErrorFile()).orElse(filename), line, column,
x.getErrorText(), null));
throw aex;
}
}
}