com.squarespace.less.match.InternPool Maven / Gradle / Ivy
The newest version!
package com.squarespace.less.match;
import static com.squarespace.less.model.Colors.TRANSPARENT;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.squarespace.less.core.LessUtils;
import com.squarespace.less.core.Pair;
import com.squarespace.less.model.Colors;
import com.squarespace.less.model.Combinator;
import com.squarespace.less.model.Dimension;
import com.squarespace.less.model.Keyword;
import com.squarespace.less.model.KeywordColor;
import com.squarespace.less.model.Node;
import com.squarespace.less.model.Property;
import com.squarespace.less.model.RGBColor;
import com.squarespace.less.model.TextElement;
import com.squarespace.less.model.Unit;
/**
* Intern pool contains a set of common values found across many stylesheets. For example
* the keyword "none" can occur many times in a stylesheet. When this keyword is encountered
* we need to construct a Keyword node to place it in the AST. Instead of allocating a new
* node we can do a fast lookup in the intern pool and reuse that instance multiple times.
*
* This saves us from two levels of memory allocation: copying substrings from the backing
* source file, and constructing the AST nodes that wrap them. It also avoids some deeper
* parsing for certain types, like decimal numbers and hex colors.
*
* TODO: we could pre-process these sets and serialize them faster to improve initialization time.
*/
public class InternPool {
// Arrays of instances
protected static final RGBColor[] COLORS_HEX;
protected static final RGBColor[] COLORS_KEYWORD;
protected static final Dimension[] DIMENSIONS;
protected static final TextElement[] NULL_ELEMENTS;
protected static final TextElement[] DESC_ELEMENTS;
protected static final TextElement[] CHILD_ELEMENTS;
protected static final TextElement[] NAMESPACE_ELEMENTS;
protected static final TextElement[] SIB_ADJ_ELEMENTS;
protected static final TextElement[] SIB_GEN_ELEMENTS;
protected static final String[] FUNCTIONS;
protected static final Node[] KEYWORDS;
protected static final Property[] PROPERTIES;
protected static final Unit[] UNITS;
// Double-array tries indexed to instances
protected static final DAT COLORS_HEX_DAT;
protected static final DAT COLORS_KEYWORD_DAT;
protected static final DAT DIMENSIONS_DAT;
protected static final DAT ELEMENT_DAT;
protected static final DAT FUNCTIONS_DAT;
protected static final DAT KEYWORD_DAT;
protected static final DAT PROPERTY_DAT;
protected static final DAT UNITS_DAT;
// Fast lookup of color names by their integer value
protected static final int[] COLOR_NAME_INDEX;
protected static final String[] COLOR_NAMES;
// Pattern for splitting value from units to construct Dimension instances
private static final Pattern RE_DIM = Pattern.compile("([-+\\d\\.]+)([%\\w]+)?");
static {
// long start = System.nanoTime();
try {
// Load interned values for the various syntax fragments
String[] _colors = load("colors.txt");
String[] _elements = load("elements.txt");
String[] _dimensions = load("dimensions.txt");
String[] _functions = load("functions.txt");
String[] _keywords = load("keywords.txt");
String[] _properties = load("properties.txt");
// Separate hex from keyword colors
List> colors_hex = buildColors(_colors, true);
List> colors_kwd = buildColors(_colors, false);
// Copy color keywords and merge in plain keywords
List> keywords = buildKeywords(colors_kwd, _keywords);
// Construct syntax tree nodes or strings we'll use at runtime
COLORS_HEX = colors_hex.stream().map(e -> e.val()).toArray(RGBColor[]::new);
COLORS_KEYWORD = colors_kwd.stream().map(e -> e.val()).toArray(RGBColor[]::new);
DIMENSIONS = buildDimensions(_dimensions).stream().toArray(Dimension[]::new);
NULL_ELEMENTS = withCombinator(null, _elements);
DESC_ELEMENTS = withCombinator(Combinator.DESC, _elements);
CHILD_ELEMENTS = withCombinator(Combinator.CHILD, _elements);
NAMESPACE_ELEMENTS = withCombinator(Combinator.NAMESPACE, _elements);
SIB_ADJ_ELEMENTS = withCombinator(Combinator.SIB_ADJ, _elements);
SIB_GEN_ELEMENTS = withCombinator(Combinator.SIB_GEN, _elements);
FUNCTIONS = _functions;
KEYWORDS = keywords.stream().map(e -> e.val()).toArray(Node[]::new);
PROPERTIES = Arrays.stream(_properties).map(s -> new Property(s)).toArray(Property[]::new);
UNITS = Unit.values();
// Build the double-array tries for fast lookups
COLORS_HEX_DAT = build(colors_hex.stream().map(e -> e.key()).collect(Collectors.toList()));
COLORS_KEYWORD_DAT = build(colors_kwd.stream().map(e -> e.key()).collect(Collectors.toList()));
DIMENSIONS_DAT = build(Arrays.asList(_dimensions));
ELEMENT_DAT = build(Arrays.asList(_elements));
FUNCTIONS_DAT = build(Arrays.asList(_functions));
KEYWORD_DAT = build(keywords.stream().map(e -> e.key()).collect(Collectors.toList()));
PROPERTY_DAT = build(Arrays.asList(_properties));
UNITS_DAT = build(Arrays.stream(UNITS).map(u -> u.repr()).collect(Collectors.toList()));
// Binary search of color integer values to the corresponding names.
List> named = buildNamedColors(colors_kwd);
COLOR_NAME_INDEX = named.stream().mapToInt(e -> e.key().intValue()).toArray();
COLOR_NAMES = named.stream().map(e -> e.val()).toArray(String[]::new);
} catch (IOException e) {
throw new RuntimeException("Interning raised an error", e);
}
// long elapsed = System.nanoTime() - start;
// System.err.println("intern pool initialized in " + (elapsed / 1000000) + " ms");
}
/**
* Lookup a Property in the intern pool or construct a new one.
*/
public Property property(String raw, int start, int end) {
int ix = PROPERTY_DAT.get(raw, start, end);
if (ix == -1) {
return new Property(raw.substring(start, end));
}
return PROPERTIES[ix];
}
/**
* Lookup a Keyword in the intern pool or construct a new one. This contains
* both color and plain keywords.
*/
public Node keyword(String raw, int start, int end) {
int ix = KEYWORD_DAT.get(raw, start, end);
if (ix == -1) {
return new Keyword(raw.substring(start, end));
}
return KEYWORDS[ix];
}
/**
* Lookup a dimension Unit in the intern pool.
*/
public Unit unit(String raw, int start, int end) {
int ix = UNITS_DAT.get(raw, start, end);
if (ix == -1) {
String rep = raw.substring(start, end);
return Unit.get(rep);
}
return UNITS[ix];
}
/**
* Lookup a Dimension in the intern pool or return null if not found,
*/
public Dimension dimension(String raw, int start, int end) {
int ix = DIMENSIONS_DAT.get(raw, start, end);
return ix == -1 ? null : DIMENSIONS[ix];
}
/**
* Lookup a TextElement in the intern pool for the given Combinator. If not
* found a new one is constructed.
*/
public TextElement element(Combinator comb, String raw, int start, int end) {
int ix = ELEMENT_DAT.get(raw, start, end);
if (ix == -1) {
return new TextElement(comb, raw.substring(start, end));
}
if (comb == null) {
return NULL_ELEMENTS[ix];
}
switch (comb) {
case CHILD:
return CHILD_ELEMENTS[ix];
case NAMESPACE:
return NAMESPACE_ELEMENTS[ix];
case SIB_ADJ:
return SIB_ADJ_ELEMENTS[ix];
case SIB_GEN:
return SIB_GEN_ELEMENTS[ix];
case DESC:
default:
return DESC_ELEMENTS[ix];
}
}
/**
* Lookup a function name in the intern pool or copy the substring.
*/
public String function(String raw, int start, int end) {
int ix = FUNCTIONS_DAT.getIgnoreCase(raw, start, end);
if (ix == -1) {
return raw.substring(start, end).toLowerCase();
}
return FUNCTIONS[ix];
}
/**
* Lookup a hex or keyword color in the intern pool.
*/
public RGBColor color(String raw, int start, int end) {
if (raw.charAt(start) == '#') {
int ix = COLORS_HEX_DAT.getIgnoreCase(raw, start, end);
return ix == -1 ? RGBColor.fromHex(raw.substring(start, end)) : COLORS_HEX[ix];
}
// Note: we only intern lowercase colors since there is some overlap between
// capitalized keywords in font names and we want to avoid adjusting the case,
// e.g. "font-family: Crimson Text;" would become "font-family: crimson Text;"
int ix = COLORS_KEYWORD_DAT.get(raw, start, end);
return ix == -1 ? null : COLORS_KEYWORD[ix];
}
/**
* Lookup a keyword color in the intern pool or return null if not found.
*/
public RGBColor keywordColor(String raw, int start, int end) {
int ix = InternPool.COLORS_KEYWORD_DAT.get(raw, start, end);
return ix == -1 ? null : InternPool.COLORS_KEYWORD[ix];
}
/**
* Given a color, return its CSS name or null if none exists.
*/
public String colorToKeyword(RGBColor color) {
if (color.alpha() != 1.0) {
return null;
}
int value = (color.red() << 16) + (color.green() << 8) + color.blue();
int i = Arrays.binarySearch(COLOR_NAME_INDEX, value);
return i < 0 ? null : COLOR_NAMES[i];
}
/**
* Merge color and plain keywords.
*/
private static List> buildKeywords(List> colors, String[] keywords) {
List> result = new ArrayList<>();
for (Pair color : colors) {
result.add(Pair.of(color.key(), (Node)color.val()));
}
for (String keyword : keywords) {
if (keyword.equals("transparent")) {
continue;
}
result.add(Pair.of(keyword, new Keyword(keyword)));
}
return result;
}
/**
* Map the array of keys to build text elements with a given combinator.
*/
private static TextElement[] withCombinator(Combinator comb, String[] keys) {
return Arrays.stream(keys).map(s -> new TextElement(comb, s)).toArray(TextElement[]::new);
}
/**
* Build a double-array trie from the list of keys.
*/
private static DAT build(List keys) {
DATBuilder builder = new DATBuilder(keys);
builder.build();
return new DAT(builder.base(), builder.check(), builder.indices());
}
/**
* Parse dimensions.
*/
private static List buildDimensions(String[] keys) {
List result = new ArrayList<>();
for (String key : keys) {
Matcher m = RE_DIM.matcher(key);
if (!m.matches()) {
throw new RuntimeException("failed to parse dimension: " + key);
}
String _num = m.group(1);
String _unit = m.group(2);
Unit unit = null;
if (_unit != null) {
unit = Unit.get(_unit);
}
result.add(new Dimension(Double.parseDouble(_num), unit));
}
return result;
}
/**
* Map color names and hex values to color instances. We index them separately
* since we want to do case-insensitive matching on hex colors, and case-sensitive
* on keyword colors.
*/
private static List> buildColors(String[] lines, boolean hex_only) {
Map unique = new HashMap<>();
for (String line : lines) {
String[] row = line.split("\\s+");
String hex = row[0];
if (row.length == 2) {
int[] rgb = Colors.hexToRGB(hex);
if (hex_only) {
RGBColor color = new RGBColor(rgb[0], rgb[1], rgb[2]);
unique.put(hex, color);
} else {
String name = row[1];
RGBColor color = new KeywordColor(swapNames(name), rgb[0], rgb[1], rgb[2]);
unique.put(name, color);
}
} else {
if (hex_only) {
RGBColor color = RGBColor.fromHex(hex);
unique.put(hex, color);
}
}
}
List> result = new ArrayList<>();
for (Map.Entry entry : unique.entrySet()) {
result.add(Pair.of(entry.getKey(), entry.getValue()));
}
if (!hex_only) {
// Add special color-like keyword "transparent"
result.add(Pair.of(TRANSPARENT.keyword(), TRANSPARENT));
}
return result;
}
/**
* Force resolved keywords containing "gray" to the "grey" form,
* for backwards-compatibility.
*/
private static String swapNames(String name) {
if (name.indexOf("gray") != -1) {
return name.replace("gray", "grey");
}
return name;
}
/**
* Map named colors to their integer value
*/
private static List> buildNamedColors(List> colors) {
List> result = new ArrayList<>();
for (Pair elem : colors) {
String key = elem.key();
if (key.equals("transparent")) {
continue;
}
// Checks for backwards-compatibility
// Prefer "grey" over "gray".
if (key.contains("gray")) {
continue;
}
RGBColor color = elem.val();
int value = (color.red() << 16) + (color.green() << 8) + color.blue();
result.add(Pair.of(value, key));
}
result.sort((a, b) -> Integer.compare(a.key(), b.key()));
return result;
}
/**
* Load a resource file, split and sort it.
*/
private static String[] load(String name) throws IOException {
String raw = LessUtils.readStream(InternPool.class.getResourceAsStream(name));
String[] res = raw.split("\n");
return Arrays.stream(res).filter(e -> !e.isEmpty()).sorted().toArray(String[]::new);
}
}