Skip to content

Commit 765b524

Browse files
committed
Add variable dependency pseudo selector support
1 parent 5c91e86 commit 765b524

11 files changed

+145
-65
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# v0.3.8 - 2015-5-28
2+
3+
- Add support for pseudo selectors `:hover` `:before`
14

25
# v0.3.7 - 2015-5-27
36

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ CSS variables or CSS Custom Properties limited subset polyfill/shim.
88

99
We strive for the most complete transformation but we/no plugin can achieve true complete parity according to the [specification](http://dev.w3.org/csswg/css-variables/) because of the DOM cascade unknowns.
1010

11-
## Latest Version: v0.3.7
11+
## Latest Version: v0.3.8
1212
### [Changelog](https://github.com/MadLittleMods/postcss-css-variables/blob/master/CHANGELOG.md)
1313

1414
### Install

index.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// PostCSS CSS Variables (postcss-css-variables)
2-
// v0.3.7
2+
// v0.3.8
33
//
44
// https://github.com/MadLittleMods/postcss-css-variables
55

@@ -46,26 +46,26 @@ function cleanUpNode(currentNodeToRemove) {
4646
}
4747

4848

49-
49+
var defaults = {
50+
// Allows you to preserve custom properties & var() usage in output.
51+
// `true`, `false`, or `'computed'`
52+
preserve: false,
53+
// Define variables via JS
54+
// Simple key-value pair
55+
// or an object with a `value` property and an optional `isImportant` bool property
56+
variables: {}
57+
};
5058

5159
module.exports = postcss.plugin('postcss-css-variables', function(options) {
52-
var defaults = {
53-
// Allows you to preserve custom properties & var() usage in output.
54-
// `true`, `false`, or `'computed'`
55-
preserve: false,
56-
// Define variables via JS
57-
// Simple key-value pair
58-
// or an object with a `value` property and an optional `isImportant` bool property
59-
variables: {}
60-
};
60+
6161
opts = extend({}, defaults, options);
6262

6363
// Work with opts here
6464

6565
return function (css, result) {
6666
// Transform CSS AST here
6767

68-
/* */
68+
/* * /
6969
try {
7070
/* */
7171

@@ -243,7 +243,7 @@ module.exports = postcss.plugin('postcss-css-variables', function(options) {
243243

244244
//console.log('map', map);
245245

246-
/* */
246+
/* * /
247247
}
248248
catch(e) {
249249
//console.log('e', e.message);

lib/is-node-under-scope.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
var isUnderScope = require('./is-under-scope');
22
var generateScopeList = require('./generate-scope-list');
33

4-
var isNodeUnderScope = function(node, scopeNode) {
4+
var isNodeUnderScope = function(node, scopeNode, /*optional*/ignorePseudo) {
55
var nodeScopeList = generateScopeList(node, true);
66
var scopeNodeScopeList = generateScopeList(scopeNode, true);
77

8-
return isUnderScope(nodeScopeList, scopeNodeScopeList);
8+
return isUnderScope(nodeScopeList, scopeNodeScopeList, ignorePseudo);
99
};
1010

1111
module.exports = isNodeUnderScope;

lib/is-under-scope.js

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ var RE_AT_RULE_SCOPE_PIECE = (/^@.*/);
77
// This will match pseudo selectors that have a base part
88
// ex. .foo:hover
99
// It will NOT match `:root`
10-
var RE_PSEUDO_SELECTOR = (/([^\s]+)((?::|::).*?)(?:\s+|$)/);
10+
var RE_PSEUDO_SELECTOR = (/([^\s:]+)((?::|::)[^\s]*?)(\s+|$)/);
1111

1212

1313
function getScopeMatchResults(nodeScopeList, scopeNodeScopeList) {
@@ -17,20 +17,9 @@ function getScopeMatchResults(nodeScopeList, scopeNodeScopeList) {
1717
// Check each comma separated piece of the complex selector
1818
var doesMatchScope = scopeNodeScopeList.some(function(scopeNodeScopePieces) {
1919
return nodeScopeList.some(function(nodeScopePieces) {
20-
// Because we only care about the scopeNodeScope matching to the nodeScope
21-
// Remove the pseudo selectors from the nodeScope so it can match a broader version
22-
// ex. `.foo:hover` can resolve variables from `.foo`
23-
nodeScopePieces = nodeScopePieces
24-
.map(function(descendantPiece) {
25-
// If not an at-rule piece, remove the pseudo selector part `@media (max-width: 300px)`
26-
if(!RE_AT_RULE_SCOPE_PIECE.test(descendantPiece)) {
27-
return descendantPiece.replace(new RegExp(RE_PSEUDO_SELECTOR.source, 'g'), function(whole, baseSelector, pseudo) {
28-
return baseSelector;
29-
});
30-
}
3120

32-
return descendantPiece;
33-
});
21+
//console.log('sp', scopeNodeScopePieces);
22+
//console.log('np', nodeScopePieces);
3423

3524
currentPieceOffset = null;
3625
var wasEveryPieceFound = true;
@@ -123,6 +112,19 @@ function getScopeMatchResults(nodeScopeList, scopeNodeScopeList) {
123112

124113

125114

115+
var stripPseudoSelectorsFromScopeList = function(scopeList) {
116+
return scopeList.map(function(scopePieces) {
117+
return scopePieces.map(function(descendantPiece) {
118+
// If not an at-rule piece, remove the pseudo selector part `@media (max-width: 300px)`
119+
if(!RE_AT_RULE_SCOPE_PIECE.test(descendantPiece)) {
120+
return descendantPiece.replace(new RegExp(RE_PSEUDO_SELECTOR.source, 'g'), function(whole, baseSelector, pseudo, trailingWhitespace) {
121+
return baseSelector + trailingWhitespace;
122+
});
123+
}
124+
return descendantPiece;
125+
});
126+
});
127+
};
126128

127129

128130
// Given the nodes scope, and the target scope,
@@ -131,8 +133,19 @@ function getScopeMatchResults(nodeScopeList, scopeNodeScopeList) {
131133
// Another way to think about it: Can the target scope cascade properties to the node?
132134
//
133135
// For scope-lists see: `generateScopeList`
134-
var isUnderScope = function isUnderScope(nodeScopeList, scopeNodeScopeList) {
136+
var isUnderScope = function(nodeScopeList, scopeNodeScopeList, /*optional*/ignorePseudo) {
137+
// Because we only care about the scopeNodeScope matching to the nodeScope
138+
// Remove the pseudo selectors from the nodeScope so it can match a broader version
139+
// ex. `.foo:hover` can resolve variables from `.foo`
140+
nodeScopeList = stripPseudoSelectorsFromScopeList(nodeScopeList);
141+
142+
if(ignorePseudo) {
143+
scopeNodeScopeList = stripPseudoSelectorsFromScopeList(scopeNodeScopeList);
144+
}
145+
135146
return getScopeMatchResults(nodeScopeList, scopeNodeScopeList).doesMatchScope;
136147
};
137148

149+
isUnderScope.RE_PSEUDO_SELECTOR = RE_PSEUDO_SELECTOR;
150+
138151
module.exports = isUnderScope;

lib/resolve-decl.js

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
var resolveValue = require('./resolve-value');
22
var generateScopeList = require('./generate-scope-list');
33
var gatherVariableDependencies = require('./gather-variable-dependencies');
4+
5+
var isUnderScope = require('./is-under-scope');
46
var isNodeUnderScope = require('./is-node-under-scope');
57

68
var findNodeAncestorWithSelector = require('./find-node-ancestor-with-selector');
@@ -24,40 +26,50 @@ function resolveDecl(decl, map, /*optional*/logResolveValueResult) {
2426

2527

2628

27-
// Resolve the cascade
29+
// Resolve the cascade dependencies
2830
// Now find any at-rule declarations that need to be added below each rule
29-
eachMapItemUnderAtRuleUsedByVariable(valueResults.variablesUsed, map, decl, function(mimicDecl, mapItem) {
30-
// Create the clean atRule for which we place the declaration under
31-
var atRuleNode = mapItem.parent.parent.clone().removeAll();
32-
31+
eachMapItemDependencyOfDecl(valueResults.variablesUsed, map, decl, function(mimicDecl, mapItem) {
3332
var ruleClone = decl.parent.clone().removeAll();
3433
var declClone = decl.clone();
35-
declClone.value = _logResolveValueResult(resolveValue(mimicDecl, map)).value;
34+
// No mangle resolve
35+
declClone.value = _logResolveValueResult(resolveValue(mimicDecl, map, true)).value;
3636

3737
// Add the declaration to our new rule
3838
ruleClone.append(declClone);
39-
// Add the rule to the atRule
40-
atRuleNode.append(ruleClone);
4139

4240

43-
// Since that atRuleNode can be nested in other atRules, we need to make the appropriate structure
44-
var parentAtRuleNode = atRuleNode;
45-
var currentAtRuleNode = mapItem.parent.parent;
46-
while(currentAtRuleNode.parent.type === 'atrule') {
47-
// Create a new clean clone of that at rule to nest under
48-
var newParentAtRuleNode = currentAtRuleNode.parent.clone().removeAll();
41+
if(mapItem.isUnderAtRule) {
42+
// Create the clean atRule for which we place the declaration under
43+
var atRuleNode = mapItem.parent.parent.clone().removeAll();
44+
45+
// Add the rule to the atRule
46+
atRuleNode.append(ruleClone);
47+
4948

50-
// Append the old parent
51-
newParentAtRuleNode.append(parentAtRuleNode);
52-
// Then set the new one as the current for next iteration
53-
parentAtRuleNode = newParentAtRuleNode;
49+
// Since that atRuleNode can be nested in other atRules, we need to make the appropriate structure
50+
var parentAtRuleNode = atRuleNode;
51+
var currentAtRuleNode = mapItem.parent.parent;
52+
while(currentAtRuleNode.parent.type === 'atrule') {
53+
// Create a new clean clone of that at rule to nest under
54+
var newParentAtRuleNode = currentAtRuleNode.parent.clone().removeAll();
55+
56+
// Append the old parent
57+
newParentAtRuleNode.append(parentAtRuleNode);
58+
// Then set the new one as the current for next iteration
59+
parentAtRuleNode = newParentAtRuleNode;
60+
61+
currentAtRuleNode = currentAtRuleNode.parent;
62+
}
5463

55-
currentAtRuleNode = currentAtRuleNode.parent;
64+
// Put the atRuleStructure after the declaration's rule
65+
decl.parent.parent.insertAfter(decl.parent, parentAtRuleNode);
5666
}
67+
else {
68+
ruleClone.selector = mimicDecl.parent.selector;
5769

58-
// Put the atRuleStructure after the declaration's rule
59-
decl.parent.parent.insertAfter(decl.parent, parentAtRuleNode);
60-
70+
// Put the atRuleStructure after the declaration's rule
71+
decl.parent.parent.insertAfter(decl.parent, ruleClone);
72+
}
6173
});
6274

6375

@@ -72,13 +84,15 @@ function resolveDecl(decl, map, /*optional*/logResolveValueResult) {
7284
}
7385

7486

75-
function eachMapItemUnderAtRuleUsedByVariable(variablesUsedList, map, decl, cb) {
87+
function eachMapItemDependencyOfDecl(variablesUsedList, map, decl, cb) {
7688
// Now find any at-rule declarations that pertains to each rule
7789
// Loop through the variables used
7890
variablesUsedList.forEach(function(variableUsedName) {
7991

8092
// Find anything in the map that corresponds to that variable
8193
gatherVariableDependencies(variablesUsedList, map).deps.forEach(function(mapItem) {
94+
95+
var mimicDecl;
8296
if(mapItem.isUnderAtRule) {
8397

8498
// Get the inner-most selector of the at-rule scope variable declaration we are matching
@@ -90,7 +104,7 @@ function eachMapItemUnderAtRuleUsedByVariable(variablesUsedList, map, decl, cb)
90104
// Splice on where the selector starts matching the selector inside at-rule
91105
// See: `test/fixtures/cascade-on-nested-rules.css`
92106
var varDeclAtRule = mapItem.parent.parent;
93-
var mimicDecl = cloneSpliceParentOntoNodeWhen(decl, varDeclAtRule, function(ancestor) {
107+
mimicDecl = cloneSpliceParentOntoNodeWhen(decl, varDeclAtRule, function(ancestor) {
94108
return ancestor === nodeToSpliceParentOnto;
95109
});
96110

@@ -99,13 +113,27 @@ function eachMapItemUnderAtRuleUsedByVariable(variablesUsedList, map, decl, cb)
99113
//console.log('amd', generateScopeList(mimicDecl.parent, true));
100114
//console.log(generateScopeList(mapItem.parent, true));
101115
//console.log('amd isNodeUnderScope', isNodeUnderScope(mimicDecl.parent, mapItem.parent), mapItem.decl.value);
116+
}
117+
// TODO: use regex from `isUnderScope`
118+
else if(isUnderScope.RE_PSEUDO_SELECTOR.test(mapItem.parent.selector)) {
119+
// Create a detached clone
120+
var ruleClone = decl.parent.clone().removeAll();
121+
ruleClone.parent = decl.parent.parent;
122+
123+
// Add the declaration to it
124+
mimicDecl = decl.clone();
125+
ruleClone.append(mimicDecl);
126+
127+
var lastPseudoSelectorMatches = mapItem.parent.selector.match(new RegExp(isUnderScope.RE_PSEUDO_SELECTOR.source + '$'));
128+
var lastPseudoSelector = lastPseudoSelectorMatches ? lastPseudoSelectorMatches[2] : '';
129+
130+
ruleClone.selector += lastPseudoSelector;
131+
}
102132

103-
// If it is under the proper scope,
104-
// we need to check because we are iterating over all map entries that are `isUnderAtRule`
105-
// Then lets create the new rules
106-
if(isNodeUnderScope(mimicDecl.parent, mapItem.parent)) {
107-
cb(mimicDecl, mapItem);
108-
}
133+
// If it is under the proper scope,
134+
// we need to check because we are iterating over all map entries
135+
if(mimicDecl && isNodeUnderScope(mimicDecl, mapItem.parent, true)) {
136+
cb(mimicDecl, mapItem);
109137
}
110138
});
111139
});

lib/resolve-value.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ var RE_VAR_FUNC = (/var\((--[^,\s]+?)(?:\s*,\s*(.+))?\)/);
1414

1515
// Pass in a value string to parse/resolve and a map of available values
1616
// and we can figure out the final value
17+
//
18+
// `ignorePseudoScope`: Optional bool to determine whether the scope resolution should be left alone or not
1719
//
1820
// Note: We do not modify the declaration
1921
// Note: Resolving a declaration value without any `var(...)` does not harm the final value.
2022
// This means, feel free to run everything through this function
21-
var resolveValue = function(decl, map, /*internal debugging*/_debugIsInternal) {
23+
var resolveValue = function(decl, map, /*optional*/ignorePseudoScope, /*internal debugging*/_debugIsInternal) {
2224

2325
var resultantValue = decl.value;
2426
var warnings = [];
@@ -51,7 +53,7 @@ var resolveValue = function(decl, map, /*internal debugging*/_debugIsInternal) {
5153
//console.log(debugIndent, 'isNodeUnderScope', isNodeUnderScope(decl.parent, varDeclMapItem.parent), varDeclMapItem.decl.value);
5254

5355
if(
54-
isNodeUnderScope(decl.parent, varDeclMapItem.parent) &&
56+
isNodeUnderScope(decl.parent, varDeclMapItem.parent, ignorePseudoScope) &&
5557
// And if the currently matched declaration is `!important`, it will take another `!important` to override it
5658
(!(matchingVarDeclMapItem || {}).isImportant || varDeclMapItem.isImportant)
5759
) {
@@ -73,7 +75,7 @@ var resolveValue = function(decl, map, /*internal debugging*/_debugIsInternal) {
7375
return ancestor === nodeToSpliceParentOnto;
7476
});
7577

76-
replaceValue = resolveValue(matchingMimicDecl, map, true).value;
78+
replaceValue = resolveValue(matchingMimicDecl, map, /*internal*/true).value;
7779
}
7880

7981
isResultantValueUndefined = replaceValue === undefined;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "postcss-css-variables",
3-
"version": "0.3.7",
3+
"version": "0.3.8",
44
"description": "PostCSS plugin to transform CSS Custom Properties(CSS variables) syntax into a static representation",
55
"keywords": [
66
"postcss",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.foo {
2+
--foo-color: #ff0000;
3+
color: var(--foo-color);
4+
}
5+
6+
.foo:hover {
7+
--foo-color: #00ff00;
8+
}
9+
10+
11+
/* This should add nothing to `.foo`, wrong scope */
12+
.bar:hover + .foo {
13+
--foo-color: #f0f000;
14+
}
15+
/* This should add nothing to `.foo`, wrong scope */
16+
.foo:hover + .bar {
17+
--foo-color: #0000ff;
18+
}
19+
/* This should add nothing to `.foo`, wrong scope */
20+
.foo:hover + .bar:focus {
21+
--foo-color: #000f0f;
22+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.foo {
2+
color: #ff0000;
3+
}
4+
5+
.foo:hover {
6+
color: #00ff00;
7+
}

test/test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ describe('postcss-css-variables', function() {
9696
return testPlugin('./test/fixtures/pseudo-selector.css', './test/fixtures/pseudo-selector.expected.css');
9797
});
9898

99+
it('should work with variables declared in pseudo selectors', function() {
100+
return testPlugin('./test/fixtures/pseudo-selector-declare-variable.css', './test/fixtures/pseudo-selector-declare-variable.expected.css');
101+
});
102+
103+
99104

100105
it('should work with variables defined in comma separated selector', function() {
101106
return testPlugin('./test/fixtures/comma-separated-variable-declaration.css', './test/fixtures/comma-separated-variable-declaration.expected.css');

0 commit comments

Comments
 (0)