/*
* Copyright 2011 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.common.css.compiler.commandline;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.css.AbstractCommandLineCompiler;
import com.google.common.css.DefaultExitCodeHandler;
import com.google.common.css.ExitCodeHandler;
import com.google.common.css.GssFunctionMapProvider;
import com.google.common.css.JobDescription;
import com.google.common.css.JobDescription.InputOrientation;
import com.google.common.css.JobDescription.OutputOrientation;
import com.google.common.css.JobDescription.SourceMapDetailLevel;
import com.google.common.css.JobDescriptionBuilder;
import com.google.common.css.OutputRenamingMapFormat;
import com.google.common.css.SourceCode;
import com.google.common.css.Vendor;
import com.google.common.css.compiler.ast.ErrorManager;
import com.google.common.io.Files;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* {@link ClosureCommandLineCompiler} is the command-line compiler for Closure
* Stylesheets.
*
* @author bolinfest@google.com (Michael Bolin)
*/
public class ClosureCommandLineCompiler extends DefaultCommandLineCompiler {
protected ClosureCommandLineCompiler(JobDescription job,
ExitCodeHandler exitCodeHandler, ErrorManager errorManager) {
super(job, exitCodeHandler, errorManager);
}
@VisibleForTesting
static class Flags {
private static final String USAGE_PREAMBLE =
Joiner.on("\n").join(new String[] {
"Closure Stylesheets",
"",
"One or more CSS/GSS files must be supplied as inputs.",
"Output will be written to standard out unless --output_file is "
+ "specified.",
"",
"command line options:",
""
});
@Option(name = "--output-file", aliases = {"-o"},
usage = "The output CSS filename. If empty, standard output will be"
+ " used. The output is always UTF-8 encoded.")
private String outputFile = null;
@Option(name = "--input-orientation", usage =
"This specifies the display orientation the input files were written"
+ " for. You can choose between: LTR, RTL. LTR is the default and means"
+ " that the input style sheets were designed for use with left to"
+ " right display User Agents. RTL sheets are designed for use with"
+ " right to left UAs. Currently, all input files must have the same"
+ " orientation, as there is no way to specify the orientation on a"
+ " per-file or per-library basis.")
private InputOrientation inputOrientation = InputOrientation.LTR;
@Option(name = "--output-orientation", usage =
"Specify this option to perform automatic right to left conversion of"
+ " the input. You can choose between: LTR, RTL, NOCHANGE. NOCHANGE"
+ " means the input will not be changed in any way with respect to"
+ " direction issues. LTR outputs a sheet suitable for left to right"
+ " display and RTL outputs a sheet suitable for right to left"
+ " display. If the input orientation is different than the requested"
+ " output orientation, 'left' and 'right' values in direction"
+ " sensitive style rules are flipped. If the input already has the"
+ " desired orientation, this option effectively does nothing except"
+ " for defining GSS_LTR and GSS_RTL, respectively. The input is LTR"
+ " by default and can be changed with the input_orientation flag.")
private OutputOrientation outputOrientation = OutputOrientation.LTR;
@Option(name = "--pretty-print",
usage = "Whether to format the output with newlines and indents so that"
+ " it is more readable.")
private boolean prettyPrint = false;
@Option(name = "--output-renaming-map", usage = "The output from"
+ " the CSS class renaming. Provides a map of class names to what they"
+ " were renammed to.")
private String renameFile = null;
@Option(name = "--output-renaming-map-format", usage = "How to format the"
+ " output from the CSS class renaming.")
private OutputRenamingMapFormat outputRenamingMapFormat =
OutputRenamingMapFormat.JSON;
@Option(name = "--output-source-map", usage = "The source map output."
+ " Provides a mapping from the generated output to their original"
+ " source code location.")
private String sourceMapFile = "";
@Option(name = "--source_map_output_level", usage = "The level to generate "
+ "source maps. You could choose between DEFAULT, which will generate "
+ "source map only for selectors, blocks, rules, variables and symbol "
+ "mappings, and ALL, which outputs mappings for all elements.")
private SourceMapDetailLevel sourceMapLevel = SourceMapDetailLevel.DEFAULT;
@Option(name = "--copyright-notice",
usage = "Copyright notice to prepend to the output")
private String copyrightNotice = null;
@Option(name = "--define", usage = "Specifies the name of a true condition."
+ " The condition name can be used in @if boolean expressions."
+ " The conditions are ignored if GSS extensions are not enabled.")
private List<String> trueConditions = Lists.newArrayList();
@Option(name = "--allow-def-propagation", usage = "Allows @defs and @mixins"
+ " from one file to propagate to other files.")
private boolean allowDefPropagation = true;
@Option(name = "--allow-unrecognized-functions", usage =
"Allow unrecognized functions.")
private boolean allowUnrecognizedFunctions = false;
@Option(name = "--allowed-non-standard-function", usage =
"Specify a non-standard function to whitelist, like alpha()")
private List<String> allowedNonStandardFunctions = Lists.newArrayList();
@Option(name = "--allowed-unrecognized-property", usage =
"Specify an unrecognized property to whitelist")
private List<String> allowedUnrecognizedProperties = Lists.newArrayList();
@Option(name = "--allow-unrecognized-properties", usage =
"Allow unrecognized properties.")
private boolean allowUnrecognizedProperties = false;
@Option(name = "--vendor", usage =
"Creates browser-vendor-specific output by stripping all proprietary "
+ "browser-vendor properties from the output except for those "
+ "associated with this vendor.")
private Vendor vendor = null;
@Option(name = "--excluded-classes-from-renaming", usage =
"Pass the compiler a list of CSS class names that shoudn't be renamed.")
private List<String> excludedClassesFromRenaming = Lists.newArrayList();
// For enum values, args4j automatically lists all possible values when it
// prints the usage information for the flag, so including them in the usage
// message defined here would be redundant.
@Option(name = "--rename",
usage = "How CSS classes should be renamed. Defaults to NONE.")
private RenamingType renamingType = RenamingType.NONE;
@Option(name = "--gss-function-map-provider",
usage = "The fully qualified class name of a map provider of custom GSS"
+ " functions to resolve.")
private String gssFunctionMapProviderClassName =
"com.google.common.css.compiler.gssfunctions."
+ "DefaultGssFunctionMapProvider";
@Option(name = "--css-renaming-prefix",
usage = "Add a prefix to all renamed css class names.")
private String cssRenamingPrefix = "";
@Option(name = "--preserve-comments", usage =
"Preserve comments from sources into pretty printed output css.")
private boolean preserveComments = false;
@Option(name = "--const",
usage = "Specify integer constants to be used in for loops. Invoke for each const, e.g.: "
+ "--const=VAR1=VALUE1 --const=VAR2=VALUE2")
private Map<String, String> compileConstants = new HashMap<>();
@Option(name = "--preserve-important-comments", usage = "Preserve important comments from "
+ "sources into minified output css. Important comments are marked with "
+ "/*! */, @license, or @preserve.")
private boolean preserveImportantComments = false;
/**
* All remaining arguments are considered input CSS files.
*/
@Argument
private List<String> arguments = Lists.newArrayList();
/**
* @return a new {@link JobDescription} using this class's flag values
*/
@VisibleForTesting
JobDescription createJobDescription() {
JobDescriptionBuilder builder = new JobDescriptionBuilder();
builder.setInputOrientation(inputOrientation);
builder.setOutputOrientation(outputOrientation);
builder.setOutputFormat(prettyPrint
? JobDescription.OutputFormat.PRETTY_PRINTED
: JobDescription.OutputFormat.COMPRESSED);
builder.setCopyrightNotice(copyrightNotice);
builder.setTrueConditionNames(trueConditions);
builder.setAllowDefPropagation(allowDefPropagation);
builder.setAllowUnrecognizedFunctions(allowUnrecognizedFunctions);
builder.setAllowedNonStandardFunctions(allowedNonStandardFunctions);
builder.setAllowedUnrecognizedProperties(allowedUnrecognizedProperties);
builder.setAllowUnrecognizedProperties(allowUnrecognizedProperties);
builder.setVendor(vendor);
builder.setAllowKeyframes(true);
builder.setAllowWebkitKeyframes(true);
builder.setProcessDependencies(true);
builder.setExcludedClassesFromRenaming(excludedClassesFromRenaming);
builder.setSimplifyCss(true);
builder.setEliminateDeadStyles(true);
builder.setCssSubstitutionMapProvider(renamingType
.getCssSubstitutionMapProvider());
builder.setCssRenamingPrefix(cssRenamingPrefix);
builder.setPreserveComments(preserveComments);
builder.setOutputRenamingMapFormat(outputRenamingMapFormat);
builder.setCompileConstants(parseCompileConstants(compileConstants));
builder.setPreserveImportantComments(preserveImportantComments);
GssFunctionMapProvider gssFunctionMapProvider =
getGssFunctionMapProviderForName(gssFunctionMapProviderClassName);
builder.setGssFunctionMapProvider(gssFunctionMapProvider);
builder.setSourceMapLevel(sourceMapLevel);
builder.setCreateSourceMap(!Strings.isNullOrEmpty(sourceMapFile));
for (String fileName : arguments) {
File file = new File(fileName);
if (!file.exists()) {
throw new RuntimeException(String.format(
"Input file %s does not exist", fileName));
}
String fileContents;
try {
fileContents = Files.toString(file, UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
builder.addInput(new SourceCode(fileName, fileContents));
}
return builder.getJobDescription();
}
private OutputInfo createOutputInfo() {
return new OutputInfo(
(outputFile == null) ? null : new File(outputFile),
(renameFile == null) ? null : new File(renameFile),
(sourceMapFile == null) ? null : new File(sourceMapFile));
}
/**
* Parses the values in the compile constants to integers.
*/
private Map<String, Integer> parseCompileConstants(
Map<String, String> compileConstants) {
Map<String, Integer> parsedConstants = new HashMap<>(compileConstants.size());
for (Map.Entry<String, String> entry : compileConstants.entrySet()) {
parsedConstants.put(entry.getKey(), Integer.parseInt(entry.getValue()));
}
return parsedConstants;
}
}
/**
* @param gssFunctionMapProviderClassName such as
* "com.google.common.css.compiler.gssfunctions.DefaultGssFunctionMapProvider"
* @return a new instance of the {@link GssFunctionMapProvider} that
* corresponds to the specified class name, or a new instance of
* {@link com.google.common.css.compiler.gssfunctions.DefaultGssFunctionMapProvider}
* if the class name is {@code null}.
*/
private static GssFunctionMapProvider getGssFunctionMapProviderForName(
String gssFunctionMapProviderClassName) {
// Verify that a class with the given name exists.
Class<?> clazz;
try {
clazz = Class.forName(gssFunctionMapProviderClassName);
} catch (ClassNotFoundException e) {
throw new RuntimeException(String.format(
"Class does not exist: %s", gssFunctionMapProviderClassName), e);
}
// The class must implement GssFunctionMapProvider.
if (!GssFunctionMapProvider.class.isAssignableFrom(clazz)) {
throw new RuntimeException(String.format(
"%s does not implement GssFunctionMapProvider",
gssFunctionMapProviderClassName));
}
// Create the GssFunctionMapProvider using reflection.
try {
return (GssFunctionMapProvider) clazz.newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static class OutputInfo {
@Nullable public final File outputFile;
@Nullable public final File renameFile;
@Nullable public final File sourceMapFile;
private OutputInfo(File outputFile, File renameFile, File sourceMapFile) {
this.outputFile = outputFile;
this.renameFile = renameFile;
this.sourceMapFile = sourceMapFile;
}
}
private static void executeJob(JobDescription job, ExitCodeHandler exitCodeHandler,
OutputInfo outputInfo) {
CompilerErrorManager errorManager = new CompilerErrorManager();
ClosureCommandLineCompiler compiler =
new ClosureCommandLineCompiler(job, exitCodeHandler, errorManager);
String compilerOutput = compiler.execute(outputInfo.renameFile, outputInfo.sourceMapFile);
if (outputInfo.outputFile == null) {
System.out.print(compilerOutput);
} else {
try {
Files.write(compilerOutput, outputInfo.outputFile, UTF_8);
} catch (IOException e) {
AbstractCommandLineCompiler.exitOnUnhandledException(e, exitCodeHandler);
}
}
}
/**
* Processes the specified args to construct a corresponding
* {@link Flags}. If the args are invalid, prints an appropriate error
* message, invokes
* {@link ExitCodeHandler#processExitCode(int)}, and returns null.
*/
@VisibleForTesting
static @Nullable Flags parseArgs(String[] args,
ExitCodeHandler exitCodeHandler) {
Flags flags = new Flags();
CmdLineParser argsParser = new CmdLineParser(flags) {
@Override
public void printUsage(OutputStream out) {
PrintWriter writer = new PrintWriter(new OutputStreamWriter(out));
writer.write(Flags.USAGE_PREAMBLE);
// Because super.printUsage() creates its own PrintWriter to wrap the
// OutputStream, we call flush() on our PrinterWriter first to make sure
// that everything from this PrintWriter is written to the OutputStream
// before any usage information.
writer.flush();
super.printUsage(out);
}
};
try {
argsParser.parseArgument(args);
} catch (CmdLineException e) {
argsParser.printUsage(System.err);
exitCodeHandler.processExitCode(ERROR_MESSAGE_EXIT_CODE);
return null;
}
if (flags.arguments.isEmpty()) {
System.err.println("\nERROR: No input files specified.\n");
argsParser.printUsage(System.err);
exitCodeHandler.processExitCode(
AbstractCommandLineCompiler.ERROR_MESSAGE_EXIT_CODE);
return null;
} else {
return flags;
}
}
public static void main(String[] args) {
ExitCodeHandler exitCodeHandler = new DefaultExitCodeHandler();
Flags flags = parseArgs(args, exitCodeHandler);
if (flags == null) {
return;
}
JobDescription job = flags.createJobDescription();
OutputInfo info = flags.createOutputInfo();
executeJob(job, exitCodeHandler, info);
}
}