com.google.common.css.compiler.passes.CompactPrintingVisitor Maven / Gradle / Ivy
Show all versions of closure-stylesheets Show documentation
/*
* 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.collect.ImmutableSet;
import com.google.common.css.compiler.ast.*;
import com.google.common.css.compiler.ast.CssPseudoClassNode.FunctionType;
import java.util.List;
import java.util.logging.Logger;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A compact-printer for {@link CssTree} instances. TODO(oana): Change this pass to stop visiting
* when definitions are encountered. The same goes for its test.
*
* @author [email protected] (Oana Florescu)
* @author [email protected] (Florian Benz)
*/
public class CompactPrintingVisitor extends DefaultTreeVisitor {
private static final Logger logger = Logger.getLogger(CompactPrintingVisitor.class.getName());
protected final VisitController visitController;
protected final CodeBuffer buffer;
private final String compactedPrintedString = null;
public CompactPrintingVisitor(VisitController visitController, CodeBuffer buffer) {
this.visitController = checkNotNull(visitController);
this.buffer = checkNotNull(buffer);
}
@Override
public boolean enterDefinition(CssDefinitionNode node) {
return false;
}
@Override
public boolean enterImportRule(CssImportRuleNode node) {
buffer.append(node.getType().toString());
for (CssValueNode param : node.getParameters()) {
buffer.append(' ');
// TODO(user): teach visit controllers to explore
// CssImportRuleNode parameters rather than leaving it to each
// pass to figure things out.
if (param instanceof CssStringNode) {
buffer.append(param.toString());
} else {
buffer.append(param.getValue());
}
}
return true;
}
@Override
public void leaveImportRule(CssImportRuleNode node) {
buffer.append(';');
}
@Override
public boolean enterMediaRule(CssMediaRuleNode node) {
buffer.append(node.getType().toString());
if (!node.getParameters().isEmpty()) {
buffer.append(' ');
}
return true;
}
/**
* This is necessary because the parser transform '(' ident ')' into a boolean expression node and
* only stores the identifier itself. For example: {@code @media all and (color)}
*/
private void appendMediaParameterWithParentheses(CssValueNode node) {
// TODO(fbenz): Try to avoid the special handling of this case.
buffer.append('(');
buffer.append(node.getValue());
buffer.append(')');
}
@Override
public void leaveMediaRule(CssMediaRuleNode node) {
buffer.append('}');
}
@Override
public boolean enterPageRule(CssPageRuleNode node) {
buffer.append(node.getType().toString());
buffer.append(' ');
// TODO(fbenz): There are only two parameters possible ('bla:left') that
// come with no whitespace in between. So it would be better to have a
// single node (maybe a selector).
for (CssValueNode param : node.getParameters()) {
buffer.append(param.getValue());
}
buffer.deleteLastCharIfCharIs(' ');
return true;
}
@Override
public boolean enterPageSelector(CssPageSelectorNode node) {
buffer.append(node.getType().toString());
for (CssValueNode param : node.getParameters()) {
buffer.append(' ');
buffer.append(param.getValue());
}
return true;
}
@Override
public boolean enterFontFace(CssFontFaceNode node) {
buffer.append(node.getType().toString());
return true;
}
@Override
public boolean enterCharSet(CssCharSetNode node) {
buffer.append(node.getType().toString());
List parameters = node.getParameters();
if (!parameters.isEmpty()) {
buffer.append(parameters.get(0));
}
return true;
}
@Override
public void leaveCharSet(CssCharSetNode node) {
buffer.append(';');
}
@Override
public boolean enterSelector(CssSelectorNode selector) {
String name = selector.getSelectorName();
if (name != null) {
buffer.append(name);
}
return true;
}
@Override
public void leaveSelector(CssSelectorNode selector) {
buffer.append(',');
}
@Override
public boolean enterClassSelector(CssClassSelectorNode node) {
appendRefiner(node);
return true;
}
@Override
public boolean enterIdSelector(CssIdSelectorNode node) {
appendRefiner(node);
return true;
}
@Override
public boolean enterPseudoClass(CssPseudoClassNode node) {
buffer.append(node.getPrefix());
buffer.append(node.getRefinerName());
if (node.getFunctionType() == FunctionType.NTH) {
buffer.append(node.getArgument().replace(" ", ""));
buffer.append(')');
} else if (node.getFunctionType() == FunctionType.LANG) {
buffer.append(node.getArgument());
buffer.append(')');
}
return true;
}
@Override
public void leavePseudoClass(CssPseudoClassNode node) {
if (node.getFunctionType() == FunctionType.NOT) {
buffer.deleteLastCharIfCharIs(',');
buffer.append(')');
}
}
@Override
public boolean enterPseudoElement(CssPseudoElementNode node) {
appendRefiner(node);
return true;
}
@Override
public boolean enterAttributeSelector(CssAttributeSelectorNode node) {
buffer.append(node.getPrefix());
buffer.append(node.getAttributeName());
buffer.append(node.getMatchSymbol());
buffer.append(node.getValue());
buffer.append(node.getSuffix());
return true;
}
/**
* Appends the representation of a class selector, an id selector, or a pseudo-element.
*/
private void appendRefiner(CssRefinerNode node) {
buffer.append(node.getPrefix());
buffer.append(node.getRefinerName());
}
@Override
public boolean enterCombinator(CssCombinatorNode combinator) {
if (combinator != null) {
buffer.append(combinator.getCombinatorType().getCanonicalName());
}
return true;
}
@Override
public void leaveCombinator(CssCombinatorNode combinator) {
buffer.deleteLastCharIfCharIs(',');
}
@Override
public void leaveSelectorBlock(CssSelectorListNode node) {
buffer.deleteLastCharIfCharIs(',');
}
@Override
public boolean enterDeclarationBlock(CssDeclarationBlockNode block) {
buffer.append('{');
return true;
}
@Override
public void leaveDeclarationBlock(CssDeclarationBlockNode block) {
buffer.deleteLastCharIfCharIs(';');
buffer.append('}');
}
@Override
public boolean enterBlock(CssBlockNode block) {
if (block.getParent() instanceof CssUnknownAtRuleNode
|| block.getParent() instanceof CssMediaRuleNode) {
buffer.append('{');
}
return true;
}
@Override
public boolean enterDeclaration(CssDeclarationNode declaration) {
if (declaration.hasStarHack()) {
buffer.append('*');
}
buffer.append(declaration.getPropertyName().getValue());
buffer.append(':');
return true;
}
@Override
public void leaveDeclaration(CssDeclarationNode declaration) {
buffer.deleteLastCharIfCharIs(' ');
buffer.append(';');
}
@Override
public boolean enterCompositeValueNode(CssCompositeValueNode node) {
if (node.hasParenthesis()) {
buffer.append('(');
}
return true;
}
@Override
public void leaveCompositeValueNode(CssCompositeValueNode node) {
buffer.deleteLastCharIfCharIs(' ');
if (node.getParent() instanceof CssPropertyValueNode) {
buffer.append(' ');
}
if (node.hasParenthesis()) {
buffer.append(')');
}
}
@Override
public boolean enterValueNode(CssValueNode node) {
if (node instanceof CssPriorityNode) {
buffer.deleteLastCharIfCharIs(' ');
}
appendValueNode(node);
return true;
}
@Override
public void leaveValueNode(CssValueNode node) {
if (node.getParent() instanceof CssPropertyValueNode) {
buffer.append(' ');
}
}
@Override
public boolean enterCompositeValueNodeOperator(CssCompositeValueNode parent) {
buffer.deleteLastCharIfCharIs(' ');
buffer.append(parent.getOperator().getOperatorName());
return true;
}
@Override
public boolean enterFunctionNode(CssFunctionNode node) {
buffer.append(node.getFunctionName());
buffer.append('(');
return true;
}
@Override
public void leaveFunctionNode(CssFunctionNode node) {
buffer.deleteLastCharIfCharIs(' ');
buffer.append(") ");
}
// We need to handle both standard function calls separated by
// commas, and IE-specific calls, with = as a separator as in
// alpha(opacity=70) or even with spaces as separators as in
// rect(0 0 0 0). In all cases, the separators each appear explicitly
// as arguments.
private static final ImmutableSet ARGUMENT_SEPARATORS = ImmutableSet.of(",", "=", " ");
@Override
public boolean enterArgumentNode(CssValueNode node) {
if (ARGUMENT_SEPARATORS.contains(node.toString())) {
// If the previous argument was a function node, then it has a
// trailing space that needs to be removed.
buffer.deleteLastCharIfCharIs(' ');
}
appendValueNode(node);
return true;
}
@Override
public boolean enterConditionalBlock(CssConditionalBlockNode node) {
visitController.stopVisit();
// TODO(oana): Collect these messages with a proper message collector.
logger.warning(
"Conditional block should not be "
+ "present: "
+ node.toString()
+ ((node.getSourceCodeLocation() != null)
? "@" + node.getSourceCodeLocation().getLineNumber()
: ""));
return true;
}
@Override
public boolean enterUnknownAtRule(CssUnknownAtRuleNode node) {
buffer.append('@').append(node.getName().toString());
if (!node.getParameters().isEmpty()) {
buffer.append(' ');
}
return true;
}
@Override
public boolean enterMediaTypeListDelimiter(CssNodesListNode extends CssNode> node) {
buffer.append(' ');
return true;
}
@Override
public void leaveUnknownAtRule(CssUnknownAtRuleNode node) {
if (node.getType().hasBlock()) {
if (!(node.getBlock() instanceof CssDeclarationBlockNode)) {
buffer.append('}');
}
} else {
buffer.append(';');
}
}
@Override
public boolean enterKeyframesRule(CssKeyframesNode node) {
buffer.append('@').append(node.getName().toString());
for (CssValueNode param : node.getParameters()) {
buffer.append(' ');
buffer.append(param.getValue());
}
if (node.getType().hasBlock()) {
buffer.append('{');
}
return true;
}
@Override
public void leaveKeyframesRule(CssKeyframesNode node) {
if (node.getType().hasBlock()) {
buffer.append('}');
} else {
buffer.append(';');
}
}
@Override
public boolean enterKey(CssKeyNode node) {
String value = node.getKeyValue();
if (value != null) {
buffer.append(value);
}
return true;
}
@Override
public void leaveKey(CssKeyNode key) {
buffer.append(',');
}
@Override
public void leaveKeyBlock(CssKeyListNode block) {
buffer.deleteLastCharIfCharIs(',');
}
/**
* Returns the CSS compacted printed output.
*/
public String getCompactPrintedString() {
return compactedPrintedString;
}
/**
* Appends the given value node to the buffer.
*
* Subclasses can modify this to provide a different serialization for particular types of
* value nodes.
*
* @param node the node to append
*/
protected void appendValueNode(CssValueNode node) {
if (node instanceof CssCompositeValueNode) {
return;
}
if (node instanceof CssBooleanExpressionNode && node.getParent() instanceof CssMediaRuleNode) {
appendMediaParameterWithParentheses(node);
return;
}
if (node instanceof CssStringNode) {
CssStringNode s = (CssStringNode) node;
buffer.append(s.toString(CssStringNode.HTML_ESCAPER));
return;
}
if (node instanceof CssNumericNode) {
// TODO(user): Consider introducing
// void CssNode.appendTo(Appendable sb)
// or
// CharSequence CssNode.asCharSequence
CssNumericNode n = (CssNumericNode) node;
buffer.append(n.getNumericPart());
buffer.append(n.getUnit());
return;
}
buffer.append(node.toString());
}
}