1 import cssTree from 'css-tree';
3 import prettier from 'prettier';
4 import tiny from 'tinycolor2';
6 import genButtonShades from './gen-button-shades';
7 import type { ThemeConfig, ThemeFileType } from './themes.config';
9 function generateTheme({ source, type }: { source: string; type: ThemeFileType }) {
20 const buttonShadeNames = ['-minor-2', '-minor-1', '', '-major-1', '-major-2', '-major-3', '-contrast'];
22 const ast = cssTree.parse(source);
24 cssTree.walk(ast, (node, item, list) => {
25 if (node.type !== 'Declaration') {
29 if (node.value.type !== 'Raw') {
33 const baseName = node.property.substring(2);
35 if (!buttonBases.includes(baseName)) {
40 * make sure we don't visit the same base name again
41 * by removing it from the array of button base names
43 buttonBases.splice(buttonBases.indexOf(baseName), 1);
45 const isLight = type === 'light';
47 const base = tiny(node.value.value);
49 const buttonShades = genButtonShades(base, isLight);
51 /* here we don't use tiny.mostReadable to prioritize white against black color. */
52 const buttonContrast = tiny(tiny.isReadable(base, 'white', { level: 'AA', size: 'large' }) ? 'white' : 'black');
54 // use original input when color contains alpha channel (opacity, eg. rgba)
55 const declarations = [...buttonShades, buttonContrast].map((color, i) =>
59 property: '--' + baseName + buttonShadeNames[i],
60 value: { type: 'Raw', value: color.getAlpha() == 1 ? color.toHexString() : color.toString() },
65 for (const declaration of declarations) {
66 list.append(declaration);
69 /* list.insert() inserts after the next element so we reverse insertion order */
70 for (let i = declarations.length - 1; i >= 0; i--) {
71 list.insert(declarations[i], item.next);
75 /* base is consumed, we don't need it any more and we don't want to re-visit */
79 return cssTree.generate(ast);
82 export async function main({ output, files }: ThemeConfig) {
83 const sources = files.map(({ path, type }) => ({
84 source: fs.readFileSync(path, { encoding: 'utf-8' }),
88 const generatedCssFiles = sources.map(generateTheme);
90 const autoGenerateDisclaimer = [
92 ' * This file is automatically generated.',
93 ' * Manual changes will be lost.',
97 const cssFile = [autoGenerateDisclaimer, ...generatedCssFiles].join('\n\n');
99 const prettierCssFile = await prettier.format(cssFile, { parser: 'css' });
101 fs.writeFileSync(output, prettierCssFile);