From 09ddbadcb5569f6d6ace11f54242c339d727d546 Mon Sep 17 00:00:00 2001 From: Toon Baeyens Date: Tue, 9 Jun 2020 09:36:34 +0200 Subject: [PATCH] Support template literals for nested calls (#392) Co-authored-by: Sindre Sorhus --- benchmark.js | 12 ++++++++++++ index.d.ts | 7 +++++++ index.test-d.ts | 5 +++++ readme.md | 3 ++- source/index.js | 9 ++++++++- test/chalk.js | 8 ++++++++ test/template-literal.js | 19 +++++++++++++++++++ 7 files changed, 61 insertions(+), 2 deletions(-) diff --git a/benchmark.js b/benchmark.js index dc24696..25b3f5d 100644 --- a/benchmark.js +++ b/benchmark.js @@ -47,4 +47,16 @@ suite('chalk', () => { bench('cached: 1 style nested non-intersecting', () => { chalkBgRed(blueStyledString); }); + + set('iterations', 10000); + + bench('cached: 1 style template literal', () => { + // eslint-disable-next-line no-unused-expressions + chalkRed`the fox jumps over the lazy dog`; + }); + + bench('cached: nested styles template literal', () => { + // eslint-disable-next-line no-unused-expressions + chalkRed`the fox {bold jumps} over the {underline lazy} dog`; + }); }); diff --git a/index.d.ts b/index.d.ts index df96b03..9cd88f3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -137,6 +137,13 @@ declare namespace chalk { DISK: {rgb(255,131,0) ${disk.used / disk.total * 100}%} `); ``` + + @example + ``` + import chalk = require('chalk'); + + log(chalk.red.bgBlack`2 + 3 = {bold ${2 + 3}}`) + ``` */ (text: TemplateStringsArray, ...placeholders: unknown[]): string; diff --git a/index.test-d.ts b/index.test-d.ts index ad5dcf5..aa9e2f5 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -152,6 +152,11 @@ expectType(chalk.bgWhiteBright`foo`); expectType(chalk.red.bgGreen.underline('foo')); expectType(chalk.underline.red.bgGreen('foo')); +// -- Complex template literal -- +expectType(chalk.underline``); +expectType(chalk.red.bgGreen.bold`Hello {italic.blue ${name}}`); +expectType(chalk.strikethrough.cyanBright.bgBlack`Works with {reset {bold numbers}} {bold.red ${1}}`); + // -- Color types == expectType('red'); expectError('hotpink'); diff --git a/readme.md b/readme.md index 816b5ce..338f42c 100644 --- a/readme.md +++ b/readme.md @@ -215,10 +215,11 @@ console.log(chalk` Blocks are delimited by an opening curly brace (`{`), a style, some content, and a closing curly brace (`}`). -Template styles are chained exactly like normal Chalk styles. The following two statements are equivalent: +Template styles are chained exactly like normal Chalk styles. The following three statements are equivalent: ```js console.log(chalk.bold.rgb(10, 100, 200)('Hello!')); +console.log(chalk.bold.rgb(10, 100, 200)`Hello!`); console.log(chalk`{bold.rgb(10,100,200) Hello!}`); ``` diff --git a/source/index.js b/source/index.js index e3b2f16..75ec663 100644 --- a/source/index.js +++ b/source/index.js @@ -6,6 +6,8 @@ const { stringEncaseCRLFWithFirstIndex } = require('./util'); +const {isArray} = Array; + // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ 'ansi', @@ -135,6 +137,11 @@ const createStyler = (open, close, parent) => { const createBuilder = (self, _styler, _isEmpty) => { const builder = (...arguments_) => { + if (isArray(arguments_[0]) && isArray(arguments_[0].raw)) { + // Called as a template literal, for example: chalk.red`2 + 3 = {bold ${2+3}}` + return applyStyle(builder, chalkTag(builder, ...arguments_)); + } + // Single argument is hot path, implicit coercion is faster than anything // eslint-disable-next-line no-implicit-coercion return applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' ')); @@ -189,7 +196,7 @@ let template; const chalkTag = (chalk, ...strings) => { const [firstString] = strings; - if (!Array.isArray(firstString)) { + if (!isArray(firstString) || !isArray(firstString.raw)) { // If chalk() was called by itself or with a string, // return the string itself as a string. return strings.join(' '); diff --git a/test/chalk.js b/test/chalk.js index 21f0346..4e78565 100644 --- a/test/chalk.js +++ b/test/chalk.js @@ -16,6 +16,14 @@ test('support multiple arguments in base function', t => { t.is(chalk('hello', 'there'), 'hello there'); }); +test('support automatic casting to string', t => { + t.is(chalk(['hello', 'there']), 'hello,there'); + t.is(chalk(123), '123'); + + t.is(chalk.bold(['foo', 'bar']), '\u001B[1mfoo,bar\u001B[22m'); + t.is(chalk.green(98765), '\u001B[32m98765\u001B[39m'); +}); + test('style string', t => { t.is(chalk.underline('foo'), '\u001B[4mfoo\u001B[24m'); t.is(chalk.red('foo'), '\u001B[31mfoo\u001B[39m'); diff --git a/test/template-literal.js b/test/template-literal.js index 98e01c6..2f75e03 100644 --- a/test/template-literal.js +++ b/test/template-literal.js @@ -30,6 +30,25 @@ test('correctly perform template substitutions', t => { instance.bold('Hello,', instance.cyan.inverse(name + '!'), 'This is a') + ' test. ' + instance.green(exclamation + '!')); }); +test('correctly perform nested template substitutions', t => { + const instance = new chalk.Instance({level: 0}); + const name = 'Sindre'; + const exclamation = 'Neat'; + t.is(instance.bold`Hello, {cyan.inverse ${name}!} This is a` + ' test. ' + instance.green`${exclamation}!`, + instance.bold('Hello,', instance.cyan.inverse(name + '!'), 'This is a') + ' test. ' + instance.green(exclamation + '!')); + + t.is(instance.red.bgGreen.bold`Hello {italic.blue ${name}}`, + instance.red.bgGreen.bold('Hello ' + instance.italic.blue(name))); + + t.is(instance.strikethrough.cyanBright.bgBlack`Works with {reset {bold numbers}} {bold.red ${1}}`, + instance.strikethrough.cyanBright.bgBlack('Works with ' + instance.reset.bold('numbers') + ' ' + instance.bold.red(1))); + + t.is(chalk.bold`Also works on the shared {bgBlue chalk} object`, + '\u001B[1mAlso works on the shared \u001B[1m' + + '\u001B[44mchalk\u001B[49m\u001B[22m' + + '\u001B[1m object\u001B[22m'); +}); + test('correctly parse and evaluate color-convert functions', t => { const instance = new chalk.Instance({level: 3}); t.is(instance`{bold.rgb(144,10,178).inverse Hello, {~inverse there!}}`,