diff --git a/CHANGELOG.md b/CHANGELOG.md index f404827..748c0f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change log +## 3.7.0 (2025-01-07) + +- fix: cast falsy values `false`, `null`, `undefined`, `NaN` to a string. + In previous versions, the empty string `''` was returned for falsy values. +- fix: functions with argument `0` , e.g. `ansis.red(0)`, returning empty string `''`, now return colored value `'0'` +- test: add tests for function arguments with various types + ## 3.6.0 (2025-01-04) - feat: remove **undocumented** pointless dummy function `ansis(any)` diff --git a/README.md b/README.md index d787626..4648694 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A [small](#compare-size) and [fast](#benchmark) Node.js library for applying ANSI colors and styles in terminal output.\ Ansis provides all the [features](#features) you need, you can compare with [similar libraries](#compare).\ -Ansis is [faster](#benchmark) than **Chalk** and **Picocolors** (in some use cases), see [benchmarks](#benchmark). +Ansis is as [small](#compare-size) as **Picocolors** but has the [functionality](#compare) of **Chalk**. ### 🚀 [Install and Quick Start](#install) 🔄 [Why switch to Ansis](#why-switch-to-ansis) diff --git a/bench/index.js b/bench/index.js index bce2b2a..8b87f3c 100644 --- a/bench/index.js +++ b/bench/index.js @@ -140,8 +140,8 @@ bench('Chained syntax'). // kolorist - (not supported) run(); -// Nested styles, like picocolors recursion -bench('Nested styles'). +// Nested calls, like colorette recursion +bench('Nested calls'). add(packages['chalk'], () => chalk.red(chalk.bold(chalk.underline(chalk.bgWhite('foo'))))). add(packages['ansis'], () => ansis.red(ansis.bold(ansis.underline(ansis.bgWhite('foo'))))). add(packages['picocolors'], () => picocolors.red(picocolors.bold(picocolors.underline(picocolors.bgWhite('foo'))))). diff --git a/package.json b/package.json index 87b8ff2..223f15f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ansis", - "version": "3.6.0", + "version": "3.7.0", "description": "A small and fast Node.js library for applying ANSI colors and styles in terminal output", "keywords": [ "ansi", diff --git a/package.npm.json b/package.npm.json index fda4689..a1ceb1d 100644 --- a/package.npm.json +++ b/package.npm.json @@ -1,6 +1,6 @@ { "name": "ansis", - "version": "3.6.0", + "version": "3.7.0", "description": "ANSI colors and styles in terminal output", "keywords": [ "ansi", diff --git a/src/index.js b/src/index.js index 34d024e..a3518b8 100644 --- a/src/index.js +++ b/src/index.js @@ -35,15 +35,14 @@ let createStyle = ({ _p: props }, { open, close }) => { * @return {string} */ let styleFn = (strings, ...values) => { - if (!strings) return ''; - let props = styleFn._p; let { _a: openStack, _b: closeStack } = props; - let str = strings.raw + // optional chaining operator `?.` available since node >= 14 + let str = strings?.raw // render template strings ? String.raw(strings, ...values) - // convert the number to the string + // convert the value to a string : '' + strings; // -> detect nested styles diff --git a/test/ansi16.test.js b/test/ansi16.test.js index 36febc4..9f70895 100644 --- a/test/ansi16.test.js +++ b/test/ansi16.test.js @@ -2,7 +2,7 @@ import { expect, describe, test } from 'vitest'; import { esc } from './utils/helpers.js'; // import env variables to simulate ANSI 16 color space -import './env/color-space.ansi16.js'; +import './env/ansi16-colors.js'; //import { hex, green } from '../src/index.mjs'; // for debugging only import { hex, green } from 'ansis'; diff --git a/test/ansi256.test.js b/test/ansi256.test.js index efd6c8f..c36e7e8 100644 --- a/test/ansi256.test.js +++ b/test/ansi256.test.js @@ -2,7 +2,7 @@ import { expect, describe, test } from 'vitest'; import { esc } from './utils/helpers.js'; // import env variables to simulate ANSI 256 color space -import './env/color-space.ansi256.js'; +import './env/ansi256-colors.js'; //import { hex, ansi256 } from '../src/index.mjs'; // for debugging only import { hex, ansi256 } from 'ansis'; diff --git a/test/env/color-space.ansi16.js b/test/env/ansi16-colors.js similarity index 100% rename from test/env/color-space.ansi16.js rename to test/env/ansi16-colors.js diff --git a/test/env/color-space.ansi256.js b/test/env/ansi256-colors.js similarity index 100% rename from test/env/color-space.ansi256.js rename to test/env/ansi256-colors.js diff --git a/test/env/color-space.mono.js b/test/env/mono-color.js similarity index 100% rename from test/env/color-space.mono.js rename to test/env/mono-color.js diff --git a/test/env/color-space.truecolor.js b/test/env/truecolor.js similarity index 100% rename from test/env/color-space.truecolor.js rename to test/env/truecolor.js diff --git a/test/flags.test.js b/test/flags.test.js index a739d8a..2ade360 100644 --- a/test/flags.test.js +++ b/test/flags.test.js @@ -3,7 +3,7 @@ import { expect, describe, test } from 'vitest'; import { esc, execScriptSync } from './utils/helpers.js'; // import env variables to simulate truecolor color space in CLI -import './env/color-space.truecolor.js'; +import './env/truecolor.js'; const TEST_PATH = path.resolve('./test/'); diff --git a/test/functional.test.js b/test/functional.test.js index a66fff5..705b5e7 100644 --- a/test/functional.test.js +++ b/test/functional.test.js @@ -2,7 +2,7 @@ import { describe, test, expect } from 'vitest'; import { esc } from './utils/helpers.js'; // import env variables to simulate truecolor color space in CLI -import './env/color-space.truecolor.js'; +import './env/truecolor.js'; //import ansis, { Ansis, red, yellow, green, bold, hex } from '../src/index.mjs'; // // for debugging only import ansis, { Ansis, red, grey, gray, green, yellow, bold, italic, underline, hex } from 'ansis'; // test npm package @@ -15,6 +15,182 @@ describe('support colors', () => { }); }); +describe('convert function argument to string', () => { + test(`no argument`, () => { + // if no argument is provided, an empty string w/o escape codes should be returned, + // but this is such an incredible case that we won't check it, + // we cast no argument as `undefined`, + // picocolors doesn't check this edge case either + const received = ansis.green(); + const expected = '\x1b[32mundefined\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`empty string`, () => { + // if argument is empty string, an empty string w/o escape codes should be returned, + // but this is such an incredible case that we won't check it, + // picocolors doesn't check this edge case either + const received = ansis.green(''); + const expected = '\x1b[32m\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`string`, () => { + const received = ansis.green('green'); + const expected = '\x1b[32mgreen\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`number`, () => { + const received = ansis.green(1974); + const expected = '\x1b[32m1974\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`number with separator`, () => { + const received = ansis.green(999_55); + const expected = '\x1b[32m99955\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`float`, () => { + const received = ansis.green(999.55); + const expected = '\x1b[32m999.55\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`true`, () => { + const received = ansis.green(true); + const expected = '\x1b[32mtrue\x1b[39m'; + expect(received).toEqual(expected); + }); + + // fixed in v3.7.0 + test(`false`, () => { + const received = ansis.green(false); + const expected = '\x1b[32mfalse\x1b[39m'; + expect(received).toEqual(expected); + }); + + // fixed in v3.7.0 + test(`falsy value 0`, () => { + const received = ansis.green(0); + const expected = '\x1b[32m0\x1b[39m'; + expect(received).toEqual(expected); + }); + + // fixed in v3.7.0 + test(`null`, () => { + const received = ansis.green(null); + const expected = '\x1b[32mnull\x1b[39m'; + expect(received).toEqual(expected); + }); + + // fixed in v3.7.0 + test(`undefined`, () => { + const received = ansis.green(undefined); + const expected = '\x1b[32mundefined\x1b[39m'; + expect(received).toEqual(expected); + }); + + // fixed in v3.7.0 + test(`NaN`, () => { + const received = ansis.green(NaN); + const expected = '\x1b[32mNaN\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`Infinity`, () => { + const received = ansis.green(Infinity); + const expected = '\x1b[32mInfinity\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`Array`, () => { + const received = ansis.green([999, 55]); + const expected = '\x1b[32m999,55\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`Array`, () => { + const received = ansis.green(['Hello', 'world']); + const expected = '\x1b[32mHello,world\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`Set`, () => { + const set = new Set(['Hello', 'world']); + const received = ansis.green([...set]); + const expected = '\x1b[32mHello,world\x1b[39m'; + expect(received).toEqual(expected); + }); +}); + +describe('arguments in template string', () => { + test(`falsy value 0 in template`, () => { + const received = ansis.green(`zero ${0} value`); + const expected = '\x1b[32mzero 0 value\x1b[39m'; + expect(received).toEqual(expected); + }); + + test(`null in template`, () => { + const received = ansis.green(`${null} value`); + const expected = '\x1b[32mnull value\x1b[39m'; + expect(received).toEqual(expected); + }); +}); + +describe('handling numbers', () => { + test(`ansis.bold(123)`, () => { + const num = 123; + const received = ansis.bold(num); + const expected = '\x1b[1m123\x1b[22m'; + expect(esc(received)).toEqual(esc(expected)); + }); + + test(`ansis.red(123)`, () => { + const num = 123; + const received = ansis.red(num); + const expected = '\x1b[31m123\x1b[39m'; + expect(esc(received)).toEqual(esc(expected)); + }); + + test(`red(123)`, () => { + const num = 123; + const received = red(num); + const expected = '\x1b[31m123\x1b[39m'; + expect(esc(received)).toEqual(esc(expected)); + }); + + test(`bold(123)`, () => { + const num = 123; + const received = bold(num); + const expected = '\x1b[1m123\x1b[22m'; + expect(esc(received)).toEqual(esc(expected)); + }); + + test(`red.bold(123)`, () => { + const num = 123; + const received = red.bold(num); + const expected = '\x1b[31m\x1b[1m123\x1b[22m\x1b[39m'; + expect(esc(received)).toEqual(esc(expected)); + }); + + test(`hex('#A00')(123)`, () => { + const num = 123; + const received = hex('#A00')(num); + const expected = '\x1b[38;2;170;0;0m123\x1b[39m'; + expect(esc(received)).toEqual(esc(expected)); + }); + + test('red`size: ${123}px`', () => { + const num = 123; + const received = red`size: ${num}px`; + const expected = '\x1b[31msize: 123px\x1b[39m'; + expect(esc(received)).toEqual(esc(expected)); + }); +}); + describe('style tests', () => { test(`ansis.visible('foo')`, () => { const received = ansis.visible('foo'); @@ -28,12 +204,6 @@ describe('style tests', () => { expect(esc(received)).toEqual(esc(expected)); }); - test(`ansis.green('')`, () => { - const received = ansis.green(''); - const expected = ''; - expect(esc(received)).toEqual(esc(expected)); - }); - test(`ansis.green('foo', 'bar')`, () => { const received = ansis.green(['foo', 'bar'].join(' ')); const expected = '\x1b[32mfoo bar\x1b[39m'; @@ -88,16 +258,6 @@ describe('style tests', () => { expect(esc(received)).toEqual(esc(expected)); }); - test(`ansis.green('\nHello\nNew line\nNext new line.\n')`, () => { - const received = ansis.green('\nHello\nNew line\nNext new line.\n'); - const expected = `\x1b[32m\x1b[39m -\x1b[32mHello\x1b[39m -\x1b[32mNew line\x1b[39m -\x1b[32mNext new line.\x1b[39m -\x1b[32m\x1b[39m`; - expect(esc(received)).toEqual(esc(expected)); - }); - // experimental link: not widely supported // test(`ansis.link('foo')`, () => { // const received = ansis.link('https://github.com/webdiscus/ansis')('foo'); @@ -106,6 +266,37 @@ describe('style tests', () => { // }); }); +describe('style new line', () => { + test(`linux new line LF`, () => { + const received = ansis.green('Hello\nWorld'); + const expected = '\x1b[32mHello\x1b[39m\n\x1b[32mWorld\x1b[39m'; + expect(esc(received)).toEqual(esc(expected)); + }); + + test(`windows new line CRLF`, () => { + const received = ansis.green('Hello\r\nWorld'); + const expected = '\x1b[32mHello\x1b[39m\r\n\x1b[32mWorld\x1b[39m'; + expect(esc(received)).toEqual(esc(expected)); + }); + + test(`linux new line LF, background`, () => { + const received = ansis.bgGreen(' Hello \n World '); + const expected = '\x1b[42m Hello \x1b[49m\n\x1b[42m World \x1b[49m'; + expect(esc(received)).toEqual(esc(expected)); + }); + + test(`multiple new line`, () => { + const received = ansis.bgGreen('\nHello\nNew line\nNext new line.\n'); + const expected = `\x1b[42m\x1b[49m +\x1b[42mHello\x1b[49m +\x1b[42mNew line\x1b[49m +\x1b[42mNext new line.\x1b[49m +\x1b[42m\x1b[49m`; + console.log(received); + expect(esc(received)).toEqual(esc(expected)); + }); +}); + describe('advanced features tests', () => { test(`nested styles`, () => { const received = ansis.red('foo' + ansis.underline.bgBlue('bar') + '!'); @@ -194,6 +385,16 @@ describe('alias tests', () => { const expected = '\x1b[38;5;96m\x1b[38;5;96mfoo\x1b[39m\x1b[39m'; expect(esc(received)).toEqual(esc(expected)); }); + + test(`caching of aliases`, () => { + const {gray, grey} = ansis.gray; + const grayBold = gray.bold.italic; + const greyBold = grey.bold.underline; + + expect(gray('foo')).toBe(grey('foo')); + expect(grayBold('bar')).not.toBe(greyBold('bar')); + expect(grey('baz')).not.toBe(greyBold('baz')); + }); }); describe('template literals tests', () => { @@ -247,57 +448,6 @@ describe('extend base colors tests', () => { }); }); -describe('handling numbers', () => { - test(`ansis.bold(123)`, () => { - const num = 123; - const received = ansis.bold(num); - const expected = '\x1b[1m123\x1b[22m'; - expect(esc(received)).toEqual(esc(expected)); - }); - - test(`ansis.red(123)`, () => { - const num = 123; - const received = ansis.red(num); - const expected = '\x1b[31m123\x1b[39m'; - expect(esc(received)).toEqual(esc(expected)); - }); - - test(`red(123)`, () => { - const num = 123; - const received = red(num); - const expected = '\x1b[31m123\x1b[39m'; - expect(esc(received)).toEqual(esc(expected)); - }); - - test(`bold(123)`, () => { - const num = 123; - const received = bold(num); - const expected = '\x1b[1m123\x1b[22m'; - expect(esc(received)).toEqual(esc(expected)); - }); - - test(`red.bold(123)`, () => { - const num = 123; - const received = red.bold(num); - const expected = '\x1b[31m\x1b[1m123\x1b[22m\x1b[39m'; - expect(esc(received)).toEqual(esc(expected)); - }); - - test(`hex('#A00')(123)`, () => { - const num = 123; - const received = hex('#A00')(num); - const expected = '\x1b[38;2;170;0;0m123\x1b[39m'; - expect(esc(received)).toEqual(esc(expected)); - }); - - test('red`size: ${123}px`', () => { - const num = 123; - const received = red`size: ${num}px`; - const expected = '\x1b[31msize: 123px\x1b[39m'; - expect(esc(received)).toEqual(esc(expected)); - }); -}); - describe('base ANSI 16 colors', () => { // foreground colors test(`ansis.black('foo')`, () => { diff --git a/test/no-color.test.js b/test/no-color.test.js index 8449513..7850955 100644 --- a/test/no-color.test.js +++ b/test/no-color.test.js @@ -1,7 +1,7 @@ import { expect, describe, test } from 'vitest'; // import env variables to simulate no color -import './env/color-space.mono.js'; +import './env/mono-color.js'; //import ansis, { red } from '../src/index.mjs'; // for debugging only import ansis, { red } from 'ansis'; diff --git a/test/package.test.js b/test/package.test.js index 93eb1fa..3b114c2 100644 --- a/test/package.test.js +++ b/test/package.test.js @@ -3,7 +3,7 @@ import { expect, describe, test } from 'vitest'; import { esc, execScriptSync } from './utils/helpers.js'; // import env variables to simulate truecolor color space in CLI -import './env/color-space.truecolor.js'; +import './env/truecolor.js'; const TEST_PATH = path.resolve('./test/'); diff --git a/test/ts-compiler.test.js b/test/ts-compiler.test.js index 4f6500b..72699ac 100644 --- a/test/ts-compiler.test.js +++ b/test/ts-compiler.test.js @@ -2,7 +2,7 @@ import { describe, test } from 'vitest'; import { executeTSFile } from './utils/helpers.js'; // import env variables to simulate truecolor color space in CLI -import './env/color-space.truecolor.js'; +import './env/truecolor.js'; // integration tests: compile TS into JS, execute compiled JS and compare the output with expected string describe('imports', () => {