Skip to content

Commit 2782db7

Browse files
committed
Fix var. reference resolution with at-rule - v0.3.4
1 parent d216e21 commit 2782db7

11 files changed

+230
-67
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11

2+
# v0.3.4 - 2015-5-12
3+
4+
- Fix variable referencing another variable resolution when being changed by at-rule
5+
26
# v0.3.3 - 2015-5-11
37

48
- Add support for last piece of combinator chain in selector resolution matching.
59
- `.foo + .bar` can match variables declared in `.bar`
610

7-
811
# v0.3.1 - 2015-5-5
912

1013
- Large overhaul of code to make it more robust on proper scope resolution.
@@ -14,7 +17,6 @@
1417

1518
- Add support for CSS4 descendant selector `>>` syntax
1619

17-
1820
# v0.2.2 - 2015-5-1
1921

2022
- Automatically prefix any variables defined in `options.variables` with `--` (according to CSS custom property syntax).

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 [speficification](http://dev.w3.org/csswg/css-variables/) because of the DOM cascade unknowns.
1010

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

1414
### Install

index.js

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

@@ -241,7 +241,7 @@ function isUnderScope(nodeScopeList, scopeNodeScopeList) {
241241
return matchesScope;
242242
}
243243

244-
function isNodeUnderNode(node, scopeNode) {
244+
function isNodeUnderNodeScope(node, scopeNode) {
245245

246246
var nodeScopeList = generateScopeList(node, true);
247247
var scopeNodeScopeList = generateScopeList(scopeNode, true);
@@ -250,48 +250,109 @@ function isNodeUnderNode(node, scopeNode) {
250250
}
251251

252252

253+
254+
// Variables that referenced in some way by the target variable
255+
function gatherVariableDependencies(variablesUsed, map, _dependencyVariablesList) {
256+
_dependencyVariablesList = _dependencyVariablesList || [];
257+
var hasCircularOrSelfReference = false;
258+
259+
if(variablesUsed) {
260+
_dependencyVariablesList = variablesUsed.reduce(function(dependencyVariablesList, variableUsedName) {
261+
var isVariableInMap = !!map[variableUsedName];
262+
var doesThisVarHaveCircularOrSelfReference = !isVariableInMap ? false : dependencyVariablesList.some(function(dep) {
263+
return map[variableUsedName].some(function(mapItem) {
264+
// If already in the list, we got a circular reference
265+
if(dep === mapItem) {
266+
return true;
267+
}
268+
269+
return false;
270+
});
271+
});
272+
// Update the overall state of dependency health
273+
hasCircularOrSelfReference = hasCircularOrSelfReference || doesThisVarHaveCircularOrSelfReference;
274+
275+
276+
if(isVariableInMap && !hasCircularOrSelfReference) {
277+
dependencyVariablesList = dependencyVariablesList.concat(map[variableUsedName]);
278+
279+
(map[variableUsedName] || []).forEach(function(mapItem) {
280+
var result = gatherVariableDependencies(mapItem.variablesUsed, map, dependencyVariablesList);
281+
dependencyVariablesList = result.deps;
282+
hasCircularOrSelfReference = hasCircularOrSelfReference || result.hasCircularOrSelfReference;
283+
});
284+
}
285+
286+
return dependencyVariablesList;
287+
}, _dependencyVariablesList);
288+
}
289+
290+
return {
291+
deps: _dependencyVariablesList,
292+
hasCircularOrSelfReference: hasCircularOrSelfReference
293+
};
294+
}
295+
296+
253297
// Pass in a value string to parse/resolve and a map of available values
254298
// and we can figure out the final value
255299
//
256300
// Note: We do not modify the declaration
257301
// Note: Resolving a declaration value without any `var(...)` does not harm the final value.
258302
// This means, feel free to run everything through this function
259-
var resolveValue = function(decl, map) {
303+
var resolveValue = function(decl, map, _debugIsInternal) {
260304

261305
var resultantValue = decl.value;
262-
var variablesUsedInValue = [];
263306
var warnings = [];
264307

308+
var variablesUsedInValueMap = {};
309+
// Use `replace` as a loop to go over all occurrences with the `g` flag
310+
resultantValue.replace(new RegExp(RE_VAR_FUNC.source, 'g'), function(match, variableName, fallback) {
311+
variablesUsedInValueMap[variableName] = true;
312+
});
313+
var variablesUsedInValue = Object.keys(variablesUsedInValueMap);
314+
315+
316+
265317
// Resolve any var(...) substitutons
266318
var isResultantValueUndefined = false;
267319
resultantValue = resultantValue.replace(new RegExp(RE_VAR_FUNC.source, 'g'), function(match, variableName, fallback) {
268-
variablesUsedInValue.push(variableName);
269-
270320
// Loop through the list of declarations for that value and find the one that best matches
271321
// By best match, we mean, the variable actually applies. Criteria:
272-
// - At the root
273-
// - Defined in the same rule
322+
// - is under the same scope
274323
// - The latest defined `!important` if any
275324
var matchingVarDeclMapItem;
325+
//gatherVariableDependencies(variablesUsedInValue, map)
276326
(map[variableName] || []).forEach(function(varDeclMapItem) {
277327
// Make sure the variable declaration came from the right spot
278328
// And if the current matching variable is already important, a new one to replace it has to be important
279329
var isRoot = varDeclMapItem.parent.type === 'root' || varDeclMapItem.parent.selectors[0] === ':root';
280330

281-
//console.log(generateScopeList(decl.parent, true));
282-
//console.log(generateScopeList(varDeclMapItem.parent, true));
283-
//console.log('isNodeUnderNode', isNodeUnderNode(decl.parent, varDeclMapItem.parent), varDeclMapItem.value);
331+
332+
//var debugIndent = _debugIsInternal ? '\t' : '';
333+
//console.log(debugIndent, generateScopeList(decl.parent, true));
334+
//console.log(debugIndent, generateScopeList(varDeclMapItem.parent, true));
335+
//console.log(debugIndent, 'isNodeUnderNodeScope', isNodeUnderNodeScope(decl.parent, varDeclMapItem.parent), varDeclMapItem.value);
336+
284337
if(
285-
isNodeUnderNode(decl.parent, varDeclMapItem.parent) &&
338+
isNodeUnderNodeScope(decl.parent, varDeclMapItem.parent) &&
286339
// And if the currently matched declaration is `!important`, it will take another `!important` to override it
287340
(!(matchingVarDeclMapItem || {}).isImportant || varDeclMapItem.isImportant)
288341
) {
289342
matchingVarDeclMapItem = varDeclMapItem;
290343
}
291344
});
292345

293-
294-
var replaceValue = (matchingVarDeclMapItem || {}).value || fallback;
346+
// Default to the calculatedInPlaceValue which might be a previous fallback, then try this declarations fallback
347+
var replaceValue = (matchingVarDeclMapItem || {}).calculatedInPlaceValue || fallback;
348+
// Otherwise if the dependency health is good(no circular or self references), dive deeper and resolve
349+
if(matchingVarDeclMapItem !== undefined && !gatherVariableDependencies(variablesUsedInValue, map).hasCircularOrSelfReference) {
350+
var asdf = false;
351+
var mimicDecl = cloneSpliceParentOntoNodeWhen(matchingVarDeclMapItem.decl, decl.parent.parent);
352+
353+
replaceValue = resolveValue(mimicDecl, map, true).value;
354+
}
355+
295356
isResultantValueUndefined = replaceValue === undefined;
296357
if(isResultantValueUndefined) {
297358
warnings.push(["variable '" + variableName + "' is undefined and used without a fallback", { node: decl }]);
@@ -312,7 +373,6 @@ var resolveValue = function(decl, map) {
312373

313374

314375

315-
316376
module.exports = postcss.plugin('postcss-css-variables', function(options) {
317377
var defaults = {
318378
// Allows you to preserve custom properties & var() usage in output.
@@ -330,7 +390,7 @@ module.exports = postcss.plugin('postcss-css-variables', function(options) {
330390
return function (css, result) {
331391
// Transform CSS AST here
332392

333-
/* * /
393+
/* */
334394
try {
335395
/* */
336396

@@ -349,34 +409,36 @@ module.exports = postcss.plugin('postcss-css-variables', function(options) {
349409
map,
350410
Object.keys(opts.variables)
351411
.reduce(function(prevVariableMap, variableName) {
352-
var variableValue = opts.variables[variableName];
412+
var variableEntry = opts.variables[variableName];
353413
// Automatically prefix any variable with `--` (CSS custom property syntax) if it doesn't have it already
354414
variableName = variableName.slice(0, 2) === '--' ? variableName : '--' + variableName;
355-
356-
357-
// If they didn't pass a object, lets construct one
358-
if(typeof variableValue !== 'object') {
359-
variableValue = {
360-
value: variableValue,
361-
isImportant: false,
362-
parent: css.root(),
363-
isUnderAtRule: false
364-
};
365-
}
366-
367-
prevVariableMap[variableName] = (prevVariableMap[variableName] || []).concat(extend({
368-
value: undefined,
369-
isImportant: false,
370-
parent: css.root(),
415+
var variableValue = typeof variableEntry=== 'object' ? variableEntry.value : variableEntry;
416+
var isImportant = typeof variableEntry === 'object' ? variableEntry.isImportant : false;
417+
418+
// Add a node to the AST
419+
var variableRootRule = postcss.rule({ selector: ':root' });
420+
css.root().prepend(variableRootRule);
421+
var varDecl = postcss.decl({ prop: variableName, value: variableValue });
422+
varDecl.moveTo(variableRootRule);
423+
424+
// Add the entry to the map
425+
prevVariableMap[variableName] = (prevVariableMap[variableName] || []).concat({
426+
decl: varDecl,
427+
prop: variableName,
428+
calculatedInPlaceValue: variableValue,
429+
isImportant: isImportant,
430+
variablesUsed: [],
431+
parent: variableRootRule,
371432
isUnderAtRule: false
372-
}, variableValue));
433+
});
373434

374435
return prevVariableMap;
375436
}, {})
376437
);
377438

439+
378440
// Chainable helper function to log any messages (warnings)
379-
function logResolveValueResult(valueResult) {
441+
var logResolveValueResult = function(valueResult) {
380442
// Log any warnings that might of popped up
381443
var warningList = [].concat(valueResult.warnings);
382444
warningList.forEach(function(warningArgs) {
@@ -386,7 +448,7 @@ module.exports = postcss.plugin('postcss-css-variables', function(options) {
386448

387449
// Keep the chain going
388450
return valueResult;
389-
}
451+
};
390452

391453

392454
// Collect all of the variables defined
@@ -410,33 +472,36 @@ module.exports = postcss.plugin('postcss-css-variables', function(options) {
410472
var prop = decl.prop;
411473
// If declaration is a variable
412474
if(RE_VAR_PROP.test(prop)) {
413-
var resolvedValue = logResolveValueResult(resolveValue(decl, map)).value;
414-
if(resolvedValue !== undefined) {
415-
// Split out each selector piece into its own declaration for easier logic down the road
416-
decl.parent.selectors.forEach(function(selector, index) {
417-
// Create a detached clone
418-
var splitOutRule = rule.clone();
419-
rule.selector = selector;
420-
splitOutRule.parent = rule.parent;
421-
422-
map[prop] = (map[prop] || []).concat({
423-
prop: prop,
424-
value: resolvedValue,
425-
isImportant: decl.important || false,
426-
// variables inside root or at-rules (eg. @media, @support)
427-
parent: splitOutRule,
428-
isUnderAtRule: splitOutRule.parent.type === 'atrule'
429-
});
475+
var valueResults = logResolveValueResult(resolveValue(decl, map));
476+
// Split out each selector piece into its own declaration for easier logic down the road
477+
decl.parent.selectors.forEach(function(selector, index) {
478+
// Create a detached clone
479+
var splitOutRule = rule.clone().removeAll();
480+
rule.selector = selector;
481+
splitOutRule.parent = rule.parent;
482+
483+
var declClone = decl.clone();
484+
declClone.moveTo(splitOutRule);
485+
486+
map[prop] = (map[prop] || []).concat({
487+
decl: declClone,
488+
prop: prop,
489+
calculatedInPlaceValue: valueResults.value,
490+
isImportant: decl.important || false,
491+
variablesUsed: valueResults.variablesUsed,
492+
// variables inside root or at-rules (eg. @media, @support)
493+
parent: splitOutRule,
494+
isUnderAtRule: splitOutRule.parent.type === 'atrule'
430495
});
431-
}
496+
});
432497

433498
// Remove the variable declaration because they are pretty much useless after we resolve them
434499
if(!opts.preserve) {
435500
decl.removeSelf();
436501
}
437502
// Or we can also just show the computed value used for that variable
438503
else if(opts.preserve === 'computed') {
439-
decl.value = resolvedValue;
504+
decl.value = valueResults.value;
440505
}
441506
// Otherwise just keep them as var declarations
442507
}
@@ -464,39 +529,49 @@ module.exports = postcss.plugin('postcss-css-variables', function(options) {
464529

465530
// Grab the balue for this declarations
466531
var valueResults = logResolveValueResult(resolveValue(decl, map));
532+
//console.log('decl v', decl.value);
533+
534+
535+
536+
537+
//console.log('deps', gatherVariableDependencies(valueResults.variablesUsed, map));
538+
467539

468540
// Resolve the cascade
469541
// Now find any at-rule declarations that need to be added below each rule
470542
// Loop through the variables used
471543
valueResults.variablesUsed.forEach(function(variableUsedName) {
544+
545+
472546
// Find anything in the map that corresponds to that variable
473-
(map[variableUsedName] || []).forEach(function(varDeclMapItem) {
547+
gatherVariableDependencies(valueResults.variablesUsed, map).deps.forEach(function(varDeclMapItem) {
474548
if(varDeclMapItem.isUnderAtRule) {
475549

476550

477551
// Get the inner-most selector of the at-rule scope variable declaration we are matching
478-
// Because the inner-most selector will be the same for each branch, we can look in any of them
552+
// Because the inner-most selector will be the same for each branch, we can look at the first one [0] or any of the others
479553
var varDeclScopeList = generateScopeList(varDeclMapItem.parent, true);
480-
var selector = varDeclScopeList[0].slice(-1)[0];
554+
var innerMostAtRuleSelector = varDeclScopeList[0].slice(-1)[0];
481555

482-
var currentNodeToSpliceParentOnto = findNodeAncestorWithSelector(selector, decl.parent);
556+
var nodeToSpliceParentOnto = findNodeAncestorWithSelector(innerMostAtRuleSelector, decl.parent);
483557

558+
// Splice on where the selector starts matching the selector inside at-rule
484559
var varDeclAtRule = varDeclMapItem.parent.parent;
485560
var mimicDecl = cloneSpliceParentOntoNodeWhen(decl, varDeclAtRule, function(ancestor) {
486-
return ancestor === currentNodeToSpliceParentOnto;
561+
return ancestor === nodeToSpliceParentOnto;
487562
});
488563

489564

490565

491566
//console.log('amd og', generateScopeList(decl.parent, true));
492567
//console.log('amd', generateScopeList(mimicDecl.parent, true));
493568
//console.log(generateScopeList(varDeclMapItem.parent, true));
494-
//console.log('amd isNodeUnderNode', isNodeUnderNode(mimicDecl.parent, varDeclMapItem.parent), varDeclMapItem.value);
569+
//console.log('amd isNodeUnderNodeScope', isNodeUnderNodeScope(mimicDecl.parent, varDeclMapItem.parent), varDeclMapItem.value);
495570

496571

497572
// If it is under the proper scope
498573
// Then lets create the new rules
499-
if(isNodeUnderNode(mimicDecl.parent, varDeclMapItem.parent)) {
574+
if(isNodeUnderNodeScope(mimicDecl.parent, varDeclMapItem.parent)) {
500575
// Create the clean atRule for which we place the declaration under
501576
var atRuleNode = varDeclMapItem.parent.parent.clone().removeAll();
502577

@@ -577,9 +652,10 @@ module.exports = postcss.plugin('postcss-css-variables', function(options) {
577652

578653
//console.log('map', map);
579654

580-
/* * /
655+
/* */
581656
}
582657
catch(e) {
658+
//console.log('e', e.message);
583659
console.log('e', e.message, e.stack);
584660
}
585661
/* */

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.3",
3+
"version": "0.3.4",
44
"description": "PostCSS plugin to transform CSS Custom Properties(CSS variables) syntax into a static representation",
55
"keywords": [
66
"postcss",

0 commit comments

Comments
 (0)