/**
* Copyright 2010 Bazaarvoice, Inc.
*
* 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.
*
* @author J. Ryan Stinnett (ryan.stinnett@bazaarvoice.com)
*/
package com.bazaarvoice.jless;
import com.bazaarvoice.jless.ast.node.Node;
import com.bazaarvoice.jless.ast.node.ScopeNode;
import com.bazaarvoice.jless.ast.visitor.FlattenNestedRuleSets;
import com.bazaarvoice.jless.ast.visitor.NestedMediaQueries;
import com.bazaarvoice.jless.ast.visitor.Printer;
import com.bazaarvoice.jless.exception.LessTranslationException;
import com.bazaarvoice.jless.parser.Parser;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import org.apache.commons.io.IOUtils;
import org.parboiled.Parboiled;
import org.parboiled.errors.ErrorUtils;
import org.parboiled.parserunners.ParseRunner;
import org.parboiled.parserunners.ReportingParseRunner;
import org.parboiled.support.DefaultValueStack;
import org.parboiled.support.ParsingResult;
import org.parboiled.support.ValueStack;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* LessProcessor is JLESS's parsing and translation engine that is used to convert input files that
* use the <a href="http://lesscss.org/">LESS</a> styling framework syntax into standard CSS.
* This implementation is based on the Ruby version of LESS by Alexis Sellier.
*
* At this time, the following LESS features are supported:
* <ul>
* <li>Variables</li>
* <li>Mixins</li>
* <li>Mixin Arguments</li>
* <li>Nesting</li>
* </ul>
*
* The following LESS features are not currently supported:
* <ul>
* <li>Operations</li>
* <li>Accessors</li>
* <li>Imports</li>
* </ul>
*
* This implementation does not attempt to generate the same output as the Ruby version.
* Translation differences from LESS Ruby include:
* <ul>
* <li>Equivalent adjacent rule sets are not grouped</li>
* <li>Empty rule sets are preserved</li>
* <li>Numbers and colors are not reformatted</li>
* </ul>
*
* This list only notes changes in the <em>translation</em> stage. See {@link com.bazaarvoice.jless.parser.Parser} for details
* on any changes to the <em>parsing</em> stage.
*
* @see com.bazaarvoice.jless.parser.Parser
*/
public class LessProcessor {
// Controls whether only parsing or both parsing and translation are performed.
private boolean _translationEnabled = true;
// Controls whether a compressed version of the output is printed.
// There is no performance penalty for enabling compression.
private boolean _compressionEnabled = false;
public boolean isTranslationEnabled() {
return _translationEnabled;
}
public void setTranslationEnabled(boolean translationEnabled) {
_translationEnabled = translationEnabled;
}
public boolean isCompressionEnabled() {
return _compressionEnabled;
}
public void setCompressionEnabled(boolean compressionEnabled) {
_compressionEnabled = compressionEnabled;
}
public Result process(InputStream input) throws IOException {
return process(null, input);
}
/**
* The {@link ScopeNode} from a parent's result is placed on the parser's {@link ValueStack} and joined
* together to allow for variable and mixin resolution across scopes.
* @return A printable {@link Result} of processing the given input.
*/
public Result process(Result parent, InputStream input) throws IOException {
ValueStack<Node> stack = new DefaultValueStack<Node>();
// Make the scope of each parent result accessible for variable and mixin resolution during parsing
ScopeNode parentScope = null;
if (parent != null) {
parentScope = parent.getScope();
stack.push(parentScope);
}
// Parse the input
ParseRunner<Node> parseRunner = new ReportingParseRunner<Node>(Parboiled.createParser(Parser.class, _translationEnabled).Document()).withValueStack(stack);
ParsingResult<Node> result = parseRunner.run(IOUtils.toString(input, "UTF-8"));
if (result.hasErrors()) {
throw new LessTranslationException("An error occurred while parsing a LESS input file:\n" +
ErrorUtils.printParseErrors(result));
}
// Retrieve the processed result
ScopeNode scope = (ScopeNode) stack.pop();
// Link the new scope to the last parent for later variable resolution
if (parentScope != null) {
scope.setParentScope(parentScope);
}
return new Result(scope);
}
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("You must specify an input file.");
System.exit(1);
}
LessProcessor translator = new LessProcessor();
String inputPath = null;
for (String arg : args) {
if (arg.equals("-c")) {
translator.setCompressionEnabled(true);
} else {
if (inputPath != null) {
System.err.println("Only one input file can be used.");
System.exit(1);
}
inputPath = arg;
}
}
try {
System.out.println(translator.process(new FileInputStream(inputPath)));
} catch (IOException e) {
System.err.println("Unable to read input file.");
}
}
public class Result {
private final ScopeNode _scope;
private final Supplier<String> _toStringSupplier;
public Result(ScopeNode scope) {
_scope = scope;
_toStringSupplier = Suppliers.memoize(new Supplier<String>() {
@Override
public String get() {
// Perform additional translation steps if needed
if (_translationEnabled) {
_scope.traverse(new NestedMediaQueries());
_scope.traverse(new FlattenNestedRuleSets());
}
// Print the output nodes
Printer printer = new Printer(_compressionEnabled);
_scope.traverse(printer);
return printer.toString();
}
});
}
public ScopeNode getScope() {
return _scope;
}
@Override
public String toString() {
return _toStringSupplier.get();
}
}
}