Skip to content

Commit cba0da4

Browse files
committed
Initial stages of major refactor
1 parent 366f5ec commit cba0da4

19 files changed

+4197
-975
lines changed

.npmrc

Lines changed: 0 additions & 1 deletion
This file was deleted.

index.js

Lines changed: 82 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
// PostCSS CSS Variables (postcss-css-variables)
2-
// v0.5.0
3-
//
4-
// https://github.com/MadLittleMods/postcss-css-variables
5-
6-
// For Debugging
7-
//var nomo = require('node-monkey').start({port: 50501});
8-
9-
var postcss = require('postcss');
10-
var extend = require('extend');
11-
12-
var shallowCloneNode = require('./lib/shallow-clone-node');
13-
var resolveValue = require('./lib/resolve-value');
14-
var resolveDecl = require('./lib/resolve-decl');
15-
1+
const debug = require('debug')('postcss-css-variables:plugin');
2+
const postcss = require('postcss');
3+
const specificityLib = require('specificity');
4+
const generateSelectorBranchesFromPostcssNode = require('postcss-node-scope-utility/lib/generate-branches');
5+
const isSelectorBranchUnderScope = require('postcss-node-scope-utility/lib/is-branch-under-scope');
166

177
// A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS)
188
// `--foo`
199
// See: http://dev.w3.org/csswg/css-variables/#custom-property
20-
var RE_VAR_PROP = (/(--(.+))/);
10+
const RE_VAR_PROP = (/(--(.+))/);
11+
const RE_VAR_FUNC = (/var\((--[^,\s]+?)(?:\s*,\s*(.+))?\)/);
2112

13+
function getSpecificity(selector) {
14+
// We only care about the first piece because we have already split the comma-separated pieces before we use this
15+
return specificityLib.calculate(selector)[0].specificityArray;
16+
}
17+
18+
function compareSpecificity(specificityArrayA, specificityArrayB) {
19+
if(!specificityArrayA) return -1;
20+
if(!specificityArrayB) return 1;
2221

22+
return specificityLib.compare(specificityArrayA, specificityArrayB);
23+
}
2324

2425

2526
function eachCssVariableDeclaration(css, cb) {
@@ -33,28 +34,6 @@ function eachCssVariableDeclaration(css, cb) {
3334
}
3435

3536

36-
37-
function cleanUpNode(node) {
38-
// If we removed all of the declarations in the rule(making it empty),
39-
// then just remove it
40-
var nodeToPossiblyCleanUp = node;
41-
while(nodeToPossiblyCleanUp && nodeToPossiblyCleanUp.nodes.length <= 0) {
42-
var nodeToRemove = nodeToPossiblyCleanUp.type !== 'root' ? nodeToPossiblyCleanUp : null;
43-
44-
if(nodeToRemove) {
45-
// Get a reference to it before we remove
46-
// and lose reference to the child after removing it
47-
nodeToPossiblyCleanUp = nodeToRemove.parent;
48-
49-
nodeToRemove.remove();
50-
}
51-
else {
52-
nodeToPossiblyCleanUp = null;
53-
}
54-
}
55-
}
56-
57-
5837
var defaults = {
5938
// Allows you to preserve custom properties & var() usage in output.
6039
// `true`, `false`, or `'computed'`
@@ -70,113 +49,46 @@ var defaults = {
7049

7150
module.exports = postcss.plugin('postcss-css-variables', function(options) {
7251

73-
var opts = extend({}, defaults, options);
52+
var opts = Object.assign({}, defaults, options);
7453

7554
// Work with opts here
7655

7756
return function (css, result) {
78-
// Transform CSS AST here
79-
80-
/* * /
81-
try {
82-
/* */
83-
84-
// List of nodes that if empty, will be removed
85-
// We use this because we don't want to modify the AST when we still need to reference these later on
86-
var nodesToRemoveAtEnd = [];
87-
88-
// Keep track of the injected from `opts.variables` to remove at the end
89-
// if user passes `opts.preserveInjectedVariables = false`
90-
var injectedDeclsToRemoveAtEnd = [];
91-
9257
// Map of variable names to a list of declarations
93-
var map = {};
58+
let map = {};
9459

9560
// Add the js defined variables `opts.variables` to the map
96-
map = extend(
97-
map,
98-
Object.keys(opts.variables).reduce(function(prevVariableMap, variableName) {
99-
var variableEntry = opts.variables[variableName];
100-
// Automatically prefix any variable with `--` (CSS custom property syntax) if it doesn't have it already
101-
variableName = variableName.slice(0, 2) === '--' ? variableName : '--' + variableName;
102-
var variableValue = (variableEntry || {}).value || variableEntry;
103-
var isImportant = (variableEntry || {}).isImportant || false;
104-
105-
106-
// Add a root node to the AST
107-
var variableRootRule = postcss.rule({ selector: ':root' });
108-
css.root().prepend(variableRootRule);
109-
// Add the variable decl to the root node
110-
var varDecl = postcss.decl({
111-
prop: variableName,
112-
value: variableValue
113-
});
114-
variableRootRule.append(varDecl);
115-
116-
// Collect JS-injected variables for removal if `opts.preserveInjectedVariables = false`
117-
if (!opts.preserveInjectedVariables) {
118-
injectedDeclsToRemoveAtEnd.push(varDecl);
119-
}
120-
121-
// Add the entry to the map
122-
prevVariableMap[variableName] = (prevVariableMap[variableName] || []).concat({
123-
decl: varDecl,
124-
prop: variableName,
125-
calculatedInPlaceValue: variableValue,
126-
isImportant: isImportant,
127-
variablesUsed: [],
128-
parent: variableRootRule,
129-
isUnderAtRule: false
130-
});
131-
132-
return prevVariableMap;
133-
}, {})
134-
);
135-
136-
137-
// Chainable helper function to log any messages (warnings)
138-
var logResolveValueResult = function(valueResult) {
139-
// Log any warnings that might of popped up
140-
var warningList = [].concat(valueResult.warnings);
141-
warningList.forEach(function(warningArgs) {
142-
warningArgs = [].concat(warningArgs);
143-
result.warn.apply(result, warningArgs);
61+
Object.keys(opts.variables).forEach(function(prevVariableMap, variableKey) {
62+
const variableEntry = opts.variables[variableKey];
63+
// Automatically prefix any variable with `--` (CSS custom property syntax) if it doesn't have it already
64+
const variableName = variableKey.slice(0, 2) === '--' ? variableKey : '--' + variableKey;
65+
const variableValue = (variableEntry || {}).value || variableEntry;
66+
const isImportant = (variableEntry || {}).isImportant || false;
67+
68+
// Add the entry to the map
69+
map[variableName] = (map[variableName] || []).concat({
70+
name: variableName,
71+
value: variableValue,
72+
isImportant,
73+
selectorBranches: [':root']
14474
});
75+
});
14576

146-
// Keep the chain going
147-
return valueResult;
148-
};
14977

15078

15179
// Collect all of the variables defined
15280
// ---------------------------------------------------------
15381
// ---------------------------------------------------------
154-
//console.log('Collecting variables defined START');
15582
eachCssVariableDeclaration(css, function(decl) {
156-
var declParentRule = decl.parent;
157-
158-
var valueResults = logResolveValueResult(resolveValue(decl, map));
159-
// Split out each selector piece into its own declaration for easier logic down the road
160-
decl.parent.selectors.forEach(function(selector) {
161-
// Create a detached clone
162-
var splitOutRule = shallowCloneNode(decl.parent);
163-
splitOutRule.selector = selector;
164-
splitOutRule.parent = decl.parent.parent;
165-
166-
var declClone = decl.clone();
167-
splitOutRule.append(declClone);
168-
169-
var prop = decl.prop;
170-
map[prop] = (map[prop] || []).concat({
171-
decl: declClone,
172-
prop: prop,
173-
calculatedInPlaceValue: valueResults.value,
174-
isImportant: decl.important || false,
175-
variablesUsed: valueResults.variablesUsed,
176-
parent: splitOutRule,
177-
// variables inside root or at-rules (eg. @media, @support)
178-
isUnderAtRule: splitOutRule.parent.type === 'atrule'
179-
});
83+
// We cache the parent rule because after decl removal, it will be undefined
84+
const declParentRule = decl.parent;
85+
const variableName = decl.prop;
86+
87+
map[variableName] = (map[variableName] || []).concat({
88+
name: variableName,
89+
value: decl.value,
90+
isImportant: decl.important || false,
91+
selectorBranches: generateSelectorBranchesFromPostcssNode(declParentRule)
18092
});
18193

18294
// Remove the variable declaration because they are pretty much useless after we resolve them
@@ -185,99 +97,62 @@ module.exports = postcss.plugin('postcss-css-variables', function(options) {
18597
}
18698
// Or we can also just show the computed value used for that variable
18799
else if(opts.preserve === 'computed') {
188-
decl.value = valueResults.value;
100+
// TODO: put computed value here
189101
}
190-
// Otherwise just keep them as var declarations
191-
//else {}
192102

193-
// We add to the clean up list if we removed some variable declarations to make it become an empty rule
194-
// We clean up later on because we don't want to modify the AST when we still need to reference these later on
103+
// Clean up the rule that declared them if it doesn't have anything left after we potentially remove the variable decl
195104
if(declParentRule.nodes.length <= 0) {
196-
nodesToRemoveAtEnd.push(declParentRule);
105+
declParentRule.remove();
197106
}
198107
});
199-
//console.log('Collecting variables defined END');
200-
201108

109+
debug('map', map);
202110

203111

204112

205113
// Resolve variables everywhere
206114
// ---------------------------------------------------------
207115
// ---------------------------------------------------------
208-
209-
// Collect all the rules that have declarations that use variables
210-
var rulesThatHaveDeclarationsWithVariablesList = [];
211-
css.walkRules(function(rule) {
212-
var doesRuleUseVariables = rule.nodes.some(function(node) {
213-
if(node.type === 'decl') {
214-
var decl = node;
215-
// If it uses variables
216-
// and is not a variable declarations that we may be preserving from earlier
217-
if(resolveValue.RE_VAR_FUNC.test(decl.value) && !RE_VAR_PROP.test(decl.prop)) {
218-
return true;
219-
}
220-
}
221-
222-
return false;
223-
});
224-
225-
if(doesRuleUseVariables) {
226-
rulesThatHaveDeclarationsWithVariablesList.push(rule);
227-
}
228-
});
229-
230-
rulesThatHaveDeclarationsWithVariablesList.forEach(function(rule) {
231-
var rulesToWorkOn = [].concat(rule);
232-
// Split out the rule into each comma separated selector piece
233-
// We only need to split if is actually comma separted(selectors > 1)
234-
if(rule.selectors.length > 1) {
235-
// Reverse the selectors so that we can cloneAfter in the same comma separated order
236-
rulesToWorkOn = rule.selectors.reverse().map(function(selector) {
237-
var ruleClone = rule.cloneAfter();
238-
ruleClone.selector = selector;
239-
240-
return ruleClone;
116+
css.walkDecls(function(decl) {
117+
// If it uses variables
118+
// and is not a variable declarations that we may be preserving from earlier
119+
if(!RE_VAR_PROP.test(decl.prop)) {
120+
const selectorBranches = generateSelectorBranchesFromPostcssNode(decl.parent);
121+
122+
decl.value = decl.value.replace(new RegExp(RE_VAR_FUNC.source, 'g'), (match, variableName) => {
123+
debug('usage', variableName);
124+
const variableEntries = map[variableName] || [];
125+
126+
let currentGreatestSpecificity = null;
127+
let currentGreatestVariableEntry = null;
128+
129+
// Go through all of the variables and find the one with the highest specificity
130+
variableEntries.forEach((variableEntry) => {
131+
// We only need to find one branch that matches
132+
variableEntry.selectorBranches.some((variableSelectorBranch) => {
133+
return selectorBranches.some((selectorBranch) => {
134+
const isUnderScope = isSelectorBranchUnderScope(variableSelectorBranch, selectorBranch);
135+
const specificity = getSpecificity(variableSelectorBranch.selector.toString());
136+
137+
debug(`isUnderScope=${isUnderScope} compareSpecificity=${compareSpecificity(specificity, currentGreatestSpecificity)} specificity=${specificity}`, variableSelectorBranch.selector.toString(), selectorBranch.selector.toString())
138+
139+
if(isUnderScope && compareSpecificity(specificity, currentGreatestSpecificity) >= 0) {
140+
currentGreatestSpecificity = specificity;
141+
currentGreatestVariableEntry = variableEntry;
142+
}
143+
144+
return isUnderScope;
145+
});
146+
});
147+
});
148+
149+
debug('currentGreatestVariableEntry', currentGreatestVariableEntry);
150+
151+
return currentGreatestVariableEntry.value;
241152
});
242-
243-
rule.remove();
244153
}
245-
246-
// Resolve the declarations
247-
rulesToWorkOn.forEach(function(ruleToWorkOn) {
248-
ruleToWorkOn.nodes.slice(0).forEach(function(node) {
249-
if(node.type === 'decl') {
250-
var decl = node;
251-
resolveDecl(decl, map, opts.preserve, logResolveValueResult);
252-
}
253-
});
254-
});
255-
256-
});
257-
258-
259-
260-
261-
262-
// Clean up any nodes we don't want anymore
263-
// We clean up at the end because we don't want to modify the AST when we still need to reference these later on
264-
nodesToRemoveAtEnd.forEach(cleanUpNode);
265-
266-
// Clean up JS-injected variables marked for removal
267-
injectedDeclsToRemoveAtEnd.forEach(function(injectedDecl) {
268-
injectedDecl.remove();
269154
});
270155

271156

272-
//console.log('map', map);
273-
274-
/* * /
275-
}
276-
catch(e) {
277-
//console.log('e', e.message);
278-
console.log('e', e.message, e.stack);
279-
}
280-
/* */
281-
282157
};
283158
});

0 commit comments

Comments
 (0)