/* * 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.base.Preconditions; 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.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.DefaultTreeVisitor; 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 mkretzschmar@google.com (Martin Kretzschmar) * @author oana@google.com (Oana Florescu) * @author fbenz@google.com (Florian Benz) */ public class PrettyPrintingVisitor extends DefaultTreeVisitor { private final CodeBuffer buffer; private final boolean stripQuotes; private final boolean preserveComments; private String indent = ""; public PrettyPrintingVisitor( @Nullable CodeBuffer buffer, boolean stripQuotes, boolean preserveComments) { this.buffer = Preconditions.checkNotNull(buffer); this.stripQuotes = stripQuotes; this.preserveComments = preserveComments; } @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(); } } } 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; } }