Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support template literals for nested calls #392

Merged
merged 5 commits into from Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions benchmark.js
Expand Up @@ -47,4 +47,16 @@ suite('chalk', () => {
bench('cached: 1 style nested non-intersecting', () => {
chalkBgRed(blueStyledString);
});

set('iterations', 10000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why manually set iterations here? This can skew results if too low.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing strings for the special {bold ...}-syntax is a lot slower.
Benchmark 'cached: 1 style' is almost 200 times faster than 'cached: 1 style template literal'. So I reduced the number of iterations accordingly.

Better than specifying the number of iterations would be to set a minimal runtime for each benchmark. So instead of

set('iterations', 1000000);

something like

set('mintime', 500); 

But this feels out of the scope of this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better than specifying the number of iterations would be to set a minimal runtime for each benchmark.

I agree. Mind opening a PR? :)


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`;
});
});
7 changes: 7 additions & 0 deletions index.d.ts
Expand Up @@ -147,6 +147,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;

Expand Down
5 changes: 5 additions & 0 deletions index.test-d.ts
Expand Up @@ -154,6 +154,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');
3 changes: 2 additions & 1 deletion readme.md
Expand Up @@ -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!}`);
```

Expand Down
9 changes: 8 additions & 1 deletion source/index.js
Expand Up @@ -6,6 +6,8 @@ const {
stringEncaseCRLFWithFirstIndex
} = require('./util');

const {isArray} = Array;

// `supportsColor.level` → `ansiStyles.color[name]` mapping
const levelMapping = [
'ansi',
Expand Down Expand Up @@ -134,6 +136,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(' '));
Expand Down Expand Up @@ -188,7 +195,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(' ');
Expand Down
8 changes: 8 additions & 0 deletions test/chalk.js
Expand Up @@ -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');
Expand Down
19 changes: 19 additions & 0 deletions test/template-literal.js
Expand Up @@ -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');
});
toonijn marked this conversation as resolved.
Show resolved Hide resolved
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved

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!}}`,
Expand Down