com.google.common.css.compiler.passes.FixupFontDeclarations Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-stylesheets Show documentation
Show all versions of closure-stylesheets Show documentation
Closure Stylesheets is an extension to CSS that adds variables,
functions,
conditionals, and mixins to standard CSS. The tool also supports
minification, linting, RTL flipping, and CSS class renaming.
/*
* Copyright 2012 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.passes;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.PeekingIterator;
import com.google.common.collect.Sets;
import com.google.common.collect.UnmodifiableIterator;
import com.google.common.css.SourceCode;
import com.google.common.css.SourceCodeLocation;
import com.google.common.css.compiler.ast.CssCommentNode;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssCompositeValueNode;
import com.google.common.css.compiler.ast.CssDeclarationNode;
import com.google.common.css.compiler.ast.CssLiteralNode;
import com.google.common.css.compiler.ast.CssNode;
import com.google.common.css.compiler.ast.CssNumericNode;
import com.google.common.css.compiler.ast.CssPriorityNode;
import com.google.common.css.compiler.ast.CssPropertyValueNode;
import com.google.common.css.compiler.ast.CssStringNode;
import com.google.common.css.compiler.ast.CssTree;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.DefaultTreeVisitor;
import com.google.common.css.compiler.ast.ErrorManager;
import com.google.common.css.compiler.ast.GssError;
import com.google.common.css.compiler.ast.MutatingVisitController;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Compiler pass that replaces font and font-family declaration subtrees
* so that the tree structure resembles the (rather idiosyncratic) grammar
* of the corresponding CSS properties.
*
*/
public class FixupFontDeclarations extends DefaultTreeVisitor
implements CssCompilerPass {
/**
* Specifies how the input tree should be interpreted.
*/
public enum InputMode {
/**
* Assume the input follows the grammar of CSS.
*/
CSS,
/**
* Perform a best-effort parse that allows unsubstituted definition uses.
*/
GSS;
}
/**
* Properties whose values may be specified in a {@code Font} declaration.
*/
public enum FontProperty {
STYLE, VARIANT, WEIGHT, SIZE, LINE_HEIGHT, FAMILY
};
/**
* A simple predicate on {@code CssCompositeValueNode}.
*/
private static class WithOperator
implements Predicate {
private final CssCompositeValueNode.Operator op;
public WithOperator(CssCompositeValueNode.Operator op) {
this.op = op;
}
@Override public boolean apply(CssCompositeValueNode n) {
if (n.getOperator() != op) {
return false;
}
return true;
}
}
private static WithOperator withOperator(CssCompositeValueNode.Operator op) {
return new WithOperator(op);
}
private static final String FONT = "font";
private static final String FONT_FAMILY = "font-family";
private static final String NORMAL = "normal";
private static final String INHERIT = "inherit";
private static final Set SYSTEM_FONTS = ImmutableSet.of(
"caption", "icon", "menu", "message-box", "small-caption", "status-bar");
private static final Set FONT_ABSOLUTE_SIZES = ImmutableSet.of(
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large");
private static final Set FONT_RELATIVE_SIZES = ImmutableSet.of(
"larger", "smaller");
private static final Set DEFINITELY_STYLE = ImmutableSet.of(
"italic", "oblique");
private static final Set DEFINITELY_VARIANT = ImmutableSet.of(
"small-caps");
private static final Set DEFINITELY_WEIGHT = ImmutableSet.of(
"bold", "bolder", "lighter");
private static final Set NUMERIC_WEIGHTS = ImmutableSet.of(
"100", "200", "300", "400", "500", "600", "700", "800", "900");
/**
* No point looking through lots of nodes for clues that are only allowed
* in a prefix. This limit is conservative, assuming we create as many
* top-level nodes as tokens. As we'll see shortly, we actually expect
* the parser to structure things slightly more elaborately, resulting
* in fewer top-level nodes.
*/
private static final int PRE_FAMILY_LIMIT = 6;
/**
* Recognizes a simple size value represented by a node in the
* original AST of a font declaration value.
*/
private static final Predicate IS_PLAIN_SIZE =
new Predicate() {
@Override public boolean apply(CssValueNode n) {
return (n instanceof CssNumericNode
&& (!CssNumericNode.NO_UNITS.equals(
((CssNumericNode) n).getUnit())))
|| FONT_ABSOLUTE_SIZES.contains(n.getValue())
|| FONT_RELATIVE_SIZES.contains(n.getValue());
}
};
/**
* Picks out the size from the original AST subtree representing
* size/line-height in a font declaration value.
*/
private static final
Function EXTRACT_SIZE =
new Function() {
@Override public CssValueNode apply(CssCompositeValueNode n) {
return n.getValues().get(0);
}
};
@VisibleForTesting
static final String SIZE_AND_FAMILY_REQUIRED =
"Size and family are required in the absence of a system font or a "
+ "simple inherit";
@VisibleForTesting
static final Map TOO_MANY =
ImmutableMap.builder()
.put(FontProperty.LINE_HEIGHT,
"The '/' can occur at most once in a font shorthand value")
.put(FontProperty.SIZE,
"Font size can occur at most once in a font shorthand value")
.put(FontProperty.STYLE,
"Font style can occur at most once in a font shorthand value")
.put(FontProperty.VARIANT,
"Font variant can occur at most once in a font shorthand value")
.put(FontProperty.WEIGHT,
"Font weight can occur at most once in a font shorthand value")
.build();
private static final ImmutableSortedSet SLOTTABLE_PROPERTIES =
ImmutableSortedSet.of(
FontProperty.STYLE, FontProperty.VARIANT, FontProperty.WEIGHT);
@VisibleForTesting
static final String TOO_MANY_NORMALS =
"The keyword normal can occur at most thrice in a font shorthand value";
@VisibleForTesting
static final String NORMAL_TOO_LATE =
"The keyword normal is only allowed in the first three tokens of a "
+ "font shorthand";
@VisibleForTesting
static final String SIZE_AFTER_HEIGHT =
"Font size must be specified before line-height";
@VisibleForTesting
static final String TOO_MANY_PRE_SIZE =
"Too many font shorthand tokens before size";
@VisibleForTesting
static final String PRE_SIZE_INTERLOPER_SIZE =
"Unrecognized tokens immediately preceding size";
/**
* Should the input be parsed strictly or do we assume gss variables
* might be substituted later?
*/
private final InputMode mode;
private final ErrorManager errorManager;
private final MutatingVisitController visitController;
private CssTree tree;
public FixupFontDeclarations(
InputMode mode, ErrorManager errorManager, CssTree tree) {
this.mode = mode;
this.errorManager = errorManager;
this.tree = tree;
visitController = tree.getMutatingVisitController();
}
@Override
public boolean enterDeclaration(CssDeclarationNode decl) {
String propertyName = decl.getPropertyName().getProperty().getName();
if (!(FONT.equals(propertyName) || FONT_FAMILY.equals(propertyName))) {
return false;
}
CssDeclarationNode d = decl.deepCopy();
List replacement = ImmutableList.of(d);
if (FONT.equals(propertyName)) {
d.setPropertyValue(reparseFont(decl.getPropertyValue()));
} else if (FONT_FAMILY.equals(propertyName)) {
d.setPropertyValue(reparseFontFamily(decl.getPropertyValue()));
}
visitController.replaceCurrentBlockChildWith(replacement, false);
return false;
}
private CssPropertyValueNode reparseFont(CssPropertyValueNode n) {
// Preliminary easy cases
if (n.numChildren() == 0) {
return n.deepCopy();
} else if (n.numChildren() == 1
&& SYSTEM_FONTS.contains(n.getChildAt(0).getValue())
|| INHERIT.equals(n.getChildAt(0).getValue())) {
return n.deepCopy();
} else if (n.numChildren() < 2) {
if (mode == InputMode.CSS) {
errorManager.report(
new GssError(SIZE_AND_FAMILY_REQUIRED, getSourceCodeLocation(n)));
}
return n.deepCopy();
}
// Some clients want to be able to whitelist typefaces or otherwise
// understand the font shorthand in detail. Unbound GSS variables make
// this pretty hopeless:
// font: CLUELESS;
// but we can try:
// font: italic bold SIZE/LEADING FAMILIES;
// and a best-effort parse tree for that might be useful.
//
// The best case for us is CSS, where we can rely on clues such as
// the keywords that can disambiguate some cases of style vs. variant
// and the slash token that tells us that the surrounding tokens are
// size and line-height and not e.g., size and a family name.
//
// Our strategy is to segment the property value and deal with each
// segment independently. Segmentation isn't straightforward because
// tokens can't always be understood independently:
// font: normal italic 100 medium medium roman regular;
// ^the next two tokens tell us this normal specifies a font-variant
// ^this is easily understood to be a font-weight
// ^obviously a font-size
// ^because we're past size, this must
// be a font-family name component
// Try to recognize things individually, then check for conflicts
// among our constraints, and finally rebuild our AST.
Iterable preFamilyCandidates =
Iterables.limit(n.childIterable(), PRE_FAMILY_LIMIT);
Iterable sizeLineHeights =
Iterables.filter(
extractByType(CssCompositeValueNode.class, preFamilyCandidates),
withOperator(CssCompositeValueNode.Operator.SLASH));
Iterable plainSizes =
Iterables.filter(
preFamilyCandidates,
IS_PLAIN_SIZE);
Iterable lhSizes =
Iterables.transform(
sizeLineHeights,
EXTRACT_SIZE);
final HashMap lexicalOrder =
EnumeratingVisitor.enumerate(tree);
Iterable sizes = Iterables.concat(plainSizes, lhSizes);
if (!validateSplitPoint(
getSourceCodeLocation(n), lexicalOrder, sizeLineHeights, sizes)) {
return n.deepCopy();
}
// Now we can split on the one and only size node.
final CssValueNode splitPoint =
Iterables.getOnlyElement(Iterables.concat(sizeLineHeights, plainSizes));
Iterable prefix =
takeWhile(n.childIterable(), new Predicate() {
@Override public boolean apply(CssValueNode n) {
return lexicalOrder.get(n).compareTo(
lexicalOrder.get(splitPoint)) < 0;
}
});
final CssPriorityNode priority = getPriority(n);
Iterable families =
dropWhile(
takeUntil(n.childIterable(), priority),
new Predicate() {
@Override public boolean apply(CssValueNode n) {
return lexicalOrder.get(splitPoint).compareTo(
lexicalOrder.get(n)) < 0;
}
});
final Map properties =
classifyNodes(prefix, sizes, sizeLineHeights);
// Validate analysis
validatePrefix(prefix);
validateProperties(prefix, properties);
// Build output
return rebuildFont(prefix, splitPoint, families, priority, properties, n);
}
private CssPriorityNode getPriority(CssPropertyValueNode n) {
if (n.numChildren() < 1) return null;
CssNode last = n.getChildAt(n.numChildren() - 1);
if (last instanceof CssPriorityNode) {
return (CssPriorityNode) last;
} else {
return null;
}
}
private Iterable takeUntil(Iterable xs, final T excludedEndpoint) {
return takeWhile(
xs,
new Predicate() {
@Override public boolean apply(T i) {
return excludedEndpoint != i;
}
});
}
private Map classifyNodes(
Iterable prefix,
Iterable sizes,
Iterable sizeLineHeights) {
final Map properties = Maps.newHashMap();
for (CssValueNode i : prefix) {
if (DEFINITELY_STYLE.contains(i.getValue())) {
properties.put(i, FontProperty.STYLE);
} else if (DEFINITELY_VARIANT.contains(i.getValue())) {
properties.put(i, FontProperty.VARIANT);
} else if (isWeight(i)) {
properties.put(i, FontProperty.WEIGHT);
}
}
properties.put(Iterables.getOnlyElement(sizes), FontProperty.SIZE);
if (!Iterables.isEmpty(sizeLineHeights)) {
properties.put(
Iterables.getOnlyElement(sizeLineHeights).getValues().get(1),
FontProperty.LINE_HEIGHT);
}
return properties;
}
private boolean isWeight(CssValueNode n) {
if (DEFINITELY_WEIGHT.contains(n.getValue())) {
return true;
}
if (!(n instanceof CssNumericNode)) {
return false;
}
CssNumericNode numeric = (CssNumericNode) n;
if (!CssNumericNode.NO_UNITS.equals(numeric.getUnit())) {
return false;
}
return (NUMERIC_WEIGHTS.contains(numeric.getNumericPart()));
}
private void validateSizeLineHeight(CssCompositeValueNode composite) {
if (composite.getValues().size() != 2) {
reportError(TOO_MANY.get(FontProperty.LINE_HEIGHT),
getSourceCodeLocation(composite));
}
}
private void reportError(String message, SourceCodeLocation location) {
errorManager.report(new GssError(message, location));
}
private boolean validateSplitPoint(
SourceCodeLocation loc,
HashMap lexicalOrder,
Iterable sizeLineHeights,
Iterable sizes) {
CssCompositeValueNode secondSLH = Iterables.get(sizeLineHeights, 1, null);
if (secondSLH != null) {
reportError(TOO_MANY.get(FontProperty.LINE_HEIGHT),
getSourceCodeLocation(secondSLH));
return false;
}
CssCompositeValueNode slashy = Iterables.find(
sizeLineHeights,
new Predicate() {
@Override public boolean apply(CssCompositeValueNode n) {
return n.getValues().size() != 2;
}
},
null);
if (slashy != null) {
reportError(TOO_MANY.get(FontProperty.LINE_HEIGHT),
getSourceCodeLocation(slashy));
}
if (Iterables.isEmpty(sizes)) {
if (mode == InputMode.CSS) {
reportError(SIZE_AND_FAMILY_REQUIRED, loc);
}
return false;
}
if (Iterables.get(sizes, 1, null) != null) {
reportError(TOO_MANY.get(FontProperty.SIZE),
getSourceCodeLocation(
max(sizes, Functions.forMap(lexicalOrder))));
return false;
}
return true;
}
private void validatePrefix(
Iterable prefix) {
CssValueNode tooMuch = Iterables.get(prefix, 3, null);
if (tooMuch != null) {
reportError(TOO_MANY_PRE_SIZE, getSourceCodeLocation(tooMuch));
}
}
private void validateProperties(
Iterable prefix,
final Map classified) {
final List normals = Lists.newLinkedList();
for (CssValueNode i : prefix) {
if (!classified.containsKey(i) && NORMAL.equals(i.getValue())) {
normals.add(i);
}
}
if (normals.size() > 3) {
errorManager.report(
new GssError(
TOO_MANY_NORMALS,
getSourceCodeLocation(normals.get(normals.size() - 1))));
}
HashSet properties = Sets.newHashSet();
for (Map.Entry p : classified.entrySet()) {
if (!properties.add(p.getValue())) {
reportError(TOO_MANY.get(p.getValue()),
getSourceCodeLocation(p.getKey()));
}
}
if (mode == InputMode.CSS) {
CssValueNode interloper =
Iterables.find(
prefix,
new Predicate() {
@Override public boolean apply(CssValueNode n) {
return !classified.containsKey(n) && !normals.contains(n);
}
},
null);
if (interloper != null) {
reportError(PRE_SIZE_INTERLOPER_SIZE,
getSourceCodeLocation(interloper));
}
}
}
private CssPropertyValueNode rebuildFont(
Iterable prefix,
CssValueNode splitPoint,
Iterable families,
CssPriorityNode priority,
final Map properties,
CssPropertyValueNode n) {
TreeMap parts =
Maps.newTreeMap();
for (Map.Entry p : properties.entrySet()) {
parts.put(p.getValue(), p.getKey());
}
List preFamily = Lists.newArrayList();
Iterables.addAll(preFamily, prefix);
if (parts.containsKey(FontProperty.SIZE)) {
preFamily.add(parts.get(FontProperty.SIZE));
}
if (parts.containsKey(FontProperty.LINE_HEIGHT)) {
CssValueNode lineHeight = parts.get(FontProperty.LINE_HEIGHT);
preFamily.add(new CssLiteralNode("/", getSourceCodeLocation(lineHeight)));
preFamily.add(lineHeight);
}
List tail =
Iterables.isEmpty(families)
? ImmutableList.of()
: ImmutableList.of(reparseFamilies(
families,
getSourceCodeLocation(Iterables.get(families, 0))));
ImmutableList.Builder resultNodes = ImmutableList.builder();
resultNodes.addAll(Iterables.concat(preFamily, tail));
if (priority != null) {
resultNodes.add(priority);
}
CssPropertyValueNode result = new CssPropertyValueNode(resultNodes.build());
return result.deepCopy();
}
private CssCompositeValueNode reparseFamilies(
Iterable families, SourceCodeLocation loc) {
// they will be a comma-delimited sequence of (strings | id-sequences)
List alternatives = Lists.newArrayList();
List commentsOnAlternatives = Lists.newArrayList();
for (CssValueNode i : families) {
if (i instanceof CssCompositeValueNode) {
CssCompositeValueNode segment = (CssCompositeValueNode) i;
if (segment.getValues().size() == 0) {
continue;
}
CssValueNode first = Iterables.getFirst(segment.getValues(), null);
collect(alternatives, first);
Iterable rest = Iterables.skip(segment.getValues(), 1);
Iterables.addAll(alternatives, rest);
for (CssNode j : rest) {
for (CssCommentNode c : j.getComments()) {
commentsOnAlternatives.add(c);
}
}
} else {
collect(alternatives, i);
}
}
CssCompositeValueNode result = new CssCompositeValueNode(
alternatives, CssCompositeValueNode.Operator.COMMA, loc);
for (CssCommentNode c : commentsOnAlternatives) {
result.appendComment(c);
}
return result;
}
private CssPropertyValueNode reparseFontFamily(CssPropertyValueNode n) {
if (n.numChildren() == 0) {
return n.deepCopy();
}
if (n.numChildren() == 1
&& INHERIT.equals(n.getChildAt(0).getValue())) {
return n.deepCopy();
}
CssPriorityNode priority = getPriority(n);
// deal with alternatives
CssCompositeValueNode altNode = reparseFamilies(
takeUntil(n.childIterable(), priority),
getSourceCodeLocation(n));
ImmutableList.Builder result = ImmutableList.builder();
result.add(altNode);
if (priority != null) {
result.add(priority);
}
return new CssPropertyValueNode(result.build());
}
/**
* Agglomerate each id sequence into a space-separated literal value,
* leaving strings as they were and fixing up comments and
* SourceCodeLocations as best we can.
*/
private void collect(List alternatives, CssValueNode item) {
if (isString(item)) {
alternatives.add(item);
} else {
CssValueNode stump;
if (alternatives.size() > 0
&& !isString(alternatives.get(alternatives.size() - 1))) {
// concatenate onto previous node
stump = alternatives.get(alternatives.size() - 1);
stump.setValue(stump.getValue() + " ");
} else {
// start a new node
stump = new CssLiteralNode("", getSourceCodeLocation(item));
alternatives.add(stump);
}
stump.setValue(stump.getValue() + item.getValue());
for (CssCommentNode c : item.getComments()) {
stump.appendComment(c);
}
}
}
private boolean isString(CssValueNode n) {
return n instanceof CssStringNode;
}
private static SourceCodeLocation getSourceCodeLocation(CssNode n) {
n = Iterables.find(n.ancestors(), new Predicate() {
@Override public boolean apply(CssNode n) {
return n.getSourceCodeLocation() != null;
}
}, null);
return n != null
? n.getSourceCodeLocation()
: new SourceCodeLocation(
new SourceCode(null, "x"),
1, 1, 1, 1, 1, 1);
}
/**
* Computes the prefix of {@code xs} for which {@code p} holds.
*/
private Iterable takeWhile(
final Iterable xs, final Predicate super T> p) {
return new Iterable() {
@Override
public Iterator iterator() {
return new UnmodifiableIterator() {
Iterator xsi = xs.iterator();
boolean validT = false;
T t;
{
next();
}
@Override
public boolean hasNext() {
return validT;
}
@Override
public T next() {
T result = t;
if (xsi.hasNext()) {
t = xsi.next();
validT = p.apply(t);
} else {
validT = false;
}
return result;
}
};
}
};
}
/**
* Computes the suffix of {@code xs} starting at the first node for which
* {@code p} fails.
*
* {@code Iterables.concat(takeWhile(xs, p), dropWhile(xs, p)) = xs}
*/
private Iterable dropWhile(
final Iterable xs, final Predicate super T> p) {
return new Iterable() {
@Override
public Iterator iterator() {
PeekingIterator xsi = Iterators.peekingIterator(xs.iterator());
while (xsi.hasNext()) {
if (p.apply(xsi.peek())) {
break;
}
xsi.next();
}
return xsi;
}
};
}
/**
* Returns an element of {@code xs} whose image under {@code f} is
* maximal.
*/
private > T max(
Iterable xs, Function super T, U> f) {
T result = Iterables.getFirst(xs, null);
U extreme = f.apply(result);
for (T x : xs) {
U u = f.apply(x);
if (extreme.compareTo(u) < 0) {
result = x;
extreme = u;
}
}
return result;
}
/**
* Filter to elements of type {@code T} designated by {@code ct}
* and cast the result.
*/
private Iterable extractByType(
final Class ct, Iterable super T> xs) {
return Iterables.transform(
Iterables.filter(xs, Predicates.instanceOf(ct)),
new Function