/*
* Copyright 2009 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.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.css.compiler.ast.CssAtRuleNode;
import com.google.common.css.compiler.ast.CssAtRuleNode.Type;
import com.google.common.css.compiler.ast.CssBlockNode;
import com.google.common.css.compiler.ast.CssBooleanExpressionNode;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssCompositeValueNode;
import com.google.common.css.compiler.ast.CssConditionalBlockNode;
import com.google.common.css.compiler.ast.CssDeclarationBlockNode;
import com.google.common.css.compiler.ast.CssFontFaceNode;
import com.google.common.css.compiler.ast.CssFunctionNode;
import com.google.common.css.compiler.ast.CssImportBlockNode;
import com.google.common.css.compiler.ast.CssImportRuleNode;
import com.google.common.css.compiler.ast.CssLiteralNode;
import com.google.common.css.compiler.ast.CssMediaRuleNode;
import com.google.common.css.compiler.ast.CssNode;
import com.google.common.css.compiler.ast.CssPageRuleNode;
import com.google.common.css.compiler.ast.CssPageSelectorNode;
import com.google.common.css.compiler.ast.CssRootNode;
import com.google.common.css.compiler.ast.CssRulesetNode;
import com.google.common.css.compiler.ast.CssStringNode;
import com.google.common.css.compiler.ast.CssUnknownAtRuleNode;
import com.google.common.css.compiler.ast.CssValueNode;
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.List;
import java.util.Set;
/**
* A compiler pass that transforms standard {@link CssUnknownAtRuleNode} instances to more specific
* at-rule nodes, or deletes them.
*
*/
public class CreateStandardAtRuleNodes implements UniformVisitor, CssCompilerPass {
@VisibleForTesting static final String NO_BLOCK_ERROR_MESSAGE = "This @-rule has to have a block";
@VisibleForTesting
static final String BLOCK_ERROR_MESSAGE = "This @-rule is not allowed to have a block";
@VisibleForTesting
static final String ONLY_DECLARATION_BLOCK_ERROR_MESSAGE =
"Only declaration blocks are allowed for this @-rule";
@VisibleForTesting
static final String INVALID_PARAMETERS_ERROR_MESSAGE = "This @-rule has invalid parameters";
@VisibleForTesting
static final String MEDIA_INVALID_CHILD_ERROR_MESSAGE =
"This is not valid inside an @media block";
@VisibleForTesting
static final String MEDIA_WITHOUT_PARAMETERS_ERROR_MESSAGE = "@media without parameters";
@VisibleForTesting
static final String PAGE_SELECTOR_PARAMETERS_ERROR_MESSAGE =
"Page selectors are not allowed to have parameters";
@VisibleForTesting
static final String FONT_FACE_PARAMETERS_ERROR_MESSAGE =
"@font-face is not allowed to have parameters";
@VisibleForTesting
static final String IGNORED_IMPORT_WARNING_MESSAGE =
"@import rules should occur outside blocks and can only be preceded "
+ "by @charset and other @import rules.";
@VisibleForTesting
static final String IGNORE_IMPORT_WARNING_MESSAGE =
"A node after which all @import rule nodes are ignored is here.";
private static final List<Type> PAGE_SELECTORS =
ImmutableList.of(
Type.TOP_LEFT_CORNER,
Type.TOP_LEFT,
Type.TOP_CENTER,
Type.TOP_RIGHT,
Type.TOP_RIGHT_CORNER,
Type.LEFT_TOP,
Type.LEFT_MIDDLE,
Type.LEFT_BOTTOM,
Type.RIGHT_TOP,
Type.RIGHT_MIDDLE,
Type.RIGHT_BOTTOM,
Type.BOTTOM_LEFT_CORNER,
Type.BOTTOM_LEFT,
Type.BOTTOM_CENTER,
Type.BOTTOM_RIGHT,
Type.BOTTOM_RIGHT_CORNER);
private static final Set<String> PSEUDO_PAGES = ImmutableSet.of(
":left", ":right", ":first");
// The @-rules are restricted because it only makes sense for them to be used
// inside an @media block. Especially @def should never be allowed because it
// can be misleading; @def rules are processed by the compiler but @media
// rules are handled by the browser.
private static final Set<String> ALLOWED_AT_RULES_IN_MEDIA = ImmutableSet.of(
"page", "if", "elseif", "else", "for");
private final MutatingVisitController visitController;
private final ErrorManager errorManager;
private final List<CssImportRuleNode> nonIgnoredImportRules =
Lists.newArrayList();
private CssRootNode root;
/**
* The first node after which import rules will be ignored, or null if
* no such nodes have been discovered.
*/
private CssNode noMoreImportRules;
public CreateStandardAtRuleNodes(
MutatingVisitController visitController, ErrorManager errorManager) {
this.visitController = visitController;
this.errorManager = errorManager;
}
private void enterTree(CssRootNode root) {
this.root = root;
noMoreImportRules = null;
}
@Override
public void leave(CssNode node) {
if (!(node instanceof CssImportRuleNode)
&& !(node instanceof CssImportBlockNode)
&& node != root.getCharsetRule()) {
noMoreImportRules = node;
}
if (node instanceof CssRootNode) {
leaveTree((CssRootNode) node);
}
}
@Override
public void enter(CssNode cssNode) {
if (cssNode instanceof CssRootNode) {
enterTree((CssRootNode) cssNode);
}
if (!(cssNode instanceof CssUnknownAtRuleNode)) {
return;
}
CssUnknownAtRuleNode node = (CssUnknownAtRuleNode) cssNode;
String charsetName = CssAtRuleNode.Type.CHARSET.getCanonicalName();
String importName = CssAtRuleNode.Type.IMPORT.getCanonicalName();
String mediaName = CssAtRuleNode.Type.MEDIA.getCanonicalName();
String pageName = CssAtRuleNode.Type.PAGE.getCanonicalName();
String fontFaceName = CssAtRuleNode.Type.FONT_FACE.getCanonicalName();
List<CssValueNode> params = node.getParameters();
if (node.getName().getValue().equals(charsetName)) {
// We don't have a specific node class for this, should be handled at the parser level.
// TODO(user): Give a warning instead that the node has been removed without processing. (?)
reportError("@" + charsetName + " removed", node);
return;
} else if (node.getName().getValue().equals(importName)) {
if (params.isEmpty()) {
reportError("@" + importName + " without a following string or uri", node);
return;
}
if (params.size() > 2) {
reportError("@" + importName + " with too many parameters", node);
return;
}
CssValueNode param = params.get(0);
if (!((param instanceof CssStringNode) || checkIfUri(param))) {
reportError("@" + importName + "'s first parameter has to be a string or an url", node);
return;
}
List<CssValueNode> paramlist = Lists.newArrayList(param);
if (params.size() == 2) {
CssValueNode param2 = params.get(1);
if (param2 instanceof CssCompositeValueNode || param2 instanceof CssLiteralNode) {
paramlist.add(param2);
} else {
reportError("@" + importName + " has illegal parameter", node);
return;
}
}
CssImportRuleNode importRule = new CssImportRuleNode(node.getComments());
importRule.setParameters(paramlist);
importRule.setSourceCodeLocation(node.getSourceCodeLocation());
if (noMoreImportRules != null) {
visitController.replaceCurrentBlockChildWith(
Lists.newArrayList((CssNode) importRule),
false /* visitTheReplacementNodes */);
reportWarning(IGNORED_IMPORT_WARNING_MESSAGE, node);
reportWarning(IGNORE_IMPORT_WARNING_MESSAGE, noMoreImportRules);
} else {
visitController.removeCurrentNode();
nonIgnoredImportRules.add(importRule);
}
return;
} else if (node.getName().getValue().equals(mediaName)) {
createMediaRule(node);
} else if (node.getName().getValue().equals(pageName)) {
createPageRule(node);
} else if (node.getName().getValue().equals(fontFaceName)) {
createFontFaceRule(node);
} else {
for (Type type : PAGE_SELECTORS) {
if (node.getName().getValue().equals(type.getCanonicalName())) {
createPageSelector(type, node);
break;
}
}
}
}
private void leaveTree(CssRootNode root) {
for (CssImportRuleNode importRule : nonIgnoredImportRules) {
root.getImportRules().addChildToBack(importRule);
}
}
/**
* See the
* <a href="http://www.w3.org/TR/CSS21/grammar.html#grammar">CSS 2.1 syntax
* </a>, and
* <a href="http://www.w3.org/TR/css3-mediaqueries/#syntax">CSS 3 syntax</a>
* for more information.
*/
private void createMediaRule(CssUnknownAtRuleNode node) {
if (node.getBlock() == null) {
reportError(NO_BLOCK_ERROR_MESSAGE, node);
return;
}
if (!(node.getBlock() instanceof CssBlockNode)) {
reportError(ONLY_DECLARATION_BLOCK_ERROR_MESSAGE, node);
return;
}
CssBlockNode block = (CssBlockNode) node.getBlock();
for (CssNode part : block.childIterable()) {
if (!isValidInMediaRule(part)) {
reportError(MEDIA_INVALID_CHILD_ERROR_MESSAGE, part);
return;
}
}
List<CssValueNode> params = node.getParameters();
if (params.isEmpty()) {
reportError(MEDIA_WITHOUT_PARAMETERS_ERROR_MESSAGE, node);
return;
}
// TODO(fbenz): Perform this check depending on the CSS version set in the
// options
if (!checkMediaParameter(params)) {
reportError(INVALID_PARAMETERS_ERROR_MESSAGE, node);
return;
}
CssMediaRuleNode mediaRule = new CssMediaRuleNode(node.getComments(),
block);
mediaRule.setParameters(params);
mediaRule.setSourceCodeLocation(node.getSourceCodeLocation());
visitController.replaceCurrentBlockChildWith(
Lists.newArrayList(mediaRule), true /* visitTheReplacementNodes */);
}
/**
* Checks whether the parameters of a @media rule match the grammar of the
* CSS 3 draft.
*/
private boolean checkMediaParameter(List<CssValueNode> params) {
if (params.get(0) instanceof CssCompositeValueNode) {
return checkMediaCompositeExpression(params, 0);
} else if (params.get(0) instanceof CssBooleanExpressionNode) {
// shorthand syntax: implicit "all and..."
return checkMediaExpression(params, 0);
} else if (!(params.get(0) instanceof CssLiteralNode)) {
// nodes like the function node are invalid
CssValueNode node = params.get(0);
reportWarning(
String.format(
"Expected CssLiteralNode but found %s",
node.getClass().getName()),
node);
return false;
}
int numberOfStartingLiterals = 1;
String firstValue = params.get(0).getValue();
if (firstValue.equals("only") || firstValue.equals("not")) {
numberOfStartingLiterals = 2;
}
if (params.size() < numberOfStartingLiterals) {
// only 'only' or 'not'
reportWarning(
"Expected CssLiteralNode after 'only' or 'not'.",
params.get(params.size() - 1));
return false;
}
if (params.size() - numberOfStartingLiterals > 0) {
return checkAndMediaExpression(params, numberOfStartingLiterals);
}
return true;
}
/**
* Checks for [ AND S* expression ]* where expression is some value.
*/
private boolean checkAndMediaExpression(
List<CssValueNode> params, int start) {
if (params.size() - start < 2) {
// at least two more: 'and X'
return false;
}
if (!params.get(start).getValue().equals("and")) {
// expected 'and'
return false;
}
return checkMediaExpression(params, start + 1);
}
/**
* Checks for S* expression | S* expression [ AND S* expression ]*
* where expression is some value.
*/
private boolean checkMediaExpression(
List<CssValueNode> params, int start) {
if (params.size() - start < 1) {
// at least one
return false;
}
if (params.get(start) instanceof CssCompositeValueNode) {
return checkMediaCompositeExpression(params, start);
} else if (params.size() > start + 1) {
return checkAndMediaExpression(params, start + 1);
}
return true;
}
/**
* Splits up a composite value so that it can be checked. Two values with
* a comma in between are combined to a composite value.
* For example:
* {@code screen and (device-width:800px), print}
* turns into this list of values:
* {@code screen}, {@code and}, {@code (device-width:800px), print}
* where the last value is a composite value that consists of two values.
*/
private boolean checkMediaCompositeExpression(List<CssValueNode> params,
int start) {
CssCompositeValueNode comp = (CssCompositeValueNode) params.get(start);
CssValueNode startValue;
if (comp.getValues().size() == 2) {
startValue = comp.getValues().get(1);
} else {
List<CssValueNode> newChildren = Lists.newArrayList(comp.getValues());
newChildren.remove(0);
startValue = new CssCompositeValueNode(newChildren,
comp.getOperator(), comp.getSourceCodeLocation());
}
List<CssValueNode> newParams = Lists.newArrayList(startValue);
for (int i = 0; i < params.size(); i++) {
if (i > start) {
newParams.add(params.get(i));
}
}
return checkMediaParameter(newParams);
}
private boolean isValidInMediaRule(CssNode node) {
if (node instanceof CssRulesetNode) {
// rulesets like .CLASS { ... }
return true;
}
if (node instanceof CssAtRuleNode && ALLOWED_AT_RULES_IN_MEDIA.contains(
((CssAtRuleNode) node).getName().getValue())) {
// like @page or @if, but not @def
return true;
}
if (node instanceof CssConditionalBlockNode) {
// @if, @elseif, @else after processing because then they are not @-rules
// anymore
return true;
}
return false;
}
/**
* See the
* <a href="http://www.w3.org/TR/css3-page/#syntax-page-selector">
* Page selector grammar</a> for more information.
*/
private void createPageRule(CssUnknownAtRuleNode node) {
if (node.getBlock() == null) {
reportError(NO_BLOCK_ERROR_MESSAGE, node);
return;
}
if (!(node.getBlock() instanceof CssDeclarationBlockNode)) {
reportError(ONLY_DECLARATION_BLOCK_ERROR_MESSAGE, node);
return;
}
List<CssValueNode> params = node.getParameters();
int numParams = params.size();
if (numParams > 2) {
reportError(INVALID_PARAMETERS_ERROR_MESSAGE, node);
return;
} else if (numParams == 2 && !PSEUDO_PAGES.contains(params.get(1).getValue())) {
reportError(INVALID_PARAMETERS_ERROR_MESSAGE, node);
return;
} else if (numParams == 1) {
if (params.get(0).getValue().startsWith(":")
&& !PSEUDO_PAGES.contains(params.get(0).getValue())) {
reportError(INVALID_PARAMETERS_ERROR_MESSAGE, node);
return;
}
}
CssDeclarationBlockNode block = (CssDeclarationBlockNode) node.getBlock();
CssPageRuleNode pageRule = new CssPageRuleNode(node.getComments(),
block);
pageRule.setParameters(params);
pageRule.setSourceCodeLocation(node.getSourceCodeLocation());
visitController.replaceCurrentBlockChildWith(
Lists.newArrayList(pageRule), true /* visitTheReplacementNodes */);
}
private void createPageSelector(Type type, CssUnknownAtRuleNode node) {
if (node.getBlock() == null) {
reportError(NO_BLOCK_ERROR_MESSAGE, node);
return;
}
if (!(node.getBlock() instanceof CssDeclarationBlockNode)) {
reportError(ONLY_DECLARATION_BLOCK_ERROR_MESSAGE, node);
return;
}
if (!node.getParameters().isEmpty()) {
reportError(PAGE_SELECTOR_PARAMETERS_ERROR_MESSAGE, node);
return;
}
CssDeclarationBlockNode block = (CssDeclarationBlockNode) node.getBlock();
CssPageSelectorNode pageSelector = new CssPageSelectorNode(type,
node.getComments(), block);
pageSelector.setSourceCodeLocation(node.getSourceCodeLocation());
visitController.replaceCurrentBlockChildWith(
Lists.newArrayList(pageSelector), true /* visitTheReplacementNodes */);
}
/**
* See the
* <a href="http://www.w3.org/TR/css3-fonts/#font-face-rule">
* font-face grammar</a> for more information.
*/
private void createFontFaceRule(CssUnknownAtRuleNode node) {
if (node.getBlock() == null) {
reportError(NO_BLOCK_ERROR_MESSAGE, node);
return;
}
if (!(node.getBlock() instanceof CssDeclarationBlockNode)) {
reportError(ONLY_DECLARATION_BLOCK_ERROR_MESSAGE, node);
return;
}
if (!node.getParameters().isEmpty()) {
reportError(FONT_FACE_PARAMETERS_ERROR_MESSAGE, node);
return;
}
// TODO(bolinfest): Verify that all declarations in the block are valid
// font-descriptions: font-family, src, font-style, etc. See
// http://www.w3.org/TR/css3-fonts/#font-resources for a complete list.
CssDeclarationBlockNode block = (CssDeclarationBlockNode) node.getBlock();
CssFontFaceNode fontFace = new CssFontFaceNode(node.getComments(), block);
fontFace.setSourceCodeLocation(node.getSourceCodeLocation());
visitController.replaceCurrentBlockChildWith(
Lists.newArrayList(fontFace), true /* visitTheReplacementNodes */);
}
private boolean checkIfUri(CssValueNode node) {
if (!(node instanceof CssFunctionNode)) {
return false;
}
CssFunctionNode function = (CssFunctionNode) node;
if (function.getFunctionName().toLowerCase().equals("url")) {
return true;
}
return false;
}
private void reportError(String message, CssNode node) {
errorManager.report(new GssError(message, node.getSourceCodeLocation()));
visitController.removeCurrentNode();
}
private void reportWarning(String message, CssNode node) {
errorManager.reportWarning(
new GssError(message, node.getSourceCodeLocation()));
}
@Override
public void runPass() {
visitController.startVisit(UniformVisitor.Adapters.asVisitor(this));
}
}