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' ) ;
16
6
17
7
// A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS)
18
8
// `--foo`
19
9
// 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 * ( .+ ) ) ? \) / ) ;
21
12
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 ;
22
21
22
+ return specificityLib . compare ( specificityArrayA , specificityArrayB ) ;
23
+ }
23
24
24
25
25
26
function eachCssVariableDeclaration ( css , cb ) {
@@ -33,28 +34,6 @@ function eachCssVariableDeclaration(css, cb) {
33
34
}
34
35
35
36
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
-
58
37
var defaults = {
59
38
// Allows you to preserve custom properties & var() usage in output.
60
39
// `true`, `false`, or `'computed'`
@@ -70,113 +49,46 @@ var defaults = {
70
49
71
50
module . exports = postcss . plugin ( 'postcss-css-variables' , function ( options ) {
72
51
73
- var opts = extend ( { } , defaults , options ) ;
52
+ var opts = Object . assign ( { } , defaults , options ) ;
74
53
75
54
// Work with opts here
76
55
77
56
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
-
92
57
// Map of variable names to a list of declarations
93
- var map = { } ;
58
+ let map = { } ;
94
59
95
60
// 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' ]
144
74
} ) ;
75
+ } ) ;
145
76
146
- // Keep the chain going
147
- return valueResult ;
148
- } ;
149
77
150
78
151
79
// Collect all of the variables defined
152
80
// ---------------------------------------------------------
153
81
// ---------------------------------------------------------
154
- //console.log('Collecting variables defined START');
155
82
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 )
180
92
} ) ;
181
93
182
94
// 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) {
185
97
}
186
98
// Or we can also just show the computed value used for that variable
187
99
else if ( opts . preserve === 'computed' ) {
188
- decl . value = valueResults . value ;
100
+ // TODO: put computed value here
189
101
}
190
- // Otherwise just keep them as var declarations
191
- //else {}
192
102
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
195
104
if ( declParentRule . nodes . length <= 0 ) {
196
- nodesToRemoveAtEnd . push ( declParentRule ) ;
105
+ declParentRule . remove ( ) ;
197
106
}
198
107
} ) ;
199
- //console.log('Collecting variables defined END');
200
-
201
108
109
+ debug ( 'map' , map ) ;
202
110
203
111
204
112
205
113
// Resolve variables everywhere
206
114
// ---------------------------------------------------------
207
115
// ---------------------------------------------------------
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 ;
241
152
} ) ;
242
-
243
- rule . remove ( ) ;
244
153
}
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 ( ) ;
269
154
} ) ;
270
155
271
156
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
-
282
157
} ;
283
158
} ) ;
0 commit comments