Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add tagged template literal (#163)
  • Loading branch information
Qix- authored and sindresorhus committed Jun 29, 2017
1 parent 23ef1c7 commit f66271e
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 7 deletions.
56 changes: 50 additions & 6 deletions index.js
Expand Up @@ -3,6 +3,8 @@ const escapeStringRegexp = require('escape-string-regexp');
const ansiStyles = require('ansi-styles');
const supportsColor = require('supports-color');

const template = require('./templates.js');

const isSimpleWindowsTerm = process.platform === 'win32' && !process.env.TERM.toLowerCase().startsWith('xterm');

// `supportsColor.level` → `ansiStyles.color[name]` mapping
Expand All @@ -11,19 +13,44 @@ const levelMapping = ['ansi', 'ansi', 'ansi256', 'ansi16m'];
// `color-convert` models to exclude from the Chalk API due to conflicts and such
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
this.level = Number(!options || options.level === undefined ? supportsColor.level : options.level);
this.enabled = options && 'enabled' in options ? options.enabled : this.level > 0;
obj.level = options.level === undefined ? supportsColor.level : options.level;
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
if (isSimpleWindowsTerm) {
ansiStyles.blue.open = '\u001B[94m';
}

const styles = Object.create(null);

for (const key of Object.keys(ansiStyles)) {
ansiStyles[key].closeRe = new RegExp(escapeStringRegexp(ansiStyles[key].close), 'g');

Expand Down Expand Up @@ -164,7 +191,24 @@ function applyStyle() {
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);

module.exports = new Chalk();
module.exports = Chalk(); // eslint-disable-line new-cap
module.exports.supportsColor = supportsColor;
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -13,7 +13,8 @@
"coveralls": "nyc report --reporter=text-lcov | coveralls"
},
"files": [
"index.js"
"index.js",
"templates.js"
],
"keywords": [
"color",
Expand Down
37 changes: 37 additions & 0 deletions readme.md
Expand Up @@ -78,6 +78,13 @@ RAM: ${chalk.green('40%')}
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.
log(chalk.keyword('orange')('Yay for orange colored text!'));
log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
Expand Down Expand Up @@ -206,6 +213,36 @@ Explicit 256/Truecolor mode can be enabled using the `--color=256` and `--color=
- `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

Chalk supports 256 colors and [Truecolor](https://gist.github.com/XVilka/8346728) (16 million colors) on supported terminal apps.
Expand Down
175 changes: 175 additions & 0 deletions templates.js
@@ -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)));

0 comments on commit f66271e

Please sign in to comment.