This repository was archived by the owner on Jan 9, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 68
Expand file tree
/
Copy pathLessCompiler.java
More file actions
390 lines (350 loc) · 14.5 KB
/
LessCompiler.java
File metadata and controls
390 lines (350 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
/* 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.apache.commons.io.Charsets;
import org.apache.commons.io.FileUtils;
import org.lesscss.logging.LessLogger;
import org.lesscss.logging.LessLoggerFactory;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.tools.shell.Global;
/**
* 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 = "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 LessLogger logger = LessLoggerFactory.getLogger(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 outputEncoding = null;
private String inputEncoding = null;
private Function doIt;
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 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 lessJs 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<URL> 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<URL> 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<URL>(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 <code>true</code>, 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 outputEncoding used by the compiler when writing the output <code>File</code>.
*
* @return The character outputEncoding used by the compiler when writing the output <code>File</code>.
*/
public String getOutputEncoding() {
return outputEncoding;
}
/**
* Sets the character encoding used by the compiler when writing the output <code>File</code>.
* If not set the platform default will be used.
* Must be set before {@link #init()} is called.
*
* @param outputEncoding The character encoding used by the compiler when writing the output <code>File</code>.
*/
public synchronized void setOutputEncoding(String outputEncoding) {
if (scope != null) {
throw new IllegalStateException("This method can only be called before init()");
}
this.outputEncoding = outputEncoding;
}
/**
* Returns the character encoding used by the compiler when reading the input <code>File</code>.
*
* @return The character encoding used by the compiler when reading the input <code>File</code>.
*/
public String getInputEncoding() {
return inputEncoding;
}
/**
* Sets the character encoding used by the compiler when reading the input <code>File</code>.
* If not set the platform default will be used.
* Must be set before {@link #init()} is called.
*
* @param inputEncoding The character encoding used by the compiler when reading the input <code>File</code>.
*/
public synchronized void setInputEncoding(String inputEncoding) {
if (scope != null) {
throw new IllegalStateException("This method can only be called before init()");
}
this.inputEncoding = inputEncoding;
}
/**
* 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 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);
scope.put("logger", scope, Context.toObject(logger, scope));
List<URL> jsUrls = new ArrayList<URL>(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.";
logger.error(message, e);
throw new IllegalStateException(message, e);
}finally{
Context.exit();
}
if (logger.isDebugEnabled()) {
logger.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 {
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 (logger.isDebugEnabled()) {
logger.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 <code>File</code> to CSS.
*
* @param input The LESS input <code>File</code> 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, Charsets.toCharset(inputEncoding));
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 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 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, Charsets.toCharset(inputEncoding));
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 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 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, outputEncoding);
}
}
}