com.google.common.css.OutputRenamingMapFormat Maven / Gradle / Ivy
Show all versions of closure-stylesheets Show documentation
/*
* 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;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.escape.CharEscaperBuilder;
import com.google.common.escape.Escaper;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
/**
* Defines the values for the --output-renaming-map-format flag in Closure
* Stylesheets.
*
* @author [email protected] (Michael Bolin)
*/
public enum OutputRenamingMapFormat {
/**
* Writes the mapping as JSON, passed as an argument to
* {@code goog.setCssNameMapping()}. Designed for use with the Closure
* Library in compiled mode.
*/
CLOSURE_COMPILED("goog.setCssNameMapping(%s);\n"),
/**
* Writes the mapping as JSON, passed as an argument to
* {@code goog.setCssNameMapping()} using the 'BY_WHOLE' mapping style.
* Designed for use with the Closure Library in compiled mode where the CSS
* name substitutions are taken as-is, which allows, e.g., using
* {@code SimpleSubstitutionMap} with class names containing hyphens.
*/
CLOSURE_COMPILED_BY_WHOLE("goog.setCssNameMapping(%s, 'BY_WHOLE');\n"),
/**
* Before writing the mapping as CLOSURE_COMPILED, split the css name maps by hyphens and write
* out each piece individually. see {@code CLOSURE_COMPILED}
*/
CLOSURE_COMPILED_SPLIT_HYPHENS("goog.setCssNameMapping(%s);\n") {
@Override
public void writeRenamingMap(Map renamingMap, Writer renamingMapWriter)
throws IOException {
super.writeRenamingMap(splitEntriesOnHyphens(renamingMap), renamingMapWriter);
}
},
/**
* Writes the mapping as JSON, assigned to the global JavaScript variable
* {@code CLOSURE_CSS_NAME_MAPPING}. Designed for use with the Closure
* Library in uncompiled mode.
*/
CLOSURE_UNCOMPILED("CLOSURE_CSS_NAME_MAPPING = %s;\n"),
/**
* Writes the mapping as JSON.
*/
JSON,
/**
* Writes the mapping in a .properties file format, such that it can be read
* by {@link Properties}.
*/
PROPERTIES {
@Override
public void writeRenamingMap(Map renamingMap, Writer renamingMapWriter)
throws IOException {
writeOnePerLine('=', renamingMap, renamingMapWriter);
// We write the properties directly rather than using
// Properties#store() because it is impossible to suppress the timestamp
// comment: http://goo.gl/6hsrN. As noted on the Stack Overflow thread,
// the timestamp results in unnecessary diffs between runs. Further, those
// who are using a language other than Java to parse this file should not
// have to worry about adding support for comments.
}
@Override
void readMapInto(
BufferedReader in, ImmutableMap.Builder super String, ? super String> builder)
throws IOException {
readOnePerLine('=', in, builder);
}
},
/**
* This is the current default behavior for output maps. Still used for
* legacy reasons.
*/
JSCOMP_VARIABLE_MAP {
@Override
public void writeRenamingMap(Map renamingMap, Writer renamingMapWriter)
throws IOException {
writeOnePerLine(':', renamingMap, renamingMapWriter);
}
@Override
void readMapInto(
BufferedReader in, ImmutableMap.Builder super String, ? super String> builder)
throws IOException {
readOnePerLine(':', in, builder);
}
};
private final String formatString;
private OutputRenamingMapFormat(String formatString) {
Preconditions.checkNotNull(formatString);
this.formatString = formatString;
}
private OutputRenamingMapFormat() {
this("%s");
}
/**
* Writes the renaming map.
*
* @see com.google.common.css.compiler.commandline.DefaultCommandLineCompiler
* #writeRenamingMap(Map, PrintWriter)
*/
public void writeRenamingMap(Map renamingMap, Writer renamingMapWriter)
throws IOException {
// Build up the renaming map as a JsonObject.
JsonObject properties = new JsonObject();
for (Map.Entry entry : renamingMap.entrySet()) {
properties.addProperty(entry.getKey(), entry.getValue());
}
// Write the JSON wrapped in this output format's formatString.
Gson gson = new GsonBuilder().setPrettyPrinting().create();
renamingMapWriter.write(String.format(formatString,
gson.toJson(properties)));
}
/**
* Like {@writeRenamingMap(java.util.Map, java.io.Writer)} but does not throw when writes fail.
*/
public final void writeRenamingMap(
Map renamingMap, PrintWriter renamingMapWriter) {
try {
writeRenamingMap(renamingMap, (Writer) renamingMapWriter);
} catch (IOException ex) {
throw (AssertionError) new AssertionError("IOException from PrintWriter").initCause(ex);
}
}
/**
* Reads the output of {@link #writeRenamingMap} so a renaming map can be reused from one compile
* to another.
*/
public ImmutableMap readRenamingMap(Reader in) throws IOException {
String subsitutionMarker = "%s";
int formatStringSubstitutionIndex = formatString.indexOf(subsitutionMarker);
Preconditions.checkState(formatStringSubstitutionIndex >= 0, formatString);
String formatPrefix = formatString.substring(0, formatStringSubstitutionIndex);
String formatSuffix =
formatString.substring(formatStringSubstitutionIndex + subsitutionMarker.length());
// GSON's JSONParser does not stop reading bytes when it sees a bracket that
// closes the value.
// We read the whole input in, then strip prefixes and suffixes and then parse
// the rest.
String content = CharStreams.toString(in);
content = content.trim();
formatPrefix = formatPrefix.trim();
formatSuffix = formatSuffix.trim();
if (!content.startsWith(formatPrefix)
|| !content.endsWith(formatSuffix)
|| content.length() < formatPrefix.length() + formatSuffix.length()) {
throw new IOException("Input does not match format " + formatString + " : " + content);
}
content = content.substring(formatPrefix.length(), content.length() - formatSuffix.length());
ImmutableMap.Builder b = ImmutableMap.builder();
BufferedReader br = new BufferedReader(new StringReader(content));
readMapInto(br, b);
requireEndOfInput(br);
return b.build();
}
/**
* Reads the mapping portion of the formatted output.
*
* This default implementation works for formats that substitute a JSON mapping from rewritten
* names to originals into their format string, and may be overridden by formats that do something
* different.
*/
void readMapInto(BufferedReader in, ImmutableMap.Builder super String, ? super String> builder)
throws IOException {
JsonElement json = new JsonParser().parse(in);
for (Map.Entry e : json.getAsJsonObject().entrySet()) {
builder.put(e.getKey(), e.getValue().getAsString());
}
}
/**
* Raises an IOException if there are any non-space characters on in, and consumes the remaining
* characters on in.
*/
private static void requireEndOfInput(BufferedReader in) throws IOException {
for (int ch; (ch = in.read()) >= 0; ) {
if (!Character.isSpace((char) ch)) {
throw new IOException("Expected end of input, not '" + escape((char) ch) + "'");
}
}
}
private static final Escaper ESCAPER =
new CharEscaperBuilder()
.addEscape('\t', "\\t")
.addEscape('\n', "\\n")
.addEscape('\r', "\\r")
.addEscape('\\', "\\\\")
.addEscape('\'', "\\'")
.toEscaper();
private static String escape(char ch) {
return ESCAPER.escape(new String(new char[] {ch}));
}
/** Splitter used for CLOSURE_COMPILED_SPLIT_HYPHENS format. */
private static final Splitter HYPHEN_SPLITTER = Splitter.on("-");
/**
* { "foo-bar": "f-b" }
=> { "foo": "f", "bar": "b" }
.
*
* @see SplittingSubstitutionMap
*/
private static Map splitEntriesOnHyphens(Map renamingMap) {
Map newSplitRenamingMap = Maps.newLinkedHashMap();
for (Map.Entry entry : renamingMap.entrySet()) {
Iterator keyParts = HYPHEN_SPLITTER.split(entry.getKey()).iterator();
Iterator valueParts = HYPHEN_SPLITTER.split(entry.getValue()).iterator();
while (keyParts.hasNext() && valueParts.hasNext()) {
String keyPart = keyParts.next();
String valuePart = valueParts.next();
String oldValuePart = newSplitRenamingMap.put(keyPart, valuePart);
// Splitting by part to make a simple map shouldn't involve mapping two old names
// to the same new name. It's ok the other way around, but the part relation should
// be a partial function.
Preconditions.checkState(oldValuePart == null || oldValuePart.equals(valuePart));
}
if (keyParts.hasNext()) {
throw new AssertionError(
"Not all parts of the original class "
+ "name were output. Class: "
+ entry.getKey()
+ " Next Part:"
+ keyParts.next());
}
if (valueParts.hasNext()) {
throw new AssertionError(
"Not all parts of the renamed class were "
+ "output. Class: "
+ entry.getKey()
+ " Renamed Class: "
+ entry.getValue()
+ " Next Part:"
+ valueParts.next());
}
}
return newSplitRenamingMap;
}
private static void writeOnePerLine(
char separator, Map renamingMap, Writer renamingMapWriter)
throws IOException {
for (Map.Entry entry : renamingMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
Preconditions.checkState(key.indexOf(separator) < 0);
Preconditions.checkState(key.indexOf('\n') < 0);
Preconditions.checkState(value.indexOf('\n') < 0);
renamingMapWriter.write(key);
renamingMapWriter.write(separator);
renamingMapWriter.write(value);
renamingMapWriter.write('\n');
}
}
private static void readOnePerLine(
char separator,
BufferedReader in,
ImmutableMap.Builder super String, ? super String> builder)
throws IOException {
for (String line; (line = in.readLine()) != null; ) {
int eq = line.indexOf(separator);
if (eq < 0 && !line.isEmpty()) {
throw new IOException("Line is missing a '" + separator + "': " + line);
}
builder.put(line.substring(0, eq), line.substring(eq + 1));
}
}
}