/* Copyright 2011-2012 The Apache Software Foundation. * * Licensed 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.lesscss; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.mozilla.javascript.Context; import org.mozilla.javascript.JavaScriptException; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.tools.shell.Global; import org.smartly.commons.logging.Logger; import org.smartly.commons.logging.util.LoggingUtils; import org.smartly.commons.util.FileUtils; /** * The LESS compiler to compile LESS sources to CSS stylesheets. * <p> * The compiler uses Rhino (JavaScript implementation written in Java), Envjs * (simulated browser environment written in JavaScript), and the official LESS * JavaScript compiler.<br /> * Note that the compiler is not a Java implementation of LESS itself, but rather * integrates the LESS JavaScript compiler within a Java/JavaScript browser * environment provided by Rhino and Envjs. * </p> * <p> * The compiler comes bundled with the Envjs and LESS JavaScript, so there is * no need to include them yourself. But if needed they can be overridden. * </p> * <h4>Basic code example:</h4> * <pre> * LessCompiler lessCompiler = new LessCompiler(); * String css = lessCompiler.compile("@color: #4D926F; #header { color: @color; }"); * </pre> * * @author Marcel Overdijk * @see <a href="http://lesscss.org/">LESS - The Dynamic Stylesheet language</a> * @see <a href="http://www.mozilla.org/rhino/">Rhino - JavaScript for Java</a> * @see <a href="http://www.envjs.com/">Envjs - Bringing the Browser</a> */ public class LessCompiler { private static final String COMPILE_STRING = "var result; var parser = new(less.Parser); parser.parse(input, function (e, tree) { if (e instanceof Object) { throw e } result = tree.toCSS({compress: %b}) });"; private Logger log = LoggingUtils.getLogger(LessCompiler.class); //LogFactory.getLog(LessCompiler.class); private URL envJs = LessCompiler.class.getClassLoader().getResource("META-INF/env.rhino.js"); private URL lessJs = LessCompiler.class.getClassLoader().getResource("META-INF/less.js"); private List<URL> customJs = Collections.emptyList(); private boolean compress = false; private String encoding = null; private Context cx; private Scriptable scope; /** * Constructs a new <code>LessCompiler</code>. */ public LessCompiler() { } /** * Returns the Envjs JavaScript file used by the compiler. * * @return The Envjs JavaScript file used by the compiler. */ public URL getEnvJs() { return envJs; } /** * Sets the Envjs JavaScript file used by the compiler. * Must be set before {@link #init()} is called. * * @param envJs The Envjs JavaScript file used by the compiler. */ public void setEnvJs(URL envJs) { this.envJs = envJs; } /** * Returns the LESS JavaScript file used by the compiler. * * @return The LESS JavaScript file used by the compiler. */ public URL getLessJs() { return lessJs; } /** * Sets the LESS JavaScript file used by the compiler. * Must be set before {@link #init()} is called. * * @param lessJs The LESS JavaScript file used by the compiler. */ public void setLessJs(URL lessJs) { this.lessJs = lessJs; } /** * Returns the custom JavaScript files used by the compiler. * * @return The custom JavaScript files used by the compiler. */ public List<URL> getCustomJs() { return customJs; } /** * Sets a single custom JavaScript file used by the compiler. * Must be set before {@link #init()} is called. * * @param customJs A single custom JavaScript file used by the compiler. */ public void setCustomJs(URL customJs) { this.customJs = new ArrayList<URL>(); this.customJs.add(customJs); } /** * Sets the custom JavaScript files used by the compiler. * Must be set before {@link #init()} is called. * * @param customJs The custom JavaScript files used by the compiler. */ public void setCustomJs(List<URL> customJs) { this.customJs = customJs; } /** * Returns whether the compiler will compress the CSS. * * @return Whether the compiler will compress the CSS. */ public boolean isCompress() { return compress; } /** * Sets the compiler to compress the CSS. * * @param compress If <code>true</code>, sets the compiler to compress the CSS. */ public void setCompress(boolean compress) { this.compress = compress; } /** * Returns the character encoding used by the compiler when writing the output <code>File</code>. * * @return The character encoding used by the compiler when writing the output <code>File</code>. */ public String getEncoding() { return encoding; } /** * Sets the character encoding used by the compiler when writing the output <code>File</code>. * If not set the platform default will be used. * * @param encoding The character encoding used by the compiler when writing the output <code>File</code>. */ public void setEncoding(String encoding) { this.encoding = encoding; } /** * Initializes this <code>LessCompiler</code>. * <p> * It is not needed to call this method manually, as it is called implicitly by the compile methods if needed. * </p> */ public void init() { long start = System.currentTimeMillis(); cx = Context.enter(); cx.setOptimizationLevel(-1); cx.setLanguageVersion(Context.VERSION_1_7); Global global = new Global(); global.init(cx); scope = cx.initStandardObjects(global); try { cx.evaluateReader(scope, new InputStreamReader(envJs.openConnection().getInputStream()), "env.rhino.js", 1, null); cx.evaluateReader(scope, new InputStreamReader(lessJs.openConnection().getInputStream()), "less.js", 1, null); for (URL url : customJs) { cx.evaluateReader(scope, new InputStreamReader(url.openConnection().getInputStream()), url.toString(), 1, null); } } catch (Exception e) { String message = "Failed to initialize LESS compiler."; log.error(message, e); throw new IllegalStateException(message, e); } if (log.isDebugEnabled()) { log.debug("Finished initialization of LESS compiler in " + (System.currentTimeMillis() - start) + " ms."); } } /** * Compiles the LESS input <code>String</code> to CSS. * * @param input The LESS input <code>String</code> to compile. * @return The CSS. */ public String compile(String input) throws LessException { if (cx == null) { init(); } long start = System.currentTimeMillis(); try { scope.put("input", scope, input); scope.put("result", scope, ""); cx.evaluateString(scope, String.format(COMPILE_STRING, compress), "compile.js", 1, null); Object result = scope.get("result", scope); if (log.isDebugEnabled()) { log.debug("Finished compilation of LESS source in " + (System.currentTimeMillis() - start) + " ms."); } return result.toString(); } catch (Exception e) { if (e instanceof JavaScriptException) { Scriptable value = (Scriptable)((JavaScriptException)e).getValue(); if (value != null && ScriptableObject.hasProperty(value, "message")) { String message = (String)ScriptableObject.getProperty(value, "message"); throw new LessException(message, e); } } throw new LessException(e); } } /** * Compiles the LESS input <code>File</code> to CSS. * * @param input The LESS input <code>File</code> to compile. * @return The CSS. * @throws java.io.IOException If the LESS file cannot be read. */ public String compile(File input) throws IOException, LessException { LessSource lessSource = new LessSource(input); return compile(lessSource); } /** * Compiles the LESS input <code>File</code> to CSS and writes it to the specified output <code>File</code>. * * @param input The LESS input <code>File</code> to compile. * @param output The output <code>File</code> to write the CSS to. * @throws java.io.IOException If the LESS file cannot be read or the output file cannot be written. */ public void compile(File input, File output) throws IOException, LessException { this.compile(input, output, true); } /** * Compiles the LESS input <code>File</code> to CSS and writes it to the specified output <code>File</code>. * * @param input The LESS input <code>File</code> to compile. * @param output The output <code>File</code> to write the CSS to. * @param force 'false' to only compile the LESS input file in case the LESS source has been modified (including imports) or the output file does not exists. * @throws java.io.IOException If the LESS file cannot be read or the output file cannot be written. */ public void compile(File input, File output, boolean force) throws IOException, LessException { LessSource lessSource = new LessSource(input); compile(lessSource, output, force); } /** * Compiles the input <code>LessSource</code> to CSS. * * @param input The input <code>LessSource</code> to compile. * @return The CSS. */ public String compile(LessSource input) throws LessException { return compile(input.getNormalizedContent()); } /** * Compiles the input <code>LessSource</code> to CSS and writes it to the specified output <code>File</code>. * * @param input The input <code>LessSource</code> to compile. * @param output The output <code>File</code> to write the CSS to. * @throws java.io.IOException If the LESS file cannot be read or the output file cannot be written. */ public void compile(LessSource input, File output) throws IOException, LessException { compile(input, output, true); } /** * Compiles the input <code>LessSource</code> to CSS and writes it to the specified output <code>File</code>. * * @param input The input <code>LessSource</code> to compile. * @param output The output <code>File</code> to write the CSS to. * @param force 'false' to only compile the input <code>LessSource</code> in case the LESS source has been modified (including imports) or the output file does not exists. * @throws java.io.IOException If the LESS file cannot be read or the output file cannot be written. */ public void compile(LessSource input, File output, boolean force) throws IOException, LessException { if (force || !output.exists() || output.lastModified() < input.getLastModifiedIncludingImports()) { String data = compile(input); FileUtils.writeStringToFile(output, data, encoding); } } }