com.google.common.css.compiler.passes.PrettyPrinter 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 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 static com.google.common.base.Preconditions.checkArgument;
import com.google.common.css.compiler.ast.CssAtRuleNode.Type;
import com.google.common.css.compiler.ast.CssAttributeSelectorNode;
import com.google.common.css.compiler.ast.CssBlockNode;
import com.google.common.css.compiler.ast.CssClassSelectorNode;
import com.google.common.css.compiler.ast.CssCombinatorNode;
import com.google.common.css.compiler.ast.CssCommentNode;
import com.google.common.css.compiler.ast.CssCompilerPass;
import com.google.common.css.compiler.ast.CssComponentNode;
import com.google.common.css.compiler.ast.CssCompositeValueNode;
import com.google.common.css.compiler.ast.CssConditionalBlockNode;
import com.google.common.css.compiler.ast.CssConditionalRuleNode;
import com.google.common.css.compiler.ast.CssDeclarationBlockNode;
import com.google.common.css.compiler.ast.CssDeclarationNode;
import com.google.common.css.compiler.ast.CssDefinitionNode;
import com.google.common.css.compiler.ast.CssFontFaceNode;
import com.google.common.css.compiler.ast.CssFunctionNode;
import com.google.common.css.compiler.ast.CssIdSelectorNode;
import com.google.common.css.compiler.ast.CssImportRuleNode;
import com.google.common.css.compiler.ast.CssKeyListNode;
import com.google.common.css.compiler.ast.CssKeyNode;
import com.google.common.css.compiler.ast.CssKeyframeRulesetNode;
import com.google.common.css.compiler.ast.CssKeyframesNode;
import com.google.common.css.compiler.ast.CssMediaRuleNode;
import com.google.common.css.compiler.ast.CssMixinDefinitionNode;
import com.google.common.css.compiler.ast.CssMixinNode;
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.CssPropertyValueNode;
import com.google.common.css.compiler.ast.CssProvideNode;
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.CssRefinerNode;
import com.google.common.css.compiler.ast.CssRequireNode;
import com.google.common.css.compiler.ast.CssRootNode;
import com.google.common.css.compiler.ast.CssRulesetNode;
import com.google.common.css.compiler.ast.CssSelectorListNode;
import com.google.common.css.compiler.ast.CssSelectorNode;
import com.google.common.css.compiler.ast.CssStringNode;
import com.google.common.css.compiler.ast.CssTree;
import com.google.common.css.compiler.ast.CssUnknownAtRuleNode;
import com.google.common.css.compiler.ast.CssValueNode;
import com.google.common.css.compiler.ast.VisitController;
import javax.annotation.Nullable;
/**
* A pretty-printer for {@link CssTree} instances. This is work in progress.
* Look at PrettyPrinterTest to see what's supported.
*
* @author [email protected] (Martin Kretzschmar)
* @author [email protected] (Oana Florescu)
* @author [email protected] (Florian Benz)
*/
public class PrettyPrinter extends CodePrinter implements CssCompilerPass {
private String prettyPrintedString = null;
private String indent = "";
private boolean stripQuotes = false;
private boolean preserveComments = false;
public PrettyPrinter(VisitController visitController,
@Nullable CodeBuffer buffer,
@Nullable GssSourceMapGenerator generator) {
super(visitController, buffer, generator);
}
public PrettyPrinter(VisitController visitController,
GssSourceMapGenerator generator) {
this(visitController, null /* buffer */, generator);
}
public PrettyPrinter(VisitController visitController) {
this(visitController, null /* buffer */, null /* generator */);
}
/**
* Whether to strip quotes from certain values. This facilitates
* tests that want to compare trees.
*/
public void setStripQuotes(boolean stripQuotes) {
this.stripQuotes = stripQuotes;
}
/**
* Whether comments in the CSS nodes are preserved in the pretty printed
* output.
* Note: Comments layout is not guaranteed, since detailed position
* information in the input files is not preserved by the parser. Line breaks
* are added after every comment with current identation as best effort.
*/
public PrettyPrinter setPreserveComments(boolean preserve) {
this.preserveComments = preserve;
return this;
}
@Override
public boolean enterImportRule(CssImportRuleNode node) {
maybeAppendComments(node);
buffer.append(node.getType().toString());
for (CssValueNode param : node.getParameters()) {
buffer.append(' ');
// TODO(user): teach visit controllers to explore this subtree
// rather than leaving it to each pass to figure things out.
if (param instanceof CssStringNode) {
CssStringNode n = (CssStringNode) param;
buffer.append(n.toString(CssStringNode.SHORT_ESCAPER));
} else {
buffer.append(param.getValue());
}
}
return true;
}
@Override
public void leaveImportRule(CssImportRuleNode node) {
buffer.append(';').startNewLine();
}
@Override
public boolean enterMediaRule(CssMediaRuleNode node) {
maybeAppendComments(node);
buffer.append(node.getType().toString());
if (node.getParameters().size() > 0
|| (node.getType().hasBlock() && node.getBlock() != null)) {
buffer.append(' ');
}
return true;
}
@Override
public void leaveMediaRule(CssMediaRuleNode node) {}
@Override
public boolean enterPageRule(CssPageRuleNode node) {
maybeAppendComments(node);
buffer.append(node.getType().toString());
buffer.append(' ');
for (CssValueNode param : node.getParameters()) {
buffer.append(param.getValue());
}
if (node.getParametersCount() > 0) {
buffer.append(' ');
}
return true;
}
@Override
public boolean enterPageSelector(CssPageSelectorNode node) {
maybeAppendComments(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) {
maybeAppendComments(node);
buffer.append(node.getType().toString());
return true;
}
@Override
public boolean enterDefinition(CssDefinitionNode node) {
maybeAppendComments(node);
buffer.append(indent);
buffer.append(node.getType());
buffer.append(' ');
buffer.append(node.getName());
// Add a space to separate it from next value.
buffer.append(' ');
return true;
}
@Override
public void leaveDefinition(CssDefinitionNode node) {
// Remove trailing space after last value.
buffer.deleteLastCharIfCharIs(' ');
buffer.append(';').startNewLine();
}
@Override
public boolean enterRuleset(CssRulesetNode ruleset) {
maybeAppendComments(ruleset);
buffer.append(indent);
return true;
}
@Override
public boolean enterKeyframeRuleset(CssKeyframeRulesetNode ruleset) {
maybeAppendComments(ruleset);
buffer.append(indent);
return true;
}
// TODO(mkretzschmar): make DeclarationBlock subclass of Block and eliminate
// this.
@Override
public boolean enterDeclarationBlock(CssDeclarationBlockNode block) {
maybeAppendComments(block);
buffer.deleteLastCharIfCharIs(' ');
buffer.append(" {").startNewLine();
indent += " ";
return true;
}
@Override
public void leaveDeclarationBlock(CssDeclarationBlockNode block) {
indent = indent.substring(0, indent.length() - 2);
buffer.append(indent);
buffer.append('}').startNewLine();
}
@Override
public boolean enterBlock(CssBlockNode block) {
maybeAppendComments(block);
if (block.getParent() instanceof CssUnknownAtRuleNode
|| block.getParent() instanceof CssMediaRuleNode) {
buffer.append('{').startNewLine();
indent += " ";
}
return true;
}
@Override
public void leaveBlock(CssBlockNode block) {
if (block.getParent() instanceof CssMediaRuleNode) {
buffer.append('}').startNewLine();
indent = indent.substring(0, indent.length() - 2);
}
}
@Override
public boolean enterDeclaration(CssDeclarationNode declaration) {
maybeAppendComments(declaration);
buffer.append(indent);
if (declaration.hasStarHack()) {
buffer.append('*');
}
buffer.append(declaration.getPropertyName().getValue());
buffer.append(": ");
return true;
}
@Override
public void leaveDeclaration(CssDeclarationNode declaration) {
buffer.deleteLastCharIfCharIs(' ');
buffer.append(';').startNewLine();
}
@Override
public boolean enterValueNode(CssValueNode node) {
maybeAppendComments(node);
checkArgument(!(node instanceof CssCompositeValueNode));
String v = node.toString();
if (stripQuotes && node.getParent() instanceof CssDefinitionNode) {
v = maybeStripQuotes(v);
}
buffer.append(v);
// NOTE(flan): When visiting function arguments, we don't want to add extra
// spaces because they are already in the arguments list if they are
// required. Yes, this sucks.
if (!node.inFunArgs()) {
buffer.append(' ');
}
return true;
}
@Override
public boolean enterCompositeValueNodeOperator(CssCompositeValueNode parent) {
maybeAppendComments(parent);
buffer.append(parent.getOperator().getOperatorName());
if (!parent.inFunArgs()) {
buffer.append(' ');
}
return true;
}
@Override
public boolean enterFunctionNode(CssFunctionNode node) {
maybeAppendComments(node);
buffer.append(node.getFunctionName());
buffer.append('(');
return true;
}
@Override
public void leaveFunctionNode(CssFunctionNode node) {
buffer.deleteLastCharIfCharIs(' ');
buffer.append(") ");
}
@Override
public boolean enterArgumentNode(CssValueNode node) {
maybeAppendComments(node);
String v = node.toString();
if (stripQuotes
&& node.getParent().getParent() instanceof CssFunctionNode
&& ((CssFunctionNode) node.getParent().getParent()).getFunctionName()
.equals("url")) {
v = maybeStripQuotes(v);
}
buffer.append(v);
return !(node instanceof CssCompositeValueNode);
}
@Override
public boolean enterSelector(CssSelectorNode selector) {
maybeAppendComments(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) {
maybeAppendComments(node);
appendRefiner(node);
return true;
}
@Override
public boolean enterIdSelector(CssIdSelectorNode node) {
maybeAppendComments(node);
appendRefiner(node);
return true;
}
@Override
public boolean enterPseudoClass(CssPseudoClassNode node) {
maybeAppendComments(node);
buffer.append(node.getPrefix());
buffer.append(node.getRefinerName());
switch (node.getFunctionType()) {
case NTH:
buffer.append(node.getArgument().replace(" ", ""));
buffer.append(')');
break;
case LANG:
buffer.append(node.getArgument());
buffer.append(')');
break;
}
return true;
}
@Override
public void leavePseudoClass(CssPseudoClassNode node) {
if (node.getFunctionType() == FunctionType.NOT) {
buffer.deleteEndingIfEndingIs(", ");
buffer.append(')');
}
}
@Override
public boolean enterPseudoElement(CssPseudoElementNode node) {
maybeAppendComments(node);
appendRefiner(node);
return true;
}
@Override
public boolean enterAttributeSelector(CssAttributeSelectorNode node) {
maybeAppendComments(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) {
maybeAppendComments(combinator);
buffer.append(combinator.getCombinatorType().getCanonicalName());
}
return true;
}
@Override
public void leaveCombinator(CssCombinatorNode combinator) {
buffer.deleteEndingIfEndingIs(", ");
}
@Override
public void leaveSelectorBlock(CssSelectorListNode node) {
buffer.deleteEndingIfEndingIs(", ");
}
@Override
public void leaveConditionalBlock(CssConditionalBlockNode block) {
buffer.startNewLine();
}
@Override
public boolean enterConditionalRule(CssConditionalRuleNode node) {
maybeAppendComments(node);
if (node.getType() != Type.IF) {
buffer.append(' ');
} else {
buffer.append(indent);
}
buffer.append(node.getType());
if (node.getParametersCount() > 0) {
buffer.append(' ');
boolean firstParameter = true;
for (CssValueNode value : node.getParameters()) {
if (!firstParameter) {
buffer.append(' ');
}
firstParameter = false;
buffer.append(value.toString());
}
}
buffer.append(" {").startNewLine();
indent += " ";
return true;
}
@Override
public void leaveConditionalRule(CssConditionalRuleNode node) {
indent = indent.substring(0, indent.length() - 2);
buffer.append(indent);
buffer.append('}');
}
@Override
public boolean enterUnknownAtRule(CssUnknownAtRuleNode node) {
maybeAppendComments(node);
buffer.append(indent);
buffer.append('@').append(node.getName().toString());
if (node.getParameters().size() > 0
|| (node.getType().hasBlock() && node.getBlock() != null)) {
buffer.append(' ');
}
return true;
}
@Override
public void leaveUnknownAtRule(CssUnknownAtRuleNode node) {
if (node.getType().hasBlock()) {
if (!(node.getBlock() instanceof CssDeclarationBlockNode)) {
indent = indent.substring(0, indent.length() - 2);
buffer.append(indent);
buffer.append('}').startNewLine();
}
} else {
buffer.deleteLastCharIfCharIs(' ');
buffer.append(';').startNewLine();
}
}
@Override
public boolean enterKeyframesRule(CssKeyframesNode node) {
maybeAppendComments(node);
buffer.append(indent);
buffer.append('@').append(node.getName().toString());
for (CssValueNode param : node.getParameters()) {
buffer.append(' ');
buffer.append(param.getValue());
}
if (node.getType().hasBlock()) {
buffer.append(" {").startNewLine();
indent += " ";
}
return true;
}
@Override
public void leaveKeyframesRule(CssKeyframesNode node) {
if (node.getType().hasBlock()) {
indent = indent.substring(0, indent.length() - 2);
buffer.append(indent);
buffer.append('}').startNewLine();
} else {
buffer.append(';').startNewLine();
}
}
@Override
public boolean enterKey(CssKeyNode key) {
maybeAppendComments(key);
String value = key.getKeyValue();
if (value != null) {
buffer.append(value);
}
return true;
}
@Override
public void leaveKey(CssKeyNode key) {
buffer.append(", ");
}
@Override
public void leaveKeyBlock(CssKeyListNode node) {
buffer.deleteEndingIfEndingIs(", ");
}
@Override
public boolean enterProvideNode(CssProvideNode node) {
maybeAppendComments(node);
return true;
}
@Override
public boolean enterRequireNode(CssRequireNode node) {
maybeAppendComments(node);
return true;
}
@Override
public boolean enterComponent(CssComponentNode node) {
maybeAppendComments(node);
return true;
}
@Override
public boolean enterMixin(CssMixinNode node) {
maybeAppendComments(node);
return true;
}
@Override
public boolean enterConditionalBlock(CssConditionalBlockNode block) {
maybeAppendComments(block);
return true;
}
@Override
public boolean enterMixinDefinition(CssMixinDefinitionNode node) {
maybeAppendComments(node);
return true;
}
@Override
public boolean enterCompositeValueNode(CssCompositeValueNode value) {
maybeAppendComments(value);
return true;
}
@Override
public boolean enterPropertyValue(CssPropertyValueNode propertyValue) {
maybeAppendComments(propertyValue);
return true;
}
@Override
public boolean enterTree(CssRootNode root) {
maybeAppendComments(root);
return true;
}
private void maybeAppendComments(CssNode node) {
if (preserveComments && !node.getComments().isEmpty()) {
for (CssCommentNode c : node.getComments()) {
buffer.append(indent);
buffer.append(c.getValue());
buffer.startNewLine();
}
}
}
public String getPrettyPrintedString() {
return prettyPrintedString;
}
@Override
public void runPass() {
resetBuffer();
visitController.startVisit(this);
prettyPrintedString = getOutputBuffer();
}
private String maybeStripQuotes(String v) {
if (v.startsWith("'") || v.startsWith("\"")) {
assert(v.endsWith(v.substring(0, 1)));
v = v.substring(1, v.length() - 1);
}
return v;
}
}