diff --git a/CHANGELOG.md b/CHANGELOG.md index f477aa6..c519f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,20 @@ # Change log +## 1.2.0 (2021-12-27) +- added supports the environment variables `NO_COLOR` `FORCE_COLOR` and flags `--no-color` `--color` +- added aliases `ansi` for `ansi256` and `bgAnsi` for `bgAnsi256` +- added to readme the compare of most popular ANSI libraries + ## 1.1.1 (2021-12-27) -- add the class Ansis to create more independent instances to increase the performance by benchmark +- added the class Ansis to create more independent instances to increase the performance by benchmark - improve performance - code refactoring - update readme ## 1.1.0 (2021-12-25) -- add supports the use of `open` and `close` properties for each style -- fix codes for methods ansi256() and bgAnsi256() -- add demo to npm package +- added supports the use of `open` and `close` properties for each style +- added demo to npm package +- fixed codes for methods ansi256() and bgAnsi256() - update package.json - update readme diff --git a/README.md b/README.md index b666044..9f652d4 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,10 @@ --- -[![npm version](https://badge.fury.io/js/ansis.svg)](https://www.npmjs.com/package/ansis) +[![npm](https://img.shields.io/npm/v/ansis?logo=npm&color=brightgreen "npm package")](https://www.npmjs.com/package/ansis "download npm package") [![codecov](https://codecov.io/gh/webdiscus/ansis/branch/master/graph/badge.svg?token=H7SFJONX1X)](https://codecov.io/gh/webdiscus/ansis) [![node](https://img.shields.io/npm/dm/ansis)](https://www.npmjs.com/package/ansis) - Color styling of text for ANSI terminals using the SGR (Select Graphic Rendition) codes defined in the [ECMA-48](https://www.ecma-international.org/publications-and-standards/standards/ecma-48/) standard.\ This is improved and faster implementation for `Node.js`. @@ -44,6 +43,7 @@ Output: - powerful and lightweight library is faster than many others such as `chalk` `kleur` `ansi-colors` etc. - supports the standard de facto API of the `chalk` - supports 256 color and Truecolor + - supports the environment variables [`NO_COLOR`](https://no-color.org) `FORCE_COLOR` and flags `--no-color` `--color` - supports styles like: `bold` `red` `yellowBright` `bgGreen` `bgCyanBright` ect. - supports chained styles, e.g.: ```js @@ -151,6 +151,11 @@ ansis.ansi256(96).bold('bold Bright Cyan'); // background color ansis.bgAnsi256(105)('Bright Magenta'); ``` +> Aliases +> +> `ansis.ansi()` is alias for `ansis.ansi256()`\ +> `ansis.bgAnsi()` is alias for `ansis.bgAnsi256()` + ## Truecolor @@ -166,6 +171,31 @@ ansis.bgHex('#96C')('Amethyst'); ansis.bgRgb(224, 17, 95)('Ruby'); ``` +## Compare most popular ANSI libraries + +| Library | Standard
style / color
naming | Chain
styles | Nested
styles | New
Line | ANSI 256
colors | Truecolor
RGB / HEX | NO_COLOR | +|------------------------------|-------------------|-----------------|------------------|-------------|---------------------------------|------------------------|:---------------------------------------------------------| +| [`colors.js`][colors.js] | no, e.g.
`brightRed` | yes | yes | yes | - | - | only
`FORCE_COLOR`
`--no-color`
`--color` | +| [`colorette`][colorette] | yes
(16 colors) | - | yes | - | - | - | yes | +| [`picocolors`][picocolors] | yes
(8 colors) | - | yes | - | - | - | yes | +| [`cli-color`][cli-color] | yes
(16 colors) | yes | yes | - | `.xterm(num)` | - | yes | +| [`color-cli`][color-cli] | no, e.g.
`red_bbt` | yes | buggy | yes | `.x` | - | only
`--no-color`
`--color` | +| [`ansi-colors`][ansi-colors] | yes
(16 colors) | yes | yes | yes | - | - | only
`FORCE_COLOR` | +| [`kleur`][kleur] | yes
(8 colors) | yes* | yes | - | - | - | yes | +| [`chalk`][chalk] | yes
(16 colors) | yes | yes | yes | `.ansi256(num)` | `.hex()` `.rgb()` | yes | +| **+ ansis** | yes
(16 colors) | yes | yes | yes | `.ansi256(num)`
`.ansi(num)` | `.hex()` `.rgb()` | yes | + +### Column description +- **Standard style and color naming**: `red` `redBright` `bgRed` `bgRedBright` etc., see above the **Foreground / Background colors**. +- **Chain styles**: `ansis.red.bold.underline('text')`.\ + `kleur` use the chain of functions: `kleur.red().bold().underline('text')`. +- **Nested styles**: + ```js + c.red(`red ${c.green(`green ${c.underline(`underline`)} green`)} red`) +- **New Line**: correct break of escape sequences at `end of line`\ + new line +- **NO_COLOR**: supports the environment variables [`NO_COLOR`](https://no-color.org) `FORCE_COLOR` and flags `--no-color` `--color` + ## Benchmark ### Initialize @@ -179,7 +209,6 @@ npm i npm run bench ``` - > ### Tested on > > MacBook Pro 16" M1 Max 64GB\ @@ -194,16 +223,16 @@ c.red(`${c.bold(`${c.cyan(`${c.yellow('yellow')}cyan`)}`)}red`); ``` ```diff - colors-js 1,152,114 ops/sec - colorette 4,548,418 ops/sec - picocolors 3,832,593 ops/sec - cli-color 471,929 ops/sec - color-cli 110,282 ops/sec - ansi-colors 1,272,164 ops/sec - kleur/colors 2,278,569 ops/sec - kleur 2,223,929 ops/sec - chalk 2,255,589 ops/sec -+ ansis 2,674,316 ops/sec + colors-js 1,158,572 ops/sec + colorette 4,572,582 ops/sec + picocolors 3,841,124 ops/sec + cli-color 470,320 ops/sec + color-cli 109,811 ops/sec + ansi-colors 1,265,615 ops/sec + kleur/colors 2,281,415 ops/sec + kleur 2,228,639 ops/sec + chalk 2,287,146 ops/sec ++ ansis 2,669,734 ops/sec ``` ### Base styles @@ -211,16 +240,16 @@ c.red(`${c.bold(`${c.cyan(`${c.yellow('yellow')}cyan`)}`)}red`); styles.forEach((style) => c[style]('foo')); ``` ```diff - colors-js 475,774 ops/sec - colorette 1,174,392 ops/sec - picocolors 5,724,714 ops/sec - cli-color 220,577 ops/sec - color-cli 73,535 ops/sec - ansi-colors 727,414 ops/sec - kleur/colors 1,275,337 ops/sec - kleur 3,843,212 ops/sec - chalk 3,144,045 ops/sec -+ ansis 4,360,629 ops/sec + colors-js 471,395 ops/sec + colorette 1,103,314 ops/sec + picocolors 5,725,578 ops/sec + cli-color 221,282 ops/sec + color-cli 73,725 ops/sec + ansi-colors 716,280 ops/sec + kleur/colors 1,259,858 ops/sec + kleur 3,829,838 ops/sec + chalk 3,165,933 ops/sec ++ ansis 4,483,217 ops/sec ``` ### Chained styles @@ -246,16 +275,16 @@ colors.forEach((color) => c[color].bold.underline.italic('foo')); colors.forEach((color) => c[color](c.bold(c.underline(c.italic('foo'))))); ``` ```diff - colors-js 165,202 ops/sec - colorette 712,604 ops/sec - picocolors 939,536 ops/sec - cli-color 64,758 ops/sec - color-cli 13,833 ops/sec - ansi-colors 258,930 ops/sec - kleur/colors 563,266 ops/sec - kleur 646,985 ops/sec - chalk 385,590 ops/sec -+ ansis 554,813 ops/sec + colors-js 166,425 ops/sec + colorette 695,350 ops/sec + picocolors 942,592 ops/sec + cli-color 65,561 ops/sec + color-cli 13,800 ops/sec + ansi-colors 260,316 ops/sec + kleur/colors 561,111 ops/sec + kleur 648,195 ops/sec + chalk 497,292 ops/sec ++ ansis 558,575 ops/sec ``` @@ -264,16 +293,48 @@ colors.forEach((color) => c[color](c.bold(c.underline(c.italic('foo'))))); c.red(`a red ${c.white('white')} red ${c.red('red')} red ${c.cyan('cyan')} red ${c.black('black')} red ${c.red('red')} red ${c.green('green')} red ${c.red('red')} red ${c.yellow('yellow')} red ${c.blue('blue')} red ${c.red('red')} red ${c.magenta('magenta')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.green('green')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.magenta('magenta')} red ${c.red('red')} red ${c.red('red')} red ${c.cyan('cyan')} red ${c.red('red')} red ${c.red('red')} red ${c.yellow('yellow')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} message`); ``` ```diff - colors-js 89,529 ops/sec - colorette 243,237 ops/sec - picocolors 242,528 ops/sec - cli-color 41,897 ops/sec - color-cli 14,245 ops/sec - ansi-colors 120,991 ops/sec - kleur/colors 233,875 ops/sec - kleur 220,233 ops/sec - chalk 157,450 ops/sec -+ ansis 205,393 ops/sec + colors-js 89,633 ops/sec + colorette 243,139 ops/sec + picocolors 243,975 ops/sec + cli-color 41,657 ops/sec + color-cli 14,264 ops/sec + ansi-colors 121,451 ops/sec + kleur/colors 234,132 ops/sec + kleur 221,446 ops/sec + chalk 189,960 ops/sec ++ ansis 211,868 ops/sec + +``` + +### Deep nested styles +```js +c.green( + `green ${c.cyan( + `cyan ${c.red( + `red ${c.yellow( + `yellow ${c.blue( + `blue ${c.magenta( + `magenta ${c.underline( + `underline ${c.italic(`italic`)} underline` + )} magenta` + )} blue` + )} yellow` + )} red` + )} cyan` + )} green` +); +``` +```diff + colors-js 451,592 ops/sec + colorette 1,131,757 ops/sec + picocolors 1,002,649 ops/sec + cli-color 213,441 ops/sec + color-cli 40,340 ops/sec + ansi-colors 362,733 ops/sec + kleur/colors 478,547 ops/sec + kleur 464,004 ops/sec + chalk 565,965 ops/sec ++ ansis 882,220 ops/sec ``` @@ -291,8 +352,8 @@ c.hex('#FBA')('foo'); ansi-colors (not supported) kleur/colors (not supported) kleur (not supported) - chalk 2,746,362 ops/sec -+ ansis 4,584,357 ops/sec + chalk 2,891,684 ops/sec ++ ansis 4,944,572 ops/sec ``` ## Testing diff --git a/bench/index.js b/bench/index.js index bac2a1b..ee28eaa 100644 --- a/bench/index.js +++ b/bench/index.js @@ -40,6 +40,8 @@ import kleur from 'kleur'; import * as kleurColors from 'kleur/colors'; import picocolors from 'picocolors'; +const log = console.log; + // create new instance of Ansis for correct measure in benchmark const ansis = new Ansis(); @@ -86,22 +88,9 @@ const colorStyles = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan let fixture = []; -function outputNested(name, lib) { - const rgb = lib.hex('#80109f'); - - const str = lib.red( - `begin ${rgb.bold('RGB')} ${lib.yellow('yellow')} red ${lib.italic.cyan('italic cyan')} red ${lib.red( - 'red' - )} red ${lib.underline.green.italic( - `underline italic green ${lib.hex('#e5850a')('underline italic blue')} underline italic green` - )} red ${lib.cyan('cyan')} red ${lib.bold.yellow('bold yellow')} red ${lib.green('green')} end` - ); - - console.log(` ${name}`, str); -} - -outputNested('chalk\t', chalk); -outputNested('ansis\t', ansis); +//showSupportOfDeepNestedStyling(); +//showSupportOfDeepNestedMultiStyling(); +//showSupportOfBreakStyleAtNewLine(); // Colorette bench // https://github.com/jorgebucaran/colorette/blob/main/bench/index.js @@ -198,18 +187,38 @@ bench('Nested styles') .add('ansis', () => fixture[8](ansis)) .run(); -// reserved -bench('Break style') - .add('colors-js', () => colorsJs.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) - .add('colorette', () => colorette.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) - .add('picocolors', () => picocolors.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) - .add('cli-color', () => cliColor.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) - .add('color-cli', () => colorCli.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) - .add('ansi-colors', () => ansiColors.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) - .add('kleur/colors', () => kleurColors.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) - .add('kleur', () => ansis.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) - .add('chalk', () => kleur.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) - .add('ansis', () => ansis.green(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) +// Deep nested styles +fixture = createFixture(vendors, deepNestedFixture); +bench('Deep nested styles') + .add('colors-js', () => fixture[9](colorsJs)) + .add('colorette', () => fixture[0](colorette)) + .add('picocolors', () => fixture[1](picocolors)) + .add('cli-color', () => fixture[2](cliColor)) + .add('color-cli', () => fixture[3](colorCli)) + .add('ansi-colors', () => fixture[4](ansiColors)) + .add('kleur/colors', () => fixture[5](kleurColors)) + .add('kleur', () => fixture[6](kleur)) + .add('chalk', () => fixture[7](chalk)) + .add('ansis', () => fixture[8](ansis)) + .run(); + +// Check support of correct break style at new line + +// Break style at new line +// bench('New Line') +// .add('colors-js', () => colorsJs.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) +// .add('ansi-colors', () => ansiColors.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) +// .add('chalk', () => chalk.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) +// .add('ansis', () => ansis.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)) +// .run(); + +bench('RGB colors') + .add('chalk', () => { + for (let i = 0; i < 256; i++) chalk.rgb(i, 150, 200)('foo'); + }) + .add('ansis', () => { + for (let i = 0; i < 256; i++) ansis.rgb(i, 150, 200)('foo'); + }) .run(); // HEX colors @@ -246,3 +255,56 @@ function nestedFixture(c) { )} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} red ${c.red('red')} message` ); } + +function deepNestedFixture(c) { + return c.green( + `green ${c.cyan( + `cyan ${c.red( + `red ${c.yellow( + `yellow ${c.blue( + `blue ${c.magenta(`magenta ${c.underline(`underline ${c.italic(`italic`)} underline`)} magenta`)} blue` + )} yellow` + )} red` + )} cyan` + )} green` + ); +} + +function complexNestedFixture(c) { + return c.red( + `red ${c.yellow('yellow')} red ${c.italic.cyan('italic cyan')} red ${c.underline.green( + `underline green ${c.yellow('underline yellow')} underline green` + )} red ${c.cyan('cyan')} red ${c.bold.yellow('bold yellow')} red ${c.green('green')} red` + ); +} + +function showSupportOfDeepNestedStyling() { + log('colors-js: ', deepNestedFixture(colorsJs)); + log('colorette: ', deepNestedFixture(colorette)); + log('picocolors: ', deepNestedFixture(picocolors)); + log('cli-color: ', deepNestedFixture(cliColor)); + log('color-cli: ', deepNestedFixture(colorCli)); // buggy + log('ansi-colors: ', deepNestedFixture(ansiColors)); + log('kleur/colors: ', deepNestedFixture(kleurColors)); + log('kleur: ', deepNestedFixture(kleur)); + log('chalk: ', deepNestedFixture(chalk)); + log('ansis: ', deepNestedFixture(ansis)); +} + +function showSupportOfDeepNestedMultiStyling() { + log('chalk: ', complexNestedFixture(chalk)); + log('ansis: ', complexNestedFixture(ansis)); +} + +function showSupportOfBreakStyleAtNewLine() { + log('colors-js: ', colorsJs.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // OK + log('colorette: ', colorette.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // (not supported) + log('picocolors: ', picocolors.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // (not supported) + log('cli-color: ', cliColor.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // (not supported) + log('color-cli: ', colorCli.green_b(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // (not supported) + log('ansi-colors: ', ansiColors.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // OK + log('kleur/colors: ', kleurColors.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // (not supported) + log('kleur: ', kleur.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // (not supported) + log('chalk: ', chalk.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // OK + log('ansis: ', ansis.bgGreen(`\nAnsis\nNEW LINE\nNEXT NEW LINE\n`)); // OK +} diff --git a/doc/img/break-style-nl.png b/doc/img/break-style-nl.png new file mode 100644 index 0000000..a9c7383 Binary files /dev/null and b/doc/img/break-style-nl.png differ diff --git a/examples/index.js b/examples/index.js index 13095e8..3c7897a 100644 --- a/examples/index.js +++ b/examples/index.js @@ -25,7 +25,8 @@ log(); // background color log(ansis.bold.italic.hex('#800909').bgHex('#ffe49e')('Hello ansis!')); log(ansis.bold.italic.bgHex('#800909').hex('#ffe49e')('Hello ansis!')); -log(ansis.black.bgRgb(200, 80, 300)(`\nAnsis\n${ansis.black.bgBlueBright('NEW LINE')}\nNEXT NEW LINE\n`)); +log(ansis.black.bgGreen(`\n Ansis \n NEW LINE \n NEXT NEW LINE \n`)); +//log(ansis.black.bgRgb(200, 80, 300)(`\nAnsis\n${ansis.black.bgBlueBright('NEW LINE')}\nNEXT NEW LINE\n`)); // example from readme log(ansis.green(`Hello ${ansis.inverse('ANSI')} World!`)); @@ -69,7 +70,7 @@ function outputNested(name, lib) { // created via https://patorjk.com/software/taag/#p=testall&h=1&f=Graceful&t=ANSIS function ansisLogo() { - const paddingLeft = 6; + const paddingLeft = 5; const logo = ` █████╗ ███╗ ██╗███████╗██╗███████╗ @@ -103,7 +104,7 @@ function ansisLogo() { for (let charWidthIdx = 0; charWidthIdx < width; charWidthIdx++) { char = logo[i++]; code = Array.isArray(codes) ? codes[row] : codes + row; - out += ansis.ansi256(code)(char); + out += ansis.ansi(code)(char); } } row++; diff --git a/package.json b/package.json index 5465743..c7a84fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ansis", - "version": "1.1.1", + "version": "1.2.0", "description": "Color styling of text for ANSI terminals using the SGR codes defined in the ECMA-48 standard.", "keywords": [ "ansi", @@ -16,7 +16,8 @@ "rgb", "hex", "log", - "logging" + "logging", + "NO_COLOR" ], "license": "ISC", "author": "webdiscus (https://github.com/webdiscus)", diff --git a/src/ansi-codes.js b/src/ansi-codes.js index 5029ce6..f080d32 100644 --- a/src/ansi-codes.js +++ b/src/ansi-codes.js @@ -1,64 +1,31 @@ -const esc = ([open, close]) => ({ open: `\x1b[${open}m`, close: `\x1b[${close}m` }); +/** + * @param {Object?} processTest Used by unit test only. + * @returns {boolean} + */ +export const isSupported = (processTest) => { + const proc = processTest ? processTest : process || {}; + const env = proc.env || {}; + const argv = proc.argv || []; + const stdout = proc.stdout && proc.stdout.isTTY; + //const stderr = proc.stderr && proc.stderr.isTTY; -export const codes = { - // commands - reset: [0, 0], - inverse: [7, 27], - hidden: [8, 28], + const isDisabled = 'NO_COLOR' in env || argv.includes('--no-color') || argv.includes('--color=false'); + const isForced = 'FORCE_COLOR' in env || argv.includes('--color'); - // styles - bold: [1, 22], - dim: [2, 22], // alias for faint - faint: [2, 22], - italic: [3, 23], - underline: [4, 24], - doubleUnderline: [21, 24], - strikethrough: [9, 29], - strike: [9, 29], // alias for strikethrough - frame: [51, 54], - encircle: [52, 54], - overline: [53, 55], - - // foreground colors - black: [30, 39], - red: [31, 39], - green: [32, 39], - yellow: [33, 39], - blue: [34, 39], - magenta: [35, 39], - cyan: [36, 39], - white: [37, 39], - gray: [90, 39], // alias for blackBright - blackBright: [90, 39], - redBright: [91, 39], - greenBright: [92, 39], - yellowBright: [93, 39], - blueBright: [94, 39], - magentaBright: [95, 39], - cyanBright: [96, 39], - whiteBright: [97, 39], + const isTerm = env.TERM !== 'dumb' && /^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM); + const isCompatibleTerminal = (stdout && isTerm) || proc.platform === 'win32'; + const isCI = 'CI' in env; - // background colors - bgBlack: [40, 49], - bgRed: [41, 49], - bgGreen: [42, 49], - bgYellow: [43, 49], - bgBlue: [44, 49], - bgMagenta: [45, 49], - bgCyan: [46, 49], - bgWhite: [47, 49], - bgBlackBright: [100, 49], - bgRedBright: [101, 49], - bgGreenBright: [102, 49], - bgYellowBright: [103, 49], - bgBlueBright: [104, 49], - bgMagentaBright: [105, 49], - bgCyanBright: [106, 49], - bgWhiteBright: [107, 49], + return !isDisabled && (isForced || isCompatibleTerminal || isCI); }; -export const ansiCodes = { - // commands +export const supported = isSupported(); + +const noColorProps = { open: '', close: '' }; +const esc = supported ? ([open, close]) => ({ open: `\x1b[${open}m`, close: `\x1b[${close}m` }) : () => noColorProps; + +export const baseCodes = { + // misc reset: esc([0, 0]), inverse: esc([7, 27]), hidden: esc([8, 28]), @@ -113,3 +80,10 @@ export const ansiCodes = { bgCyanBright: esc([106, 49]), bgWhiteBright: esc([107, 49]), }; + +export const extendedCodes = { + ansi256: supported ? (code) => ({ open: `\x1B[38;5;${code}m`, close: '\x1B[39m' }) : () => noColorProps, + bgAnsi256: supported ? (code) => ({ open: `\x1B[48;5;${code}m`, close: '\x1B[49m' }) : () => noColorProps, + rgb: supported ? (r, g, b) => ({ open: `\x1B[38;2;${r};${g};${b}m`, close: '\x1B[39m' }) : () => noColorProps, + bgRgb: supported ? (r, g, b) => ({ open: `\x1B[48;2;${r};${g};${b}m`, close: '\x1B[49m' }) : () => noColorProps, +}; diff --git a/src/index.d.ts b/src/index.d.ts index 5fada2f..391329a 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -36,10 +36,15 @@ export interface AnsisInstance extends StyleFunction, StyleProperties { * ``` * * @param {number} code in range [0, 255]. - * @return {AnsisInstance} */ ansi256: (code: number) => AnsisInstance; + /** + * Alias to ansi256. + * @param {number} code in range [0, 255]. + */ + ansi: (code: number) => AnsisInstance; + /** * Set RGB values for foreground color. * @@ -71,6 +76,12 @@ export interface AnsisInstance extends StyleFunction, StyleProperties { */ bgAnsi256: (code: number) => AnsisInstance; + /** + * Alias to bgAnsi256. + * @param {number} code in range [0, 255]. + */ + bgAnsi: (code: number) => AnsisInstance; + /** * Set RGB values for background color. * diff --git a/src/index.js b/src/index.js index 1f7df2a..360be56 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,8 @@ import { clamp, hexToRgb, strReplaceAll } from './utils.js'; -import { ansiCodes } from './ansi-codes.js'; +import { baseCodes, extendedCodes } from './ansi-codes.js'; /** - * All methods are implemented in prototype of the `styleProxy` object. + * Note: all methods are implemented in prototype of the `styleProxy` object. * @implements {AnsisInstance} */ export class Ansis { @@ -73,8 +73,8 @@ const createStyle = (open, close, parent) => { /** * Create base styles. */ -for (let name in ansiCodes) { - const { open, close } = ansiCodes[name]; +for (let name in baseCodes) { + const { open, close } = baseCodes[name]; styles[name] = { get() { const style = createStyle(open, close, this.props); @@ -98,25 +98,39 @@ styles.visible = { */ styles.ansi256 = { get() { - return (num) => { - num = clamp(num, 0, 255); - return createStyle(`\x1B[38;5;${num}m`, '\x1B[39m', this.props); + return (code) => { + code = clamp(code, 0, 255); + const { open, close } = extendedCodes.ansi256(code); + return createStyle(open, close, this.props); }; }, }; +/** + * Alias to ansi256. + * @type {AnsisInstance.ansi256} + */ +styles.ansi = styles.ansi256; + /** * @type {AnsisInstance.bgAnsi256} */ styles.bgAnsi256 = { get() { - return (num) => { - num = clamp(num, 0, 255); - return createStyle(`\x1B[48;5;${num}m`, '\x1B[49m', this.props); + return (code) => { + code = clamp(code, 0, 255); + const { open, close } = extendedCodes.bgAnsi256(code); + return createStyle(open, close, this.props); }; }, }; +/** + * Alias to bgAnsi256. + * @type {AnsisInstance.bgAnsi256} + */ +styles.bgAnsi = styles.bgAnsi256; + /** * @type {AnsisInstance.rgb} */ @@ -126,7 +140,8 @@ styles.rgb = { r = clamp(r, 0, 255); g = clamp(g, 0, 255); b = clamp(b, 0, 255); - return createStyle(`\x1B[38;2;${r};${g};${b}m`, '\x1B[39m', this.props); + const { open, close } = extendedCodes.rgb(r, g, b); + return createStyle(open, close, this.props); }; }, }; @@ -137,8 +152,8 @@ styles.rgb = { styles.hex = { get() { return (hex) => { - const [r, g, b] = hexToRgb(hex); - return createStyle(`\x1B[38;2;${r};${g};${b}m`, '\x1B[39m', this.props); + const { open, close } = extendedCodes.rgb(...hexToRgb(hex)); + return createStyle(open, close, this.props); }; }, }; @@ -152,7 +167,8 @@ styles.bgRgb = { r = clamp(r, 0, 255); g = clamp(g, 0, 255); b = clamp(b, 0, 255); - return createStyle(`\x1B[48;2;${r};${g};${b}m`, '\x1B[49m', this.props); + const { open, close } = extendedCodes.bgRgb(r, g, b); + return createStyle(open, close, this.props); }; }, }; @@ -163,8 +179,8 @@ styles.bgRgb = { styles.bgHex = { get() { return (hex) => { - const [r, g, b] = hexToRgb(hex); - return createStyle(`\x1B[48;2;${r};${g};${b}m`, '\x1B[49m', this.props); + const { open, close } = extendedCodes.bgRgb(...hexToRgb(hex)); + return createStyle(open, close, this.props); }; }, }; diff --git a/src/utils.js b/src/utils.js index 16480d7..417b5b5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,20 +1,3 @@ -import tty from 'tty'; - -// todo Add supports NO_COLOR -// export const isSupported = () => { -// if (!process) return false; -// -// const env = process.env || {}; -// const argv = process.argv || []; -// const isDisabled = 'NO_COLOR' in env || argv.includes('--no-color'); -// const isForced = 'FORCE_COLOR' in env || argv.includes('--color'); -// -// const isCompatibleTerminal = (tty.isatty(1) && env.TERM && env.TERM !== 'dumb') || process.platform === 'win32'; -// const isCI = 'CI' in env; -// -// return !isDisabled && (isForced || isCompatibleTerminal || isCI); -// }; - /** * Convert hex color string to RGB values. * diff --git a/test/cli/output.js b/test/cli/output.js new file mode 100644 index 0000000..2a9e25c --- /dev/null +++ b/test/cli/output.js @@ -0,0 +1,14 @@ +import ansis from '../../src/index.js'; +const c = ansis; + +console.log( + c.red('red') + + '|' + + c.rgb(80, 80, 80)('rgb') + + '|' + + c.bgRgb(80, 80, 80)('bgRgb') + + '|' + + c.hex('#fff')('hex') + + '|' + + c.bgHex('#fff')('bgHex') +); \ No newline at end of file diff --git a/test/index.test.js b/test/index.test.js index d0a0b8a..74159db 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,9 +1,27 @@ +import { execSync } from 'child_process'; +import path from 'path'; + import ansis from '../src/index.js'; +import { hexToRgb, clamp } from '../src/utils.js'; +import { isSupported } from '../src/ansi-codes.js'; -import { hexToRgb } from '../src/utils.js'; +const TEST_PATH = path.resolve('./test/'); /** - * Escape ESC-symbol. + * Return output of javascript file. + * + * @param {string} file + * @param {array} flags + * @return {string} + */ +const execScriptSync = (file, flags = []) => { + const result = execSync('node ' + file + ' ' + flags.join(' ')); + // replace last newline in result + return result.toString().replace(/\n$/, ''); +}; + +/** + * Escape the slash `\` in ESC-symbol. * Use it to show by an error the received ESC sequence string in console output. * * @param {string} str @@ -35,6 +53,40 @@ describe('default tests', () => { }); }); +describe('isSupported', () => { + test(`true`, (done) => { + let received = isSupported({}); + const expected = false; + expect(received).toEqual(expected); + done(); + }); + + test(`true`, (done) => { + let received = isSupported({ + platform: 'win32', + env: { FORCE_COLOR: true, CI: 'GITLAB_CI', TERM: 'ansi' }, + argv: ['--color'], + stdout: { isTTY: true }, + stderr: { isTTY: true }, + }); + const expected = true; + expect(received).toEqual(expected); + done(); + }); + + test(`false`, (done) => { + let received = isSupported({ + env: { NO_COLOR: true, TERM: 'dumb' }, + argv: ['--color=false', '--no-color'], + stdout: {}, + stderr: {}, + }); + const expected = false; + expect(received).toEqual(expected); + done(); + }); +}); + describe('utils tests', () => { test(`hexToRgb('FFAA99')`, (done) => { let received = hexToRgb('FFAA99'); @@ -70,6 +122,47 @@ describe('utils tests', () => { expect(received).toEqual(expected); done(); }); + + test(`clamp(3, 0, 2)`, (done) => { + let received = clamp(3, 0, 2); + const expected = 2; + expect(received).toEqual(expected); + done(); + }); + + test(`clamp(0, 1, 2)`, (done) => { + let received = clamp(0, 1, 2); + const expected = 1; + expect(received).toEqual(expected); + done(); + }); +}); + +describe('node script flags', () => { + test(`flag --color`, (done) => { + const filename = path.join(TEST_PATH, './cli/output.js'); + let received = execScriptSync(filename, ['--color']); + const expected = + '\x1b[31mred\x1b[39m|\x1b[38;2;80;80;80mrgb\x1b[39m|\x1b[48;2;80;80;80mbgRgb\x1b[49m|\x1b[38;2;255;255;255mhex\x1b[39m|\x1b[48;2;255;255;255mbgHex\x1b[49m'; + expect(esc(received)).toEqual(esc(expected)); + done(); + }); + + test(`flag --color=false`, (done) => { + const filename = path.join(TEST_PATH, './cli/output.js'); + let received = execScriptSync(filename, ['--color=false']); + const expected = 'red|rgb|bgRgb|hex|bgHex'; + expect(esc(received)).toEqual(esc(expected)); + done(); + }); + + test(`flag --no-color`, (done) => { + const filename = path.join(TEST_PATH, './cli/output.js'); + let received = execScriptSync(filename, ['--no-color']); + const expected = 'red|rgb|bgRgb|hex|bgHex'; + expect(esc(received)).toEqual(esc(expected)); + done(); + }); }); describe('style tests', () => { @@ -150,6 +243,13 @@ describe('style tests', () => { done(); }); + test(`ansis.ansi(97)`, (done) => { + let received = ansis.ansi(97)('foo'); + const expected = '\x1b[38;5;97mfoo\x1b[39m'; + expect(esc(received)).toEqual(esc(expected)); + done(); + }); + test(`ansis.bgAnsi256(97)`, (done) => { let received = ansis.bgAnsi256(97)('foo'); const expected = '\x1b[48;5;97mfoo\x1b[49m'; @@ -157,6 +257,13 @@ describe('style tests', () => { done(); }); + test(`ansis.bgAnsi(97)`, (done) => { + let received = ansis.bgAnsi(97)('foo'); + const expected = '\x1b[48;5;97mfoo\x1b[49m'; + expect(esc(received)).toEqual(esc(expected)); + done(); + }); + test(`ansis.green('\nHello\nNew line\nNext new line.\n')`, (done) => { let received = ansis.green('\nHello\nNew line\nNext new line.\n'); const expected = `\x1b[32m\x1b[39m