diff --git a/.travis.yml b/.travis.yml index f98fed0..94ab01f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,3 @@ language: node_js node_js: - '12' - '10' - - '8' diff --git a/index.d.ts b/index.d.ts index 44a907e..723c2d4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,214 +1,4 @@ -declare type CSSColor = - | 'aliceblue' - | 'antiquewhite' - | 'aqua' - | 'aquamarine' - | 'azure' - | 'beige' - | 'bisque' - | 'black' - | 'blanchedalmond' - | 'blue' - | 'blueviolet' - | 'brown' - | 'burlywood' - | 'cadetblue' - | 'chartreuse' - | 'chocolate' - | 'coral' - | 'cornflowerblue' - | 'cornsilk' - | 'crimson' - | 'cyan' - | 'darkblue' - | 'darkcyan' - | 'darkgoldenrod' - | 'darkgray' - | 'darkgreen' - | 'darkgrey' - | 'darkkhaki' - | 'darkmagenta' - | 'darkolivegreen' - | 'darkorange' - | 'darkorchid' - | 'darkred' - | 'darksalmon' - | 'darkseagreen' - | 'darkslateblue' - | 'darkslategray' - | 'darkslategrey' - | 'darkturquoise' - | 'darkviolet' - | 'deeppink' - | 'deepskyblue' - | 'dimgray' - | 'dimgrey' - | 'dodgerblue' - | 'firebrick' - | 'floralwhite' - | 'forestgreen' - | 'fuchsia' - | 'gainsboro' - | 'ghostwhite' - | 'gold' - | 'goldenrod' - | 'gray' - | 'green' - | 'greenyellow' - | 'grey' - | 'honeydew' - | 'hotpink' - | 'indianred' - | 'indigo' - | 'ivory' - | 'khaki' - | 'lavender' - | 'lavenderblush' - | 'lawngreen' - | 'lemonchiffon' - | 'lightblue' - | 'lightcoral' - | 'lightcyan' - | 'lightgoldenrodyellow' - | 'lightgray' - | 'lightgreen' - | 'lightgrey' - | 'lightpink' - | 'lightsalmon' - | 'lightseagreen' - | 'lightskyblue' - | 'lightslategray' - | 'lightslategrey' - | 'lightsteelblue' - | 'lightyellow' - | 'lime' - | 'limegreen' - | 'linen' - | 'magenta' - | 'maroon' - | 'mediumaquamarine' - | 'mediumblue' - | 'mediumorchid' - | 'mediumpurple' - | 'mediumseagreen' - | 'mediumslateblue' - | 'mediumspringgreen' - | 'mediumturquoise' - | 'mediumvioletred' - | 'midnightblue' - | 'mintcream' - | 'mistyrose' - | 'moccasin' - | 'navajowhite' - | 'navy' - | 'oldlace' - | 'olive' - | 'olivedrab' - | 'orange' - | 'orangered' - | 'orchid' - | 'palegoldenrod' - | 'palegreen' - | 'paleturquoise' - | 'palevioletred' - | 'papayawhip' - | 'peachpuff' - | 'peru' - | 'pink' - | 'plum' - | 'powderblue' - | 'purple' - | 'rebeccapurple' - | 'red' - | 'rosybrown' - | 'royalblue' - | 'saddlebrown' - | 'salmon' - | 'sandybrown' - | 'seagreen' - | 'seashell' - | 'sienna' - | 'silver' - | 'skyblue' - | 'slateblue' - | 'slategray' - | 'slategrey' - | 'snow' - | 'springgreen' - | 'steelblue' - | 'tan' - | 'teal' - | 'thistle' - | 'tomato' - | 'turquoise' - | 'violet' - | 'wheat' - | 'white' - | 'whitesmoke' - | 'yellow' - | 'yellowgreen'; - declare namespace ansiStyles { - interface ColorConvert { - /** - The RGB color space. - - @param red - (`0`-`255`) - @param green - (`0`-`255`) - @param blue - (`0`-`255`) - */ - rgb(red: number, green: number, blue: number): string; - - /** - The RGB HEX color space. - - @param hex - A hexadecimal string containing RGB data. - */ - hex(hex: string): string; - - /** - @param keyword - A CSS color name. - */ - keyword(keyword: CSSColor): string; - - /** - The HSL color space. - - @param hue - (`0`-`360`) - @param saturation - (`0`-`100`) - @param lightness - (`0`-`100`) - */ - hsl(hue: number, saturation: number, lightness: number): string; - - /** - The HSV color space. - - @param hue - (`0`-`360`) - @param saturation - (`0`-`100`) - @param value - (`0`-`100`) - */ - hsv(hue: number, saturation: number, value: number): string; - - /** - The HSV color space. - - @param hue - (`0`-`360`) - @param whiteness - (`0`-`100`) - @param blackness - (`0`-`100`) - */ - hwb(hue: number, whiteness: number, blackness: number): string; - - /** - Use a [4-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4-bit) to set text color. - */ - ansi(ansi: number): string; - - /** - Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color. - */ - ansi256(ansi: number): string; - } - interface CSPair { /** The ANSI terminal control sequence for starting this style. @@ -222,9 +12,9 @@ declare namespace ansiStyles { } interface ColorBase { - readonly ansi: ColorConvert; - readonly ansi256: ColorConvert; - readonly ansi16m: ColorConvert; + ansi256(code: number): string; + + ansi16m(red: number, green: number, blue: number): string; /** The ANSI terminal control sequence for ending this color. @@ -333,6 +123,31 @@ declare namespace ansiStyles { readonly bgMagentaBright: CSPair; readonly bgWhiteBright: CSPair; } + + interface ConvertColor { + /** + Convert from the RGB color space to the ANSI 256 color space. + + @param red - (`0...255`) + @param green - (`0...255`) + @param blue - (`0...255`) + */ + rgbToAnsi256(red: number, green: number, blue: number): number; + + /** + Convert from the RGB HEX color space to the RGB color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToRgb(hex: string): [red: number, green: number, blue: number]; + + /** + Convert from the RGB HEX color space to the ANSI 256 color space. + + @param hex - A hexadecimal string containing RGB data. + */ + hexToAnsi256(hex: string): number; + } } declare const ansiStyles: { @@ -340,6 +155,6 @@ declare const ansiStyles: { readonly color: ansiStyles.ForegroundColor & ansiStyles.ColorBase; readonly bgColor: ansiStyles.BackgroundColor & ansiStyles.ColorBase; readonly codes: ReadonlyMap; -} & ansiStyles.BackgroundColor & ansiStyles.ForegroundColor & ansiStyles.Modifier; +} & ansiStyles.BackgroundColor & ansiStyles.ForegroundColor & ansiStyles.Modifier & ansiStyles.ConvertColor; export = ansiStyles; diff --git a/index.js b/index.js index 5d82581..6eca670 100644 --- a/index.js +++ b/index.js @@ -1,62 +1,10 @@ 'use strict'; -const wrapAnsi16 = (fn, offset) => (...args) => { - const code = fn(...args); - return `\u001B[${code + offset}m`; -}; - -const wrapAnsi256 = (fn, offset) => (...args) => { - const code = fn(...args); - return `\u001B[${38 + offset};5;${code}m`; -}; - -const wrapAnsi16m = (fn, offset) => (...args) => { - const rgb = fn(...args); - return `\u001B[${38 + offset};2;${rgb[0]};${rgb[1]};${rgb[2]}m`; -}; - -const ansi2ansi = n => n; -const rgb2rgb = (r, g, b) => [r, g, b]; - -const setLazyProperty = (object, property, get) => { - Object.defineProperty(object, property, { - get: () => { - const value = get(); - - Object.defineProperty(object, property, { - value, - enumerable: true, - configurable: true - }); - - return value; - }, - enumerable: true, - configurable: true - }); -}; - -/** @type {typeof import('color-convert')} */ -let colorConvert; -const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { - if (colorConvert === undefined) { - colorConvert = require('color-convert'); - } - - const offset = isBackground ? 10 : 0; - const styles = {}; +const ANSI_BACKGROUND_OFFSET = 10; - for (const [sourceSpace, suite] of Object.entries(colorConvert)) { - const name = sourceSpace === 'ansi16' ? 'ansi' : sourceSpace; - if (sourceSpace === targetSpace) { - styles[name] = wrap(identity, offset); - } else if (typeof suite === 'object') { - styles[name] = wrap(suite[targetSpace], offset); - } - } +const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`; - return styles; -}; +const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`; function assembleStyles() { const codes = new Map(); @@ -146,12 +94,55 @@ function assembleStyles() { styles.color.close = '\u001B[39m'; styles.bgColor.close = '\u001B[49m'; - setLazyProperty(styles.color, 'ansi', () => makeDynamicStyles(wrapAnsi16, 'ansi16', ansi2ansi, false)); - setLazyProperty(styles.color, 'ansi256', () => makeDynamicStyles(wrapAnsi256, 'ansi256', ansi2ansi, false)); - setLazyProperty(styles.color, 'ansi16m', () => makeDynamicStyles(wrapAnsi16m, 'rgb', rgb2rgb, false)); - setLazyProperty(styles.bgColor, 'ansi', () => makeDynamicStyles(wrapAnsi16, 'ansi16', ansi2ansi, true)); - setLazyProperty(styles.bgColor, 'ansi256', () => makeDynamicStyles(wrapAnsi256, 'ansi256', ansi2ansi, true)); - setLazyProperty(styles.bgColor, 'ansi16m', () => makeDynamicStyles(wrapAnsi16m, 'rgb', rgb2rgb, true)); + styles.color.ansi256 = wrapAnsi256(); + styles.color.ansi16m = wrapAnsi16m(); + styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET); + styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET); + + // From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js + styles.rgbToAnsi256 = (red, green, blue) => { + // We use the extended greyscale palette here, with the exception of + // black and white. normal palette only has 4 greyscale shades. + if (red === green && green === blue) { + if (red < 8) { + return 16; + } + + if (red > 248) { + return 231; + } + + return Math.round(((red - 8) / 247) * 24) + 232; + } + + return 16 + + (36 * Math.round(red / 255 * 5)) + + (6 * Math.round(green / 255 * 5)) + + Math.round(blue / 255 * 5); + }; + + styles.hexToRgb = hex => { + const matches = /(?[a-f\d]{6}|[a-f\d]{3})/i.exec(hex.toString(16)); + if (!matches) { + return [0, 0, 0]; + } + + let {colorString} = matches.groups; + + if (colorString.length === 3) { + colorString = colorString.split('').map(character => character + character).join(''); + } + + const integer = Number.parseInt(colorString, 16); + + return [ + (integer >> 16) & 0xFF, + (integer >> 8) & 0xFF, + integer & 0xFF + ]; + }; + + styles.hexToAnsi256 = hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)); return styles; } diff --git a/index.test-d.ts b/index.test-d.ts index 548b51b..ac256ec 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,28 +1,8 @@ -import {expectType, expectError} from 'tsd'; -import * as cssColors from 'color-name'; -import {KEYWORD} from 'color-convert/conversions'; -import colorConvert = require('color-convert'); +import {expectType} from 'tsd'; import ansiStyles = require('.'); -declare function keyof(type: T): keyof T; -declare function params unknown>(type: T): Parameters - -type CSS_COLOR_NAMES = keyof typeof cssColors; -let CSS_COLOR_NAMES!: CSS_COLOR_NAMES; -let KEYWORD!: KEYWORD; - -expectType(CSS_COLOR_NAMES); -expectType(KEYWORD); - expectType>(ansiStyles.codes); -expectType<[CSS_COLOR_NAMES]>(params(ansiStyles.color.ansi.keyword)); -expectType<[CSS_COLOR_NAMES]>(params(ansiStyles.color.ansi256.keyword)); -expectType<[CSS_COLOR_NAMES]>(params(ansiStyles.color.ansi16m.keyword)); -expectType<[CSS_COLOR_NAMES]>(params(ansiStyles.bgColor.ansi.keyword)); -expectType<[CSS_COLOR_NAMES]>(params(ansiStyles.bgColor.ansi256.keyword)); -expectType<[CSS_COLOR_NAMES]>(params(ansiStyles.bgColor.ansi16m.keyword)); - // - Static colors - // -- Namespaced -- // --- Foreground color --- diff --git a/package.json b/package.json index 7539328..9841929 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "url": "sindresorhus.com" }, "engines": { - "node": ">=8" + "node": ">=10" }, "scripts": { "test": "xo && ava && tsd", @@ -43,9 +43,6 @@ "command-line", "text" ], - "dependencies": { - "color-convert": "^2.0.1" - }, "devDependencies": { "@types/color-convert": "^1.9.0", "ava": "^2.3.0", diff --git a/readme.md b/readme.md index 24883de..709dabd 100644 --- a/readme.md +++ b/readme.md @@ -25,9 +25,8 @@ console.log(`${style.green.open}Hello world!${style.green.close}`); // may be degraded to fit that color palette. This means terminals // that do not support 16 million colors will best-match the // original color. -console.log(style.bgColor.ansi.hsl(120, 80, 72) + 'Hello world!' + style.bgColor.close); -console.log(style.color.ansi256.rgb(199, 20, 250) + 'Hello world!' + style.color.close); -console.log(style.color.ansi16m.hex('#abcdef') + 'Hello world!' + style.color.close); +console.log(`${style.color.ansi256(style.rgbToAnsi256(199, 20, 250))}Hello World${style.color.close}`) +console.log(`${style.color.ansi16m(...style.hexToRgb('#abcdef'))}Hello World${style.color.close}`) ``` ## API @@ -110,30 +109,22 @@ console.log(style.codes.get(36)); ## [256 / 16 million (TrueColor) support](https://gist.github.com/XVilka/8346728) -`ansi-styles` uses the [`color-convert`](https://github.com/Qix-/color-convert) package to allow for converting between various colors and ANSI escapes, with support for 256 and 16 million colors. +`ansi-styles` allows converting between various color formats and ANSI escapes, with support for 256 and 16 million colors. The following color spaces from `color-convert` are supported: - `rgb` - `hex` -- `keyword` -- `hsl` -- `hsv` -- `hwb` -- `ansi` - `ansi256` To use these, call the associated conversion function with the intended output, for example: ```js -style.color.ansi.rgb(100, 200, 15); // RGB to 16 color ansi foreground code -style.bgColor.ansi.rgb(100, 200, 15); // RGB to 16 color ansi background code +style.color.ansi256(style.rgbToAnsi256(100, 200, 15)); // RGB to 256 color ansi foreground code +style.bgColor.ansi256(style.hexToAnsi256('#C0FFEE')); // HEX to 256 color ansi foreground code -style.color.ansi256.hsl(120, 100, 60); // HSL to 256 color ansi foreground code -style.bgColor.ansi256.hsl(120, 100, 60); // HSL to 256 color ansi foreground code - -style.color.ansi16m.hex('#C0FFEE'); // Hex (RGB) to 16 million color foreground code -style.bgColor.ansi16m.hex('#C0FFEE'); // Hex (RGB) to 16 million color background code +style.color.ansi16m(100, 200, 15); // RGB to 16 million color foreground code +style.bgColor.ansi16m(...style.hexToRgb('#C0FFEE')); // Hex (RGB) to 16 million color foreground code ``` ## Related diff --git a/test/test.js b/test/test.js index bd2ca70..cbaa06b 100644 --- a/test/test.js +++ b/test/test.js @@ -27,61 +27,24 @@ test('don\'t pollute other objects', t => { t.not(obj1.foo, obj2.foo); }); -test('all color types are always available', t => { - const {ansi, ansi256, ansi16m} = style.color; - - t.truthy(ansi); - t.truthy(ansi.ansi); - t.truthy(ansi.ansi256); - - t.truthy(ansi256); - t.truthy(ansi256.ansi); - t.truthy(ansi256.ansi256); - - t.truthy(ansi16m); - t.truthy(ansi16m.ansi); - t.truthy(ansi16m.ansi256); - - // There are no such things as ansi16m source colors - t.falsy(ansi.ansi16m); - t.falsy(ansi256.ansi16m); - t.falsy(ansi16m.ansi16m); -}); - -test('support conversion to ansi (16 colors)', t => { - t.is(style.color.ansi.rgb(255, 255, 255), '\u001B[97m'); - t.is(style.color.ansi.hsl(140, 100, 50), '\u001B[92m'); - t.is(style.color.ansi.hex('#990099'), '\u001B[35m'); - t.is(style.color.ansi.hex('#FF00FF'), '\u001B[95m'); - - t.is(style.bgColor.ansi.rgb(255, 255, 255), '\u001B[107m'); - t.is(style.bgColor.ansi.hsl(140, 100, 50), '\u001B[102m'); - t.is(style.bgColor.ansi.hex('#990099'), '\u001B[45m'); - t.is(style.bgColor.ansi.hex('#FF00FF'), '\u001B[105m'); -}); - test('support conversion to ansi (256 colors)', t => { - t.is(style.color.ansi256.rgb(255, 255, 255), '\u001B[38;5;231m'); - t.is(style.color.ansi256.hsl(140, 100, 50), '\u001B[38;5;48m'); - t.is(style.color.ansi256.hex('#990099'), '\u001B[38;5;127m'); - t.is(style.color.ansi256.hex('#FF00FF'), '\u001B[38;5;201m'); + t.is(style.color.ansi256(style.rgbToAnsi256(255, 255, 255)), '\u001B[38;5;231m'); + t.is(style.color.ansi256(style.hexToAnsi256('#990099')), '\u001B[38;5;127m'); + t.is(style.color.ansi256(style.hexToAnsi256('#FF00FF')), '\u001B[38;5;201m'); - t.is(style.bgColor.ansi256.rgb(255, 255, 255), '\u001B[48;5;231m'); - t.is(style.bgColor.ansi256.hsl(140, 100, 50), '\u001B[48;5;48m'); - t.is(style.bgColor.ansi256.hex('#990099'), '\u001B[48;5;127m'); - t.is(style.bgColor.ansi256.hex('#FF00FF'), '\u001B[48;5;201m'); + t.is(style.bgColor.ansi256(style.rgbToAnsi256(255, 255, 255)), '\u001B[48;5;231m'); + t.is(style.bgColor.ansi256(style.hexToAnsi256('#990099')), '\u001B[48;5;127m'); + t.is(style.bgColor.ansi256(style.hexToAnsi256('#FF00FF')), '\u001B[48;5;201m'); }); test('support conversion to ansi (16 million colors)', t => { - t.is(style.color.ansi16m.rgb(255, 255, 255), '\u001B[38;2;255;255;255m'); - t.is(style.color.ansi16m.hsl(140, 100, 50), '\u001B[38;2;0;255;85m'); - t.is(style.color.ansi16m.hex('#990099'), '\u001B[38;2;153;0;153m'); - t.is(style.color.ansi16m.hex('#FF00FF'), '\u001B[38;2;255;0;255m'); + t.is(style.color.ansi16m(255, 255, 255), '\u001B[38;2;255;255;255m'); + t.is(style.color.ansi16m(...style.hexToRgb('#990099')), '\u001B[38;2;153;0;153m'); + t.is(style.color.ansi16m(...style.hexToRgb('#FF00FF')), '\u001B[38;2;255;0;255m'); - t.is(style.bgColor.ansi16m.rgb(255, 255, 255), '\u001B[48;2;255;255;255m'); - t.is(style.bgColor.ansi16m.hsl(140, 100, 50), '\u001B[48;2;0;255;85m'); - t.is(style.bgColor.ansi16m.hex('#990099'), '\u001B[48;2;153;0;153m'); - t.is(style.bgColor.ansi16m.hex('#FF00FF'), '\u001B[48;2;255;0;255m'); + t.is(style.bgColor.ansi16m(255, 255, 255), '\u001B[48;2;255;255;255m'); + t.is(style.bgColor.ansi16m(...style.hexToRgb('#990099')), '\u001B[48;2;153;0;153m'); + t.is(style.bgColor.ansi16m(...style.hexToRgb('#FF00FF')), '\u001B[48;2;255;0;255m'); }); test('16/256/16m color close escapes', t => { @@ -98,5 +61,5 @@ test('export raw ANSI escape codes', t => { }); test('rgb -> truecolor is stubbed', t => { - t.is(style.color.ansi16m.rgb(123, 45, 67), '\u001B[38;2;123;45;67m'); + t.is(style.color.ansi16m(123, 45, 67), '\u001B[38;2;123;45;67m'); });