diff --git a/README.md b/README.md index 053501a..fa41d99 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,44 @@ Look at the simple example below to compile LESS to CSS: // Instantiate the LESS compiler LessCompiler lessCompiler = new LessCompiler(); - // Compile LESS input string to CSS output string + // Compile LESS input string to CSS output string. + // Note: When input is string it can not have imports to other less files String css = lessCompiler.compile("@color: #4D926F; #header { color: @color; }"); - - // Or compile LESS input file to CSS output file + + // Compile LESS input string to CSS output string by providing all imports + Map imports = new LinkedHashMap(); + imports.put( + "import1.less", + new LessSource(FileUtils + .readFileToString(toFile("import/less/import1.less")))); + imports.put( + "import4.less", + new LessSource(FileUtils + .readFileToString(toFile("import/less/import4.less")))); + + imports.put( + "import1/import1a.less", + new LessSource( + FileUtils + .readFileToString(toFile("import/less/import1/import1a.less")))); + + imports.put( + "import1/import1b.less", + new LessSource( + FileUtils + .readFileToString(toFile("import/less/import1/import1b.less")))); + + lessCompiler.compile(new LessSource(FileUtils + .readFileToString(toFile("import/less/import.less")), imports)); + + // Compile LESS input file to CSS output file lessCompiler.compile(new File("main.less"), new File("main.css")); + + // Providing latest envjs and rhinojs and custom logjs + lessCompiler.setLogJs(new File("tools/log.js").toURI().toURL()); + lessCompiler.setEnvJs(new File("tools/envjs.js").toURI().toURL()); + lessCompiler.setLessJs(new File("tools/less.js").toURI().toURL()); + LessCompiler is thread safe. In other words, an application only needs one LessCompiler that it can reuse whenever necessary. diff --git a/src/main/java/org/lesscss/LessCompiler.java b/src/main/java/org/lesscss/LessCompiler.java index 6b71866..9168817 100644 --- a/src/main/java/org/lesscss/LessCompiler.java +++ b/src/main/java/org/lesscss/LessCompiler.java @@ -34,21 +34,23 @@ /** * The LESS compiler to compile LESS sources to CSS stylesheets. *

- * The compiler uses Rhino (JavaScript implementation written in Java), Envjs - * (simulated browser environment written in JavaScript), and the official LESS + * The compiler uses Rhino (JavaScript implementation written in Java), Envjs + * (simulated browser environment written in JavaScript), and the official LESS * JavaScript compiler.
- * 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. + * 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. *

*

- * 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. + * 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. *

*

Basic code example:

+ * *
  * LessCompiler lessCompiler = new LessCompiler();
- * String css = lessCompiler.compile("@color: #4D926F; #header { color: @color; }");
+ * String css = lessCompiler
+ * 		.compile("@color: #4D926F; #header { color: @color; }");
  * 
* * @author Marcel Overdijk @@ -58,306 +60,397 @@ */ public class LessCompiler { - private static final String COMPILE_STRING = "function doIt(input, compress) { var result; var parser = new less.Parser(); parser.parse(input, function(e, tree) { if (e instanceof Object) { throw e; } ; result = tree.toCSS({compress: compress}); }); return result; }"; - - private static final Log log = 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 customJs = Collections.emptyList(); - private boolean compress = false; - private String encoding = null; - - private Function doIt; - - private Scriptable scope; - - /** - * Constructs a new LessCompiler. - */ - 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 synchronized void setEnvJs(URL envJs) { - if (scope != null) { - throw new IllegalStateException("This method can only be called before init()"); - } - this.envJs = envJs; - } - - /** - * Returns the LESS JavaScript file used by the compiler. - * COMPILE_STRING - * @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 The LESS JavaScript file used by the compiler. - */ - public synchronized void setLessJs(URL lessJs) { - if (scope != null) { - throw new IllegalStateException("This method can only be called before init()"); - } - this.lessJs = lessJs; - } - + private static final String COMPILE_STRING = "function doIt(input, compress) { var result; var parser = new less.Parser(); parser.parse(input, function(e, tree) { if (e instanceof Object) { throw e; } ; result = tree.toCSS({compress: compress}); }); return result; }"; + + private static final Log log = LogFactory.getLog(LessCompiler.class); + + private URL logJs = LessCompiler.class.getClassLoader().getResource( + "META-INF/log.js"); + + 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 customJs = Collections.emptyList(); + private boolean compress = false; + private String encoding = null; + + private Function doIt; + + private Scriptable scope; + + /** + * Constructs a new LessCompiler. + */ + 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 synchronized void setEnvJs(URL envJs) { + if (scope != null) { + throw new IllegalStateException( + "This method can only be called before init()"); + } + this.envJs = envJs; + } + /** + * Returns the LogJs JavaScript file used by the compiler. + * + * @return The LogJs JavaScript file used by the compiler. + */ + public URL getLogJs() { + return logJs; + } + + /** + * Sets the LogJs JavaScript file used by the compiler. Must be set before + * {@link #init()} is called. + * + * @param logJs + * The LogJs JavaScript file used by the compiler. + */ + public synchronized void setlogJs(URL logJs) { + if (scope != null) { + throw new IllegalStateException( + "This method can only be called before init()"); + } + this.logJs = logJs; + } + /** + * Returns the LESS JavaScript file used by the compiler. COMPILE_STRING + * + * @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 The + * LESS JavaScript file used by the compiler. + */ + public synchronized void setLessJs(URL lessJs) { + if (scope != null) { + throw new IllegalStateException( + "This method can only be called before init()"); + } + this.lessJs = lessJs; + } + + /** + * Returns the custom JavaScript files used by the compiler. + * + * @return The custom JavaScript files used by the compiler. + */ + public List getCustomJs() { + return Collections.unmodifiableList(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 synchronized void setCustomJs(URL customJs) { + if (scope != null) { + throw new IllegalStateException( + "This method can only be called before init()"); + } + this.customJs = Collections.singletonList(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 synchronized void setCustomJs(List customJs) { + if (scope != null) { + throw new IllegalStateException( + "This method can only be called before init()"); + } + // copy the list so there's no way for anyone else who holds a reference + // to the list to modify it + this.customJs = new ArrayList(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. Must be set before {@link #init()} + * is called. + * + * @param compress + * If true, sets the compiler to compress the CSS. + */ + public synchronized void setCompress(boolean compress) { + if (scope != null) { + throw new IllegalStateException( + "This method can only be called before init()"); + } + this.compress = compress; + } + + /** + * Returns the character encoding used by the compiler when writing the + * output File. + * + * @return The character encoding used by the compiler when writing the + * output File. + */ + public String getEncoding() { + return encoding; + } + + /** + * Sets the character encoding used by the compiler when writing the output + * File. If not set the platform default will be used. Must be + * set before {@link #init()} is called. + * + * @param The + * character encoding used by the compiler when writing the + * output File. + */ + public synchronized void setEncoding(String encoding) { + if (scope != null) { + throw new IllegalStateException( + "This method can only be called before init()"); + } + this.encoding = encoding; + } + + /** + * Initializes this LessCompiler. + *

+ * It is not needed to call this method manually, as it is called implicitly + * by the compile methods if needed. + *

+ */ + public synchronized void init() { + long start = System.currentTimeMillis(); + + try { + Context cx = Context.enter(); + cx.setOptimizationLevel(-1); + cx.setLanguageVersion(Context.VERSION_1_7); + + Global global = new Global(); + global.init(cx); + + scope = cx.initStandardObjects(global); + + List jsUrls = new ArrayList(2 + customJs.size()); + jsUrls.add(logJs); + jsUrls.add(envJs); + jsUrls.add(lessJs); + jsUrls.addAll(customJs); + + for (URL url : jsUrls) { + InputStreamReader inputStreamReader = new InputStreamReader(url + .openConnection().getInputStream()); + try { + cx.evaluateReader(scope, inputStreamReader, url.toString(), + 1, null); + } finally { + inputStreamReader.close(); + } + } + doIt = cx + .compileFunction(scope, COMPILE_STRING, "doIt.js", 1, null); + } catch (Exception e) { + String message = "Failed to initialize LESS compiler."; + log.error(message, e); + throw new IllegalStateException(message, e); + } finally { + Context.exit(); + } + + if (log.isDebugEnabled()) { + log.debug("Finished initialization of LESS compiler in " + + (System.currentTimeMillis() - start) + " ms."); + } + } + + /** + * Compiles the LESS input String to CSS. + * + * @param input + * The LESS input String to compile. + * @return The CSS. + */ + public String compile(String input) throws LessException { + synchronized (this) { + if (scope == null) { + init(); + } + } + + long start = System.currentTimeMillis(); + + try { + Context cx = Context.enter(); + Object result = doIt.call(cx, scope, null, new Object[] { input, + compress }); + + 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 = ScriptableObject.getProperty(value, + "message").toString(); + message = "When source is string it should not have imports to less files or use LessSource to construct source and also provide all imports to the constructor. " + + message; + throw new LessException(message, e); + } + } + throw new LessException(e); + } finally { + Context.exit(); + } + } + + /** + * Compiles the LESS input File to CSS. + * + * @param input + * The LESS input File to compile. + * @return The CSS. + * @throws 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 File to CSS and writes it to the + * specified output File. + * + * @param input + * The LESS input File to compile. + * @param output + * The output File to write the CSS to. + * @throws 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 File to CSS and writes it to the + * specified output File. + * + * @param input + * The LESS input File to compile. + * @param output + * The output File 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 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 LessSource to CSS. + * + * @param input + * The input LessSource to compile. + * @return The CSS. + */ + public String compile(LessSource input) throws LessException { + return compile(input.getNormalizedContent()); + } + + /** + * Compiles the input LessSource to CSS and writes it to the + * specified output File. + * + * @param input + * The input LessSource to compile. + * @param output + * The output File to write the CSS to. + * @throws 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); + } + /** - * Returns the custom JavaScript files used by the compiler. - * - * @return The custom JavaScript files used by the compiler. - */ - public List getCustomJs() { - return Collections.unmodifiableList(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 synchronized void setCustomJs(URL customJs) { - if (scope != null) { - throw new IllegalStateException("This method can only be called before init()"); - } - this.customJs = Collections.singletonList(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 synchronized void setCustomJs(List customJs) { - if (scope != null) { - throw new IllegalStateException("This method can only be called before init()"); - } - // copy the list so there's no way for anyone else who holds a reference to the list to modify it - this.customJs = new ArrayList(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. - * Must be set before {@link #init()} is called. - * - * @param compress If true, sets the compiler to compress the CSS. - */ - public synchronized void setCompress(boolean compress) { - if (scope != null) { - throw new IllegalStateException("This method can only be called before init()"); - } - this.compress = compress; - } - - /** - * Returns the character encoding used by the compiler when writing the output File. - * - * @return The character encoding used by the compiler when writing the output File. - */ - public String getEncoding() { - return encoding; - } - - /** - * Sets the character encoding used by the compiler when writing the output File. - * If not set the platform default will be used. - * Must be set before {@link #init()} is called. - * - * @param The character encoding used by the compiler when writing the output File. - */ - public synchronized void setEncoding(String encoding) { - if (scope != null) { - throw new IllegalStateException("This method can only be called before init()"); - } - this.encoding = encoding; - } - - /** - * Initializes this LessCompiler. - *

- * It is not needed to call this method manually, as it is called implicitly by the compile methods if needed. - *

- */ - public synchronized void init() { - long start = System.currentTimeMillis(); - - try { - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); - cx.setLanguageVersion(Context.VERSION_1_7); - - Global global = new Global(); - global.init(cx); - - scope = cx.initStandardObjects(global); - - List jsUrls = new ArrayList(2 + customJs.size()); - jsUrls.add(envJs); - jsUrls.add(lessJs); - jsUrls.addAll(customJs); - - for(URL url : jsUrls){ - InputStreamReader inputStreamReader = new InputStreamReader(url.openConnection().getInputStream()); - try{ - cx.evaluateReader(scope, inputStreamReader, url.toString(), 1, null); - }finally{ - inputStreamReader.close(); - } - } - doIt = cx.compileFunction(scope, COMPILE_STRING, "doIt.js", 1, null); - } - catch (Exception e) { - String message = "Failed to initialize LESS compiler."; - log.error(message, e); - throw new IllegalStateException(message, e); - }finally{ - Context.exit(); - } - - if (log.isDebugEnabled()) { - log.debug("Finished initialization of LESS compiler in " + (System.currentTimeMillis() - start) + " ms."); - } - } - - /** - * Compiles the LESS input String to CSS. - * - * @param input The LESS input String to compile. - * @return The CSS. - */ - public String compile(String input) throws LessException { - synchronized(this){ - if (scope == null) { - init(); - } - } - - long start = System.currentTimeMillis(); - - try { - Context cx = Context.enter(); - Object result = doIt.call(cx, scope, null, new Object[]{input, compress}); - - 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 = ScriptableObject.getProperty(value, "message").toString(); - throw new LessException(message, e); - } - } - throw new LessException(e); - }finally{ - Context.exit(); - } - } - - /** - * Compiles the LESS input File to CSS. - * - * @param input The LESS input File to compile. - * @return The CSS. - * @throws 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 File to CSS and writes it to the specified output File. - * - * @param input The LESS input File to compile. - * @param output The output File to write the CSS to. - * @throws 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 File to CSS and writes it to the specified output File. - * - * @param input The LESS input File to compile. - * @param output The output File 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 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 LessSource to CSS. - * - * @param input The input LessSource to compile. - * @return The CSS. - */ - public String compile(LessSource input) throws LessException { - return compile(input.getNormalizedContent()); - } - - /** - * Compiles the input LessSource to CSS and writes it to the specified output File. - * - * @param input The input LessSource to compile. - * @param output The output File to write the CSS to. - * @throws 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 LessSource to CSS and writes it to the specified output File. - * - * @param input The input LessSource to compile. - * @param output The output File to write the CSS to. - * @param force 'false' to only compile the input LessSource in case the LESS source has been modified (including imports) or the output file does not exists. - * @throws 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); - } - } + * Compiles the input LessSource to CSS and writes it to the + * specified output File. + * + * @param input + * The input LessSource to compile. + * @param output + * The output File to write the CSS to. + * @param force + * 'false' to only compile the input LessSource in + * case the LESS source has been modified (including imports) or + * the output file does not exists. + * @throws 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); + } + } } diff --git a/src/main/java/org/lesscss/LessSource.java b/src/main/java/org/lesscss/LessSource.java index 2ae8937..5838610 100644 --- a/src/main/java/org/lesscss/LessSource.java +++ b/src/main/java/org/lesscss/LessSource.java @@ -31,122 +31,171 @@ */ public class LessSource { - /** - * The Pattern used to match imported files. - */ - private static final Pattern IMPORT_PATTERN = Pattern.compile("^(?!\\s*//\\s*)@import\\s+(url\\()?\\s*\"(.+)\\s*\"(\\))?\\s*;.*$", MULTILINE); - - private File file; - private String content; - private String normalizedContent; - private Map imports = new LinkedHashMap(); - - /** - * Constructs a new LessSource. - *

- * This will read the metadata and content of the LESS source, and will automatically resolve the imports. - *

- * - * @param file The File reference to the LESS source to read. - * @throws FileNotFoundException If the LESS source (or one of its imports) could not be found. - * @throws IOException If the LESS source cannot be read. - */ - public LessSource(File file) throws FileNotFoundException, IOException { - if (file == null) { - throw new IllegalArgumentException("File must not be null."); - } - if (!file.exists()) { - throw new FileNotFoundException("File " + file.getAbsolutePath() + " not found."); - } - this.file = file; - this.content = this.normalizedContent = FileUtils.readFileToString(file); - resolveImports(); - } - - /** - * Returns the absolute pathname of the LESS source. - * - * @return The absolute pathname of the LESS source. - */ - public String getAbsolutePath() { - return file.getAbsolutePath(); - } - - /** - * Returns the content of the LESS source. - * - * @return The content of the LESS source. - */ - public String getContent() { - return content; - } - - /** - * Returns the normalized content of the LESS source. - *

- * The normalized content represents the LESS source as a flattened source - * where import statements have been resolved and replaced by the actual - * content. - *

- * - * @return The normalized content of the LESS source. - */ - public String getNormalizedContent() { - return normalizedContent; - } - - /** - * Returns the time that the LESS source was last modified. - * - * @return A long value representing the time the file was last modified, measured in milliseconds since the epoch (00:00:00 GMT, January 1, 1970). - */ - public long getLastModified() { - return file.lastModified(); - } - - /** - * Returns the time that the LESS source, or one of its imports, was last modified. - * - * @return A long value representing the time the file was last modified, measured in milliseconds since the epoch (00:00:00 GMT, January 1, 1970). - */ - public long getLastModifiedIncludingImports() { - long lastModified = getLastModified(); - for (Map.Entry entry : imports.entrySet()) { - LessSource importedLessSource = entry.getValue(); - long importedLessSourceLastModified = importedLessSource.getLastModifiedIncludingImports(); - if (importedLessSourceLastModified > lastModified) { - lastModified = importedLessSourceLastModified; - } - } - return lastModified; - } - - /** - * Returns the LESS sources imported by this LESS source. - *

- * The returned imports are represented by a - * Map<String, LessSource> which contains the filename and the - * LessSource. - *

- * - * @return The LESS sources imported by this LESS source. - */ - public Map getImports() { - return imports; - } - - private void resolveImports() throws FileNotFoundException, IOException { - Matcher importMatcher = IMPORT_PATTERN.matcher(normalizedContent); - while (importMatcher.find()) { - String importedFile = importMatcher.group(2); - importedFile = importedFile.matches(".*\\.(le?|c)ss$") ? importedFile : importedFile + ".less"; - boolean css = importedFile.matches(".*css$"); - if (!css) { - LessSource importedLessSource = new LessSource(new File(file.getParentFile(), importedFile)); - imports.put(importedFile, importedLessSource); - normalizedContent = normalizedContent.substring(0, importMatcher.start()) + importedLessSource.getNormalizedContent() + normalizedContent.substring(importMatcher.end()); - importMatcher = IMPORT_PATTERN.matcher(normalizedContent); - } - } - } + /** + * The Pattern used to match imported files. + */ + private static final Pattern IMPORT_PATTERN = Pattern + .compile( + "^(?!\\s*//\\s*)@import\\s+(url\\()?\\s*\"(.+)\\s*\"(\\))?\\s*;.*$", + MULTILINE); + + private File file; + private String content; + private String normalizedContent; + private Map imports = new LinkedHashMap(); + + /** + * Constructs a new LessSource. + *

+ * This will read the metadata and content of the LESS source, and will + * automatically resolve the imports. + *

+ * + * @param file + * The File reference to the LESS source to read. + * @throws FileNotFoundException + * If the LESS source (or one of its imports) could not be + * found. + * @throws IOException + * If the LESS source cannot be read. + */ + public LessSource(File file) throws FileNotFoundException, IOException { + if (file == null) { + throw new IllegalArgumentException("File must not be null."); + } + if (!file.exists()) { + throw new FileNotFoundException("File " + file.getAbsolutePath() + + " not found."); + } + this.file = file; + this.content = this.normalizedContent = FileUtils + .readFileToString(file); + resolveImports(); + } + + public LessSource(String source) { + if (source == null) { + throw new IllegalArgumentException("Source must not be null."); + } + this.content = this.normalizedContent = source; + } + + public LessSource(String source, Map imports) + throws FileNotFoundException, IOException { + if (source == null) { + throw new IllegalArgumentException("Source must not be null."); + } + this.content = this.normalizedContent = source; + this.imports.putAll(imports); + resolveImports(); + } + + /** + * Returns the absolute pathname of the LESS source. When the LessSource is + * given raw String then this method returns empty string + * + * @return The absolute pathname of the LESS source. + */ + public String getAbsolutePath() { + return file != null ? file.getAbsolutePath() : ""; + } + + /** + * Returns the content of the LESS source. + * + * @return The content of the LESS source. + */ + public String getContent() { + return content; + } + + /** + * Returns the normalized content of the LESS source. + *

+ * The normalized content represents the LESS source as a flattened source + * where import statements have been resolved and replaced by the actual + * content. + *

+ * + * @return The normalized content of the LESS source. + */ + public String getNormalizedContent() { + return normalizedContent; + } + + /** + * Returns the time that the LESS source was last modified. When the + * LessSource is given raw String then this method returns 0 + * + * @return A long value representing the time the file was last + * modified, measured in milliseconds since the epoch (00:00:00 GMT, + * January 1, 1970). + */ + public long getLastModified() { + return file != null ? file.lastModified() : 0; + } + + /** + * Returns the time that the LESS source, or one of its imports, was last + * modified. + * + * @return A long value representing the time the file was last + * modified, measured in milliseconds since the epoch (00:00:00 GMT, + * January 1, 1970). + */ + public long getLastModifiedIncludingImports() { + long lastModified = getLastModified(); + for (Map.Entry entry : imports.entrySet()) { + LessSource importedLessSource = entry.getValue(); + long importedLessSourceLastModified = importedLessSource + .getLastModifiedIncludingImports(); + if (importedLessSourceLastModified > lastModified) { + lastModified = importedLessSourceLastModified; + } + } + return lastModified; + } + + /** + * Returns the LESS sources imported by this LESS source. + *

+ * The returned imports are represented by a + * Map<String, LessSource> which contains the filename + * and the LessSource. + *

+ * + * @return The LESS sources imported by this LESS source. + */ + public Map getImports() { + return imports; + } + + private void resolveImports() throws FileNotFoundException, IOException { + Matcher importMatcher = IMPORT_PATTERN.matcher(normalizedContent); + while (importMatcher.find()) { + String importedFile = importMatcher.group(2); + importedFile = importedFile.matches(".*\\.(le?|c)ss$") ? importedFile + : importedFile + ".less"; + boolean css = importedFile.matches(".*css$"); + if (!css) { + LessSource importedLessSource = null; + if (imports.get(importedFile) != null) { + importedLessSource = imports.get(importedFile); + } else if (file != null) { + importedLessSource = new LessSource(new File( + file.getParentFile(), importedFile)); + imports.put(importedFile, importedLessSource); + } else { + throw new IllegalArgumentException( + "When source is string it should not have imports to less files or use LessSource to construct source and also provide all imports to the constructor. Provide " + + importedFile); + } + normalizedContent = normalizedContent.substring(0, + importMatcher.start()) + + importedLessSource.getNormalizedContent() + + normalizedContent.substring(importMatcher.end()); + importMatcher = IMPORT_PATTERN.matcher(normalizedContent); + } + } + } } diff --git a/src/main/resources/META-INF/env.rhino.js b/src/main/resources/META-INF/env.rhino.js index 47ea2ee..2351d01 100644 --- a/src/main/resources/META-INF/env.rhino.js +++ b/src/main/resources/META-INF/env.rhino.js @@ -1,8 +1,3 @@ -// Override the print function so that the messages go to commons logging -print = function(message) { - Packages.org.apache.commons.logging.LogFactory.getLog('rhino').debug(message); -}; - /* * Envjs core-env.1.2.13 * Pure JavaScript Browser Environment diff --git a/src/main/resources/META-INF/log.js b/src/main/resources/META-INF/log.js new file mode 100644 index 0000000..200a873 --- /dev/null +++ b/src/main/resources/META-INF/log.js @@ -0,0 +1,4 @@ +// Override the print function so that the messages go to commons logging +print = function(message) { + Packages.org.apache.commons.logging.LogFactory.getLog('rhino').debug(message); +}; \ No newline at end of file diff --git a/src/test/java/integration/CompileString.java b/src/test/java/integration/CompileString.java new file mode 100644 index 0000000..3b1be5b --- /dev/null +++ b/src/test/java/integration/CompileString.java @@ -0,0 +1,80 @@ +/* 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 integration; + +import static org.junit.Assert.assertEquals; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.junit.Test; +import org.lesscss.LessException; +import org.lesscss.LessSource; + +public class CompileString extends AbstractCompileIT { + + @Test(expected = LessException.class) + public void testCompileStringWithImports() throws Exception { + lessCompiler.compile(FileUtils + .readFileToString(toFile("import/less/import.less"))); + } + + @Test + public void testCompileStringWithImportsProvidingAllImports() + throws Exception { + String expected = FileUtils + .readFileToString(toFile("import/css/import.css")); + + Map imports = new LinkedHashMap(); + imports.put( + "import1.less", + new LessSource(FileUtils + .readFileToString(toFile("import/less/import1.less")))); + imports.put( + "import4.less", + new LessSource(FileUtils + .readFileToString(toFile("import/less/import4.less")))); + + imports.put( + "import1/import1a.less", + new LessSource( + FileUtils + .readFileToString(toFile("import/less/import1/import1a.less")))); + + imports.put( + "import1/import1b.less", + new LessSource( + FileUtils + .readFileToString(toFile("import/less/import1/import1b.less")))); + + String actual = lessCompiler.compile(new LessSource(FileUtils + .readFileToString(toFile("import/less/import.less")), imports)); + + assertEquals(expected.replace("\r\n", "\n"), actual); + } + + @Test(expected = IllegalArgumentException.class) + public void testCompileStringWithImportsProvidingFewImports() + throws Exception { + Map imports = new LinkedHashMap(); + imports.put( + "import1.less", + new LessSource(FileUtils + .readFileToString(toFile("import/less/import1.less")))); + lessCompiler.compile(new LessSource(FileUtils + .readFileToString(toFile("import/less/import.less")), imports)); + } +} diff --git a/src/test/java/org/lesscss/LessSourceTest.java b/src/test/java/org/lesscss/LessSourceTest.java index 502c5e0..15f964e 100644 --- a/src/test/java/org/lesscss/LessSourceTest.java +++ b/src/test/java/org/lesscss/LessSourceTest.java @@ -82,7 +82,8 @@ public void testNewLessSourceWithoutImports() throws Exception { @Test(expected = IllegalArgumentException.class) public void testNewLessSourceFileNull() throws Exception { - lessSource = new LessSource(null); + File file = null; + lessSource = new LessSource(file); } @Test(expected = FileNotFoundException.class)