postcss-css-variables
Advanced tools
| // Splice on a parent scope onto a node | ||
| // And return a detached clone | ||
| var cloneSpliceParentOntoNodeWhen = function(node, parent, /*optional*/whenCb) { | ||
| whenCb = whenCb || function() { | ||
| return true; | ||
| }; | ||
| var cloneList = []; | ||
| // Gather node ancestors and clone along the way | ||
| var current = node; | ||
| var isWhenNow = false; | ||
| while(current && !isWhenNow) { | ||
| if(current.type === 'decl') { | ||
| cloneList.push(current.clone()); | ||
| } | ||
| else { | ||
| cloneList.push(current.clone().removeAll()); | ||
| } | ||
| isWhenNow = whenCb(current); | ||
| current = current.parent; | ||
| } | ||
| // Gather parent ancestors all the way up and clone along the way | ||
| // The list goes from lowest to highest ancestor | ||
| var cloneParentList = []; | ||
| var currentParent = parent; | ||
| while(currentParent) { | ||
| cloneParentList.push(currentParent.clone().removeAll()); | ||
| currentParent = currentParent.parent; | ||
| } | ||
| // Assign parents to our parent clones | ||
| cloneParentList.forEach(function(parentClone, index, cloneParentList) { | ||
| // Keep assigning parents detached until just very end | ||
| if(index+1 < cloneParentList.length) { | ||
| //parentClone.moveTo(cloneParentList[index+1]); | ||
| parentClone.parent = cloneParentList[index+1]; | ||
| } | ||
| }); | ||
| // Assign parents to our node clones | ||
| cloneList.forEach(function(clone, index, cloneList) { | ||
| // Keep assigning parents detached until just very end | ||
| if(index+1 < cloneList.length) { | ||
| //clone.moveTo(cloneList[index+1]); | ||
| clone.parent = cloneList[index+1]; | ||
| // Then splice on the new parent scope | ||
| } else { | ||
| // Set the highest parent ancestor to back to where we should splice in | ||
| cloneParentList.slice(-1)[0].parent = current; | ||
| // Set the node clone to the lowest parent ancestor to finish off the splice | ||
| //clone.moveTo(cloneParentList[0]); | ||
| clone.parent = cloneParentList[0]; | ||
| } | ||
| }); | ||
| return cloneList[0]; | ||
| }; | ||
| module.exports = cloneSpliceParentOntoNodeWhen; |
| var generateScopeList = require('./generate-scope-list'); | ||
| // Find a node starting from the given node that matches | ||
| // Works on a PostCSS AST tree | ||
| var findNodeAncestorWithSelector = function(selector, node) { | ||
| var matchingNode; | ||
| var currentNode = node; | ||
| var stillFindingNode = true; | ||
| // Keep going until we run out of parents to search | ||
| // or we found the node | ||
| while(currentNode.parent && !matchingNode) { | ||
| // A trick to get the selector split up. Generate a scope list on a clone(clean parent) | ||
| var currentNodeScopeList = generateScopeList(currentNode.clone(), true); | ||
| currentNodeScopeList.some(function(scopePieces) { | ||
| return scopePieces.some(function(scopePiece) { | ||
| if(scopePiece === selector) { | ||
| matchingNode = currentNode; | ||
| return true; | ||
| } | ||
| return false; | ||
| }); | ||
| }); | ||
| currentNode = currentNode.parent; | ||
| } | ||
| return matchingNode; | ||
| }; | ||
| module.exports = findNodeAncestorWithSelector; |
| // Variables that referenced in some way by the target variable | ||
| // | ||
| // `variablesUsed`: Array of string variable names that may be in the map | ||
| // | ||
| // Returns: `object` | ||
| // - `deps`: array of complete dependecies recursively gathered (entries from the `map`) | ||
| // - `hasCircularOrSelfReference`: bool of whether there is some circular or self reference of dependencies. | ||
| // - If true, the variable can't be deduced | ||
| var gatherVariableDependencies = function(variablesUsed, map, _dependencyVariablesList) { | ||
| _dependencyVariablesList = _dependencyVariablesList || []; | ||
| var hasCircularOrSelfReference = false; | ||
| if(variablesUsed) { | ||
| _dependencyVariablesList = variablesUsed.reduce(function(dependencyVariablesList, variableUsedName) { | ||
| var isVariableInMap = !!map[variableUsedName]; | ||
| var doesThisVarHaveCircularOrSelfReference = !isVariableInMap ? false : dependencyVariablesList.some(function(dep) { | ||
| return map[variableUsedName].some(function(mapItem) { | ||
| // If already in the list, we got a circular reference | ||
| if(dep === mapItem) { | ||
| return true; | ||
| } | ||
| return false; | ||
| }); | ||
| }); | ||
| // Update the overall state of dependency health | ||
| hasCircularOrSelfReference = hasCircularOrSelfReference || doesThisVarHaveCircularOrSelfReference; | ||
| if(isVariableInMap && !hasCircularOrSelfReference) { | ||
| dependencyVariablesList = dependencyVariablesList.concat(map[variableUsedName]); | ||
| (map[variableUsedName] || []).forEach(function(mapItem) { | ||
| var result = gatherVariableDependencies(mapItem.variablesUsed, map, dependencyVariablesList); | ||
| dependencyVariablesList = result.deps; | ||
| hasCircularOrSelfReference = hasCircularOrSelfReference || result.hasCircularOrSelfReference; | ||
| }); | ||
| } | ||
| return dependencyVariablesList; | ||
| }, _dependencyVariablesList); | ||
| } | ||
| return { | ||
| deps: _dependencyVariablesList, | ||
| hasCircularOrSelfReference: hasCircularOrSelfReference | ||
| }; | ||
| }; | ||
| module.exports = gatherVariableDependencies; |
| // Unit Tests: https://regex101.com/r/oP0fM9/13 | ||
| // | ||
| // It is a shame the regex has to be this long. Maybe a CSS selector parser would be better. | ||
| // We could almost use `/\b\s(?![><+~][\s]+?)/` to split the selector but this doesn't work with attribute selectors | ||
| var RE_SELECTOR_DESCENDANT_SPLIT = (/(.*?(?:(?:\[[^\]]+\]|(?![><+~\s]).)+)(?:(?:(?:\s(?!>>))|(?:\t(?!>>))|(?:\s?>>\s?))(?!\s+))(?![><+~][\s]+?))/); | ||
| var generateDescendantPiecesFromSelector = function(selector) { | ||
| return selector.split(RE_SELECTOR_DESCENDANT_SPLIT) | ||
| .filter(function(piece) { | ||
| if(piece.length > 0) { | ||
| return true; | ||
| } | ||
| return false; | ||
| }) | ||
| .map(function(piece) { | ||
| // Trim whitespace which would be a normal descendant selector | ||
| // and trim off the CSS4 descendant `>>` into a normal descendant selector | ||
| return piece.trim().replace(/\s*?>>\s*?/, function(match) { | ||
| return ''; | ||
| }); | ||
| }); | ||
| }; | ||
| module.exports = generateDescendantPiecesFromSelector; |
| var generateDescendantPiecesFromSelector = require('./generate-descendant-pieces-from-selector'); | ||
| var generateScopeList = function(node, /*optional*/includeSelf) { | ||
| includeSelf = includeSelf || false; | ||
| var selectorScopeList = [ | ||
| // Start off with one branch | ||
| [] | ||
| ]; | ||
| var currentNodeParent = includeSelf ? node : node.parent; | ||
| while(currentNodeParent) { | ||
| // `currentNodeParent.selectors` is a list of each comma separated piece of the selector | ||
| var scopePieces = (currentNodeParent.selectors || []).map(function(selectorPiece) { | ||
| return { | ||
| value: selectorPiece, | ||
| type: 'selector' | ||
| }; | ||
| }); | ||
| // If it is a at-rule, then we need to construct the proper piece | ||
| if(currentNodeParent.type === 'atrule') { | ||
| scopePieces = [].concat(currentNodeParent.params).map(function(param, index) { | ||
| return { | ||
| value: '@' + currentNodeParent.name + ' ' + param, | ||
| type: 'atrule' | ||
| }; | ||
| }); | ||
| } | ||
| // Branch each current scope for each comma separated selector | ||
| // Otherwise just keep the [1] branch going | ||
| var branches = (scopePieces.length > 0 ? scopePieces : [1]).map(function() { | ||
| return selectorScopeList.map(function(scopePieces) { | ||
| return scopePieces.slice(0); | ||
| }); | ||
| }); | ||
| scopePieces.forEach(function(scopeObject, index) { | ||
| // Update each selector string with the new piece | ||
| branches[index] = branches[index].map(function(scopeStringPieces) { | ||
| var descendantPieces = [scopeObject.value]; | ||
| // Split at any descendant combinators to properly make the scope list | ||
| if(scopeObject.type === 'selector') { | ||
| descendantPieces = generateDescendantPiecesFromSelector(scopeObject.value); | ||
| } | ||
| // Add to the front of the array | ||
| scopeStringPieces.unshift.apply(scopeStringPieces, descendantPieces); | ||
| return scopeStringPieces; | ||
| }); | ||
| }); | ||
| // Start from a new list so we can | ||
| // Flatten out the branches a bit and and merge back into the list | ||
| selectorScopeList = []; | ||
| branches.forEach(function(branch) { | ||
| selectorScopeList = selectorScopeList.concat(branch); | ||
| }); | ||
| currentNodeParent = currentNodeParent.parent; | ||
| } | ||
| return selectorScopeList; | ||
| } | ||
| module.exports = generateScopeList; |
| var isUnderScope = require('./is-under-scope'); | ||
| var generateScopeList = require('./generate-scope-list'); | ||
| var isNodeUnderScope = function(node, scopeNode) { | ||
| var nodeScopeList = generateScopeList(node, true); | ||
| var scopeNodeScopeList = generateScopeList(scopeNode, true); | ||
| return isUnderScope(nodeScopeList, scopeNodeScopeList); | ||
| }; | ||
| module.exports = isNodeUnderScope; |
| var alwaysAncestorSelector = { | ||
| '*': true, | ||
| ':root': true, | ||
| 'html': true | ||
| }; | ||
| // This means it will be always be an ancestor of any other selector | ||
| var isPieceIsAlwaysAncestorSelector = function(piece) { | ||
| return !!alwaysAncestorSelector[piece]; | ||
| }; | ||
| module.exports = isPieceIsAlwaysAncestorSelector; |
| var escapeStringRegexp = require('escape-string-regexp'); | ||
| var isPieceIsAlwaysAncestorSelector = require('./is-piece-always-ancestor-selector'); | ||
| // Given the nodes scope, and the target scope, | ||
| // Is the node in the same or under the target scope (cascade wise) | ||
| // | ||
| // Another way to think about it: Can the target cascade properties to the node? | ||
| // | ||
| // For scope-lists see: `generateScopeList` | ||
| var isUnderScope = function(nodeScopeList, scopeNodeScopeList) { | ||
| var matchesScope = scopeNodeScopeList.some(function(scopeNodeScopePieces) { | ||
| return nodeScopeList.some(function(nodeScopePieces) { | ||
| var currentPieceOffset; | ||
| var wasEveryPieceFound = scopeNodeScopePieces.every(function(scopePiece) { | ||
| var pieceOffset = currentPieceOffset || 0; | ||
| var foundIndex = -1; | ||
| var firstAlwaysAncestorPieceIndex = -1; | ||
| // Look through the remaining pieces(start from the offset) | ||
| var piecesWeCanMatch = nodeScopePieces.slice(pieceOffset); | ||
| piecesWeCanMatch.some(function(nodeScopePiece, index) { | ||
| var overallIndex = pieceOffset + index; | ||
| if(firstAlwaysAncestorPieceIndex < 0 && isPieceIsAlwaysAncestorSelector(nodeScopePiece)) { | ||
| firstAlwaysAncestorPieceIndex = overallIndex; | ||
| } | ||
| // Find the scope piece at the end of the node selector | ||
| // Last-occurence | ||
| if(new RegExp(escapeStringRegexp(scopePiece) + '$').test(nodeScopePiece)) { | ||
| foundIndex = overallIndex; | ||
| // Escape | ||
| return true; | ||
| } | ||
| return false; | ||
| }); | ||
| // If the scope piece is a always-ancestor, then it is valid no matter what | ||
| if(foundIndex < 0 && isPieceIsAlwaysAncestorSelector(scopePiece)) { | ||
| foundIndex = pieceOffset + 1; | ||
| } | ||
| // The piece could be a always-ancestor selector itself | ||
| // And we only want the first occurence so we can keep matching future scope pieces | ||
| else if(foundIndex < 0 && firstAlwaysAncestorPieceIndex > 0) { | ||
| foundIndex = firstAlwaysAncestorPieceIndex; | ||
| } | ||
| var isFurther = foundIndex > pieceOffset || (foundIndex >= 0 && currentPieceOffset === undefined); | ||
| currentPieceOffset = foundIndex; | ||
| return isFurther; | ||
| }); | ||
| return wasEveryPieceFound; | ||
| }); | ||
| }); | ||
| return matchesScope; | ||
| }; | ||
| module.exports = isUnderScope; |
| var generateScopeList = require('./generate-scope-list'); | ||
| var isNodeUnderScope = require('./is-node-under-scope'); | ||
| var gatherVariableDependencies = require('./gather-variable-dependencies'); | ||
| var findNodeAncestorWithSelector = require('./find-node-ancestor-with-selector'); | ||
| var cloneSpliceParentOntoNodeWhen = require('./clone-splice-parent-onto-node-when'); | ||
| // matches `name[, fallback]`, captures "name" and "fallback" | ||
| // var() = var( <custom-property-name> [, <any-value> ]? ) | ||
| // See: http://dev.w3.org/csswg/css-variables/#funcdef-var | ||
| var RE_VAR_FUNC = (/var\((--[^,\s]+?)(?:\s*,\s*(.+))?\)/); | ||
| // Pass in a value string to parse/resolve and a map of available values | ||
| // and we can figure out the final value | ||
| // | ||
| // Note: We do not modify the declaration | ||
| // Note: Resolving a declaration value without any `var(...)` does not harm the final value. | ||
| // This means, feel free to run everything through this function | ||
| var resolveValue = function(decl, map, _debugIsInternal) { | ||
| var resultantValue = decl.value; | ||
| var warnings = []; | ||
| var variablesUsedInValueMap = {}; | ||
| // Use `replace` as a loop to go over all occurrences with the `g` flag | ||
| resultantValue.replace(new RegExp(RE_VAR_FUNC.source, 'g'), function(match, variableName, fallback) { | ||
| variablesUsedInValueMap[variableName] = true; | ||
| }); | ||
| var variablesUsedInValue = Object.keys(variablesUsedInValueMap); | ||
| // Resolve any var(...) substitutons | ||
| var isResultantValueUndefined = false; | ||
| resultantValue = resultantValue.replace(new RegExp(RE_VAR_FUNC.source, 'g'), function(match, variableName, fallback) { | ||
| // Loop through the list of declarations for that value and find the one that best matches | ||
| // By best match, we mean, the variable actually applies. Criteria: | ||
| // - is under the same scope | ||
| // - The latest defined `!important` if any | ||
| var matchingVarDeclMapItem; | ||
| (map[variableName] || []).forEach(function(varDeclMapItem) { | ||
| // Make sure the variable declaration came from the right spot | ||
| // And if the current matching variable is already important, a new one to replace it has to be important | ||
| var isRoot = varDeclMapItem.parent.type === 'root' || varDeclMapItem.parent.selectors[0] === ':root'; | ||
| //var debugIndent = _debugIsInternal ? '\t' : ''; | ||
| //console.log(debugIndent, generateScopeList(decl.parent, true)); | ||
| //console.log(debugIndent, generateScopeList(varDeclMapItem.parent, true)); | ||
| //console.log(debugIndent, 'isNodeUnderScope', isNodeUnderScope(decl.parent, varDeclMapItem.parent), varDeclMapItem.decl.value); | ||
| if( | ||
| isNodeUnderScope(decl.parent, varDeclMapItem.parent) && | ||
| // And if the currently matched declaration is `!important`, it will take another `!important` to override it | ||
| (!(matchingVarDeclMapItem || {}).isImportant || varDeclMapItem.isImportant) | ||
| ) { | ||
| matchingVarDeclMapItem = varDeclMapItem; | ||
| } | ||
| }); | ||
| // Default to the calculatedInPlaceValue which might be a previous fallback, then try this declarations fallback | ||
| var replaceValue = (matchingVarDeclMapItem || {}).calculatedInPlaceValue || fallback; | ||
| // Otherwise if the dependency health is good(no circular or self references), dive deeper and resolve | ||
| if(matchingVarDeclMapItem !== undefined && !gatherVariableDependencies(variablesUsedInValue, map).hasCircularOrSelfReference) { | ||
| // Splice the declaration parent onto the matching entry | ||
| var varDeclScopeList = generateScopeList(decl.parent.parent, true); | ||
| var innerMostAtRuleSelector = varDeclScopeList[0].slice(-1)[0]; | ||
| var nodeToSpliceParentOnto = findNodeAncestorWithSelector(innerMostAtRuleSelector, matchingVarDeclMapItem.decl.parent); | ||
| // See: `test/fixtures/cascade-with-calc-expression-on-nested-rules` | ||
| var matchingMimicDecl = cloneSpliceParentOntoNodeWhen(matchingVarDeclMapItem.decl, decl.parent.parent, function(ancestor) { | ||
| return ancestor === nodeToSpliceParentOnto; | ||
| }); | ||
| replaceValue = resolveValue(matchingMimicDecl, map, true).value; | ||
| } | ||
| isResultantValueUndefined = replaceValue === undefined; | ||
| if(isResultantValueUndefined) { | ||
| warnings.push(["variable '" + variableName + "' is undefined and used without a fallback", { node: decl }]); | ||
| } | ||
| return replaceValue; | ||
| }); | ||
| return { | ||
| // The resolved value | ||
| value: !isResultantValueUndefined ? resultantValue : undefined, | ||
| // Array of variable names used in resolving this value | ||
| variablesUsed: variablesUsedInValue, | ||
| // Any warnings generated from parsing this value | ||
| warnings: warnings | ||
| }; | ||
| }; | ||
| module.exports = resolveValue; |
+9
-2
| # v0.3.5 - 2015-5-12 | ||
| - Big refactor of code to reduce cyclomatic complexity. Still needs work though. | ||
| - Fix variable referencing another variable resolution when being changed by at-rule in non-root rule | ||
| # v0.3.4 - 2015-5-12 | ||
| - Fix variable referencing another variable resolution when being changed by at-rule | ||
| # v0.3.3 - 2015-5-11 | ||
@@ -7,3 +16,2 @@ | ||
| # v0.3.1 - 2015-5-5 | ||
@@ -18,3 +26,2 @@ | ||
| # v0.2.2 - 2015-5-1 | ||
@@ -21,0 +28,0 @@ |
+135
-408
| // PostCSS CSS Variables (postcss-css-variables) | ||
| // v0.3.3 | ||
| // v0.3.5 | ||
| // | ||
@@ -11,4 +11,11 @@ // https://github.com/MadLittleMods/postcss-css-variables | ||
| var extend = require('extend'); | ||
| var escapeStringRegexp = require('escape-string-regexp'); | ||
| var cloneSpliceParentOntoNodeWhen = require('./lib/clone-splice-parent-onto-node-when'); | ||
| var findNodeAncestorWithSelector = require('./lib/find-node-ancestor-with-selector'); | ||
| var resolveValue = require('./lib/resolve-value'); | ||
| var isNodeUnderScope = require('./lib/is-node-under-scope'); | ||
| var generateScopeList = require('./lib/generate-scope-list'); | ||
| var gatherVariableDependencies = require('./lib/gather-variable-dependencies'); | ||
| // A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS) | ||
@@ -18,301 +25,60 @@ // `--foo` | ||
| var RE_VAR_PROP = (/(--(.+))/); | ||
| // matches `name[, fallback]`, captures "name" and "fallback" | ||
| // var() = var( <custom-property-name> [, <any-value> ]? ) | ||
| // See: http://dev.w3.org/csswg/css-variables/#funcdef-var | ||
| var RE_VAR_FUNC = (/var\((--[^,\s]+?)(?:\s*,\s*(.+))?\)/); | ||
| // Unit Tests: https://regex101.com/r/oP0fM9/13 | ||
| // | ||
| // It is a shame the regex has to be this long. Maybe a CSS selector parser would be better. | ||
| // We could almost use `/\b\s(?![><+~][\s]+?)/` to split the selector but this doesn't work with attribute selectors | ||
| var RE_SELECTOR_DESCENDANT_SPLIT = (/(.*?(?:(?:\[[^\]]+\]|(?![><+~\s]).)+)(?:(?:(?:\s(?!>>))|(?:\t(?!>>))|(?:\s?>>\s?))(?!\s+))(?![><+~][\s]+?))/); | ||
| // Splice on a parent scope onto a node | ||
| // And return a detached clone | ||
| function cloneSpliceParentOntoNodeWhen(node, parent, /*optional*/whenCb) { | ||
| whenCb = whenCb || function() { | ||
| return true; | ||
| }; | ||
| var cloneList = []; | ||
| // Gather node ancestors and clone along the way | ||
| var current = node; | ||
| var isWhenNow = false; | ||
| while(current && !isWhenNow) { | ||
| if(current.type === 'decl') { | ||
| cloneList.push(current.clone()); | ||
| function eachCssVariableDeclaration(css, cb) { | ||
| // Loop through all of the declarations and grab the variables and put them in the map | ||
| css.eachDecl(function(decl, index) { | ||
| // If declaration is a variable | ||
| if(RE_VAR_PROP.test(decl.prop)) { | ||
| cb(decl); | ||
| } | ||
| else { | ||
| cloneList.push(current.clone().removeAll()); | ||
| } | ||
| isWhenNow = whenCb(current); | ||
| current = current.parent; | ||
| } | ||
| // Gather parent ancestors all the way up and clone along the way | ||
| var cloneParentList = []; | ||
| var currentParent = parent; | ||
| while(currentParent) { | ||
| cloneParentList.push(currentParent.clone().removeAll()); | ||
| currentParent = currentParent.parent; | ||
| } | ||
| // Assign parents to our parent clones | ||
| cloneParentList.forEach(function(parentClone, index, cloneParentList) { | ||
| // Keep assigning parents detached until the very end | ||
| if(index+1 < cloneParentList.length) { | ||
| parentClone.parent = cloneParentList[index+1]; | ||
| } | ||
| }); | ||
| // Assign parents to our node clones | ||
| cloneList.forEach(function(clone, index, cloneList) { | ||
| // Keep assigning parents detached until the very end | ||
| if(index+1 < cloneList.length) { | ||
| clone.parent = cloneList[index+1]; | ||
| // Then splice on the new parent scope | ||
| } else { | ||
| // Set the highest parent ancestor to back to where we should splice in | ||
| cloneParentList.slice(-1)[0].parent = current; | ||
| // Set the node clone to the lowest parent ancestor | ||
| clone.parent = cloneParentList[0]; | ||
| } | ||
| }); | ||
| return cloneList[0]; | ||
| } | ||
| // Find a node starting from the given node that matches | ||
| function findNodeAncestorWithSelector(selector, node) { | ||
| var matchingNode; | ||
| function eachMapItemUnderAtRuleUsedByVariable(variablesUsedList, map, decl, cb) { | ||
| // Now find any at-rule declarations that pertains to each rule | ||
| // Loop through the variables used | ||
| variablesUsedList.forEach(function(variableUsedName) { | ||
| var currentNode = node; | ||
| var stillFindingNode = true; | ||
| // Keep going until we run out of parents to search | ||
| // or we found the node | ||
| while(currentNode.parent && !matchingNode) { | ||
| // A trick to get the selector split up. Generate a scope list on a clone(clean parent) | ||
| var currentNodeScopeList = generateScopeList(currentNode.clone(), true); | ||
| currentNodeScopeList.some(function(scopePieces) { | ||
| return scopePieces.some(function(scopePiece) { | ||
| if(scopePiece === selector) { | ||
| matchingNode = currentNode; | ||
| return true; | ||
| } | ||
| return false; | ||
| }); | ||
| }); | ||
| currentNode = currentNode.parent; | ||
| } | ||
| return matchingNode; | ||
| } | ||
| function generateDescendantPieces(selector) { | ||
| return selector.split(RE_SELECTOR_DESCENDANT_SPLIT) | ||
| .filter(function(piece) { | ||
| if(piece.length > 0) { | ||
| return true; | ||
| } | ||
| return false; | ||
| }) | ||
| .map(function(piece) { | ||
| // Trim whitespace which would be a normal descendant selector | ||
| // and trim off the CSS4 descendant `>>` into a normal descendant selector | ||
| return piece.trim().replace(/\s*?>>\s*?/, function(match) { | ||
| return ''; | ||
| }); | ||
| }); | ||
| } | ||
| function generateScopeList(node, /*optional*/includeSelf) { | ||
| includeSelf = includeSelf || false; | ||
| var selectorScopeList = [ | ||
| // Start off with one branch | ||
| [] | ||
| ]; | ||
| var currentNodeParent = includeSelf ? node : node.parent; | ||
| while(currentNodeParent) { | ||
| // `currentNodeParent.selectors` is a list of each comma separated piece of the selector | ||
| var scopePieces = (currentNodeParent.selectors || []).map(function(selectorPiece) { | ||
| return { | ||
| value: selectorPiece, | ||
| type: 'selector' | ||
| }; | ||
| }); | ||
| // If it is a at-rule, then we need to construct the proper piece | ||
| if(currentNodeParent.type === 'atrule') { | ||
| scopePieces = [].concat(currentNodeParent.params).map(function(param, index) { | ||
| return { | ||
| value: '@' + currentNodeParent.name + ' ' + param, | ||
| type: 'atrule' | ||
| }; | ||
| }); | ||
| } | ||
| // Branch each current scope for each comma separated selector | ||
| // Otherwise just keep the [1] branch going | ||
| var branches = (scopePieces.length > 0 ? scopePieces : [1]).map(function() { | ||
| return selectorScopeList.map(function(scopePieces) { | ||
| return scopePieces.slice(0); | ||
| }); | ||
| }); | ||
| scopePieces.forEach(function(scopeObject, index) { | ||
| // Update each selector string with the new piece | ||
| branches[index] = branches[index].map(function(scopeStringPieces) { | ||
| var descendantPieces = [scopeObject.value]; | ||
| // Split at any descendant combinators to properly make the scope list | ||
| if(scopeObject.type === 'selector') { | ||
| descendantPieces = generateDescendantPieces(scopeObject.value); | ||
| } | ||
| // Add to the front of the array | ||
| scopeStringPieces.unshift.apply(scopeStringPieces, descendantPieces); | ||
| // Find anything in the map that corresponds to that variable | ||
| gatherVariableDependencies(variablesUsedList, map).deps.forEach(function(mapItem) { | ||
| if(mapItem.isUnderAtRule) { | ||
| return scopeStringPieces; | ||
| }); | ||
| }); | ||
| // Get the inner-most selector of the at-rule scope variable declaration we are matching | ||
| // 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 | ||
| var varDeclScopeList = generateScopeList(mapItem.parent, true); | ||
| var innerMostAtRuleSelector = varDeclScopeList[0].slice(-1)[0]; | ||
| var nodeToSpliceParentOnto = findNodeAncestorWithSelector(innerMostAtRuleSelector, decl.parent); | ||
| // Start from a new list so we can | ||
| // Flatten out the branches a bit and and merge back into the list | ||
| selectorScopeList = []; | ||
| branches.forEach(function(branch) { | ||
| selectorScopeList = selectorScopeList.concat(branch); | ||
| }); | ||
| // Splice on where the selector starts matching the selector inside at-rule | ||
| // See: `test/fixtures/cascade-on-nested-rules.css` | ||
| var varDeclAtRule = mapItem.parent.parent; | ||
| var mimicDecl = cloneSpliceParentOntoNodeWhen(decl, varDeclAtRule, function(ancestor) { | ||
| return ancestor === nodeToSpliceParentOnto; | ||
| }); | ||
| currentNodeParent = currentNodeParent.parent; | ||
| } | ||
| return selectorScopeList; | ||
| } | ||
| //console.log('amd og', generateScopeList(decl.parent, true)); | ||
| //console.log('amd', generateScopeList(mimicDecl.parent, true)); | ||
| //console.log(generateScopeList(mapItem.parent, true)); | ||
| //console.log('amd isNodeUnderScope', isNodeUnderScope(mimicDecl.parent, mapItem.parent), mapItem.decl.value); | ||
| function isUnderScope(nodeScopeList, scopeNodeScopeList) { | ||
| var matchesScope = scopeNodeScopeList.some(function(scopeNodeScopePieces) { | ||
| return nodeScopeList.some(function(nodeScopePieces) { | ||
| var currentPieceOffset; | ||
| var wasEveryPieceFound = scopeNodeScopePieces.every(function(scopePiece) { | ||
| var pieceOffset = currentPieceOffset || 0; | ||
| // Start from the previous index and make sure we can find it | ||
| //var foundIndex = nodeScopePieces.indexOf(scopePiece, pieceOffset); | ||
| var foundIndex = -1; | ||
| var piecesWeCanMatch = nodeScopePieces.slice(pieceOffset); | ||
| piecesWeCanMatch.some(function(nodeScopePiece, index) { | ||
| // Find the scope piece at the end of the node selector | ||
| // Last-occurence | ||
| if(new RegExp(escapeStringRegexp(scopePiece) + '$').test(nodeScopePiece)) { | ||
| foundIndex = pieceOffset + index; | ||
| // Escape | ||
| return true; | ||
| } | ||
| return false; | ||
| }); | ||
| // If it is a star or root, then it is valid no matter what | ||
| // We might consider adding `html` and `body` to this list as well | ||
| if(foundIndex < 0 && (scopePiece === '*' || scopePiece === ':root')) { | ||
| foundIndex = pieceOffset + 1; | ||
| // If it is under the proper scope, | ||
| // we need to check because we are iterating over all map entries that are `isUnderAtRule` | ||
| // Then lets create the new rules | ||
| if(isNodeUnderScope(mimicDecl.parent, mapItem.parent)) { | ||
| cb(mimicDecl, mapItem); | ||
| } | ||
| var isFurther = foundIndex > pieceOffset || (foundIndex >= 0 && currentPieceOffset === undefined); | ||
| currentPieceOffset = foundIndex; | ||
| return isFurther; | ||
| }); | ||
| return wasEveryPieceFound; | ||
| } | ||
| }); | ||
| }); | ||
| return matchesScope; | ||
| } | ||
| function isNodeUnderNode(node, scopeNode) { | ||
| var nodeScopeList = generateScopeList(node, true); | ||
| var scopeNodeScopeList = generateScopeList(scopeNode, true); | ||
| return isUnderScope(nodeScopeList, scopeNodeScopeList); | ||
| } | ||
| // Pass in a value string to parse/resolve and a map of available values | ||
| // and we can figure out the final value | ||
| // | ||
| // Note: We do not modify the declaration | ||
| // Note: Resolving a declaration value without any `var(...)` does not harm the final value. | ||
| // This means, feel free to run everything through this function | ||
| var resolveValue = function(decl, map) { | ||
| var resultantValue = decl.value; | ||
| var variablesUsedInValue = []; | ||
| var warnings = []; | ||
| // Resolve any var(...) substitutons | ||
| var isResultantValueUndefined = false; | ||
| resultantValue = resultantValue.replace(new RegExp(RE_VAR_FUNC.source, 'g'), function(match, variableName, fallback) { | ||
| variablesUsedInValue.push(variableName); | ||
| // Loop through the list of declarations for that value and find the one that best matches | ||
| // By best match, we mean, the variable actually applies. Criteria: | ||
| // - At the root | ||
| // - Defined in the same rule | ||
| // - The latest defined `!important` if any | ||
| var matchingVarDeclMapItem; | ||
| (map[variableName] || []).forEach(function(varDeclMapItem) { | ||
| // Make sure the variable declaration came from the right spot | ||
| // And if the current matching variable is already important, a new one to replace it has to be important | ||
| var isRoot = varDeclMapItem.parent.type === 'root' || varDeclMapItem.parent.selectors[0] === ':root'; | ||
| //console.log(generateScopeList(decl.parent, true)); | ||
| //console.log(generateScopeList(varDeclMapItem.parent, true)); | ||
| //console.log('isNodeUnderNode', isNodeUnderNode(decl.parent, varDeclMapItem.parent), varDeclMapItem.value); | ||
| if( | ||
| isNodeUnderNode(decl.parent, varDeclMapItem.parent) && | ||
| // And if the currently matched declaration is `!important`, it will take another `!important` to override it | ||
| (!(matchingVarDeclMapItem || {}).isImportant || varDeclMapItem.isImportant) | ||
| ) { | ||
| matchingVarDeclMapItem = varDeclMapItem; | ||
| } | ||
| }); | ||
| var replaceValue = (matchingVarDeclMapItem || {}).value || fallback; | ||
| isResultantValueUndefined = replaceValue === undefined; | ||
| if(isResultantValueUndefined) { | ||
| warnings.push(["variable '" + variableName + "' is undefined and used without a fallback", { node: decl }]); | ||
| } | ||
| return replaceValue; | ||
| }); | ||
| return { | ||
| // The resolved value | ||
| value: !isResultantValueUndefined ? resultantValue : undefined, | ||
| // Array of variable names used in resolving this value | ||
| variablesUsed: variablesUsedInValue, | ||
| // Any warnings generated from parsing this value | ||
| warnings: warnings | ||
| }; | ||
| }; | ||
| module.exports = postcss.plugin('postcss-css-variables', function(options) { | ||
@@ -352,32 +118,38 @@ var defaults = { | ||
| map, | ||
| Object.keys(opts.variables) | ||
| .reduce(function(prevVariableMap, variableName) { | ||
| var variableValue = opts.variables[variableName]; | ||
| // Automatically prefix any variable with `--` (CSS custom property syntax) if it doesn't have it already | ||
| variableName = variableName.slice(0, 2) === '--' ? variableName : '--' + variableName; | ||
| Object.keys(opts.variables).reduce(function(prevVariableMap, variableName) { | ||
| var variableEntry = opts.variables[variableName]; | ||
| // Automatically prefix any variable with `--` (CSS custom property syntax) if it doesn't have it already | ||
| variableName = variableName.slice(0, 2) === '--' ? variableName : '--' + variableName; | ||
| var variableValue = (variableEntry || {}).value || variableEntry; | ||
| var isImportant = (variableEntry || {}).isImportant || false; | ||
| // If they didn't pass a object, lets construct one | ||
| if(typeof variableValue !== 'object') { | ||
| variableValue = { | ||
| value: variableValue, | ||
| isImportant: false, | ||
| parent: css.root(), | ||
| isUnderAtRule: false | ||
| }; | ||
| } | ||
| // Add a root node to the AST | ||
| var variableRootRule = postcss.rule({ selector: ':root' }); | ||
| css.root().prepend(variableRootRule); | ||
| // Add the variable decl to the root node | ||
| var varDecl = postcss.decl({ | ||
| prop: variableName, | ||
| value: variableValue | ||
| }); | ||
| varDecl.moveTo(variableRootRule); | ||
| prevVariableMap[variableName] = (prevVariableMap[variableName] || []).concat(extend({ | ||
| value: undefined, | ||
| isImportant: false, | ||
| parent: css.root(), | ||
| isUnderAtRule: false | ||
| }, variableValue)); | ||
| // Add the entry to the map | ||
| prevVariableMap[variableName] = (prevVariableMap[variableName] || []).concat({ | ||
| decl: varDecl, | ||
| prop: variableName, | ||
| calculatedInPlaceValue: variableValue, | ||
| isImportant: isImportant, | ||
| variablesUsed: [], | ||
| parent: variableRootRule, | ||
| isUnderAtRule: false | ||
| }); | ||
| return prevVariableMap; | ||
| }, {}) | ||
| return prevVariableMap; | ||
| }, {}) | ||
| ); | ||
| // Chainable helper function to log any messages (warnings) | ||
| function logResolveValueResult(valueResult) { | ||
| var logResolveValueResult = function(valueResult) { | ||
| // Log any warnings that might of popped up | ||
@@ -392,3 +164,3 @@ var warningList = [].concat(valueResult.warnings); | ||
| return valueResult; | ||
| } | ||
| }; | ||
@@ -399,57 +171,44 @@ | ||
| // --------------------------------------------------------- | ||
| var addedRules = []; | ||
| css.eachRule(function(rule) { | ||
| // We don't want infinite recursion possibly, so don't iterate over the rules we add inside | ||
| var shouldNotIterateOverThisRule = addedRules.some(function(addedRule) { | ||
| return addedRule === rule; | ||
| }); | ||
| if(shouldNotIterateOverThisRule) { | ||
| return; | ||
| } | ||
| eachCssVariableDeclaration(css, function(decl) { | ||
| var declParentRule = decl.parent; | ||
| var valueResults = logResolveValueResult(resolveValue(decl, map)); | ||
| // Split out each selector piece into its own declaration for easier logic down the road | ||
| decl.parent.selectors.forEach(function(selector) { | ||
| // Create a detached clone | ||
| var splitOutRule = decl.parent.clone().removeAll(); | ||
| splitOutRule.selector = selector; | ||
| splitOutRule.parent = decl.parent.parent; | ||
| var numberOfStartingChildren = rule.nodes.length; | ||
| var declClone = decl.clone(); | ||
| declClone.moveTo(splitOutRule); | ||
| // Loop through all of the declarations and grab the variables and put them in the map | ||
| rule.eachDecl(function(decl, index) { | ||
| var prop = decl.prop; | ||
| // If declaration is a variable | ||
| if(RE_VAR_PROP.test(prop)) { | ||
| var resolvedValue = logResolveValueResult(resolveValue(decl, map)).value; | ||
| if(resolvedValue !== undefined) { | ||
| // Split out each selector piece into its own declaration for easier logic down the road | ||
| decl.parent.selectors.forEach(function(selector, index) { | ||
| // Create a detached clone | ||
| var splitOutRule = rule.clone(); | ||
| rule.selector = selector; | ||
| splitOutRule.parent = rule.parent; | ||
| map[prop] = (map[prop] || []).concat({ | ||
| prop: prop, | ||
| value: resolvedValue, | ||
| isImportant: decl.important || false, | ||
| // variables inside root or at-rules (eg. @media, @support) | ||
| parent: splitOutRule, | ||
| isUnderAtRule: splitOutRule.parent.type === 'atrule' | ||
| }); | ||
| }); | ||
| } | ||
| // Remove the variable declaration because they are pretty much useless after we resolve them | ||
| if(!opts.preserve) { | ||
| decl.removeSelf(); | ||
| } | ||
| // Or we can also just show the computed value used for that variable | ||
| else if(opts.preserve === 'computed') { | ||
| decl.value = resolvedValue; | ||
| } | ||
| // Otherwise just keep them as var declarations | ||
| } | ||
| map[prop] = (map[prop] || []).concat({ | ||
| decl: declClone, | ||
| prop: prop, | ||
| calculatedInPlaceValue: valueResults.value, | ||
| isImportant: decl.important || false, | ||
| variablesUsed: valueResults.variablesUsed, | ||
| parent: splitOutRule, | ||
| // variables inside root or at-rules (eg. @media, @support) | ||
| isUnderAtRule: splitOutRule.parent.type === 'atrule' | ||
| }); | ||
| }); | ||
| // We don't want to mess up their code if they wrote a empty rule | ||
| // We add to the clean up list if we removed some variable declarations to make it become empty | ||
| if(numberOfStartingChildren > 0 && rule.nodes.length <= 0) { | ||
| nodesToRemoveAtEnd.push(rule); | ||
| // Remove the variable declaration because they are pretty much useless after we resolve them | ||
| if(!opts.preserve) { | ||
| decl.removeSelf(); | ||
| } | ||
| // Or we can also just show the computed value used for that variable | ||
| else if(opts.preserve === 'computed') { | ||
| decl.value = valueResults.value; | ||
| } | ||
| // Otherwise just keep them as var declarations | ||
| //else {} | ||
| // We add to the clean up list if we removed some variable declarations to make it become an empty rule | ||
| if(declParentRule.nodes.length <= 0) { | ||
| nodesToRemoveAtEnd.push(declParentRule); | ||
| } | ||
| }); | ||
@@ -459,3 +218,2 @@ | ||
| // Resolve variables everywhere | ||
@@ -474,69 +232,37 @@ // --------------------------------------------------------- | ||
| // Resolve the cascade | ||
| // Now find any at-rule declarations that need to be added below each rule | ||
| // Loop through the variables used | ||
| valueResults.variablesUsed.forEach(function(variableUsedName) { | ||
| // Find anything in the map that corresponds to that variable | ||
| (map[variableUsedName] || []).forEach(function(varDeclMapItem) { | ||
| if(varDeclMapItem.isUnderAtRule) { | ||
| eachMapItemUnderAtRuleUsedByVariable(valueResults.variablesUsed, map, decl, function(mimicDecl, mapItem) { | ||
| // Create the clean atRule for which we place the declaration under | ||
| var atRuleNode = mapItem.parent.parent.clone().removeAll(); | ||
| // Get the inner-most selector of the at-rule scope variable declaration we are matching | ||
| // Because the inner-most selector will be the same for each branch, we can look in any of them | ||
| var varDeclScopeList = generateScopeList(varDeclMapItem.parent, true); | ||
| var selector = varDeclScopeList[0].slice(-1)[0]; | ||
| var ruleClone = decl.parent.clone().removeAll(); | ||
| var declClone = decl.clone(); | ||
| declClone.value = logResolveValueResult(resolveValue(mimicDecl, map)).value; | ||
| var currentNodeToSpliceParentOnto = findNodeAncestorWithSelector(selector, decl.parent); | ||
| // Add the declaration to our new rule | ||
| ruleClone.append(declClone); | ||
| // Add the rule to the atRule | ||
| atRuleNode.append(ruleClone); | ||
| var varDeclAtRule = varDeclMapItem.parent.parent; | ||
| var mimicDecl = cloneSpliceParentOntoNodeWhen(decl, varDeclAtRule, function(ancestor) { | ||
| return ancestor === currentNodeToSpliceParentOnto; | ||
| }); | ||
| // Since that atRuleNode can be nested in other atRules, we need to make the appropriate structure | ||
| var parentAtRuleNode = atRuleNode; | ||
| var currentAtRuleNode = mapItem.parent.parent; | ||
| while(currentAtRuleNode.parent.type === 'atrule') { | ||
| // Create a new clean clone of that at rule to nest under | ||
| var newParentAtRuleNode = currentAtRuleNode.parent.clone().removeAll(); | ||
| // Append the old parent | ||
| newParentAtRuleNode.append(parentAtRuleNode); | ||
| // Then set the new one as the current for next iteration | ||
| parentAtRuleNode = newParentAtRuleNode; | ||
| //console.log('amd og', generateScopeList(decl.parent, true)); | ||
| //console.log('amd', generateScopeList(mimicDecl.parent, true)); | ||
| //console.log(generateScopeList(varDeclMapItem.parent, true)); | ||
| //console.log('amd isNodeUnderNode', isNodeUnderNode(mimicDecl.parent, varDeclMapItem.parent), varDeclMapItem.value); | ||
| currentAtRuleNode = currentAtRuleNode.parent; | ||
| } | ||
| // If it is under the proper scope | ||
| // Then lets create the new rules | ||
| if(isNodeUnderNode(mimicDecl.parent, varDeclMapItem.parent)) { | ||
| // Create the clean atRule for which we place the declaration under | ||
| var atRuleNode = varDeclMapItem.parent.parent.clone().removeAll(); | ||
| var ruleClone = decl.parent.clone().removeAll(); | ||
| var declClone = decl.clone(); | ||
| declClone.value = logResolveValueResult(resolveValue(mimicDecl, map)).value; | ||
| // Add the declaration to our new rule | ||
| ruleClone.append(declClone); | ||
| // Add the rule to the atRule | ||
| atRuleNode.append(ruleClone); | ||
| // Since that atRuleNode can be nested in other atRules, we need to make the appropriate structure | ||
| var parentAtRuleNode = atRuleNode; | ||
| var currentAtRuleNode = varDeclMapItem.parent.parent; | ||
| while(currentAtRuleNode.parent.type === 'atrule') { | ||
| // Create a new clean clone of that at rule to nest under | ||
| var newParentAtRuleNode = currentAtRuleNode.parent.clone().removeAll(); | ||
| // Append the old parent | ||
| newParentAtRuleNode.append(parentAtRuleNode); | ||
| // Then set the new one as the current for next iteration | ||
| parentAtRuleNode = newParentAtRuleNode; | ||
| currentAtRuleNode = currentAtRuleNode.parent; | ||
| } | ||
| createNodeCallbackList.push(function() { | ||
| // Put the atRuleStructure after the declaration's rule | ||
| decl.parent.parent.insertAfter(decl.parent, parentAtRuleNode); | ||
| }); | ||
| } | ||
| } | ||
| createNodeCallbackList.push(function() { | ||
| // Put the atRuleStructure after the declaration's rule | ||
| decl.parent.parent.insertAfter(decl.parent, parentAtRuleNode); | ||
| }); | ||
@@ -590,2 +316,3 @@ }); | ||
| catch(e) { | ||
| //console.log('e', e.message); | ||
| console.log('e', e.message, e.stack); | ||
@@ -592,0 +319,0 @@ } |
+1
-1
| { | ||
| "name": "postcss-css-variables", | ||
| "version": "0.3.3", | ||
| "version": "0.3.5", | ||
| "description": "PostCSS plugin to transform CSS Custom Properties(CSS variables) syntax into a static representation", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
+1
-1
@@ -11,3 +11,3 @@ [](http://badge.fury.io/js/postcss-css-variables) [](https://travis-ci.org/MadLittleMods/postcss-css-variables) | ||
| ## Latest Version: v0.3.3 | ||
| ## Latest Version: v0.3.5 | ||
| ### [Changelog](https://github.com/MadLittleMods/postcss-css-variables/blob/master/CHANGELOG.md) | ||
@@ -14,0 +14,0 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
38743
22.76%15
150%584
24.26%1
-83.33%1
Infinity%