@forumone/postcss-advanced-variables 0.0.0-stage → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +108 -0
- package/README.md +575 -2
- package/index.js +730 -0
- package/index.mjs +728 -0
- package/package.json +55 -4
package/index.mjs
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import postcss, { list } from 'postcss';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import resolve from '@csstools/sass-import-resolve';
|
|
4
|
+
|
|
5
|
+
// return the closest variable from a node
|
|
6
|
+
function getClosestVariable(name, node, opts) {
|
|
7
|
+
const variables = getVariables(node);
|
|
8
|
+
|
|
9
|
+
let variable = variables[name];
|
|
10
|
+
|
|
11
|
+
if (requiresAncestorVariable(variable, node)) {
|
|
12
|
+
variable = getClosestVariable(name, node.parent, opts);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (requiresFnVariable(variable, opts)) {
|
|
16
|
+
variable = getFnVariable(name, node, opts.variables);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return variable;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// return the variables object of a node
|
|
23
|
+
const getVariables = node => Object(Object(node).variables);
|
|
24
|
+
|
|
25
|
+
// return whether the variable should be replaced using an ancestor variable
|
|
26
|
+
const requiresAncestorVariable = (variable, node) => undefined === variable && node && node.parent;
|
|
27
|
+
|
|
28
|
+
// return whether variable should be replaced using a variables function
|
|
29
|
+
const requiresFnVariable = (value, opts) => value === undefined && Object(opts).variables === Object(Object(opts).variables);
|
|
30
|
+
|
|
31
|
+
// return whether variable should be replaced using a variables function
|
|
32
|
+
const getFnVariable = (name, node, variables) => 'function' === typeof variables
|
|
33
|
+
? variables(name, node)
|
|
34
|
+
: variables[name];
|
|
35
|
+
|
|
36
|
+
function manageUnresolved(node, opts, word, message) {
|
|
37
|
+
if ('warn' === opts.unresolved) {
|
|
38
|
+
node.warn(opts.result, message, { word });
|
|
39
|
+
} else if ('ignore' !== opts.unresolved) {
|
|
40
|
+
throw node.error(message, { word });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// tooling
|
|
45
|
+
|
|
46
|
+
// return content with its variables replaced by the corresponding values of a node
|
|
47
|
+
function getReplacedString(string, node, opts) {
|
|
48
|
+
const replacedString = string.replace(
|
|
49
|
+
matchVariables,
|
|
50
|
+
(match, before, name1, name2, name3) => {
|
|
51
|
+
// conditionally return an (unescaped) match
|
|
52
|
+
if (before === '\\') {
|
|
53
|
+
return match.slice(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// the first matching variable name
|
|
57
|
+
const name = name1 || name2 || name3;
|
|
58
|
+
|
|
59
|
+
// the closest variable value
|
|
60
|
+
const value = getClosestVariable(name, node.parent, opts);
|
|
61
|
+
|
|
62
|
+
// if a variable has not been resolved
|
|
63
|
+
if (undefined === value) {
|
|
64
|
+
manageUnresolved(node, opts, name, `Could not resolve the variable "$${name}" within "${string}"`);
|
|
65
|
+
|
|
66
|
+
return match;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// the stringified value
|
|
70
|
+
const stringifiedValue = `${before}${stringify(value)}`;
|
|
71
|
+
|
|
72
|
+
return stringifiedValue;
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return replacedString;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// match all $name, $(name), and #{$name} variables (and catch the character before it)
|
|
80
|
+
const matchVariables = /(.?)(?:\$([a-zA-Z][\w-]*)|\$\(([a-zA-Z][\w-]*)\)|#\{\$([a-zA-Z][\w-]*)\})/g;
|
|
81
|
+
|
|
82
|
+
// return a sass stringified variable
|
|
83
|
+
const stringify = object => Array.isArray(object)
|
|
84
|
+
? `(${object.map(stringify).join(',')})`
|
|
85
|
+
: Object(object) === object
|
|
86
|
+
? `(${Object.keys(object).map(
|
|
87
|
+
key => `${key}:${stringify(object[key])}`
|
|
88
|
+
).join(',')})`
|
|
89
|
+
: String(object);
|
|
90
|
+
|
|
91
|
+
// tooling
|
|
92
|
+
|
|
93
|
+
// set a variable on a node
|
|
94
|
+
function setVariable(node, name, value, opts) {
|
|
95
|
+
// if the value is not a default with a value already defined
|
|
96
|
+
if (!matchDefault.test(value) || getClosestVariable(name, node, opts) === undefined) {
|
|
97
|
+
// the value without a default suffix
|
|
98
|
+
const undefaultedValue = matchDefault.test(value)
|
|
99
|
+
? value.replace(matchDefault, '')
|
|
100
|
+
: value;
|
|
101
|
+
|
|
102
|
+
// ensure the node has a variables object
|
|
103
|
+
node.variables = node.variables || {};
|
|
104
|
+
|
|
105
|
+
// set the variable
|
|
106
|
+
node.variables[name] = undefaultedValue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// match anything ending with a valid !default
|
|
111
|
+
const matchDefault = /\s+!default$/;
|
|
112
|
+
|
|
113
|
+
// tooling
|
|
114
|
+
|
|
115
|
+
// transform declarations
|
|
116
|
+
function transformDecl(decl, opts) {
|
|
117
|
+
// update the declaration value with its variables replaced by their corresponding values
|
|
118
|
+
decl.value = getReplacedString(decl.value, decl, opts);
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// if the declaration is a variable declaration
|
|
122
|
+
if (isVariableDeclaration(decl)) {
|
|
123
|
+
// set the variable on the parent of the declaration
|
|
124
|
+
setVariable(decl.parent, decl.prop.slice(1), decl.value, opts);
|
|
125
|
+
|
|
126
|
+
// remove the declaration
|
|
127
|
+
decl.remove();
|
|
128
|
+
} else {
|
|
129
|
+
decl.prop = getReplacedString(decl.prop, decl, opts);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// return whether the declaration property is a variable declaration
|
|
134
|
+
const isVariableDeclaration = decl => matchVariable.test(decl.prop);
|
|
135
|
+
|
|
136
|
+
// match a variable ($any-name)
|
|
137
|
+
const matchVariable = /^\$[\w-]+$/;
|
|
138
|
+
|
|
139
|
+
// tooling
|
|
140
|
+
|
|
141
|
+
// transform generic at-rules
|
|
142
|
+
function transformAtrule(rule, opts) {
|
|
143
|
+
// update the at-rule params with its variables replaced by their corresponding values
|
|
144
|
+
rule.params = getReplacedString(rule.params, rule, opts);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// tooling
|
|
148
|
+
|
|
149
|
+
// return sass-like arrays as literal arrays ('(hello), (goodbye)' to [[hello], [goodbye]])
|
|
150
|
+
function getValueAsObject(value) {
|
|
151
|
+
const hasWrappingParens = matchWrappingParens.test(value);
|
|
152
|
+
const unwrappedValue = String(hasWrappingParens ? value.replace(matchWrappingParens, '$1') : value).replace(matchTrailingComma, '');
|
|
153
|
+
const separatedValue = list.comma(unwrappedValue);
|
|
154
|
+
const firstValue = separatedValue[0];
|
|
155
|
+
|
|
156
|
+
if (firstValue === value) {
|
|
157
|
+
return value;
|
|
158
|
+
} else {
|
|
159
|
+
const objectValue = {};
|
|
160
|
+
const arrayValue = [];
|
|
161
|
+
|
|
162
|
+
separatedValue.forEach(
|
|
163
|
+
(subvalue, index) => {
|
|
164
|
+
const [ match, key, keyvalue ] = subvalue.match(matchDeclaration) || [];
|
|
165
|
+
|
|
166
|
+
if (match) {
|
|
167
|
+
objectValue[key] = getValueAsObject(keyvalue);
|
|
168
|
+
} else {
|
|
169
|
+
arrayValue[index] = getValueAsObject(subvalue);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const transformedValue = Object.keys(objectValue).length > 0
|
|
175
|
+
? Object.assign(objectValue, arrayValue)
|
|
176
|
+
: arrayValue;
|
|
177
|
+
|
|
178
|
+
return transformedValue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// match wrapping parentheses ((), (anything), (anything (anything)))
|
|
183
|
+
const matchWrappingParens = /^\(([\W\w]*)\)$/g;
|
|
184
|
+
|
|
185
|
+
// match a property name (any-possible_name)
|
|
186
|
+
const matchDeclaration = /^([\w-]+)\s*:\s*([\W\w]+)\s*$/;
|
|
187
|
+
|
|
188
|
+
// match a trailing comma
|
|
189
|
+
const matchTrailingComma = /\s*,\s*$/;
|
|
190
|
+
|
|
191
|
+
var waterfall = (items, asyncFunction) => items.reduce(
|
|
192
|
+
(lastPromise, item) => lastPromise.then(
|
|
193
|
+
() => asyncFunction(item)
|
|
194
|
+
),
|
|
195
|
+
Promise.resolve()
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// tooling
|
|
199
|
+
|
|
200
|
+
// transform @each at-rules
|
|
201
|
+
function transformEachAtrule(rule, opts) {
|
|
202
|
+
// if @each is supported
|
|
203
|
+
if (opts.transform.includes('@each')) {
|
|
204
|
+
// @each options
|
|
205
|
+
const { varname, incname, list } = getEachOpts(rule, opts);
|
|
206
|
+
const replacements = [];
|
|
207
|
+
const ruleClones = [];
|
|
208
|
+
|
|
209
|
+
Object.keys(list).forEach(
|
|
210
|
+
key => {
|
|
211
|
+
// set the current variable
|
|
212
|
+
setVariable(rule, varname, list[key], opts);
|
|
213
|
+
|
|
214
|
+
// conditionally set the incremenator variable
|
|
215
|
+
if (incname) {
|
|
216
|
+
setVariable(rule, incname, key, opts);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// clone the @each at-rule
|
|
220
|
+
const clone = rule.clone({
|
|
221
|
+
parent: rule.parent,
|
|
222
|
+
variables: Object.assign({}, rule.variables)
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
ruleClones.push(clone);
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return waterfall(
|
|
230
|
+
ruleClones,
|
|
231
|
+
clone => transformNode(clone, opts).then(
|
|
232
|
+
() => {
|
|
233
|
+
replacements.push(...clone.nodes);
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
).then(
|
|
237
|
+
() => {
|
|
238
|
+
// replace the @each at-rule with the replacements
|
|
239
|
+
rule.parent.insertBefore(rule, replacements);
|
|
240
|
+
|
|
241
|
+
rule.remove();
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// return the @each statement options (@each NAME in LIST, @each NAME ITERATOR in LIST)
|
|
248
|
+
const getEachOpts = (node, opts) => {
|
|
249
|
+
const params = node.params.split(matchInOperator);
|
|
250
|
+
const args = (params[0] || '').trim().split(' ');
|
|
251
|
+
const varname = args[0].trim().slice(1);
|
|
252
|
+
const incname = args.length > 1 && args[1].trim().slice(1);
|
|
253
|
+
const rawlist = getValueAsObject(
|
|
254
|
+
getReplacedString(
|
|
255
|
+
params.slice(1).join(matchInOperator),
|
|
256
|
+
node,
|
|
257
|
+
opts
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
const list = 'string' === typeof rawlist ? [rawlist] : rawlist;
|
|
261
|
+
|
|
262
|
+
return { varname, incname, list };
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// match the opertor separating the name and iterator from the list
|
|
266
|
+
const matchInOperator = ' in ';
|
|
267
|
+
|
|
268
|
+
// tooling
|
|
269
|
+
|
|
270
|
+
// transform @if at-rules
|
|
271
|
+
function transformIfAtrule(rule, opts) {
|
|
272
|
+
// @if options
|
|
273
|
+
const isTruthy = isIfTruthy(rule, opts);
|
|
274
|
+
const next = rule.next();
|
|
275
|
+
|
|
276
|
+
const transformAndInsertBeforeParent = node => transformNode(node, opts).then(
|
|
277
|
+
() => node.parent.insertBefore(node, node.nodes)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
return ifPromise(
|
|
281
|
+
opts.transform.includes('@if'),
|
|
282
|
+
() => ifPromise(
|
|
283
|
+
isTruthy,
|
|
284
|
+
() => transformAndInsertBeforeParent(rule)
|
|
285
|
+
).then(() => {
|
|
286
|
+
rule.remove();
|
|
287
|
+
})
|
|
288
|
+
).then(() => ifPromise(
|
|
289
|
+
opts.transform.includes('@else') && isElseRule(next),
|
|
290
|
+
() => ifPromise(
|
|
291
|
+
!isTruthy,
|
|
292
|
+
() => transformAndInsertBeforeParent(next)
|
|
293
|
+
).then(() => {
|
|
294
|
+
next.remove();
|
|
295
|
+
})
|
|
296
|
+
))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const ifPromise = (condition, trueFunction) => Promise.resolve(condition && trueFunction());
|
|
300
|
+
|
|
301
|
+
// return whether the @if at-rule is truthy
|
|
302
|
+
const isIfTruthy = (node, opts) => {
|
|
303
|
+
// @if statement options (@if EXPRESSION, @if LEFT OPERATOR RIGHT)
|
|
304
|
+
const params = list.space(node.params);
|
|
305
|
+
const left = getInterprettedString(getReplacedString(params[0] || '', node, opts));
|
|
306
|
+
const operator = params[1];
|
|
307
|
+
const right = getInterprettedString(getReplacedString(params[2] || '', node, opts));
|
|
308
|
+
|
|
309
|
+
// evaluate the expression
|
|
310
|
+
const isTruthy = !operator && left ||
|
|
311
|
+
operator === '==' && left === right ||
|
|
312
|
+
operator === '!=' && left !== right ||
|
|
313
|
+
operator === '<' && left < right ||
|
|
314
|
+
operator === '<=' && left <= right ||
|
|
315
|
+
operator === '>' && left > right ||
|
|
316
|
+
operator === '>=' && left >= right;
|
|
317
|
+
|
|
318
|
+
return isTruthy;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// return the value as a boolean, number, or string
|
|
322
|
+
const getInterprettedString = value => 'true' === value
|
|
323
|
+
? true
|
|
324
|
+
: 'false' === value
|
|
325
|
+
? false
|
|
326
|
+
: isNaN(value)
|
|
327
|
+
? value
|
|
328
|
+
: Number(value);
|
|
329
|
+
|
|
330
|
+
// return whether the node is an else at-rule
|
|
331
|
+
const isElseRule = node => Object(node) === node && 'atrule' === node.type && 'else' === node.name;
|
|
332
|
+
|
|
333
|
+
// tooling
|
|
334
|
+
|
|
335
|
+
// transform @import at-rules
|
|
336
|
+
function transformImportAtrule(rule, opts) {
|
|
337
|
+
// if @import is supported
|
|
338
|
+
if (opts.transform.includes('@import')) {
|
|
339
|
+
// @import options
|
|
340
|
+
const { id, media, cwf, cwd } = getImportOpts(rule, opts);
|
|
341
|
+
|
|
342
|
+
// PostCSS options
|
|
343
|
+
const options = opts.result.opts;
|
|
344
|
+
const parser = options.parser || options.syntax && options.syntax.parse || null;
|
|
345
|
+
|
|
346
|
+
if (
|
|
347
|
+
opts.importFilter instanceof Function && opts.importFilter(id, media) ||
|
|
348
|
+
opts.importFilter instanceof RegExp && opts.importFilter.test(id)
|
|
349
|
+
) {
|
|
350
|
+
const cwds = [cwd].concat(opts.importPaths);
|
|
351
|
+
|
|
352
|
+
// promise the resolved file and its contents using the file resolver
|
|
353
|
+
const importPromise = cwds.reduce(
|
|
354
|
+
(promise, thiscwd) => promise.catch(
|
|
355
|
+
() => opts.importResolve(id, thiscwd, opts)
|
|
356
|
+
),
|
|
357
|
+
Promise.reject()
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
return importPromise.then(
|
|
361
|
+
// promise the processed file
|
|
362
|
+
({ file, contents }) => processor.process(contents, { from: file, parser: parser }).then(
|
|
363
|
+
({ root }) => {
|
|
364
|
+
// push a dependency message
|
|
365
|
+
opts.result.messages.push({ type: 'dependency', file: file, parent: cwf });
|
|
366
|
+
|
|
367
|
+
// imported nodes
|
|
368
|
+
const nodes = root.nodes.slice(0);
|
|
369
|
+
|
|
370
|
+
// if media params were detected
|
|
371
|
+
if (media) {
|
|
372
|
+
// create a new media rule
|
|
373
|
+
const mediaRule = postcss.atRule({
|
|
374
|
+
name: 'media',
|
|
375
|
+
params: media,
|
|
376
|
+
source: rule.source
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// append with the imported nodes
|
|
380
|
+
mediaRule.append(nodes);
|
|
381
|
+
|
|
382
|
+
// replace the @import at-rule with the @media at-rule
|
|
383
|
+
rule.replaceWith(mediaRule);
|
|
384
|
+
} else {
|
|
385
|
+
// replace the @import at-rule with the imported nodes
|
|
386
|
+
rule.replaceWith(nodes);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// transform all nodes from the import
|
|
390
|
+
return transformNode({ nodes }, opts);
|
|
391
|
+
}
|
|
392
|
+
),
|
|
393
|
+
() => {
|
|
394
|
+
// otherwise, if the @import could not be found
|
|
395
|
+
manageUnresolved(rule, opts, '@import', `Could not resolve the @import for "${id}"`);
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const noopPlugin = () => {
|
|
403
|
+
return { postcssPlugin: 'noop-plugin', Once() {} };
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
noopPlugin.postcss = true;
|
|
407
|
+
|
|
408
|
+
const processor = postcss([noopPlugin()]);
|
|
409
|
+
|
|
410
|
+
// return the @import statement options (@import ID, @import ID MEDIA)
|
|
411
|
+
const getImportOpts = (node, opts) => {
|
|
412
|
+
const [ rawid, ...medias ] = list.space(node.params);
|
|
413
|
+
const id = getReplacedString(trimWrappingURL(rawid), node, opts);
|
|
414
|
+
const media = medias.join(' ');
|
|
415
|
+
|
|
416
|
+
// current working file and directory
|
|
417
|
+
const cwf = node.source && node.source.input && node.source.input.file || opts.result.from;
|
|
418
|
+
const cwd = cwf ? path.dirname(cwf) : opts.importRoot;
|
|
419
|
+
|
|
420
|
+
return { id, media, cwf, cwd };
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// return a string with the wrapping url() and quotes trimmed
|
|
424
|
+
const trimWrappingURL = string => trimWrappingQuotes(string.replace(/^url\(([\W\w]*)\)$/, '$1'));
|
|
425
|
+
|
|
426
|
+
// return a string with the wrapping quotes trimmed
|
|
427
|
+
const trimWrappingQuotes = string => string.replace(/^("|')([\W\w]*)\1$/, '$2');
|
|
428
|
+
|
|
429
|
+
// tooling
|
|
430
|
+
|
|
431
|
+
// transform @include at-rules
|
|
432
|
+
function transformIncludeAtrule(rule, opts) {
|
|
433
|
+
// if @include is supported
|
|
434
|
+
if (opts.transform.includes('@include')) {
|
|
435
|
+
// @include options
|
|
436
|
+
const { name, args } = getIncludeOpts(rule);
|
|
437
|
+
|
|
438
|
+
// the closest @mixin variable
|
|
439
|
+
const mixin = getClosestVariable(`@mixin ${name}`, rule.parent, opts);
|
|
440
|
+
|
|
441
|
+
// if the @mixin variable exists
|
|
442
|
+
if (mixin) {
|
|
443
|
+
// set @mixin variables on the @include at-rule
|
|
444
|
+
mixin.params.forEach(
|
|
445
|
+
(param, index) => {
|
|
446
|
+
const arg = index in args
|
|
447
|
+
? getReplacedString(args[index], rule, opts)
|
|
448
|
+
: param.value;
|
|
449
|
+
|
|
450
|
+
setVariable(rule, param.name, arg, opts);
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// clone the @mixin at-rule
|
|
455
|
+
const clone = mixin.rule.clone({
|
|
456
|
+
original: rule,
|
|
457
|
+
parent: rule.parent,
|
|
458
|
+
variables: rule.variables
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// transform the clone children
|
|
462
|
+
return transformNode(clone, opts).then(() => {
|
|
463
|
+
// replace the @include at-rule with the clone children
|
|
464
|
+
rule.parent.insertBefore(rule, clone.nodes);
|
|
465
|
+
|
|
466
|
+
rule.remove();
|
|
467
|
+
})
|
|
468
|
+
} else {
|
|
469
|
+
// otherwise, if the @mixin variable does not exist
|
|
470
|
+
manageUnresolved(rule, opts, name, `Could not resolve the mixin for "${name}"`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// return the @include statement options (@include NAME, @include NAME(ARGS))
|
|
476
|
+
const getIncludeOpts = node => {
|
|
477
|
+
// @include name and args
|
|
478
|
+
const [ name, sourceArgs ] = node.params.split(matchOpeningParen$1, 2);
|
|
479
|
+
const args = sourceArgs
|
|
480
|
+
? list.comma(sourceArgs.slice(0, -1))
|
|
481
|
+
: [];
|
|
482
|
+
|
|
483
|
+
return { name, args };
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// match an opening parenthesis
|
|
487
|
+
const matchOpeningParen$1 = /(?<!var)\(/;
|
|
488
|
+
|
|
489
|
+
// tooling
|
|
490
|
+
|
|
491
|
+
// transform @for at-rules
|
|
492
|
+
function transformForAtrule(rule, opts) {
|
|
493
|
+
// if @for is supported
|
|
494
|
+
if (opts.transform.includes('@for')) {
|
|
495
|
+
// @for options
|
|
496
|
+
const { varname, start, end, increment } = getForOpts(rule, opts);
|
|
497
|
+
const direction = start <= end ? 1 : -1;
|
|
498
|
+
const replacements = [];
|
|
499
|
+
const ruleClones = [];
|
|
500
|
+
|
|
501
|
+
// for each iteration
|
|
502
|
+
for (let incrementor = start; incrementor * direction <= end * direction; incrementor += increment * direction) {
|
|
503
|
+
// set the current variable
|
|
504
|
+
setVariable(rule, varname, incrementor, opts);
|
|
505
|
+
|
|
506
|
+
// clone the @for at-rule
|
|
507
|
+
const clone = rule.clone({
|
|
508
|
+
parent: rule.parent,
|
|
509
|
+
variables: Object.assign({}, rule.variables)
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
ruleClones.push(clone);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return waterfall(
|
|
516
|
+
ruleClones,
|
|
517
|
+
clone => transformNode(clone, opts).then(
|
|
518
|
+
() => {
|
|
519
|
+
replacements.push(...clone.nodes);
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
).then(
|
|
523
|
+
() => {
|
|
524
|
+
// replace the @for at-rule with the replacements
|
|
525
|
+
rule.parent.insertBefore(rule, replacements);
|
|
526
|
+
|
|
527
|
+
rule.remove();
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// return the @for statement options (@for NAME from START through END, @for NAME from START through END by INCREMENT)
|
|
534
|
+
const getForOpts = (node, opts) => {
|
|
535
|
+
const params = list.space(node.params);
|
|
536
|
+
const varname = params[0].trim().slice(1);
|
|
537
|
+
const start = Number(getReplacedString(params[2], node, opts));
|
|
538
|
+
const end = Number(getReplacedString(params[4], node, opts));
|
|
539
|
+
const increment = 6 in params && Number(getReplacedString(params[6], node, opts)) || 1;
|
|
540
|
+
|
|
541
|
+
return { varname, start, end, increment };
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// tooling
|
|
545
|
+
|
|
546
|
+
// transform @mixin at-rules
|
|
547
|
+
function transformMixinAtrule(rule, opts) {
|
|
548
|
+
// if @mixin is supported
|
|
549
|
+
if (opts.transform.includes('@mixin')) {
|
|
550
|
+
// @mixin options
|
|
551
|
+
const { name, params } = getMixinOpts(rule, opts);
|
|
552
|
+
|
|
553
|
+
// set the mixin as a variable on the parent of the @mixin at-rule
|
|
554
|
+
setVariable(rule.parent, `@mixin ${name}`, { params, rule }, opts);
|
|
555
|
+
|
|
556
|
+
// remove the @mixin at-rule
|
|
557
|
+
rule.remove();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// return the @mixin statement options (@mixin NAME, @mixin NAME(PARAMS))
|
|
562
|
+
const getMixinOpts = (node, opts) => {
|
|
563
|
+
// @mixin name and default params ([{ name, value }, ...])
|
|
564
|
+
const [ name, sourceParams ] = node.params.split(matchOpeningParen, 2);
|
|
565
|
+
const params = sourceParams && sourceParams.slice(0, -1).trim()
|
|
566
|
+
? list.comma(sourceParams.slice(0, -1).trim()).map(
|
|
567
|
+
param => {
|
|
568
|
+
const parts = list.split(param, ':');
|
|
569
|
+
const paramName = parts[0].slice(1);
|
|
570
|
+
const paramValue = parts.length > 1
|
|
571
|
+
? getReplacedString(parts.slice(1).join(':'), node, opts)
|
|
572
|
+
: undefined;
|
|
573
|
+
|
|
574
|
+
return { name: paramName, value: paramValue };
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
: [];
|
|
578
|
+
|
|
579
|
+
return { name, params };
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// match an opening parenthesis
|
|
583
|
+
const matchOpeningParen = /(?<!var)\(/;
|
|
584
|
+
|
|
585
|
+
// tooling
|
|
586
|
+
|
|
587
|
+
// transform rule nodes
|
|
588
|
+
function transformRule(rule, opts) {
|
|
589
|
+
// update the rule selector with its variables replaced by their corresponding values
|
|
590
|
+
rule.selector = getReplacedString(rule.selector, rule, opts);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// tooling
|
|
594
|
+
|
|
595
|
+
// transform @content at-rules
|
|
596
|
+
function transformContentAtrule(rule, opts) {
|
|
597
|
+
// if @content is supported
|
|
598
|
+
if (opts.transform.includes('@content')) {
|
|
599
|
+
// the closest @mixin at-rule
|
|
600
|
+
const mixin = getClosestMixin(rule);
|
|
601
|
+
|
|
602
|
+
// if the @mixin at-rule exists
|
|
603
|
+
if (mixin) {
|
|
604
|
+
// clone the @mixin at-rule
|
|
605
|
+
const clone = mixin.original.clone({
|
|
606
|
+
parent: rule.parent,
|
|
607
|
+
variables: rule.variables
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// transform the clone children
|
|
611
|
+
return transformNode(clone, opts).then(() => {
|
|
612
|
+
// replace the @content at-rule with the clone children
|
|
613
|
+
rule.parent.insertBefore(rule, clone.nodes);
|
|
614
|
+
|
|
615
|
+
rule.remove();
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
} else {
|
|
619
|
+
// otherwise, if the @mixin at-rule does not exist
|
|
620
|
+
manageUnresolved(rule, opts, '@content', 'Could not resolve the mixin for @content');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// return the closest @mixin at-rule
|
|
626
|
+
const getClosestMixin = node => 'atrule' === node.type && 'mixin' === node.name
|
|
627
|
+
? node
|
|
628
|
+
: node.parent && getClosestMixin(node.parent);
|
|
629
|
+
|
|
630
|
+
// tooling
|
|
631
|
+
|
|
632
|
+
function transformNode(node, opts) {
|
|
633
|
+
return waterfall(
|
|
634
|
+
getNodesArray(node),
|
|
635
|
+
child => transformRuleOrDecl(child, opts).then(() => {
|
|
636
|
+
// conditionally walk the children of the attached child
|
|
637
|
+
if (child.parent) {
|
|
638
|
+
return transformNode(child, opts);
|
|
639
|
+
}
|
|
640
|
+
})
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function transformRuleOrDecl(child, opts) {
|
|
645
|
+
return Promise.resolve().then(() => {
|
|
646
|
+
const type = child.type;
|
|
647
|
+
|
|
648
|
+
if ('atrule' === type) {
|
|
649
|
+
const name = (child.name || '').toLowerCase();
|
|
650
|
+
|
|
651
|
+
if ('content' === name) {
|
|
652
|
+
// transform @content at-rules
|
|
653
|
+
return transformContentAtrule(child, opts);
|
|
654
|
+
} else if ('each' === name) {
|
|
655
|
+
// transform @each at-rules
|
|
656
|
+
return transformEachAtrule(child, opts);
|
|
657
|
+
} else if ('if' === name) {
|
|
658
|
+
// transform @if at-rules
|
|
659
|
+
return transformIfAtrule(child, opts);
|
|
660
|
+
} else if ('import' === name) {
|
|
661
|
+
return transformImportAtrule(child, opts);
|
|
662
|
+
} else if ('include' === name) {
|
|
663
|
+
// transform @include at-rules
|
|
664
|
+
return transformIncludeAtrule(child, opts);
|
|
665
|
+
} else if ('for' === name) {
|
|
666
|
+
// transform @for at-rules
|
|
667
|
+
return transformForAtrule(child, opts);
|
|
668
|
+
} else if ('mixin' === name) {
|
|
669
|
+
// transform mixin at-rules
|
|
670
|
+
return transformMixinAtrule(child, opts);
|
|
671
|
+
} else {
|
|
672
|
+
// transform all other at-rules
|
|
673
|
+
return transformAtrule(child, opts);
|
|
674
|
+
}
|
|
675
|
+
} else if ('decl' === type) {
|
|
676
|
+
// transform declarations
|
|
677
|
+
return transformDecl(child, opts);
|
|
678
|
+
} else if ('rule' === type) {
|
|
679
|
+
// transform rule
|
|
680
|
+
return transformRule(child, opts);
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// return the children of a node as an array
|
|
686
|
+
const getNodesArray = node => Array.from(Object(node).nodes || []);
|
|
687
|
+
|
|
688
|
+
// tooling
|
|
689
|
+
|
|
690
|
+
const matchProtocol = /^(?:[a-zA-Z]+:)?\/\//;
|
|
691
|
+
|
|
692
|
+
// plugin
|
|
693
|
+
const plugin = opts => ({
|
|
694
|
+
postcssPlugin: "postcss-advanced-variables",
|
|
695
|
+
Once(root, { result }) {
|
|
696
|
+
// process options
|
|
697
|
+
const transformOpt = ['@content', '@each', '@else', '@if', '@include', '@import', '@for', '@mixin'].filter(
|
|
698
|
+
feature => !String(Object(opts).disable || '').split(/\s*,\s*|\s+,?\s*|\s,?\s+/).includes(feature)
|
|
699
|
+
);
|
|
700
|
+
const unresolvedOpt = String(Object(opts).unresolved || 'throw').toLowerCase();
|
|
701
|
+
const variablesOpt = Object(opts).variables;
|
|
702
|
+
const importCache = Object(Object(opts).importCache);
|
|
703
|
+
const importFilter = Object(opts).importFilter || (id => {
|
|
704
|
+
return !matchProtocol.test(id);
|
|
705
|
+
});
|
|
706
|
+
const importPaths = [].concat(Object(opts).importPaths || []);
|
|
707
|
+
const importResolve = Object(opts).importResolve || (
|
|
708
|
+
(id, cwd) => resolve(id, { cwd, readFile: true, cache: importCache })
|
|
709
|
+
);
|
|
710
|
+
const importRoot = Object(opts).importRoot || process.cwd();
|
|
711
|
+
|
|
712
|
+
return transformNode(root, {
|
|
713
|
+
result,
|
|
714
|
+
importCache,
|
|
715
|
+
importFilter,
|
|
716
|
+
importPaths,
|
|
717
|
+
importResolve,
|
|
718
|
+
importRoot,
|
|
719
|
+
transform: transformOpt,
|
|
720
|
+
unresolved: unresolvedOpt,
|
|
721
|
+
variables: variablesOpt
|
|
722
|
+
});
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
plugin.postcss = true;
|
|
727
|
+
|
|
728
|
+
export { plugin as default };
|