diff --git a/src/com/google/common/css/compiler/ast/CssClassSelectorNode.java b/src/com/google/common/css/compiler/ast/CssClassSelectorNode.java index e2ad17f2..e6314869 100644 --- a/src/com/google/common/css/compiler/ast/CssClassSelectorNode.java +++ b/src/com/google/common/css/compiler/ast/CssClassSelectorNode.java @@ -26,29 +26,36 @@ * @author fbenz@google.com (Florian Benz) */ public class CssClassSelectorNode extends CssRefinerNode { - private final boolean componentScoped; + /** Specifies the kind or absence of a component scoping prefix. */ + public static enum ComponentScoping { + /** The classname has no prefix. */ + DEFAULT, + /** The classname has a % prefix, to force scoping. */ + FORCE_SCOPED, + /** The classname has a ^ prefix, to prevent scoping. */ + FORCE_UNSCOPED + } + + private final ComponentScoping scoping; - public CssClassSelectorNode(String refinerName, boolean componentScoped, + public CssClassSelectorNode(String refinerName, ComponentScoping scoping, SourceCodeLocation sourceCodeLocation) { super(Refiner.CLASS, refinerName, sourceCodeLocation); - this.componentScoped = componentScoped; + this.scoping = scoping; } public CssClassSelectorNode(String refinerName, SourceCodeLocation sourceCodeLocation) { - this(refinerName, false, sourceCodeLocation); + this(refinerName, ComponentScoping.DEFAULT, sourceCodeLocation); } protected CssClassSelectorNode(CssClassSelectorNode node) { - this(node.refinerName, node.componentScoped, node.getSourceCodeLocation()); + this(node.refinerName, node.scoping, node.getSourceCodeLocation()); this.setComments(node.getComments()); } - /** - * Returns {@code true} if this class selector was prefixed with a percent-sign, indicating that - * it should be prefixed with the current @component's class prefix. - */ - public boolean isComponentScoped() { - return componentScoped; + /** Returns the kind or absence of a component scoping prefix. */ + public ComponentScoping getScoping() { + return scoping; } @Override diff --git a/src/com/google/common/css/compiler/ast/GssParserCC.jj b/src/com/google/common/css/compiler/ast/GssParserCC.jj index 14349727..447e4f7e 100644 --- a/src/com/google/common/css/compiler/ast/GssParserCC.jj +++ b/src/com/google/common/css/compiler/ast/GssParserCC.jj @@ -345,8 +345,9 @@ public class GssParserCC { } public CssClassSelectorNode buildClassSelectorNode(String name, - SourceCodeLocation location, boolean isComponentScoped, List tokens) { - CssClassSelectorNode node = new CssClassSelectorNode(name, isComponentScoped, location); + SourceCodeLocation location, CssClassSelectorNode.ComponentScoping scoping, + List tokens) { + CssClassSelectorNode node = new CssClassSelectorNode(name, scoping, location); attachComments(tokens, node); return node; } @@ -595,7 +596,7 @@ PARSER_END(GssParserCC) | < #HASH: "#" > | < #UNDERSCORE: "_" > | < #AMPERSAND: "&" > - | < #CARET: "^" > + | < CARET: "^" > | < #DOLLAR: "$" > | < #PIPE: "|" > | < AND: > @@ -863,16 +864,18 @@ CssClassSelectorNode className() : { Token t; List tokens = Lists.newArrayList(); - boolean isComponentScoped = false; + CssClassSelectorNode.ComponentScoping scoping = CssClassSelectorNode.ComponentScoping.DEFAULT; } { t = { tokens.add(t); } - ( { isComponentScoped = true; } )? + ( + ( { scoping = CssClassSelectorNode.ComponentScoping.FORCE_SCOPED; } ) | + ( { scoping = CssClassSelectorNode.ComponentScoping.FORCE_UNSCOPED; } ) + )? t = { tokens.add(t); - return nodeBuilder.buildClassSelectorNode(t.image, this.getLocation(), isComponentScoped, - tokens); + return nodeBuilder.buildClassSelectorNode(t.image, this.getLocation(), scoping, tokens); } } diff --git a/src/com/google/common/css/compiler/passes/LoopVariableReplacementPass.java b/src/com/google/common/css/compiler/passes/LoopVariableReplacementPass.java index 73c4dbb4..b30337db 100644 --- a/src/com/google/common/css/compiler/passes/LoopVariableReplacementPass.java +++ b/src/com/google/common/css/compiler/passes/LoopVariableReplacementPass.java @@ -85,7 +85,7 @@ public boolean enterClassSelector(CssClassSelectorNode classSelector) { visitController.replaceCurrentBlockChildWith( ImmutableList.of(new CssClassSelectorNode( refinerName, - classSelector.isComponentScoped(), + classSelector.getScoping(), classSelector.getSourceCodeLocation())), true); } diff --git a/src/com/google/common/css/compiler/passes/ProcessComponents.java b/src/com/google/common/css/compiler/passes/ProcessComponents.java index 72c38eb2..4e2fa903 100644 --- a/src/com/google/common/css/compiler/passes/ProcessComponents.java +++ b/src/com/google/common/css/compiler/passes/ProcessComponents.java @@ -28,6 +28,7 @@ import com.google.common.css.compiler.ast.CssAtRuleNode; import com.google.common.css.compiler.ast.CssBlockNode; import com.google.common.css.compiler.ast.CssClassSelectorNode; +import com.google.common.css.compiler.ast.CssClassSelectorNode.ComponentScoping; import com.google.common.css.compiler.ast.CssCombinatorNode; import com.google.common.css.compiler.ast.CssCompilerPass; import com.google.common.css.compiler.ast.CssComponentNode; @@ -141,11 +142,16 @@ public boolean enterClassSelector(CssClassSelectorNode node) { // Note that this works because enterComponent, above, returns false - // this visitor never sees class selectors inside components (the other // visitor does). - if (node.isComponentScoped()) { + if (node.getScoping() == ComponentScoping.FORCE_SCOPED) { reportError("'%' prefix for class selectors may only be used in the scope of an @component", node); return false; } + if (node.getScoping() == ComponentScoping.FORCE_UNSCOPED) { + reportError("'^' prefix for class selectors may only be used in the scope of an @component", + node); + return false; + } return true; } @@ -362,7 +368,13 @@ public void leavePseudoClass(CssPseudoClassNode pseudoClass) { @Override public boolean enterClassSelector(CssClassSelectorNode node) { Preconditions.checkState(!isAbstract); - if (firstClassSelector || node.isComponentScoped()) { + if (!firstClassSelector && node.getScoping() == ComponentScoping.FORCE_UNSCOPED) { + errorManager.report(new GssError( + "'^' prefix may only be used on the first classname in a selector.", + node.getSourceCodeLocation())); + } + if (firstClassSelector && node.getScoping() != ComponentScoping.FORCE_UNSCOPED + || node.getScoping() == ComponentScoping.FORCE_SCOPED) { CssClassSelectorNode newNode = new CssClassSelectorNode( classPrefix + node.getRefinerName(), inAncestorBlock ? sourceCodeLocation : node.getSourceCodeLocation()); diff --git a/tests/com/google/common/css/compiler/passes/ProcessComponentsTest.java b/tests/com/google/common/css/compiler/passes/ProcessComponentsTest.java index 6e101aab..f9244a8a 100644 --- a/tests/com/google/common/css/compiler/passes/ProcessComponentsTest.java +++ b/tests/com/google/common/css/compiler/passes/ProcessComponentsTest.java @@ -251,6 +251,7 @@ public class ProcessComponentsTest extends PassesTestBase { " TD.PREFIX_F1 TD.NO_PREFIX_F2,", // Multiple element refiners " #X.PREFIX_G1.NO_PREFIX_G2,", // ID refiner " #X.PREFIX_H1 .NO_PREFIX_H2,", // ID refiner with combinator + " .^NO_PREFIX_A1.NO_PREFIX_A2,", // Explicit unscoped with complex selector " .PREFIX_I1.%PREFIX_I2,", // Explicit scoped with complex selector " .PREFIX_J1 .%PREFIX_J2,", // Explicit scoped with descendant combinator " .PREFIX_K1 > .%PREFIX_K2,", // Explicit scoped with child combinator @@ -291,6 +292,7 @@ public class ProcessComponentsTest extends PassesTestBase { "TD.someExamplePackagePREFIX_F1 TD.NO_PREFIX_F2," + "#X.someExamplePackagePREFIX_G1.NO_PREFIX_G2," + "#X.someExamplePackagePREFIX_H1 .NO_PREFIX_H2," + + ".NO_PREFIX_A1.NO_PREFIX_A2," + ".someExamplePackagePREFIX_I1.someExamplePackagePREFIX_I2," + ".someExamplePackagePREFIX_J1 .someExamplePackagePREFIX_J2," + ".someExamplePackagePREFIX_K1>.someExamplePackagePREFIX_K2," +