com.google.common.css.compiler.passes.ProcessRefiners 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 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.compiler.passes;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.css.compiler.ast.*;
import com.google.common.css.compiler.ast.CssPseudoClassNode.FunctionType;
/**
* Compiler pass which ensures that refiners are correctly formated because
* not every invalid format is rejected by the parser. This pass checks for
* a correct nth-format and can make it compact. In addition, the pass checks
* the constraints for the :not pseudo-class.
*
* @author [email protected] (Florian Benz)
*/
public class ProcessRefiners extends DefaultTreeVisitor
implements CssCompilerPass {
@VisibleForTesting
static final String INVALID_NTH_ERROR_MESSAGE =
"the format for NTH is not in the form an+b, 'odd', or 'even' where a " +
"and b are (signed) integers that can be omitted";
@VisibleForTesting
static final String INVALID_NOT_SELECTOR_ERROR_MESSAGE =
"a :not selector and pseudo-elements ('::') are not allowed inside of" +
" a :not";
@VisibleForTesting
static final String NOT_LANG_ERROR_MESSAGE =
"a pseudo-class which takes arguments has to be ':lang()' or has to " +
"start with 'nth-'";
private static final CharMatcher CSS_WHITESPACE =
CharMatcher.anyOf(" \t\r\n\f");
private final MutatingVisitController visitController;
private final ErrorManager errorManager;
private final boolean simplifyCss;
public ProcessRefiners(MutatingVisitController visitController,
ErrorManager errorManager, boolean simplifyCss) {
this.visitController = visitController;
this.errorManager = errorManager;
this.simplifyCss = simplifyCss;
}
@Override
public boolean enterPseudoClass(CssPseudoClassNode refiner) {
FunctionType functionType = refiner.getFunctionType();
switch (functionType) {
case NONE:
return true;
case LANG:
return handleLang(refiner);
case NTH:
return handleNth(refiner);
case NOT:
return handleNot(refiner);
}
return true;
}
private static String trim(String input) {
return CSS_WHITESPACE.trimFrom(input);
}
private boolean handleLang(CssPseudoClassNode refiner) {
if (!refiner.getRefinerName().equals("lang(")) {
errorManager.report(new GssError(NOT_LANG_ERROR_MESSAGE,
refiner.getSourceCodeLocation()));
return false;
}
return true;
}
private boolean handleNot(CssPseudoClassNode refiner) {
if (refiner.getNotSelector() == null) {
errorManager.report(new GssError(INVALID_NOT_SELECTOR_ERROR_MESSAGE,
refiner.getSourceCodeLocation()));
return false;
}
CssRefinerListNode refinerList = refiner.getNotSelector().getRefiners();
if (refinerList.numChildren() == 0) {
return true;
} else if (refinerList.numChildren() > 1) {
// should not be possible due to the grammar
errorManager.report(new GssError(INVALID_NOT_SELECTOR_ERROR_MESSAGE,
refiner.getSourceCodeLocation()));
return false;
}
CssRefinerNode nestedRefiner = refinerList.getChildAt(0);
// a pseudo-element is not allowed inside a :not
if (nestedRefiner instanceof CssPseudoElementNode) {
errorManager.report(new GssError(INVALID_NOT_SELECTOR_ERROR_MESSAGE,
refiner.getSourceCodeLocation()));
return false;
}
// the negation pseudo-class is not allowed inside a :not
if (nestedRefiner instanceof CssPseudoClassNode) {
CssPseudoClassNode pseudoClass = (CssPseudoClassNode) nestedRefiner;
if (pseudoClass.getFunctionType() == FunctionType.NOT) {
errorManager.report(new GssError(INVALID_NOT_SELECTOR_ERROR_MESSAGE,
refiner.getSourceCodeLocation()));
return false;
}
}
return true;
}
private boolean handleNth(CssPseudoClassNode refiner) {
String argument = trim(refiner.getArgument());
if (argument.contains(".")) {
errorManager.report(new GssError(INVALID_NTH_ERROR_MESSAGE,
refiner.getSourceCodeLocation()));
return false;
}
// general pattern: an+b
int a, b;
if (argument.equals("even")) {
// 2n
a = 2;
b = 0;
} else if (argument.equals("odd")) {
// 2n+1
a = 2;
b = 1;
} else {
try {
int indexOfN = argument.indexOf('n');
a = parseA(argument, indexOfN);
b = parseB(argument, indexOfN);
} catch (NumberFormatException e) {
errorManager.report(new GssError(INVALID_NTH_ERROR_MESSAGE,
refiner.getSourceCodeLocation()));
return false;
}
}
if (simplifyCss) {
refiner.setArgument(compactRepresentation(a, b));
}
return true;
}
private String compactRepresentation(int a, int b) {
if (a == 2 && b == 1) {
// 2n+1 -> odd
return "odd";
}
if (a == 0 && b == 0) {
return "0";
}
StringBuilder compact = new StringBuilder();
if (a != 0) {
if (a != 1 || b == 0 /* for WebKit */) {
compact.append(a);
}
compact.append("n");
}
if (b > 0 && a != 0) {
compact.append("+");
}
if (b != 0) {
compact.append(b);
}
return compact.toString();
}
@VisibleForTesting
int parseA(String argument, int indexOfN) {
if (indexOfN == -1) {
// b
return 0;
} else {
if (indexOfN > 0) {
String aStr = trim(argument.substring(0, indexOfN));
if (aStr.equals("+")) {
// +n+b
return 1;
} else if (aStr.equals("-")) {
// -n+b
return -1;
} else {
// an+b
aStr = aStr.replace("+", "");
return Integer.parseInt(aStr);
}
} else {
// n+b
return 1;
}
}
}
@VisibleForTesting
int parseB(String argument, int indexOfN) {
if (indexOfN == -1) {
// b
argument = trim(argument.replace("+", ""));
return Integer.parseInt(argument);
} else {
if (indexOfN + 1 < argument.length()) {
// an+b
String bStr = argument.substring(indexOfN + 1);
bStr = trim(bStr);
bStr = bStr.replace("+", "");
return Integer.parseInt(bStr);
} else {
// an
return 0;
}
}
}
@Override
public void runPass() {
visitController.startVisit(this);
}
}