forked from MadLittleMods/postcss-css-variables
-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
283 lines (220 loc) · 8.38 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// PostCSS CSS Variables (postcss-css-variables)
// v0.5.0
//
// https://github.com/MadLittleMods/postcss-css-variables
// For Debugging
//var nomo = require('node-monkey').start({port: 50501});
var postcss = require('postcss');
var extend = require('extend');
var shallowCloneNode = require('./lib/shallow-clone-node');
var resolveValue = require('./lib/resolve-value');
var resolveDecl = require('./lib/resolve-decl');
// A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS)
// `--foo`
// See: http://dev.w3.org/csswg/css-variables/#custom-property
var RE_VAR_PROP = (/(--(.+))/);
function eachCssVariableDeclaration(css, cb) {
// Loop through all of the declarations and grab the variables and put them in the map
css.walkDecls(function(decl) {
// If declaration is a variable
if(RE_VAR_PROP.test(decl.prop)) {
cb(decl);
}
});
}
function cleanUpNode(node) {
// If we removed all of the declarations in the rule(making it empty),
// then just remove it
var nodeToPossiblyCleanUp = node;
while(nodeToPossiblyCleanUp && nodeToPossiblyCleanUp.nodes.length <= 0) {
var nodeToRemove = nodeToPossiblyCleanUp.type !== 'root' ? nodeToPossiblyCleanUp : null;
if(nodeToRemove) {
// Get a reference to it before we remove
// and lose reference to the child after removing it
nodeToPossiblyCleanUp = nodeToRemove.parent;
nodeToRemove.remove();
}
else {
nodeToPossiblyCleanUp = null;
}
}
}
var defaults = {
// Allows you to preserve custom properties & var() usage in output.
// `true`, `false`, or `'computed'`
preserve: false,
// Define variables via JS
// Simple key-value pair
// or an object with a `value` property and an optional `isImportant` bool property
variables: {},
// Preserve variables injected via JS with the `variables` option above
// before serializing to CSS (`false` will remove these variables from output)
preserveInjectedVariables: true
};
module.exports = postcss.plugin('postcss-css-variables', function(options) {
var opts = extend({}, defaults, options);
// Work with opts here
return function (css, result) {
// Transform CSS AST here
/* * /
try {
/* */
// List of nodes that if empty, will be removed
// We use this because we don't want to modify the AST when we still need to reference these later on
var nodesToRemoveAtEnd = [];
// Keep track of the injected from `opts.variables` to remove at the end
// if user passes `opts.preserveInjectedVariables = false`
var injectedDeclsToRemoveAtEnd = [];
// Map of variable names to a list of declarations
var map = {};
// Add the js defined variables `opts.variables` to the map
map = extend(
map,
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;
// 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
});
variableRootRule.append(varDecl);
// Collect JS-injected variables for removal if `opts.preserveInjectedVariables = false`
if (!opts.preserveInjectedVariables) {
injectedDeclsToRemoveAtEnd.push(varDecl);
}
// 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;
}, {})
);
// Chainable helper function to log any messages (warnings)
var logResolveValueResult = function(valueResult) {
// Log any warnings that might of popped up
var warningList = [].concat(valueResult.warnings);
warningList.forEach(function(warningArgs) {
warningArgs = [].concat(warningArgs);
result.warn.apply(result, warningArgs);
});
// Keep the chain going
return valueResult;
};
// Collect all of the variables defined
// ---------------------------------------------------------
// ---------------------------------------------------------
//console.log('Collecting variables defined START');
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 = shallowCloneNode(decl.parent);
splitOutRule.selector = selector;
splitOutRule.parent = decl.parent.parent;
var declClone = decl.clone();
splitOutRule.append(declClone);
var prop = decl.prop;
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'
});
});
// Remove the variable declaration because they are pretty much useless after we resolve them
if(!opts.preserve) {
decl.remove();
}
// 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
// We clean up later on because we don't want to modify the AST when we still need to reference these later on
if(declParentRule.nodes.length <= 0) {
nodesToRemoveAtEnd.push(declParentRule);
}
});
//console.log('Collecting variables defined END');
// Resolve variables everywhere
// ---------------------------------------------------------
// ---------------------------------------------------------
// Collect all the rules that have declarations that use variables
var rulesThatHaveDeclarationsWithVariablesList = [];
css.walkRules(function(rule) {
var doesRuleUseVariables = rule.nodes.some(function(node) {
if(node.type === 'decl') {
var decl = node;
// If it uses variables
// and is not a variable declarations that we may be preserving from earlier
if(resolveValue.RE_VAR_FUNC.test(decl.value) && !RE_VAR_PROP.test(decl.prop)) {
return true;
}
}
return false;
});
if(doesRuleUseVariables) {
rulesThatHaveDeclarationsWithVariablesList.push(rule);
}
});
rulesThatHaveDeclarationsWithVariablesList.forEach(function(rule) {
var rulesToWorkOn = [].concat(rule);
// Split out the rule into each comma separated selector piece
// We only need to split if is actually comma separted(selectors > 1)
if(rule.selectors.length > 1) {
// Reverse the selectors so that we can cloneAfter in the same comma separated order
rulesToWorkOn = rule.selectors.reverse().map(function(selector) {
var ruleClone = rule.cloneAfter();
ruleClone.selector = selector;
return ruleClone;
});
rule.remove();
}
// Resolve the declarations
rulesToWorkOn.forEach(function(ruleToWorkOn) {
ruleToWorkOn.nodes.slice(0).forEach(function(node) {
if(node.type === 'decl') {
var decl = node;
resolveDecl(decl, map, opts.preserve, logResolveValueResult);
}
});
});
});
// Clean up any nodes we don't want anymore
// We clean up at the end because we don't want to modify the AST when we still need to reference these later on
nodesToRemoveAtEnd.forEach(cleanUpNode);
// Clean up JS-injected variables marked for removal
injectedDeclsToRemoveAtEnd.forEach(function(injectedDecl) {
injectedDecl.remove();
});
//console.log('map', map);
/* * /
}
catch(e) {
//console.log('e', e.message);
console.log('e', e.message, e.stack);
}
/* */
};
});