001package org.jszip.less;
002
003import org.apache.maven.plugin.logging.Log;
004import org.codehaus.plexus.util.FileUtils;
005import org.codehaus.plexus.util.IOUtil;
006import org.jszip.css.CssCompilationError;
007import org.jszip.css.CssEngine;
008import org.jszip.pseudo.io.PseudoFileSystem;
009import org.jszip.rhino.GlobalFunctions;
010import org.jszip.rhino.JavaScriptTerminationException;
011import org.jszip.rhino.MavenLogErrorReporter;
012import org.mozilla.javascript.Context;
013import org.mozilla.javascript.ContextFactory;
014import org.mozilla.javascript.Function;
015import org.mozilla.javascript.JavaScriptException;
016import org.mozilla.javascript.Script;
017import org.mozilla.javascript.Scriptable;
018import org.mozilla.javascript.ScriptableObject;
019import org.mozilla.javascript.tools.shell.Global;
020import org.mozilla.javascript.tools.shell.QuitAction;
021import org.mozilla.javascript.tools.shell.ShellContextFactory;
022
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.InputStreamReader;
028
029/**
030 * @author stephenc
031 * @since 31/01/2013 23:43
032 */
033public class LessEngine implements CssEngine {
034
035    private final PseudoFileSystem fs;
036    private final ContextFactory contextFactory;
037    private final Global global;
038    private final Scriptable scope;
039    private final Log log;
040    private final boolean lessCompress;
041    private final boolean showErrorExtracts;
042    private final Function function;
043    private final String encoding;
044
045    public LessEngine(PseudoFileSystem fs, String encoding, Log log, boolean lessCompress, File customLessScript,
046                      boolean showErrorExtracts) throws IOException {
047        this.fs = fs;
048        this.encoding = encoding;
049        this.lessCompress = lessCompress;
050        this.showErrorExtracts = showErrorExtracts;
051        this.contextFactory = new ShellContextFactory();
052        this.global = new Global();
053        this.log = log;
054        global.initQuitAction(new QuitAction() {
055            public void quit(Context context, int exitCode) {
056                if (exitCode != 0) {
057                    throw new JavaScriptTerminationException("Script exited with exit code of " + exitCode, exitCode);
058                }
059            }
060        });
061        if (!global.isInitialized()) {
062            global.init(contextFactory);
063        }
064        global.defineFunctionProperties(new String[]{"print", "debug", "warn", "quit", "readFile"},
065                GlobalFunctions.class,
066                ScriptableObject.DONTENUM);
067        final Context context = contextFactory.enterContext();
068        try {
069            context.setErrorReporter(new MavenLogErrorReporter(log));
070            context.putThreadLocal(Log.class, log);
071            global.defineProperty("arguments", new Object[0], ScriptableObject.DONTENUM);
072            scope = GlobalFunctions.createPseudoFileSystemScope(global, context);
073
074            compileScript(context, "less-env.js", null, "/org/jszip/less/less-env.js")
075                    .exec(context, scope);
076
077            // now load less-rhino.js
078
079            compileScript(context, "less-rhino.js", customLessScript, "/org/jszip/less/less-rhino.js")
080                    .exec(context, scope);
081
082            global.defineProperty("showErrorExtracts", showErrorExtracts, ScriptableObject.DONTENUM);
083
084            compileScript(context, "less-engine.js", null, "/org/jszip/less/less-engine.js")
085                    .exec(context, scope);
086
087            function = (Function) scope.get("engine", scope);
088
089        } finally {
090            fs.removeFromContext();
091            Context.exit();
092            context.putThreadLocal(Log.class, null);
093        }
094    }
095
096    public String mapName(String sourceFileName) {
097        return sourceFileName.replaceFirst("\\.[lL][eE][sS][sS]$", ".css");
098    }
099
100    public String toCSS(String name) throws CssCompilationError {
101
102        final Context context = contextFactory.enterContext();
103        try {
104            context.setErrorReporter(new MavenLogErrorReporter(log));
105            context.putThreadLocal(Log.class, log);
106            fs.installInContext();
107
108            GlobalFunctions.setExitCode(0);
109
110            final String result =
111                    (String) function.call(context, scope, scope, new Object[]{name, encoding, lessCompress});
112
113            // check for errors
114
115            final Integer exitCode = GlobalFunctions.getExitCode();
116            if (exitCode != 0) {
117                throw new CssCompilationError(name, -1, -1);
118            }
119            return result;
120        } catch (JavaScriptException e) {
121            if (e.getValue() instanceof Scriptable) {
122                Scriptable jse = (Scriptable) e.getValue();
123                int line = jse.has("line", jse) ? ((Number) jse.get("line", jse)).intValue() : -1;
124                int col = jse.has("col", jse) ? ((Number) jse.get("col", jse)).intValue() : -1;
125                throw new CssCompilationError(name, line, col, e);
126            }
127            throw new CssCompilationError(name, -1, -1, e);
128        } finally {
129            fs.removeFromContext();
130            Context.exit();
131            context.putThreadLocal(Log.class, null);
132        }
133    }
134
135    private Script compileScript(Context context, String scriptName, File customScriptFile,
136                                 String bundledScriptResource) throws IOException {
137        String source;
138        int lineNo = 0;
139        InputStream inputStream = null;
140        InputStreamReader reader = null;
141        try {
142            if (customScriptFile != null && customScriptFile.isFile()) {
143                log.debug("Using custom " + scriptName + " from: " + customScriptFile);
144                inputStream = new FileInputStream(customScriptFile);
145            } else {
146                log.debug("Using bundled " + scriptName);
147                inputStream = getClass().getResourceAsStream(bundledScriptResource);
148            }
149            source = IOUtil.toString(inputStream, "UTF-8");
150            if (source.startsWith("#!")) {
151                int i1 = source.indexOf('\n');
152                int i2 = source.indexOf('\r');
153                int index = (i1 == -1 || i2 == -1) ? Math.max(i1, i2) : Math.min(i1, i2);
154                if (index > 0) {
155                    source = source.substring(index);
156                    lineNo++;
157                }
158            }
159        } finally {
160            IOUtil.close(reader);
161            IOUtil.close(inputStream);
162        }
163        return context.compileString(source, scriptName, lineNo, null);
164    }
165
166}