UNPKG

colorally

Version:

Name colors by well-known definitions

241 lines (199 loc) 8.64 kB
#!/usr/bin/env node 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var chalk = require('chalk'); var clipboardy = require('clipboardy'); var colors = require('../data/colors.json'); var deltaE = require('delta-e'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk); var colors__default = /*#__PURE__*/_interopDefaultLegacy(colors); var version = "2.0.8"; // ╻ ╻╺┳╸╻╻ ╻╺┳╸╻ ╻ // ┃ ┃ ┃ ┃┃ ┃ ┃ ┗┳┛ // ┗━┛ ╹ ╹┗━╸╹ ╹ ╹ const duplicate = x => x + x; const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args))); const flatten = arr => arr.reduce((acc, v) => acc.concat(Array.isArray(v) ? flatten(v) : v), []); const isBoolean = val => typeof val === 'boolean'; const isEmpty = x => x == null || !Object.keys(x).length; const isString = x => typeof x === 'string'; const map = fn => arr => arr.map(fn); const take = n => arr => arr.slice(0, n); const split = x => str => str.split(x); const toCase = convertWord => separator => str => str.toLowerCase().split(/\s+/).map(convertWord).join(separator); const toCamelCase = toCase(([head, ...tail], idx) => idx === 0 ? [head, ...tail].join('') : head.toUpperCase() + tail.join(''))(''); const toConstantCase = toCase(word => word.toUpperCase())('_'); const toDotCase = toCase(word => word.toLowerCase())('.'); const toKebabCase = toCase(word => word.toLowerCase())('-'); const toLowerCase = toCase(word => word.toLowerCase())(' '); const toPascalCase = toCase(([head, ...tail]) => head.toUpperCase() + tail.join(''))(''); const toSnakeCase = toCase(word => word.toLowerCase())('_'); const toTitleCase = toCase(([head, ...tail]) => head.toUpperCase() + tail.join(''))(' '); const toUpperCase = toCase(word => word.toUpperCase())(' '); const zipObj = ks => vs => ks.reduce((acc, k, idx) => ({ ...acc, [k]: vs[idx] }), {}); // ┏━╸┏━┓┏┓╻╻ ╻┏━╸┏━┓╺┳╸┏━╸┏━┓┏━┓ const omitTypeIndicators = str => str.replace(/^#|0x/, ''); const expandShortHex = str => str.length === 3 ? Array.from(str).map(duplicate).join('') : str; const convertStringToHex = str => parseInt(str, 16); const rgbToXyz = rgb => { const [r, g, b] = rgb.map(x => { const y = x / 255; return y > 0.04045 ? Math.pow((y + 0.055) / 1.055, 2.4) : y / 12.92; }); return [(r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047, (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.0, (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883].map(x => x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116); }; const xyzToLab = ([x, y, z]) => [116 * y - 16, 500 * (x - y), 200 * (y - z)]; const hexToRgb = hex => { const r = hex >> 16; const g = hex >> 8 & 0xff; const b = hex & 0xff; return [r, g, b]; }; const strToHex = compose(convertStringToHex, expandShortHex, take(6), omitTypeIndicators); const rgbToLab = compose(xyzToLab, rgbToXyz); // ┏━┓╻ ╻╻ ┏━╸┏━┓ const measureDistance = (...rgbs) => deltaE.getDeltaE00(...rgbs.map(compose(zipObj(['L', 'A', 'B']), rgbToLab))); const findSimilarDefinition = definitions => rgb => { const getNearestDefinition = nearest => current => nearest == null || nearest.distance > current.distance ? current : nearest; const traverseDefinitions = (nearest, idx = 0) => { if (idx === definitions.length) return nearest; const def = definitions[idx]; const current = { ...def, distance: measureDistance(rgb, def.rgb) }; return current.distance === 0 ? current : traverseDefinitions(getNearestDefinition(nearest)(current), idx + 1); }; return traverseDefinitions(); }; // ┏━╸┏━┓╻ ┏━┓┏━┓┏━┓╻ ╻ ╻ ╻ /** * Find visually similar color definition. * * @param {(string|number|number[])} color A color value in hex-string, hex or RGB array. * @returns {object} A color definition object with name, rgb array and optional match statistics. */ function colorally(color) { const rgb = Array.isArray(color) ? color.map(Number) : hexToRgb(isString(color) ? strToHex(color) : color); return findSimilarDefinition(colors__default["default"])(rgb); } // ┏━╸╻ ╻ const { argv, env } = process; const defaults = { options: { copy: false, format: false, help: false, verbose: false, version: false }, values: [] }; const optionDefinitions = [{ name: 'copy', identifier: /^--?c(opy)?$/ }, { name: 'format', identifier: /^--?f(ormat)?(=\w+)?$$/, input: /^\w+$/ }, { name: 'help', identifier: /^--?h(elp)?$/ }, { name: 'verbose', identifier: /^--?v(erbose)?$/ }, { name: 'version', identifier: /^(-V)|(--version)$/ }]; const printHelp = exitCode => { console.log(chalk__default["default"]`{blue colorally} - Name colors by well-known definitions {bold USAGE} {blue colorally} [option...] [{underline hex}] {blue colorally} [option...] [{underline red}] [{underline green}] [{underline blue}] The {underline hex} argument can contain either {bold 3} or {bold 6 digits} ` + chalk__default["default"]`with an optional leading {bold 0x} or {bold #}. The {underline red}, {underline green} and {underline blue} arguments may only ` + chalk__default["default"]`contain digits. {bold OPTIONS} -V, --version output version number -c, --copy copy definition to clipboard -f, --format {underline case} output definition in specified format -h, --help output usage information -v, --verbose provide a more talkative result The {underline case} argument can be either {bold camel} {italic fooBar}, ` + chalk__default["default"]`{bold constant} {italic FOO_BAR}, {bold dot} {italic foo.bar}, {bold kebab} {italic foo-bar}, {bold lower} {italic foo bar}, {bold pascal} ` + chalk__default["default"]` {italic FooBar}, {bold snake} {italic foo_bar} {bold title} {italic Foo Bar} or {bold upper} {italic FOO BAR}. By default, {blue colorally} sets {underline case} as {bold title}. `); return process.exit(exitCode); }; const printVersion = () => { return console.log('v' + version); }; const findOption = str => optionDefinitions.find(opt => opt.identifier.test(str)); const parseArgs = args => { const omitFalsyValues = arr => arr.filter(Boolean); const parse = acc => idx => arr => { const current = arr[idx]; const next = arr[idx + 1] || false; const opt = findOption(current); const optValue = opt && opt.input ? opt.input.test(next) ? next : false : true; const increment = isBoolean(optValue) ? 1 : 2; if (idx >= arr.length) return acc; return parse(opt != null ? { options: { ...acc.options, [opt.name]: optValue }, values: acc.values } : { options: acc.options, values: [...acc.values, current] })(idx + increment)(arr); }; const parsedArgs = compose(parse(defaults)(0), flatten, map(split('=')))(args); return { ...parsedArgs, values: omitFalsyValues(parsedArgs.values) }; }; const formatDefinitionName = format => name => { const formatMap = { camel: toCamelCase, constant: toConstantCase, dot: toDotCase, kebab: toKebabCase, lower: toLowerCase, pascal: toPascalCase, snake: toSnakeCase, title: toTitleCase, upper: toUpperCase }; const validFormat = format && Object.keys(formatMap).find(key => key === format.toLowerCase()) || 'title'; return formatMap[validFormat](name); }; const run = args => { const { options, values } = parseArgs(args); if (options.help) return printHelp(0); if (options.version) return printVersion(); if (isEmpty(values)) return printHelp(1); const definition = colorally(values.length > 2 ? values.slice(0, 3) : values[0]); const name = formatDefinitionName(options.format)(definition.name); if (options.copy) { clipboardy.writeSync(name); } const colorBlock = // Only display if terminal supports truecolor. chalk__default["default"].level > 2 ? chalk__default["default"].bgRgb(...definition.rgb)(' ') + ' ' : ''; return console.log(options.verbose ? // prettier-ignore chalk__default["default"]`Found ${colorBlock}{bold ${name}} {italic (${definition.rgb.join(', ')})} ` + chalk__default["default"]`by a distance of {italic ${definition.distance.toFixed(4)}}.` : name); }; // Exclude runner from test coverage. // istanbul ignore next if (env.NODE_ENV !== 'test') { run(argv.slice(2, argv.length)); } exports.run = run;