/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.sass.compiler.ruby.internal;
import com.liferay.sass.compiler.SassCompiler;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import org.jruby.RubyInstanceConfig;
import org.jruby.embed.LocalContextScope;
import org.jruby.embed.ScriptingContainer;
import org.jruby.embed.internal.LocalContextProvider;
/**
* @author David Truong
*/
public class RubySassCompiler implements AutoCloseable, SassCompiler {
public RubySassCompiler() throws Exception {
this(_PRECISION_DEFAULT);
}
public RubySassCompiler(int precision) throws Exception {
this(
_COMPILE_MODE_JIT, _COMPILE_THRESHOLD_DEFAULT, precision,
System.getProperty("java.io.tmpdir"));
}
public RubySassCompiler(
String compileMode, int compilerThreshold, int precision,
String tmpDirName)
throws Exception {
_precision = precision;
_tmpDirName = tmpDirName;
_scriptingContainer = new ScriptingContainer(
LocalContextScope.THREADSAFE);
LocalContextProvider localContextProvider =
_scriptingContainer.getProvider();
RubyInstanceConfig rubyInstanceConfig =
localContextProvider.getRubyInstanceConfig();
if (_COMPILE_MODE_FORCE.equals(compileMode)) {
rubyInstanceConfig.setCompileMode(
RubyInstanceConfig.CompileMode.FORCE);
}
else if (_COMPILE_MODE_JIT.equals(compileMode)) {
rubyInstanceConfig.setCompileMode(
RubyInstanceConfig.CompileMode.JIT);
}
List<String> loadPaths = new ArrayList<>();
loadPaths.add("META-INF/jruby.home/lib/ruby/site_ruby/1.8");
loadPaths.add("META-INF/jruby.home/lib/ruby/site_ruby/shared");
loadPaths.add("META-INF/jruby.home/lib/ruby/1.8");
loadPaths.add("gems/chunky_png-1.3.4/lib");
loadPaths.add("gems/compass-1.0.1/lib");
loadPaths.add("gems/compass-core-1.0.3/lib");
loadPaths.add("gems/compass-import-once-1.0.5/lib");
loadPaths.add("gems/ffi-1.9.10-java/lib");
loadPaths.add("gems/multi_json-1.11.2/lib");
loadPaths.add("gems/rb-fsevent-0.9.5/lib");
loadPaths.add("gems/rb-inotify-0.9.5/lib");
loadPaths.add("gems/sass-3.4.16/lib");
rubyInstanceConfig.setLoadPaths(loadPaths);
rubyInstanceConfig.setJitThreshold(compilerThreshold);
String rubyScript = null;
Class<?> clazz = getClass();
try (InputStream inputStream =
clazz.getResourceAsStream("dependencies/main.rb")) {
Scanner scanner = new Scanner(inputStream, "UTF-8");
scanner.useDelimiter("\\A");
rubyScript = scanner.next();
}
_scriptObject = _scriptingContainer.runScriptlet(rubyScript);
}
@Override
public void close() throws Exception {
_scriptingContainer.terminate();
}
@Override
public String compileFile(String inputFileName, String includeDirName)
throws RubySassCompilerException {
return compileFile(inputFileName, includeDirName, false, "");
}
@Override
public String compileFile(
String inputFileName, String includeDirName,
boolean generateSourceMap)
throws RubySassCompilerException {
return compileFile(
inputFileName, includeDirName, generateSourceMap, "");
}
@Override
public String compileFile(
String inputFileName, String includeDirName,
boolean generateSourceMap, String sourceMapFileName)
throws RubySassCompilerException {
try {
File inputFile = new File(inputFileName);
String includeDirNames =
includeDirName + File.pathSeparator + inputFile.getParent();
String outputFileName = _getOutputFileName(inputFileName);
if ((sourceMapFileName == null) || sourceMapFileName.equals("")) {
sourceMapFileName = outputFileName + ".map";
}
String[] results = _scriptingContainer.callMethod(
_scriptObject, "process",
new Object[] {
inputFileName, includeDirNames, _tmpDirName, false,
outputFileName, _precision, generateSourceMap,
sourceMapFileName
},
String[].class);
if (generateSourceMap) {
try {
_write(new File(sourceMapFileName), results[1]);
}
catch (Exception e) {
System.out.println("Unable to create source map");
}
}
return results[0];
}
catch (Exception e) {
throw new RubySassCompilerException(
"Unable to parse " + inputFileName);
}
}
@Override
public String compileString(String input, String includeDirName)
throws RubySassCompilerException {
return compileString(input, "", includeDirName, false);
}
@Override
public String compileString(
String input, String inputFileName, String includeDirName,
boolean generateSourceMap)
throws RubySassCompilerException {
return compileString(
input, inputFileName, includeDirName, generateSourceMap, "");
}
@Override
public String compileString(
String input, String inputFileName, String includeDirName,
boolean generateSourceMap, String sourceMapFileName)
throws RubySassCompilerException {
try {
if ((inputFileName == null) || inputFileName.equals("")) {
inputFileName = _tmpDirName + File.separator + "tmp.scss";
if (generateSourceMap) {
System.out.println("Source maps require a valid fileName");
generateSourceMap = false;
}
}
int index = inputFileName.lastIndexOf(File.separatorChar);
if ((index == -1) && (File.separatorChar != '/')) {
index = inputFileName.lastIndexOf('/');
}
index += 1;
String path = inputFileName.substring(0, index);
String fileName = inputFileName.substring(index);
String outputFileName = _getOutputFileName(fileName);
if ((sourceMapFileName == null) || sourceMapFileName.equals("")) {
sourceMapFileName = path + outputFileName + ".map";
}
File tempFile = new File(path, "tmp.scss");
tempFile.deleteOnExit();
_write(tempFile, input);
String output = compileFile(
tempFile.getCanonicalPath(), includeDirName, generateSourceMap,
sourceMapFileName);
if (generateSourceMap) {
File sourceMapFile = new File(sourceMapFileName);
String sourceMapContent = new String(
Files.readAllBytes(sourceMapFile.toPath()));
sourceMapContent = sourceMapContent.replaceAll(
"tmp\\.scss", fileName);
sourceMapContent = sourceMapContent.replaceAll(
"tmp\\.css", outputFileName);
_write(sourceMapFile, sourceMapContent);
}
return output;
}
catch (Exception e) {
throw new RubySassCompilerException(e);
}
}
private String _getOutputFileName(String fileName) {
return fileName.replaceAll("scss$", "css");
}
private void _write(File file, String string) throws IOException {
if (!file.exists()) {
File parentFile = file.getParentFile();
parentFile.mkdirs();
file.createNewFile();
}
try (Writer writer = new OutputStreamWriter(
new FileOutputStream(file, false), "UTF-8")) {
writer.write(string);
}
}
private static final String _COMPILE_MODE_FORCE = "force";
private static final String _COMPILE_MODE_JIT = "jit";
private static final int _COMPILE_THRESHOLD_DEFAULT = 5;
private static final int _PRECISION_DEFAULT = 5;
private final int _precision;
private final ScriptingContainer _scriptingContainer;
private final Object _scriptObject;
private final String _tmpDirName;
}