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 = ( / v a r \( ( - - [ ^ , \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
2526function 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-
5837var defaults = {
5938 // Allows you to preserve custom properties & var() usage in output.
6039 // `true`, `false`, or `'computed'`
@@ -70,113 +49,46 @@ var defaults = {
7049
7150module . 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