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