Repository: chalk/chalk (js)

Issue:
Template literals are unsupported for nested calls
<!-- Issuehunt Badges -->
[<img alt="Issuehunt badges" src="https://img.shields.io/badge/IssueHunt-%2440%20Funded-%2300A156.svg" />](https://issuehunt.io/r/chalk/chalk/issues/341)
<!-- /Issuehunt Badges -->


The following is unsupported despite it being allowed by the type system:
```ts
chalk.bold`Hello, {cyan.inverse ${name}!} This is a test. {green ${exclamation}!}`;
```
Produces:
```
Hello, {cyan.inverse ,!} This is a test. {green ,!} Sindre Neat
```
Instead of:
```
Hello, Sindre! This is a test. Neat!
```


<!-- Issuehunt content -->

---

<details>
<summary>
<b>IssueHunt Summary</b>
</summary>


### Backers (Total: $40.00)

- [<img src='https://avatars3.githubusercontent.com/u/44827199?v=4' alt='issuehunt' width=24 height=24> issuehunt](https://issuehunt.io/u/issuehunt) ($40.00)

### Submitted pull Requests
- [#392 Support for template literals for nested calls](https://issuehunt.io/r/chalk/chalk/pull/392)
- [#398 Support nested calls](https://issuehunt.io/r/chalk/chalk/pull/398)
---

#### [Become a backer now!](https://issuehunt.io/r/chalk/chalk/issues/341)
#### [Or submit a pull request to get the deposits!](https://issuehunt.io/r/chalk/chalk/issues/341)
### Tips

- Checkout the [Issuehunt explorer](https://issuehunt.io/r/chalk/chalk/) to discover more funded issues.
- Need some help from other developers? [Add your repositories](https://issuehunt.io/r/new) on IssueHunt to raise funds.
</details>
<!-- /Issuehunt content-->

Pull request:
Support template literals for nested calls
Fixes #341
Closes #398 
Closes #380

Example:
```
chalk.red`2 + 3 = {bold ${2+3}}`
```


<!-- Issuehunt content -->

---

<details>
<summary>
<b>IssueHunt Summary</b>
</summary>

### Referenced issues

This pull request has been submitted to:
- [#341: Template literals are unsupported for nested calls](https://issuehunt.io/repos/11855195/issues/341)
---

IssueHunt has been backed by the following sponsors. [Become a sponsor](https://issuehunt.io/membership/members)
</details>
<!-- /Issuehunt content-->

Patch:
diff --git a/.travis.yml b/.travis.yml
index 1492647..fd301d7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,6 +2,5 @@ language: node_js
 node_js:
   - '12'
   - '10'
-  - '8'
 after_success:
   - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'
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/examples/screenshot.js b/examples/screenshot.js
index 37f5850..fc64030 100644
--- a/examples/screenshot.js
+++ b/examples/screenshot.js
@@ -4,15 +4,15 @@ const chalk = require('..');
 
 // Generates screenshot
 for (const key of Object.keys(styles)) {
-	let ret = key;
+	let returnValue = key;
 
 	if (key === 'reset' || key === 'hidden' || key === 'grey') {
 		continue;
 	}
 
 	if (/^bg[^B]/.test(key)) {
-		ret = chalk.black(ret);
+		returnValue = chalk.black(returnValue);
 	}
 
-	process.stdout.write(chalk[key](ret) + ' ');
+	process.stdout.write(chalk[key](returnValue) + ' ');
 }
diff --git a/index.d.ts b/index.d.ts
index 7e22c45..9cd88f3 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,25 +1,3 @@
-declare const enum LevelEnum {
-	/**
-	All colors disabled.
-	*/
-	None = 0,
-
-	/**
-	Basic 16 colors support.
-	*/
-	Basic = 1,
-
-	/**
-	ANSI 256 colors support.
-	*/
-	Ansi256 = 2,
-
-	/**
-	Truecolor 16 million colors support.
-	*/
-	TrueColor = 3
-}
-
 /**
 Basic foreground colors.
 
@@ -89,22 +67,34 @@ declare type Modifiers =
 	| 'visible';
 
 declare namespace chalk {
-	type Level = LevelEnum;
+	/**
+	Levels:
+	- `0` - All colors disabled.
+	- `1` - Basic 16 colors support.
+	- `2` - ANSI 256 colors support.
+	- `3` - Truecolor 16 million colors support.
+	*/
+	type Level = 0 | 1 | 2 | 3;
 
 	interface Options {
 		/**
 		Specify the color support for Chalk.
+
 		By default, color support is automatically detected based on the environment.
+
+		Levels:
+		- `0` - All colors disabled.
+		- `1` - Basic 16 colors support.
+		- `2` - ANSI 256 colors support.
+		- `3` - Truecolor 16 million colors support.
 		*/
 		level?: Level;
 	}
 
-	interface Instance {
-		/**
-		Return a new Chalk instance.
-		*/
-		new (options?: Options): Chalk;
-	}
+	/**
+	Return a new Chalk instance.
+	*/
+	type Instance = new (options?: Options) => Chalk;
 
 	/**
 	Detect whether the terminal supports color.
@@ -147,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;
 
@@ -161,7 +158,14 @@ declare namespace chalk {
 
 		/**
 		The color support for Chalk.
+
 		By default, color support is automatically detected based on the environment.
+
+		Levels:
+		- `0` - All colors disabled.
+		- `1` - Basic 16 colors support.
+		- `2` - ANSI 256 colors support.
+		- `3` - Truecolor 16 million colors support.
 		*/
 		level: Level;
 
@@ -400,7 +404,7 @@ This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
 */
 declare const chalk: chalk.Chalk & chalk.ChalkFunction & {
 	supportsColor: chalk.ColorSupport | false;
-	Level: typeof LevelEnum;
+	Level: chalk.Level;
 	Color: Color;
 	ForegroundColor: ForegroundColor;
 	BackgroundColor: BackgroundColor;
diff --git a/package.json b/package.json
index 0a4aaa9..6231a2c 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,13 @@
 {
 	"name": "chalk",
-	"version": "3.0.0",
+	"version": "4.0.0",
 	"description": "Terminal string styling done right",
 	"license": "MIT",
 	"repository": "chalk/chalk",
 	"funding": "https://github.com/chalk/chalk?sponsor=1",
 	"main": "source",
 	"engines": {
-		"node": ">=8"
+		"node": ">=10"
 	},
 	"scripts": {
 		"test": "xo && nyc ava && tsd",
@@ -47,18 +47,22 @@
 	"devDependencies": {
 		"ava": "^2.4.0",
 		"coveralls": "^3.0.7",
-		"execa": "^3.2.0",
+		"execa": "^4.0.0",
 		"import-fresh": "^3.1.0",
 		"matcha": "^0.7.0",
 		"nyc": "^15.0.0",
 		"resolve-from": "^5.0.0",
 		"tsd": "^0.7.4",
-		"xo": "^0.25.3"
+		"xo": "^0.28.2"
 	},
 	"xo": {
 		"rules": {
 			"unicorn/prefer-string-slice": "off",
-			"unicorn/prefer-includes": "off"
+			"unicorn/prefer-includes": "off",
+			"@typescript-eslint/member-ordering": "off",
+			"no-redeclare": "off",
+			"unicorn/string-content": "off",
+			"unicorn/better-regex": "off"
 		}
 	}
 }
diff --git a/readme.md b/readme.md
index a0ca245..338f42c 100644
--- a/readme.md
+++ b/readme.md
@@ -9,7 +9,7 @@
 
 > Terminal string styling done right
 
-[![Build Status](https://travis-ci.org/chalk/chalk.svg?branch=master)](https://travis-ci.org/chalk/chalk) [![Coverage Status](https://coveralls.io/repos/github/chalk/chalk/badge.svg?branch=master)](https://coveralls.io/github/chalk/chalk?branch=master) [![npm dependents](https://badgen.net/npm/dependents/chalk)](https://www.npmjs.com/package/chalk?activeTab=dependents) [![Downloads](https://badgen.net/npm/dt/chalk)](https://www.npmjs.com/package/chalk) [![](https://img.shields.io/badge/unicorn-approved-ff69b4.svg)](https://www.youtube.com/watch?v=9auOCbH5Ns4) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) ![TypeScript-ready](https://img.shields.io/npm/types/chalk.svg) [![run on repl.it](http://repl.it/badge/github/chalk/chalk)](https://repl.it/github/chalk/chalk)
+[![Build Status](https://travis-ci.org/chalk/chalk.svg?branch=master)](https://travis-ci.org/chalk/chalk) [![Coverage Status](https://coveralls.io/repos/github/chalk/chalk/badge.svg?branch=master)](https://coveralls.io/github/chalk/chalk?branch=master) [![npm dependents](https://badgen.net/npm/dependents/chalk)](https://www.npmjs.com/package/chalk?activeTab=dependents) [![Downloads](https://badgen.net/npm/dt/chalk)](https://www.npmjs.com/package/chalk) [![](https://img.shields.io/badge/unicorn-approved-ff69b4.svg)](https://www.youtube.com/watch?v=9auOCbH5Ns4) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) ![TypeScript-ready](https://img.shields.io/npm/types/chalk.svg) [![run on repl.it](https://repl.it/badge/github/chalk/chalk)](https://repl.it/github/chalk/chalk)
 
 <img src="https://cdn.jsdelivr.net/gh/chalk/ansi-styles@8261697c95bf34b6c7767e2cbe9941a851d59385/screenshot.svg" width="900">
 
@@ -199,7 +199,7 @@ Explicit 256/Truecolor mode can be enabled using the `--color=256` and `--color=
 
 ## Tagged template literal
 
-Chalk can be used as a [tagged template literal](http://exploringjs.com/es6/ch_template-literals.html#_tagged-template-literals).
+Chalk can be used as a [tagged template literal](https://exploringjs.com/es6/ch_template-literals.html#_tagged-template-literals).
 
 ```js
 const chalk = require('chalk');
@@ -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 53fcc16..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',
@@ -17,7 +19,7 @@ const levelMapping = [
 const styles = Object.create(null);
 
 const applyOptions = (object, options = {}) => {
-	if (options.level > 3 || options.level < 0) {
+	if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
 		throw new Error('The `level` option should be an integer from 0 to 3');
 	}
 
@@ -28,6 +30,7 @@ const applyOptions = (object, options = {}) => {
 
 class ChalkClass {
 	constructor(options) {
+		// eslint-disable-next-line no-constructor-return
 		return chalkFactory(options);
 	}
 }
@@ -134,14 +137,19 @@ 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(' '));
 	};
 
-	// `__proto__` is used because we must return a function, but there is
+	// We alter the prototype because we must return a function, but there is
 	// no way to create a function with a different prototype
-	builder.__proto__ = proto; // eslint-disable-line no-proto
+	Object.setPrototypeOf(builder, proto);
 
 	builder._generator = self;
 	builder._styler = _styler;
@@ -188,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(' ');
@@ -218,16 +226,4 @@ chalk.supportsColor = stdoutColor;
 chalk.stderr = Chalk({level: stderrColor ? stderrColor.level : 0}); // eslint-disable-line new-cap
 chalk.stderr.supportsColor = stderrColor;
 
-// For TypeScript
-chalk.Level = {
-	None: 0,
-	Basic: 1,
-	Ansi256: 2,
-	TrueColor: 3,
-	0: 'None',
-	1: 'Basic',
-	2: 'Ansi256',
-	3: 'TrueColor'
-};
-
 module.exports = chalk;
diff --git a/source/templates.js b/source/templates.js
index fe94642..b130949 100644
--- a/source/templates.js
+++ b/source/templates.js
@@ -2,7 +2,7 @@
 const TEMPLATE_REGEX = /(?:\\(u(?:[a-f\d]{4}|\{[a-f\d]{1,6}\})|x[a-f\d]{2}|.))|(?:\{(~)?(\w+(?:\([^)]*\))?(?:\.\w+(?:\([^)]*\))?)*)(?:[ \t]|(?=\r?\n)))|(\})|((?:.|[\r\n\f])+?)/gi;
 const STYLE_REGEX = /(?:^|\.)(\w+)(?:\(([^)]*)\))?/g;
 const STRING_REGEX = /^(['"])((?:\\.|(?!\1)[^\\])*)\1$/;
-const ESCAPE_REGEX = /\\(u(?:[a-f\d]{4}|\{[a-f\d]{1,6}\})|x[a-f\d]{2}|.)|([^\\])/gi;
+const ESCAPE_REGEX = /\\(u(?:[a-f\d]{4}|{[a-f\d]{1,6}})|x[a-f\d]{2}|.)|([^\\])/gi;
 
 const ESCAPES = new Map([
 	['n', '\n'],
@@ -126,8 +126,8 @@ module.exports = (chalk, temporary) => {
 	chunks.push(chunk.join(''));
 
 	if (styles.length > 0) {
-		const errMsg = `Chalk template literal is missing ${styles.length} closing bracket${styles.length === 1 ? '' : 's'} (\`}\`)`;
-		throw new Error(errMsg);
+		const errMessage = `Chalk template literal is missing ${styles.length} closing bracket${styles.length === 1 ? '' : 's'} (\`}\`)`;
+		throw new Error(errMessage);
 	}
 
 	return chunks.join('');


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 177d6de..aa9e2f5 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -4,24 +4,22 @@ import chalk = require('.');
 // - Helpers -
 type colorReturn = chalk.Chalk & {supportsColor?: never};
 
-// - Level -
-expectType<number>(chalk.Level.None);
-expectType<number>(chalk.Level.Basic);
-expectType<number>(chalk.Level.Ansi256);
-expectType<number>(chalk.Level.TrueColor);
-
 // - supportsColor -
 expectType<chalk.ColorSupport | false>(chalk.supportsColor);
-expectType<boolean>((chalk.supportsColor as chalk.ColorSupport).hasBasic);
-expectType<boolean>((chalk.supportsColor as chalk.ColorSupport).has256);
-expectType<boolean>((chalk.supportsColor as chalk.ColorSupport).has16m);
+if (chalk.supportsColor) {
+	expectType<boolean>(chalk.supportsColor.hasBasic);
+	expectType<boolean>(chalk.supportsColor.has256);
+	expectType<boolean>(chalk.supportsColor.has16m);
+}
 
 // - stderr -
 expectType<chalk.Chalk>(chalk.stderr);
 expectType<chalk.ColorSupport | false>(chalk.stderr.supportsColor);
-expectType<boolean>((chalk.stderr.supportsColor as chalk.ColorSupport).hasBasic);
-expectType<boolean>((chalk.stderr.supportsColor as chalk.ColorSupport).has256);
-expectType<boolean>((chalk.stderr.supportsColor as chalk.ColorSupport).has16m);
+if (chalk.stderr.supportsColor) {
+	expectType<boolean>(chalk.stderr.supportsColor.hasBasic);
+	expectType<boolean>(chalk.stderr.supportsColor.has256);
+	expectType<boolean>(chalk.stderr.supportsColor.has16m);
+}
 
 // -- `stderr` is not a member of the Chalk interface --
 expectError(chalk.reset.stderr);
@@ -154,6 +152,11 @@ expectType<string>(chalk.bgWhiteBright`foo`);
 expectType<string>(chalk.red.bgGreen.underline('foo'));
 expectType<string>(chalk.underline.red.bgGreen('foo'));
 
+// -- Complex template literal --
+expectType<string>(chalk.underline``);
+expectType<string>(chalk.red.bgGreen.bold`Hello {italic.blue ${name}}`);
+expectType<string>(chalk.strikethrough.cyanBright.bgBlack`Works with {reset {bold numbers}} {bold.red ${1}}`);
+
 // -- Color types ==
 expectType<typeof chalk.Color>('red');
 expectError<typeof chalk.Color>('hotpink');
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/level.js b/test/level.js
index 4fe8fae..65d4720 100644
--- a/test/level.js
+++ b/test/level.js
@@ -43,15 +43,3 @@ test('disable colors if they are not supported', async t => {
 	const {stdout} = await execa.node(path.join(__dirname, '_fixture'));
 	t.is(stdout, 'testout testerr');
 });
-
-test('chalk.Level enum object', t => {
-	const {Level} = chalk;
-	t.is(Level.None, 0);
-	t.is(Level.Basic, 1);
-	t.is(Level.Ansi256, 2);
-	t.is(Level.TrueColor, 3);
-	t.is(Level[Level.None], 'None');
-	t.is(Level[Level.Basic], 'Basic');
-	t.is(Level[Level.Ansi256], 'Ansi256');
-	t.is(Level[Level.TrueColor], 'TrueColor');
-});
diff --git a/test/template-literal.js b/test/template-literal.js
index ece1428..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!}}`,
@@ -121,8 +140,8 @@ test('correctly parse newline escapes (bug #177)', t => {
 
 test('correctly parse escape in parameters (bug #177 comment 318622809)', t => {
 	const instance = new chalk.Instance({level: 0});
-	const str = '\\';
-	t.is(instance`{blue ${str}}`, '\\');
+	const string = '\\';
+	t.is(instance`{blue ${string}}`, '\\');
 });
 
 test('correctly parses unicode/hex escapes', t => {