/* * 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); } }