package com.github.sommeri.less4j.core; import java.io.File; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import com.github.sommeri.less4j.Less4jException; import com.github.sommeri.less4j.LessCompiler; import com.github.sommeri.less4j.LessSource; import com.github.sommeri.less4j.LessSource.CannotReadFile; import com.github.sommeri.less4j.LessSource.FileNotFound; import com.github.sommeri.less4j.core.ast.ASTCssNode; import com.github.sommeri.less4j.core.ast.StyleSheet; import com.github.sommeri.less4j.core.ast.VariableDeclaration; import com.github.sommeri.less4j.core.compiler.LessToCssCompiler; import com.github.sommeri.less4j.core.parser.ANTLRParser; import com.github.sommeri.less4j.core.parser.ANTLRParser.ParseResult; import com.github.sommeri.less4j.core.parser.ASTBuilder; import com.github.sommeri.less4j.core.parser.HiddenTokenAwareTree; import com.github.sommeri.less4j.core.problems.GeneralProblem; import com.github.sommeri.less4j.core.problems.ProblemsHandler; import com.github.sommeri.less4j.core.problems.UnableToFinish; import com.github.sommeri.less4j.platform.Constants; import com.github.sommeri.less4j.utils.CssPrinter; import com.github.sommeri.less4j.utils.PrintUtils; import com.github.sommeri.less4j.utils.URIUtils; public class ThreadUnsafeLessCompiler implements LessCompiler { private ProblemsHandler problemsHandler; private ASTBuilder astBuilder; private LessToCssCompiler compiler; private ANTLRParser parser = new ANTLRParser(); @Override public CompilationResult compile(String lessContent) throws Less4jException { return compile(new LessSource.StringSource(lessContent), null); } @Override public CompilationResult compile(String lessContent, Configuration options) throws Less4jException { return compile(new LessSource.StringSource(lessContent), options); } @Override public CompilationResult compile(File lessFile) throws Less4jException { LessSource.FileSource lessSource = new LessSource.FileSource(lessFile); return compile(lessSource, null); } @Override public CompilationResult compile(File lessFile, Configuration options) throws Less4jException { return compile(new LessSource.FileSource(lessFile, "utf-8"), options); } @Override public CompilationResult compile(URL lessURL) throws Less4jException { return compile(new LessSource.URLSource(lessURL)); } @Override public CompilationResult compile(URL lessURL, Configuration options) throws Less4jException { return compile(new LessSource.URLSource(lessURL), options); } @Override public CompilationResult compile(LessSource source) throws Less4jException { return compile(source, new Configuration()); } @Override public CompilationResult compile(LessSource source, Configuration options) throws Less4jException { if (options == null) options = new Configuration(); problemsHandler = new ProblemsHandler(); astBuilder = new ASTBuilder(problemsHandler); compiler = new LessToCssCompiler(problemsHandler, options); CompilationResult compilationResult = doCompile(source, options); if (problemsHandler.hasErrors()) { throw new Less4jException(problemsHandler.getErrors(), compilationResult); } return compilationResult; } private CompilationResult doCompile(LessSource source, Configuration options) throws Less4jException { StyleSheet lessStyleSheet = null; if (options != null && options.getCache() != null) { lessStyleSheet = (StyleSheet) options.getCache().getAst(source); if (lessStyleSheet != null) { lessStyleSheet = lessStyleSheet.clone(); // need to leave cached version unchanged } } if (lessStyleSheet == null) { ParseResult result = toAntlrTree(source); lessStyleSheet = astBuilder.parseStyleSheet(result.getTree()); if (options != null && options.getCache() != null) { options.getCache().setAst(source, lessStyleSheet); lessStyleSheet = lessStyleSheet.clone(); // need to leave cached version unchanged } } Map<String, HiddenTokenAwareTree> variables = toAntlrTree(options.getVariables()); List<VariableDeclaration> externalVariables = astBuilder.parseVariables(variables); lessStyleSheet.addMembers(externalVariables); lessStyleSheet.configureParentToAllChilds(); try { ASTCssNode cssStyleSheet = compiler.compileToCss(lessStyleSheet, source, options); CompilationResult compilationResult = createCompilationResult(cssStyleSheet, source, externalVariables, compiler.getImportedsources(), options); return compilationResult; } catch (UnableToFinish ex) { problemsHandler.unableToFinish(lessStyleSheet, ex); return createEmptyCompilationResult(); } } private ParseResult toAntlrTree(LessSource source) throws Less4jException { ParseResult result; try { result = parser.parseStyleSheet(source.getContent(), source); } catch (FileNotFound ex) { throw new Less4jException(new GeneralProblem("The file " + source + " does not exists."), new CompilationResult(null)); } catch (CannotReadFile ex) { throw new Less4jException(new GeneralProblem("Cannot read the file " + source + "."), new CompilationResult(null)); } if (result.hasErrors()) { CompilationResult compilationResult = new CompilationResult("Errors during parsing phase, partial result is not available."); throw new Less4jException(result.getErrors(), compilationResult); } return result; } private Map<String, HiddenTokenAwareTree> toAntlrTree(Map<String, String> variables) throws Less4jException { Map<String, HiddenTokenAwareTree> result = new HashMap<String, HiddenTokenAwareTree>(); List<Problem> problems = new ArrayList<Problem>(); for (Entry<String, String> entry : variables.entrySet()) { String nameStr = entry.getKey(); String valueStr = entry.getValue(); ParseResult valueParseResult = toAntlrExpressionTree(nameStr, valueStr); problems.addAll(valueParseResult.getErrors()); result.put(nameStr, valueParseResult.getTree()); } if (!problems.isEmpty()) { CompilationResult compilationResult = new CompilationResult("Errors parsing custom variables, partial result is not available."); throw new Less4jException(problems, compilationResult); } return result; } private ParseResult toAntlrExpressionTree(String dummySourceName, String expression) { LessSource source = new DummyLessSource(dummySourceName, expression); return parser.parseFullExpression(expression, source); } private CompilationResult createCompilationResult(ASTCssNode cssStyleSheet, LessSource lessSource, List<VariableDeclaration> externalVariables, Collection<LessSource> additionalSourceFiles, Configuration options) { LessSource cssDestination = options == null ? null : options.getCssResultLocation(); if (cssDestination == null) { String guessedCssName = URIUtils.changeSuffix(lessSource.getName(), Constants.CSS_SUFFIX); URI guessedURI = URIUtils.changeSuffix(lessSource.getURI(), Constants.CSS_SUFFIX); cssDestination = new LessSource.StringSource("", guessedCssName, guessedURI); } CssPrinter builder = new CssPrinter(lessSource, cssDestination, extractSources(externalVariables), additionalSourceFiles, options); builder.append(cssStyleSheet); StringBuilder css = builder.toCss(); String sourceMap = builder.toSourceMap(); handleSourceMapLink(cssStyleSheet, css, options, lessSource, sourceMap); CompilationResult compilationResult = new CompilationResult(css.toString(), sourceMap, problemsHandler.getWarnings()); return compilationResult; } private List<LessSource> extractSources(List<VariableDeclaration> externalVariables) { List<LessSource> result = new ArrayList<LessSource>(); for (VariableDeclaration variableDeclaration : externalVariables) { result.add(variableDeclaration.getSource()); } return result; } private CompilationResult createEmptyCompilationResult() { CompilationResult compilationResult = new CompilationResult("", null, problemsHandler.getWarnings()); return compilationResult; } private void handleSourceMapLink(ASTCssNode cssAst, StringBuilder css, Configuration options, LessSource source, String sourceMap) { String cssResultLocation = getCssResultLocationName(options, source); LessCompiler.SourceMapConfiguration sourceMapConfiguration = options.getSourceMapConfiguration(); if (!sourceMapConfiguration.shouldLinkSourceMap() && !sourceMapConfiguration.isInline()) return; if (!sourceMapConfiguration.isInline() && cssResultLocation == null) { problemsHandler.warnSourceMapLinkWithoutCssResultLocation(cssAst); return; } addNewLine(css); String commentText; String encodingCharset = sourceMapConfiguration.getEncodingCharset(); if (sourceMapConfiguration.isInline()) { String encodedSourceMap = PrintUtils.base64Encode(sourceMap, encodingCharset, problemsHandler, cssAst); commentText = "/*# sourceMappingURL=data:application/json;base64," + encodedSourceMap + " */"; } else { // compose linking comment String url = sourceMapConfiguration.getSourceMapNameGenerator().generateUrl(cssResultLocation); String encodedUrl = PrintUtils.urlEncode(url, encodingCharset, problemsHandler, cssAst); commentText = "/*# sourceMappingURL=" + encodedUrl + " */"; } css.append(commentText).append("\n"); } private void addNewLine(StringBuilder css) { if (css == null) return; int length = css.length(); if (length == 0) { css.append("\n"); return; } String endingSymbol = css.substring(length - 1); if ("\n".equals(endingSymbol)) return; css.append("\n"); } private String getCssResultLocationName(Configuration options, LessSource source) { LessSource location = options.getCssResultLocation(); String name = location == null ? null : location.getName(); if (name == null) name = URIUtils.changeSuffix(source.getName(), Constants.CSS_SUFFIX); return name; } } class DummyLessSource extends LessSource { private final String content; private final String name; public DummyLessSource(String name, String content) { super(); this.name = name; this.content = content; } @Override public LessSource relativeSource(String filename) throws FileNotFound, CannotReadFile, StringSourceException { return this; } @Override public String getContent() throws FileNotFound, CannotReadFile { return content; } @Override public byte[] getBytes() throws FileNotFound, CannotReadFile { return content == null ? null : content.getBytes(); } public String getName() { return name; } }