blob: cec6e1b73d323992d4b6203a087104a2dbb1f31d [file] [log] [blame]
/*
* Copyright 2014 Google 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.
*/
package com.google.gwt.resources.converter;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.util.DefaultTextOutput;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.google.gwt.resources.css.GenerateCssAst;
import com.google.gwt.resources.css.ast.CssStylesheet;
import com.google.gwt.thirdparty.guava.common.base.Predicate;
import com.google.gwt.thirdparty.guava.common.base.Predicates;
import com.google.gwt.thirdparty.guava.common.base.Splitter;
import com.google.gwt.thirdparty.guava.common.collect.FluentIterable;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.google.gwt.thirdparty.guava.common.io.Files;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Converter from Css to Gss.
*/
public class Css2Gss {
private final URL cssFile;
private final TreeLogger treeLogger;
private final boolean lenient;
private PrintWriter printWriter;
private Map<String, String> defNameMapping;
private Predicate<String> simpleBooleanConditionPredicate;
private final Set<URL> scopeFiles;
public Css2Gss(String filePath) throws MalformedURLException {
this(new File(filePath).toURI().toURL(), false);
}
public Css2Gss(URL resource, boolean lenient) {
this(resource, lenient, Predicates.<String>alwaysFalse(), new HashSet<URL>());
}
public Css2Gss(URL resource, boolean lenient,
Predicate<String> simpleBooleanConditionPredicate, Set<URL> scopeFiles) {
cssFile = resource;
printWriter = new PrintWriter(System.err);
PrintWriterTreeLogger printWriterTreeLogger = new PrintWriterTreeLogger(printWriter);
printWriterTreeLogger.setMaxDetail(Type.WARN);
this.treeLogger = printWriterTreeLogger;
this.lenient = lenient;
this.simpleBooleanConditionPredicate = simpleBooleanConditionPredicate;
this.scopeFiles = scopeFiles;
}
public Css2Gss(URL fileUrl, TreeLogger treeLogger, boolean lenient,
Predicate<String> simpleBooleanConditionPredicate, Set<URL> scopeFiles) {
cssFile = fileUrl;
this.treeLogger = treeLogger;
this.lenient = lenient;
this.simpleBooleanConditionPredicate = simpleBooleanConditionPredicate;
this.scopeFiles = scopeFiles;
}
public Css2Gss(URL url, TreeLogger logger, boolean lenientConversion,
Predicate<String> simpleBooleanConditionPredicate) {
this(url, logger, lenientConversion, simpleBooleanConditionPredicate, new HashSet<URL>());
}
public String toGss() throws UnableToCompleteException {
try {
CssStylesheet sheet = GenerateCssAst.exec(treeLogger, cssFile);
DefCollectorVisitor defCollectorVisitor = new DefCollectorVisitor(lenient, treeLogger);
defCollectorVisitor.accept(sheet);
defNameMapping = defCollectorVisitor.getDefMapping();
addScopeDefs(scopeFiles, defNameMapping);
new UndefinedConstantVisitor(new HashSet<String>(defNameMapping.values()),
lenient, treeLogger).accept(sheet);
new ElseNodeCreator().accept(sheet);
new AlternateAnnotationCreatorVisitor().accept(sheet);
GssGenerationVisitor gssGenerationVisitor = new GssGenerationVisitor(
new DefaultTextOutput(false), defNameMapping, lenient, treeLogger,
simpleBooleanConditionPredicate);
gssGenerationVisitor.accept(sheet);
return gssGenerationVisitor.getContent();
} finally {
if (printWriter != null) {
printWriter.flush();
}
}
}
private void addScopeDefs(Set<URL> scopeFiles, Map<String, String> defNameMapping)
throws UnableToCompleteException {
for (URL fileName : scopeFiles) {
CssStylesheet sheet = GenerateCssAst.exec(treeLogger, fileName);
DefCollectorVisitor defCollectorVisitor = new DefCollectorVisitor(lenient, treeLogger);
defCollectorVisitor.accept(sheet);
defNameMapping.putAll(defCollectorVisitor.getDefMapping());
}
}
/**
* GSS allows only uppercase letters and numbers for a name of the constant. The constants
* need to be renamed in order to be compatible with GSS. This method returns a mapping
* between the old name and the new name compatible with GSS.
*/
public Map<String, String> getDefNameMapping() {
return defNameMapping;
}
public static void main(String... args) {
Options options = Options.parseOrQuit(args);
if (options.singleFile) {
try {
System.out.println(convertFile(options.resource, options.simpleBooleanConditions,
options.scopeFiles));
System.exit(0);
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
}
Collection<File> filesToConvert =
FileUtils.listFiles(options.resource, new String[] {"css"}, options.recurse);
for (File cssFile : filesToConvert) {
try {
if (doesCorrespondingGssFileExists(cssFile)) {
System.out.println(
"GSS file already exists - will not convert, file: " + cssFile.getAbsolutePath());
continue;
}
String gss = convertFile(cssFile, options.simpleBooleanConditions,
options.scopeFiles);
writeGss(gss, cssFile);
System.out.println("Converted " + cssFile.getAbsolutePath());
} catch (Exception e) {
System.err.println("Failed to convert " + cssFile.getAbsolutePath());
e.printStackTrace();
}
}
}
private static boolean doesCorrespondingGssFileExists(File cssFile) {
File gssFile = getCorrespondingGssFile(cssFile);
return gssFile.exists();
}
private static File getCorrespondingGssFile(File cssFile) {
String name = cssFile.getName();
assert name.endsWith(".css");
name = name.substring(0, name.length() - ".css".length()) + ".gss";
return new File(cssFile.getParentFile(), name);
}
private static void writeGss(String gss, File cssFile) throws IOException {
File gssFile = getCorrespondingGssFile(cssFile);
Files.asCharSink(gssFile, Charsets.UTF_8).write(gss);
}
private static String convertFile(File resource, Set<String> simpleBooleanConditions,
Set<URL> scopeFiles) throws MalformedURLException, UnableToCompleteException {
Predicate<String> simpleConditionPredicate;
if (simpleBooleanConditions != null) {
simpleConditionPredicate = Predicates.in(simpleBooleanConditions);
} else {
simpleConditionPredicate = Predicates.alwaysFalse();
}
return new Css2Gss(resource.toURI().toURL(), false, simpleConditionPredicate,
scopeFiles).toGss();
}
private static void printUsage() {
System.err.println("Usage :");
System.err.println("java " + Css2Gss.class.getName() + " [Options] [file or directory]");
System.err.println("Options:");
System.err.println(" -r -> Recursively convert all css files on the given directory"
+ "(leaves .css files in place)");
System.err.println(" -condition list_of_condition -> Specify a comma-separated list of " +
"variables that are used in conditionals and that will be mapped to configuration " +
"properties. The converter will not use the is() function when it will convert these " +
"conditions");
System.err.println(" -scope list_of_files -> Specify a comma-separated list of "
+ "css files to be used in this conversion to determine all defined variables");
}
static class Options {
static final Map<String, ArgumentConsumer> argumentConsumers;
static {
argumentConsumers = new LinkedHashMap<String, ArgumentConsumer>();
argumentConsumers.put("-r", new ArgumentConsumer() {
@Override
public boolean consume(Options option, String nextArg) {
option.recurse = true;
return false;
}
});
argumentConsumers.put("-condition", new ArgumentConsumer() {
@Override
public boolean consume(Options option, String nextArg) {
if (nextArg == null) {
quitEarly("-condition option must be followed by a comma separated list of conditions");
}
option.simpleBooleanConditions = FluentIterable.from(Splitter.on(',').split(nextArg))
.toSet();
return true;
}
});
argumentConsumers.put("-basedir", new ArgumentConsumer() {
@Override
public boolean consume(Options option, String nextArg) {
nextArg += nextArg.endsWith(File.separator) ? "" : File.separator;
option.baseDir = new File(nextArg);
if (!option.baseDir.exists() || !option.baseDir.isDirectory()) {
quitEarly("Basedir is does not exist");
}
return true;
}
});
argumentConsumers.put("-scope", new ArgumentConsumer() {
@Override
public boolean consume(Options option, String nextArg) {
option.scope = nextArg;
return true;
}
});
}
boolean recurse;
boolean singleFile;
ImmutableSet<URL> scopeFiles = ImmutableSet.of();
File resource;
Set<String> simpleBooleanConditions;
File baseDir;
private String scope;
private static Options parseOrQuit(String[] args) {
if (!validateArgs(args)) {
quitEarly(null);
}
Options options = new Options();
int index = 0;
// consume options
while (index < args.length - 1) {
String arg = args[index++];
String nextArg = index < args.length - 1 ? args[index] : null;
ArgumentConsumer consumer = argumentConsumers.get(arg);
if (consumer == null) {
quitEarly("Unknown argument: " + arg);
}
boolean skipNextArg = consumer.consume(options, nextArg);
if (skipNextArg) {
index++;
}
}
if (index == args.length) {
quitEarly("Missing file or directly as last parameter");
}
if (options.scope != null) {
ImmutableSet<String> scopeFileSet =
FluentIterable.from(Splitter.on(',').split(options.scope)).toSet();
HashSet<URL> set = new HashSet<URL>();
for (String scopeFile : scopeFileSet) {
File file = null;
if (options.baseDir != null && !scopeFile.startsWith(File.separator)) {
file = new File(options.baseDir, scopeFile).getAbsoluteFile();
} else {
file = new File(scopeFile).getAbsoluteFile();
}
if (!file.exists() && !file.isFile()) {
quitEarly("The scope file '" + file.getAbsolutePath() + "' does not exist");
}
try {
set.add(file.toURI().toURL());
} catch (MalformedURLException e) {
quitEarly("Can not create url for scope file: '" + scopeFile + "'");
}
}
options.scopeFiles = ImmutableSet.copyOf(set);
}
// last argument is always the file or directory path
if (options.baseDir != null && !args[index].startsWith(File.separator)) {
options.resource = new File(options.baseDir, args[index]).getAbsoluteFile();
} else {
options.resource = new File(args[index]).getAbsoluteFile();
}
options.singleFile = !options.resource.isDirectory();
// validate options
if (!options.resource.exists()) {
quitEarly("File or Directory does not exists: " + options.resource.getAbsolutePath());
}
if (options.recurse && !options.resource.isDirectory()) {
quitEarly("When using -r the last parameter needs to be a directory");
}
return options;
}
private static void quitEarly(String errorMsg) {
if (errorMsg != null) {
System.err.println("Error: " + errorMsg);
}
printUsage();
System.exit(-1);
}
private static boolean validateArgs(String[] args) {
return args.length > 0 && args.length < 9;
}
}
private interface ArgumentConsumer {
/**
*Returns true if the next argument has been consumed.
*/
boolean consume(Options option, String nextArg);
}
}