/
index.js
443 lines (443 loc) Β· 18.5 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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
"use strict";
var __assign = (this && this.__assign) || Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
var json_util_1 = require("./helpers/json.util");
var css_util_1 = require("./helpers/css.util");
var postcss = require('postcss');
var fs = require('fs-extra');
var hexToRgba = require('hex-to-rgba');
var THEMIFY = 'themify';
var JSToSass = require('./helpers/js-sass');
var defaultOptions = {
createVars: true,
palette: {},
classPrefix: '',
screwIE11: true,
fallback: {
cssPath: null,
dynamicPath: null
}
};
/** supported color variations */
var ColorVariation = {
DARK: 'dark',
LIGHT: 'light'
};
function buildOptions(options) {
if (!options) {
throw new Error("options is required.");
}
// make sure we have a palette
if (!options.palette) {
throw new Error("The 'palette' option is required.");
}
return __assign({}, defaultOptions, options);
}
/**
*
* @param {string} filePath
* @param {string} output
* @returns {Promise<any>}
*/
function writeToFile(filePath, output) {
return fs.outputFile(filePath, output);
}
/**
* Get the rgba as 88, 88, 33 instead rgba(88, 88, 33, 1)
* @param value
*/
function getRgbaNumbers(value) {
return hexToRgba(value)
.replace('rgba(', '')
.replace(', 1)', '');
}
/** Define the default variation */
var defaultVariation = ColorVariation.LIGHT;
/** An array of variation values */
var variationValues = Object.values(ColorVariation);
/** An array of all non-default variations */
var nonDefaultVariations = variationValues.filter(function (v) { return v !== defaultVariation; });
function themify(options) {
/** Regex to get the value inside the themify parenthesis */
var themifyRegExp = /themify\(([^)]+)\)/gi;
options = buildOptions(options);
return function (root) {
// process fallback CSS, without mutating the rules
if (options.screwIE11 === false) {
processFallbackRules(root);
}
// mutate the existing rules
processRules(root);
};
/**
* @example themify({"light": ["primary-0", 0.5], "dark": "primary-700"})
* @example themify({"light": "primary-0", "dark": "primary-700"})
* @example linear-gradient(themify({"color": "primary-200", "opacity": "1"}), themify({"color": "primary-300", "opacity": "1"}))
* @example themify({"light": ["primary-100", "1"], "dark": ["primary-100", "1"]})
* @example 1px solid themify({"light": ["primary-200", "1"], "dark": ["primary-200", "1"]})
*/
function getThemifyValue(propertyValue, execMode) {
/** Remove the start and end ticks **/
propertyValue = propertyValue.replace(/'/g, '');
var colorVariations = {};
function normalize(value, variationName) {
var parsedValue;
try {
parsedValue = JSON.parse(value);
}
catch (ex) {
throw new Error("fail to parse the following expression: " + value + ".");
}
var currentValue = parsedValue[variationName];
/** For example: background-color: themify((light: primary-100)); */
if (!currentValue) {
throw new Error(value + " has one variation.");
}
// convert to array
if (!Array.isArray(currentValue)) {
// color, alpha
parsedValue[variationName] = [currentValue, 1];
}
else if (!currentValue.length || !currentValue[0]) {
throw new Error('Oops. Received an empty color!');
}
if (options.palette)
return parsedValue[variationName];
}
// iterate through all variations
variationValues.forEach(function (variationName) {
// replace all 'themify' tokens with the right string
colorVariations[variationName] = propertyValue.replace(themifyRegExp, function (occurrence, value) {
// parse and normalize the color
var parsedColor = normalize(value, variationName);
// convert it to the right format
return translateColor(parsedColor, variationName, execMode);
});
});
return colorVariations;
}
/**
* Get the underline color, according to the execution mode
* @param colorArr two sized array with the color and the alpha
* @param variationName the name of the variation. e.g. light / dark
* @param execMode
*/
function translateColor(colorArr, variationName, execMode) {
var colorVar = colorArr[0], alpha = colorArr[1];
// returns the real color representation
var underlineColor = options.palette[variationName][colorVar];
if (!underlineColor) {
// variable is not mandatory in non-default variations
if (variationName !== defaultVariation) {
return null;
}
throw new Error("The variable name '" + colorVar + "' doesn't exists in your palette.");
}
switch (execMode) {
case "CSS_COLOR" /* CSS_COLOR */:
// with default alpha - just returns the color
if (alpha === '1') {
return underlineColor;
}
// with custom alpha, convert it to rgba
var rgbaColorArr = getRgbaNumbers(underlineColor);
return "rgba(" + rgbaColorArr + ", " + alpha + ")";
case "DYNAMIC_EXPRESSION" /* DYNAMIC_EXPRESSION */:
// returns it in a unique pattern, so it will be easy to replace it in runtime
return "%[" + variationName + ", " + colorVar + ", " + alpha + "]%";
default:
// return an rgba with the CSS variable name
return "rgba(var(--" + colorVar + "), " + alpha + ")";
}
}
/**
* Walk through all rules, and replace each themify occurrence with the corresponding CSS variable.
* @example background-color: themify(primary-300, 0.5) => background-color: rgba(var(--primary-300),0.6)
* @param root
*/
function processRules(root) {
root.walkRules(function (rule) {
if (!hasThemify(rule.toString())) {
return;
}
var aggragatedSelectorsMap = {};
var aggragatedSelectors = [];
var createdRules = [];
var variationRules = (_a = {},
_a[defaultVariation] = rule,
_a);
rule.walkDecls(function (decl) {
var propertyValue = decl.value;
if (!hasThemify(propertyValue))
return;
var property = decl.prop;
var variationValueMap = getThemifyValue(propertyValue, "CSS_VAR" /* CSS_VAR */);
var defaultVariationValue = variationValueMap[defaultVariation];
decl.value = defaultVariationValue;
// indicate if we have a global rule, that cannot be nested
var createNonDefaultVariationRules = isAtRule(rule);
// don't create extra CSS for global rules
if (createNonDefaultVariationRules) {
return;
}
// create a new declaration and append it to each rule
nonDefaultVariations.forEach(function (variationName) {
var currentValue = variationValueMap[variationName];
// variable for non-default variation is optional
if (!currentValue || currentValue === 'null') {
return;
}
// when the declaration is the same as the default variation,
// we just need to concatenate our selector to the default rule
if (currentValue === defaultVariationValue) {
var selector = getSelectorName(rule, variationName);
// append the selector once
if (!aggragatedSelectorsMap[variationName]) {
aggragatedSelectorsMap[variationName] = true;
aggragatedSelectors.push(selector);
}
}
else {
// creating the rule for the first time
if (!variationRules[variationName]) {
var clonedRule = createRuleWithVariation(rule, variationName);
variationRules[variationName] = clonedRule;
// append the new rule to the array, so we can append it later
createdRules.push(clonedRule);
}
var variationDecl = createDecl(property, variationValueMap[variationName]);
variationRules[variationName].append(variationDecl);
}
});
});
if (aggragatedSelectors.length) {
rule.selectors = rule.selectors.concat(aggragatedSelectors);
}
// append each created rule
if (createdRules.length) {
createdRules.forEach(function (r) { return root.append(r); });
}
var _a;
});
}
/**
* indicate if we have a global rule, that cannot be nested
* @param rule
* @return {boolean}
*/
function isAtRule(rule) {
return rule.parent && rule.parent.type === 'atrule';
}
/**
* Walk through all rules, and generate a CSS fallback for legacy browsers.
* Two files shall be created for full compatibility:
* 1. A CSS file, contains all the rules with the original color representation.
* 2. A JSON with the themify rules, in the following form:
* themify(primary-100, 0.5) => %[light,primary-100,0.5)%
* @param root
*/
function processFallbackRules(root) {
// an output for each execution mode
var output = (_a = {},
_a["CSS_COLOR" /* CSS_COLOR */] = [],
_a["DYNAMIC_EXPRESSION" /* DYNAMIC_EXPRESSION */] = {},
_a);
// initialize DYNAMIC_EXPRESSION with all existing variations
variationValues.forEach(function (variation) { return (output["DYNAMIC_EXPRESSION" /* DYNAMIC_EXPRESSION */][variation] = []); });
// define which modes need to be processed
var execModes = ["CSS_COLOR" /* CSS_COLOR */, "DYNAMIC_EXPRESSION" /* DYNAMIC_EXPRESSION */];
walkFallbackAtRules(root, execModes, output);
walkFallbackRules(root, execModes, output);
writeFallbackCSS(output);
var _a;
}
function writeFallbackCSS(output) {
// write the CSS & JSON to external files
if (output["CSS_COLOR" /* CSS_COLOR */].length) {
// write CSS fallback;
var fallbackCss = output["CSS_COLOR" /* CSS_COLOR */].join('');
writeToFile(options.fallback.cssPath, css_util_1.minifyCSS(fallbackCss));
// creating a JSON for the dynamic expressions
var jsonOutput_1 = {};
variationValues.forEach(function (variationName) {
jsonOutput_1[variationName] = output["DYNAMIC_EXPRESSION" /* DYNAMIC_EXPRESSION */][variationName] || [];
jsonOutput_1[variationName] = json_util_1.minifyJSON(jsonOutput_1[variationName].join(''));
// minify the CSS output
jsonOutput_1[variationName] = css_util_1.minifyCSS(jsonOutput_1[variationName]);
});
// stringify and save
var dynamicCss = JSON.stringify(jsonOutput_1);
writeToFile(options.fallback.dynamicPath, dynamicCss);
}
}
function walkFallbackAtRules(root, execModes, output) {
root.walkAtRules(function (atRule) {
if (atRule.nodes && hasThemify(atRule.toString())) {
execModes.forEach(function (mode) {
var clonedAtRule = atRule.clone();
clonedAtRule.nodes.forEach(function (rule) {
rule.walkDecls(function (decl) {
var propertyValue = decl.value;
// replace the themify token, if exists
if (hasThemify(propertyValue)) {
var colorMap = getThemifyValue(propertyValue, mode);
decl.value = colorMap[defaultVariation];
}
});
});
var rulesOutput = mode === "DYNAMIC_EXPRESSION" /* DYNAMIC_EXPRESSION */ ? output[mode][defaultVariation] : output[mode];
rulesOutput.push(clonedAtRule);
});
}
});
}
function walkFallbackRules(root, execModes, output) {
root.walkRules(function (rule) {
if (isAtRule(rule) || !hasThemify(rule.toString())) {
return;
}
var ruleModeMap = {};
rule.walkDecls(function (decl) {
var propertyValue = decl.value;
if (!hasThemify(propertyValue))
return;
var property = decl.prop;
execModes.forEach(function (mode) {
var colorMap = getThemifyValue(propertyValue, mode);
// lazily creating a new rule for each variation, for the specific mode
if (!ruleModeMap.hasOwnProperty(mode)) {
ruleModeMap[mode] = {};
variationValues.forEach(function (variationName) {
var newRule;
if (variationName === defaultVariation) {
newRule = cloneEmptyRule(rule);
}
else {
newRule = createRuleWithVariation(rule, variationName);
}
// push the new rule into the right place,
// so we can write them later to external file
var rulesOutput = mode === "DYNAMIC_EXPRESSION" /* DYNAMIC_EXPRESSION */ ? output[mode][variationName] : output[mode];
rulesOutput.push(newRule);
ruleModeMap[mode][variationName] = newRule;
});
}
// create and append a new declaration
variationValues.forEach(function (variationName) {
var underlineColor = colorMap[variationName];
if (underlineColor && underlineColor !== 'null') {
var newDecl = createDecl(property, colorMap[variationName]);
ruleModeMap[mode][variationName].append(newDecl);
}
});
});
});
});
}
function createDecl(prop, value) {
return postcss.decl({ prop: prop, value: value });
}
/**
* check if there's a themify keyword in this declaration
* @param propertyValue
*/
function hasThemify(propertyValue) {
return propertyValue.indexOf(THEMIFY) > -1;
}
/**
* Create a new rule for the given variation, out of the original rule
* @param rule
* @param variationName
*/
function createRuleWithVariation(rule, variationName) {
var selector = getSelectorName(rule, variationName);
return postcss.rule({ selector: selector });
}
/**
* Get a selector name for the given rule and variation
* @param rule
* @param variationName
*/
function getSelectorName(rule, variationName) {
var selectorPrefix = "." + (options.classPrefix || '') + variationName;
return rule.selectors
.map(function (selector) {
return selectorPrefix + " " + selector;
})
.join(',');
}
function cloneEmptyRule(rule, overrideConfig) {
var clonedRule = rule.clone(overrideConfig);
// remove all the declaration from this rule
clonedRule.removeAll();
return clonedRule;
}
}
/**
* Generating a SASS definition file with the palette map and the CSS variables.
* This file should be injected into your bundle.
*/
function init(options) {
options = buildOptions(options);
return function (root) {
var palette = options.palette;
var css = generateVars(palette, options.classPrefix);
var parsedCss = postcss.parse(css);
root.prepend(parsedCss);
};
/**
* This function responsible for creating the CSS variable.
*
* The output should look like the following:
*
* .light {
--primary-700: 255, 255, 255;
--primary-600: 248, 248, 249;
--primary-500: 242, 242, 244;
* }
*
* .dark {
--primary-700: 255, 255, 255;
--primary-600: 248, 248, 249;
--primary-500: 242, 242, 244;
* }
*
*/
function generateVars(palette, prefix) {
var cssOutput = '';
prefix = prefix || '';
// iterate through the different variations
Object.keys(palette).forEach(function (variationName) {
var selector = variationName === ColorVariation.LIGHT ? ':root' : "." + prefix + variationName;
var variationColors = palette[variationName];
// make sure we got colors for this variation
if (!variationColors) {
throw new Error("Expected map of colors for the variation name " + variationName);
}
var variationKeys = Object.keys(variationColors);
// generate CSS variables
var vars = variationKeys
.map(function (varName) {
return "--" + varName + ": " + getRgbaNumbers(variationColors[varName]) + ";";
})
.join(' ');
// concatenate the variables to the output
var output = selector + " {" + vars + "}";
cssOutput = cssOutput + " " + output;
});
// generate the $palette variable
cssOutput += "$palette: " + JSToSass(palette) + ";";
return cssOutput;
}
}
module.exports = {
initThemify: postcss.plugin('datoThemes', init),
themify: postcss.plugin('datoThemes', themify)
};