/* * 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.CssCompilerPass; import com.google.common.css.compiler.ast.CssPseudoClassNode; import com.google.common.css.compiler.ast.CssPseudoClassNode.FunctionType; import com.google.common.css.compiler.ast.CssPseudoElementNode; import com.google.common.css.compiler.ast.CssRefinerListNode; import com.google.common.css.compiler.ast.CssRefinerNode; 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; /** * 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 fbenz@google.com (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(Integer.toString(a)); } compact.append("n"); } if (b > 0 && a != 0) { compact.append("+"); } if (b != 0) { compact.append(Integer.toString(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, argument.length()); bStr = trim(bStr); bStr = bStr.replace("+", ""); return Integer.parseInt(bStr); } else { // an return 0; } } } @Override public void runPass() { visitController.startVisit(this); } }