/*
* Copyright 2008 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.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
/**
* MinimalSubstitutionMap is a SubstitutionMap that renames CSS classes to the
* shortest string possible.
*
* @author bolinfest@google.com (Michael Bolin)
*/
public class MinimalSubstitutionMap implements SubstitutionMap.Initializable {
/** Possible first chars in a CSS class name */
private static final char[] START_CHARS = {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
};
/** Possible non-first chars in a CSS class name */
private static final char[] CHARS = {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
};
/**
* Last value used with toShortString().
*/
private int lastIndex;
/**
* Characters that can be used at the start of a CSS class name.
*/
private final char[] startChars;
/**
* Characters that can be used in a CSS class name (though not necessarily as
* the first character).
*/
private final char[] chars;
/**
* Number of startChars.
*/
private final int startCharsRadix;
/**
* Number of chars.
*/
private final int charsRadix;
/**
* Value equal to Math.log(charsRadix). Stored as a field so it does not need
* to be recomputed each time toShortString() is invoked.
*/
private final double logCharsRadix;
/**
* Map of CSS classes that were renamed. Keys are original class names and
* values are their renamed equivalents.
*/
private final Map<String, String> renamedCssClasses;
/**
* A set of CSS class names that may not be output from this substitution map.
*/
private ImmutableSet<String> outputValueBlacklist;
public MinimalSubstitutionMap() {
this(ImmutableSet.<String>of());
}
/**
* @param outputValueBlacklist A set of CSS class names that may not be
* returned as the output from a substitution lookup.
*/
public MinimalSubstitutionMap(Set<String> outputValueBlacklist) {
this(START_CHARS, CHARS, outputValueBlacklist);
}
/**
* Creates a new MinimalSubstitutionMap that generates CSS class names from
* the specified set of characters.
* @param startChars Possible values for the first character of a CSS class
* name.
* @param chars Possible values for the characters other than the first
* character in a CSS class name.
*/
@VisibleForTesting
MinimalSubstitutionMap(char[] startChars, char[] chars) {
this(startChars, chars, ImmutableSet.<String>of());
}
/**
* Creates a new MinimalSubstitutionMap that generates CSS class names from
* the specified set of characters.
* @param startChars Possible values for the first character of a CSS class
* name.
* @param chars Possible values for the characters other than the first
* character in a CSS class name.
* @param outputValueBlacklist A set of CSS class names that may not be
* returned as the output from a substitution lookup.
*/
@VisibleForTesting
MinimalSubstitutionMap(
char[] startChars, char[] chars, Set<String> outputValueBlacklist) {
this.lastIndex = 0;
this.startChars = Arrays.copyOf(startChars, startChars.length);
this.startCharsRadix = this.startChars.length;
this.chars = Arrays.copyOf(chars, chars.length);
this.charsRadix = this.chars.length;
this.logCharsRadix = Math.log(charsRadix);
this.renamedCssClasses = Maps.newHashMap();
this.outputValueBlacklist =
ImmutableSet.copyOf(Preconditions.checkNotNull(outputValueBlacklist));
}
/** {@inheritDoc} */
@Override
public String get(String key) {
String value = renamedCssClasses.get(key);
if (value == null) {
do {
value = toShortString(lastIndex++);
} while (this.outputValueBlacklist.contains(value));
renamedCssClasses.put(key, value);
}
return value;
}
@Override
public void initializeWithMappings(Map<? extends String, ? extends String> m) {
Preconditions.checkState(renamedCssClasses.isEmpty());
this.outputValueBlacklist =
ImmutableSet.<String>builder().addAll(outputValueBlacklist).addAll(m.values()).build();
this.renamedCssClasses.putAll(m);
}
/**
* Converts a 32-bit integer to a unique short string whose first character
* is in {@link #START_CHARS} and whose subsequent characters, if any, are
* in {@link #CHARS}. The result is 1-6 characters in length.
* @param index The index into the enumeration of possible CSS class names
* given the set of valid CSS characters in this class.
* @return The CSS class name that corresponds to the index of the
* enumeration.
*/
@VisibleForTesting
String toShortString(int index) {
// Given the number of non-start characters, C, then for each start
// character, S, there will be:
// 1 one-letter CSS class name that starts with S
// C two-letter CSS class names that start with S
// C^2 three-letter CSS class names that start with S
// C^3 four-letter CSS class names that start with S
// and so on...
//
// That means that the number of non-start characters, n, in terms of i is
// defined as the greatest value of n that satisfies the following:
//
// 1 + C + C^2 + ... + C^(n - 1) <= i
//
// Substituting (C^n - 1) / (C - 1) for the geometric series, we get:
//
// (C^n - 1) / (C - 1) <= i
// (C^n - 1) <= i * (C - 1)
// C^n <= i * (C - 1) + 1
// log C^n <= log (i * (C - 1) + 1)
// n log C <= log (i * (C - 1) + 1)
// n <= log (i * (C - 1) + 1) / log C
//
// Because we are looking for the largest value of n that satisfies the
// inequality and we require n to be an integer, n can be expressed as:
//
// n = [[ log (i * (C - 1) + 1) / log C ]]
//
// where [[ x ]] is the greatest integer not exceeding x.
//
// Once n is known, the standard modulo-then-divide approach can be used to
// determine each character that should be appended to s.
int i = index / startCharsRadix;
final int n = (int) (Math.log(i * (charsRadix - 1) + 1) / logCharsRadix);
// The array is 1 more than the number of secondary chars to account for the
// first char.
char[] cssNameChars = new char[n + 1];
cssNameChars[0] = startChars[index % startCharsRadix];
for (int k = 1; k <= n; ++k) {
cssNameChars[k] = chars[i % charsRadix];
i /= charsRadix;
}
return new String(cssNameChars);
}
}