Add tagged template literal (#163)
This commit is contained in:
parent
23ef1c7ca2
commit
f66271e01a
5 changed files with 359 additions and 7 deletions
56
index.js
56
index.js
|
|
@ -3,6 +3,8 @@ const escapeStringRegexp = require('escape-string-regexp');
|
||||||
const ansiStyles = require('ansi-styles');
|
const ansiStyles = require('ansi-styles');
|
||||||
const supportsColor = require('supports-color');
|
const supportsColor = require('supports-color');
|
||||||
|
|
||||||
|
const template = require('./templates.js');
|
||||||
|
|
||||||
const isSimpleWindowsTerm = process.platform === 'win32' && !process.env.TERM.toLowerCase().startsWith('xterm');
|
const isSimpleWindowsTerm = process.platform === 'win32' && !process.env.TERM.toLowerCase().startsWith('xterm');
|
||||||
|
|
||||||
// `supportsColor.level` → `ansiStyles.color[name]` mapping
|
// `supportsColor.level` → `ansiStyles.color[name]` mapping
|
||||||
|
|
@ -11,10 +13,37 @@ const levelMapping = ['ansi', 'ansi', 'ansi256', 'ansi16m'];
|
||||||
// `color-convert` models to exclude from the Chalk API due to conflicts and such
|
// `color-convert` models to exclude from the Chalk API due to conflicts and such
|
||||||
const skipModels = new Set(['gray']);
|
const skipModels = new Set(['gray']);
|
||||||
|
|
||||||
function Chalk(options) {
|
const styles = Object.create(null);
|
||||||
|
|
||||||
|
function applyOptions(obj, options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
// Detect level if not set manually
|
// Detect level if not set manually
|
||||||
this.level = Number(!options || options.level === undefined ? supportsColor.level : options.level);
|
obj.level = options.level === undefined ? supportsColor.level : options.level;
|
||||||
this.enabled = options && 'enabled' in options ? options.enabled : this.level > 0;
|
obj.enabled = 'enabled' in options ? options.enabled : obj.level > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Chalk(options) {
|
||||||
|
// We check for this.template here since calling chalk.constructor()
|
||||||
|
// by itself will have a `this` of a previously constructed chalk object.
|
||||||
|
if (!this || !(this instanceof Chalk) || this.template) {
|
||||||
|
const chalk = {};
|
||||||
|
applyOptions(chalk, options);
|
||||||
|
|
||||||
|
chalk.template = function () {
|
||||||
|
const args = [].slice.call(arguments);
|
||||||
|
return chalkTag.apply(null, [chalk.template].concat(args));
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.setPrototypeOf(chalk, Chalk.prototype);
|
||||||
|
Object.setPrototypeOf(chalk.template, chalk);
|
||||||
|
|
||||||
|
chalk.template.constructor = Chalk;
|
||||||
|
|
||||||
|
return chalk.template;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyOptions(this, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use bright blue on Windows as the normal blue color is illegible
|
// Use bright blue on Windows as the normal blue color is illegible
|
||||||
|
|
@ -22,8 +51,6 @@ if (isSimpleWindowsTerm) {
|
||||||
ansiStyles.blue.open = '\u001B[94m';
|
ansiStyles.blue.open = '\u001B[94m';
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = Object.create(null);
|
|
||||||
|
|
||||||
for (const key of Object.keys(ansiStyles)) {
|
for (const key of Object.keys(ansiStyles)) {
|
||||||
ansiStyles[key].closeRe = new RegExp(escapeStringRegexp(ansiStyles[key].close), 'g');
|
ansiStyles[key].closeRe = new RegExp(escapeStringRegexp(ansiStyles[key].close), 'g');
|
||||||
|
|
||||||
|
|
@ -164,7 +191,24 @@ function applyStyle() {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chalkTag(chalk, strings) {
|
||||||
|
const args = [].slice.call(arguments, 2);
|
||||||
|
|
||||||
|
if (!Array.isArray(strings)) {
|
||||||
|
return strings.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [strings.raw[0]];
|
||||||
|
|
||||||
|
for (let i = 1; i < strings.length; i++) {
|
||||||
|
parts.push(args[i - 1].toString().replace(/[{}]/g, '\\$&'));
|
||||||
|
parts.push(strings.raw[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return template(chalk, parts.join(''));
|
||||||
|
}
|
||||||
|
|
||||||
Object.defineProperties(Chalk.prototype, styles);
|
Object.defineProperties(Chalk.prototype, styles);
|
||||||
|
|
||||||
module.exports = new Chalk();
|
module.exports = Chalk(); // eslint-disable-line new-cap
|
||||||
module.exports.supportsColor = supportsColor;
|
module.exports.supportsColor = supportsColor;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
"coveralls": "nyc report --reporter=text-lcov | coveralls"
|
"coveralls": "nyc report --reporter=text-lcov | coveralls"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"index.js"
|
"index.js",
|
||||||
|
"templates.js"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"color",
|
"color",
|
||||||
|
|
|
||||||
37
readme.md
37
readme.md
|
|
@ -78,6 +78,13 @@ RAM: ${chalk.green('40%')}
|
||||||
DISK: ${chalk.yellow('70%')}
|
DISK: ${chalk.yellow('70%')}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// ES2015 tagged template literal
|
||||||
|
log(chalk`
|
||||||
|
CPU: {red ${cpu.totalPercent}%}
|
||||||
|
RAM: {green ${ram.used / ram.total * 100}%}
|
||||||
|
DISK: {rgb(255,131,0) ${disk.used / disk.total * 100}%}
|
||||||
|
`);
|
||||||
|
|
||||||
// Use RGB colors in terminal emulators that support it.
|
// Use RGB colors in terminal emulators that support it.
|
||||||
log(chalk.keyword('orange')('Yay for orange colored text!'));
|
log(chalk.keyword('orange')('Yay for orange colored text!'));
|
||||||
log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
|
log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
|
||||||
|
|
@ -206,6 +213,36 @@ Explicit 256/Truecolor mode can be enabled using the `--color=256` and `--color=
|
||||||
- `bgWhiteBright`
|
- `bgWhiteBright`
|
||||||
|
|
||||||
|
|
||||||
|
## Tagged template literal
|
||||||
|
|
||||||
|
Chalk can be used as a [tagged template literal](http://exploringjs.com/es6/ch_template-literals.html#_tagged-template-literals).
|
||||||
|
|
||||||
|
```js
|
||||||
|
const chalk = require('chalk');
|
||||||
|
|
||||||
|
const miles = 18;
|
||||||
|
const calculateFeet = miles => miles * 5280;
|
||||||
|
|
||||||
|
console.log(chalk`
|
||||||
|
There are {bold 5280 feet} in a mile.
|
||||||
|
In {bold ${miles} miles}, there are {green.bold ${calculateFeet(miles)} feet}.
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
console.log(chalk.bold.rgb(10, 100, 200)('Hello!'));
|
||||||
|
console.log(chalk`{bold.rgb(10,100,200) Hello!}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that function styles (`rgb()`, `hsl()`, `keyword()`, etc.) may not contain spaces between parameters.
|
||||||
|
|
||||||
|
All interpolated values (`` chalk`${foo}` ``) are converted to strings via the `.toString()` method. All curly braces (`{` and `}`) in interpolated value strings are escaped.
|
||||||
|
|
||||||
|
|
||||||
## 256 and Truecolor color support
|
## 256 and Truecolor color support
|
||||||
|
|
||||||
Chalk supports 256 colors and [Truecolor](https://gist.github.com/XVilka/8346728) (16 million colors) on supported terminal apps.
|
Chalk supports 256 colors and [Truecolor](https://gist.github.com/XVilka/8346728) (16 million colors) on supported terminal apps.
|
||||||
|
|
|
||||||
175
templates.js
Normal file
175
templates.js
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function data(parent) {
|
||||||
|
return {
|
||||||
|
styles: [],
|
||||||
|
parent,
|
||||||
|
contents: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const zeroBound = n => n < 0 ? 0 : n;
|
||||||
|
const lastIndex = a => zeroBound(a.length - 1);
|
||||||
|
|
||||||
|
const last = a => a[lastIndex(a)];
|
||||||
|
|
||||||
|
const takeWhileReverse = (array, predicate, start) => {
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
for (let i = start; i >= 0 && i <= start; i--) {
|
||||||
|
if (predicate(array[i])) {
|
||||||
|
out.unshift(array[i]);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the character at position i in string is a normal character a.k.a a non control character.
|
||||||
|
* */
|
||||||
|
const isNormalCharacter = (string, i) => {
|
||||||
|
const char = string[i];
|
||||||
|
const backslash = '\\';
|
||||||
|
|
||||||
|
if (!(char === backslash || char === '{' || char === '}')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = i === 0 ? 0 : takeWhileReverse(string, x => x === '\\', zeroBound(i - 1)).length;
|
||||||
|
|
||||||
|
return n % 2 === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectStyles = data => data ? collectStyles(data.parent).concat(data.styles) : ['reset'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the style for a given data based on it's style and the style of it's parent. Also accounts for !style styles
|
||||||
|
* which remove a style from the list if present.
|
||||||
|
* */
|
||||||
|
const sumStyles = data => {
|
||||||
|
const negateRegex = /^~.+/;
|
||||||
|
let out = [];
|
||||||
|
|
||||||
|
for (const style of collectStyles(data)) {
|
||||||
|
if (negateRegex.test(style)) {
|
||||||
|
const exclude = style.slice(1);
|
||||||
|
out = out.filter(x => x !== exclude);
|
||||||
|
} else {
|
||||||
|
out.push(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a string and parses it into a tree of data objects which inherit styles from their parent.
|
||||||
|
* */
|
||||||
|
function parse(string) {
|
||||||
|
const root = data(null);
|
||||||
|
let pushingStyle = false;
|
||||||
|
|
||||||
|
let current = root;
|
||||||
|
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
const char = string[i];
|
||||||
|
|
||||||
|
const addNormalCharacter = () => {
|
||||||
|
const lastChunk = last(current.contents);
|
||||||
|
|
||||||
|
if (typeof lastChunk === 'string') {
|
||||||
|
current.contents[lastIndex(current.contents)] = lastChunk + char;
|
||||||
|
} else {
|
||||||
|
current.contents.push(char);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pushingStyle) {
|
||||||
|
if (' \t'.indexOf(char) > -1) {
|
||||||
|
pushingStyle = false;
|
||||||
|
} else if (char === '\n') {
|
||||||
|
pushingStyle = false;
|
||||||
|
addNormalCharacter();
|
||||||
|
} else if (char === '.') {
|
||||||
|
current.styles.push('');
|
||||||
|
} else {
|
||||||
|
current.styles[lastIndex(current.styles)] = (last(current.styles) || '') + char;
|
||||||
|
}
|
||||||
|
} else if (isNormalCharacter(string, i)) {
|
||||||
|
addNormalCharacter();
|
||||||
|
} else if (char === '{') {
|
||||||
|
pushingStyle = true;
|
||||||
|
const nCurrent = data(current);
|
||||||
|
current.contents.push(nCurrent);
|
||||||
|
current = nCurrent;
|
||||||
|
} else if (char === '}') {
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current !== root) {
|
||||||
|
throw new Error('literal template has an unclosed block');
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a tree of data objects and flattens it to a list of data objects with the inherited and negations styles
|
||||||
|
* accounted for.
|
||||||
|
* */
|
||||||
|
function flatten(data) {
|
||||||
|
let flat = [];
|
||||||
|
|
||||||
|
for (const content of data.contents) {
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
flat.push({
|
||||||
|
styles: sumStyles(data),
|
||||||
|
content
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
flat = flat.concat(flatten(content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertStyle(chalk, style) {
|
||||||
|
if (!chalk[style]) {
|
||||||
|
throw new Error(`invalid Chalk style: ${style}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given style is valid and parses style functions.
|
||||||
|
* */
|
||||||
|
function parseStyle(chalk, style) {
|
||||||
|
const fnMatch = style.match(/^\s*(\w+)\s*\(\s*([^)]*)\s*\)\s*/);
|
||||||
|
if (!fnMatch) {
|
||||||
|
assertStyle(chalk, style);
|
||||||
|
return chalk[style];
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = fnMatch[1].trim();
|
||||||
|
const args = fnMatch[2].split(/,/g).map(s => s.trim());
|
||||||
|
|
||||||
|
assertStyle(chalk, name);
|
||||||
|
|
||||||
|
return chalk[name].apply(chalk, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the actual styling of the string, essentially lifted from cli.js.
|
||||||
|
* */
|
||||||
|
function style(chalk, flat) {
|
||||||
|
return flat.map(data => {
|
||||||
|
const fn = data.styles.reduce(parseStyle, chalk);
|
||||||
|
return fn(data.content.replace(/\n$/, ''));
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (chalk, string) => style(chalk, flatten(parse(string)));
|
||||||
95
test.js
95
test.js
|
|
@ -19,6 +19,10 @@ console.log('host TERM=', process.env.TERM || '[none]');
|
||||||
console.log('host platform=', process.platform || '[unknown]');
|
console.log('host platform=', process.platform || '[unknown]');
|
||||||
|
|
||||||
describe('chalk', () => {
|
describe('chalk', () => {
|
||||||
|
it('should not add any styling when called as the base function', () => {
|
||||||
|
assert.equal(chalk('foo'), 'foo');
|
||||||
|
});
|
||||||
|
|
||||||
it('should style string', () => {
|
it('should style string', () => {
|
||||||
assert.equal(chalk.underline('foo'), '\u001B[4mfoo\u001B[24m');
|
assert.equal(chalk.underline('foo'), '\u001B[4mfoo\u001B[24m');
|
||||||
assert.equal(chalk.red('foo'), '\u001B[31mfoo\u001B[39m');
|
assert.equal(chalk.red('foo'), '\u001B[31mfoo\u001B[39m');
|
||||||
|
|
@ -242,3 +246,94 @@ describe('chalk.constructor', () => {
|
||||||
assert.equal(ctx.red('foo'), '\u001B[31mfoo\u001B[39m');
|
assert.equal(ctx.red('foo'), '\u001B[31mfoo\u001B[39m');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('tagged template literal', () => {
|
||||||
|
it('should return an empty string for an empty literal', () => {
|
||||||
|
const ctx = chalk.constructor();
|
||||||
|
assert.equal(ctx``, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a regular string for a literal with no templates', () => {
|
||||||
|
const ctx = chalk.constructor({level: 0});
|
||||||
|
assert.equal(ctx`hello`, 'hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly perform template parsing', () => {
|
||||||
|
const ctx = chalk.constructor({level: 0});
|
||||||
|
assert.equal(ctx`{bold Hello, {cyan World!} This is a} test. {green Woo!}`,
|
||||||
|
ctx.bold('Hello,', ctx.cyan('World!'), 'This is a') + ' test. ' + ctx.green('Woo!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly perform template substitutions', () => {
|
||||||
|
const ctx = chalk.constructor({level: 0});
|
||||||
|
const name = 'Sindre';
|
||||||
|
const exclamation = 'Neat';
|
||||||
|
assert.equal(ctx`{bold Hello, {cyan.inverse ${name}!} This is a} test. {green ${exclamation}!}`,
|
||||||
|
ctx.bold('Hello,', ctx.cyan.inverse(name + '!'), 'This is a') + ' test. ' + ctx.green(exclamation + '!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly parse and evaluate color-convert functions', () => {
|
||||||
|
const ctx = chalk.constructor({level: 3});
|
||||||
|
assert.equal(ctx`{bold.rgb(144,10,178).inverse Hello, {~inverse there!}}`,
|
||||||
|
'\u001B[0m\u001B[1m\u001B[38;2;144;10;178m\u001B[7mHello, ' +
|
||||||
|
'\u001B[27m\u001B[39m\u001B[22m\u001B[0m\u001B[0m\u001B[1m' +
|
||||||
|
'\u001B[38;2;144;10;178mthere!\u001B[39m\u001B[22m\u001B[0m');
|
||||||
|
|
||||||
|
assert.equal(ctx`{bold.bgRgb(144,10,178).inverse Hello, {~inverse there!}}`,
|
||||||
|
'\u001B[0m\u001B[1m\u001B[48;2;144;10;178m\u001B[7mHello, ' +
|
||||||
|
'\u001B[27m\u001B[49m\u001B[22m\u001B[0m\u001B[0m\u001B[1m' +
|
||||||
|
'\u001B[48;2;144;10;178mthere!\u001B[49m\u001B[22m\u001B[0m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly handle escapes', () => {
|
||||||
|
const ctx = chalk.constructor({level: 3});
|
||||||
|
assert.equal(ctx`{bold hello \{in brackets\}}`,
|
||||||
|
'\u001B[0m\u001B[1mhello {in brackets}\u001B[22m\u001B[0m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if there is an unclosed block', () => {
|
||||||
|
const ctx = chalk.constructor({level: 3});
|
||||||
|
try {
|
||||||
|
console.log(ctx`{bold this shouldn't appear ever\}`);
|
||||||
|
assert.fail();
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.message, 'literal template has an unclosed block');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if there is an invalid style', () => {
|
||||||
|
const ctx = chalk.constructor({level: 3});
|
||||||
|
try {
|
||||||
|
console.log(ctx`{abadstylethatdoesntexist this shouldn't appear ever}`);
|
||||||
|
assert.fail();
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.message, 'invalid Chalk style: abadstylethatdoesntexist');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly style multiline color blocks', () => {
|
||||||
|
const ctx = chalk.constructor({level: 3});
|
||||||
|
assert.equal(
|
||||||
|
ctx`{bold
|
||||||
|
Hello! This is a
|
||||||
|
${'multiline'} block!
|
||||||
|
:)
|
||||||
|
} {underline
|
||||||
|
I hope you enjoy
|
||||||
|
}`,
|
||||||
|
'\u001B[0m\u001B[1m\u001B[22m\u001B[0m\n' +
|
||||||
|
'\u001B[0m\u001B[1m\t\t\t\tHello! This is a\u001B[22m\u001B[0m\n' +
|
||||||
|
'\u001B[0m\u001B[1m\t\t\t\tmultiline block!\u001B[22m\u001B[0m\n' +
|
||||||
|
'\u001B[0m\u001B[1m\t\t\t\t:)\u001B[22m\u001B[0m\n' +
|
||||||
|
'\u001B[0m\u001B[1m\t\t\t\u001B[22m\u001B[0m\u001B[0m \u001B[0m\u001B[0m\u001B[4m\u001B[24m\u001B[0m\n' +
|
||||||
|
'\u001B[0m\u001B[4m\t\t\t\tI hope you enjoy\u001B[24m\u001B[0m\n' +
|
||||||
|
'\u001B[0m\u001B[4m\t\t\t\u001B[24m\u001B[0m'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape interpolated values', () => {
|
||||||
|
const ctx = chalk.constructor({level: 0});
|
||||||
|
assert.equal(ctx`Hello {bold hi}`, 'Hello hi');
|
||||||
|
assert.equal(ctx`Hello ${'{bold hi}'}`, 'Hello {bold hi}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue