UNPKG

palette-webpack-plugin

Version:

Generate a JSON file containing your color palette from existing Sass maps and/or Tailwind.

338 lines (299 loc) 8.39 kB
const path = require('path'); const fs = require('fs'); const webpack = require('webpack'); const _ = require('lodash'); const { color: d3Color } = require('d3-color'); const { hsv: d3Hsv } = require('d3-hsv'); class PaletteWebpackPlugin { /** * Register the component. * * @param {Object} options */ constructor(options) { this.options = _.merge( { output: 'palette.json', blacklist: ['transparent', 'inherit'], priority: 'tailwind', pretty: false, tailwind: { config: './tailwind.config.js', shades: false, path: 'colors', }, sass: { path: 'resources/assets/styles/config', files: ['variables.scss'], variables: ['colors'], }, }, options || {} ); this.palette = this.options.priority.includes('tailwind') ? this.build(this.tailwind(), this.sass()) : this.build(this.sass(), this.tailwind()); } /** * Add Palette to the webpack build process. * * @param {Object} compiler */ apply(compiler) { const palette = JSON.stringify( this.palette, null, this.options.pretty ? 2 : null ); if (compiler.hooks) { if (webpack.version.startsWith('5')) { compiler.hooks.thisCompilation.tap( this.constructor.name, (compilation) => { Object.assign(compilation.assets, { [this.options.output]: { source() { return palette; }, size() { return palette.length; }, }, }); } ); } else { compiler.hooks.emit.tapAsync( this.constructor.name, (compilation, callback) => { Object.assign(compilation.assets, { [this.options.output]: { source() { return palette; }, size() { return palette.length; }, }, }); callback(); } ); } } } /** * Builds a flattened array containing descriptive color objects in a format * compatible with the WordPress `editor-color-palette` theme support feature. * * @see {@link https://developer.wordpress.org/block-editor/developers/themes/theme-support/} * @param {Object} objects */ build(...objects) { const collection = _.uniqBy(_.union(...objects), 'name'); const [colors, maybeColors] = _.partition( collection, (value) => !!d3Color(value.color) ); const [falsePositives, notColors] = _.partition(maybeColors, (value) => /^(?:rgb|hsl)a?\(.+?\)$/i.test(value.color) ); const [grayscale, notGrayscale] = _.partition( colors, (value) => this.isGrayscale(value.color) || this.maybeGrayscale(value.color) ); return [ [...notGrayscale, ...falsePositives, ...notColors], grayscale, ].flatMap((color) => _.sortBy(color, 'name')); } /** * Fetch and parse Sass theme colors if they are available. */ sass() { if (!this.options.sass || !this.options.sass.files) { return; } const paths = this.options.sass.path ? _.endsWith('/', this.options.sass.path) ? this.options.sass.path : [this.options.sass.path, '/'].join('') : null; const files = [this.options.sass.files].map((file) => { if (this.exists([paths, file].join(''))) { return [paths, file].join(''); } }); if (!files) { return; } const variables = require('sass-export') .exporter({ inputFiles: files }) .getArray(); if (!variables.length) { return; } return variables .filter( (key) => [this.options.sass.variables].some( (value) => key.name === (_.startsWith(value, '$') ? value : ['$', value].join('')) ) && key.mapValue ) .flatMap((colors) => colors.mapValue.map((color) => this.transform(color.name, color.compiledValue, true) ) ); } /** * Fetch and parse Tailwind theme colors if they are available. */ tailwind() { if (!this.options.tailwind || !this.exists(this.options.tailwind.config)) { return []; } const config = require('tailwindcss/resolveConfig')( require(path.resolve(this.options.tailwind.config)) ); this.tailwind = _.get( config, `theme.${this.options.tailwind.path || 'colors'}`, {} ); return Object.keys(this.tailwind) .flatMap((key) => { if (!key || this.options.blacklist.includes(key)) { return; } if (_.isString(this.tailwind[key])) { return this.transform(key); } if ( !this.options.tailwind.shades && this.tailwind[key].hasOwnProperty('500') ) { return this.transform(key, '500'); } if (_.isArray(this.options.tailwind.shades)) { return Object.keys(this.tailwind[key]) .filter((value) => this.options.tailwind.shades.includes(value)) .map((value) => this.transform(key, value)); } if (_.isObject(this.options.tailwind.shades)) { return Object.keys(this.tailwind[key]) .filter((value) => Object.keys(this.options.tailwind.shades).includes(value) ) .map((value) => this.transform(key, value)); } return Object.keys(this.tailwind[key]).map((value) => this.transform(key, value) ); }) .filter((value) => !!value); } /** * Transform a color key and value into a more descriptive object. * * @param {String} key * @param {String} value * @param {Boolean} isSass */ transform(key, value, isSass = false) { if (isSass) { return { name: this.title(key), slug: key, color: value, }; } if (!value) { return { name: this.title(key), slug: key, color: this.tailwind[key], }; } return { name: isNaN(value) ? this.title(value) : this.options.tailwind.shades ? this.title(key, value) : this.title(key), slug: `${key}-${value}`, color: this.tailwind[key][value], }; } /** * Returns a title cased string. * * @param {String} value * @param {String} description */ title(value, description) { value = _.startCase(_.camelCase(value)); if ( !_.isEmpty(description) && _.isObject(this.options.tailwind.shades) && _.has(this.options.tailwind.shades, description) ) { return _.trim(`${this.options.tailwind.shades[description]} ${value}`); } return ( value + (!_.isEmpty(description) ? ` (${this.title(description)})` : '') ); } /** * Checks if a file exists. * * @param {String|Array} files */ exists(files) { if (Array.isArray(files) && files.length) { return (this.options.sass.files = files.filter((file) => { return fs.existsSync(file); }) || false); } return fs.existsSync(files); } /** * Check if a color is grayscale. * * @param {String} color */ isGrayscale(color) { const { r, g, b } = d3Color(color); return r === g && r === b; } /** * Build a curve to find colors that visually look like grayscale. * * Shout out to Austin Pray <austin@austinpray.com> * for the big brain plays on color sorting. * * @param {String} color */ maybeGrayscale(color) { const { h, s, v } = d3Hsv(color); /** * HSV is a cylinder where the central vertical axis comprises * the neutral, achromatic, or gray colors. * (image: https://w.wiki/Fsg) * * Let's build a curve to find colors that look like grayscale... * * v = 1.3/(1+8.5*s) * https://www.wolframalpha.com/input/?i=plot+v+%3D+1.3%2F%281%2B8.5*s%29+from+v%3D0+to+1+and+s%3D0+to+1 * * Good enough for government work. Now let's see if the value * falls below the curve. */ return v < 1.3 / (1 + 8.5 * s); } } module.exports = PaletteWebpackPlugin;