diff --git a/benchmark.js b/benchmark.js index f4e9cf3..dc24696 100644 --- a/benchmark.js +++ b/benchmark.js @@ -3,22 +3,48 @@ const chalk = require('.'); suite('chalk', () => { - set('iterations', 100000); + set('iterations', 1000000); - bench('single style', () => { + const chalkRed = chalk.red; + const chalkBgRed = chalk.bgRed; + const chalkBlueBgRed = chalk.blue.bgRed; + const chalkBlueBgRedBold = chalk.blue.bgRed.bold; + + const blueStyledString = 'the fox jumps' + chalk.blue('over the lazy dog') + '!'; + + bench('1 style', () => { chalk.red('the fox jumps over the lazy dog'); }); - bench('several styles', () => { + bench('2 styles', () => { + chalk.blue.bgRed('the fox jumps over the lazy dog'); + }); + + bench('3 styles', () => { chalk.blue.bgRed.bold('the fox jumps over the lazy dog'); }); - const cached = chalk.blue.bgRed.bold; - bench('cached styles', () => { - cached('the fox jumps over the lazy dog'); + bench('cached: 1 style', () => { + chalkRed('the fox jumps over the lazy dog'); + }); + + bench('cached: 2 styles', () => { + chalkBlueBgRed('the fox jumps over the lazy dog'); + }); + + bench('cached: 3 styles', () => { + chalkBlueBgRedBold('the fox jumps over the lazy dog'); + }); + + bench('cached: 1 style with newline', () => { + chalkRed('the fox jumps\nover the lazy dog'); + }); + + bench('cached: 1 style nested intersecting', () => { + chalkRed(blueStyledString); }); - bench('nested styles', () => { - chalk.red('the fox jumps', chalk.underline.bgBlue('over the lazy dog') + '!'); + bench('cached: 1 style nested non-intersecting', () => { + chalkBgRed(blueStyledString); }); }); diff --git a/examples/screenshot.js b/examples/screenshot.js index 7d195a6..37f5850 100644 --- a/examples/screenshot.js +++ b/examples/screenshot.js @@ -1,6 +1,6 @@ 'use strict'; -const chalk = require('..'); const styles = require('ansi-styles'); +const chalk = require('..'); // Generates screenshot for (const key of Object.keys(styles)) { diff --git a/index.js b/index.js index 9f5575a..21d3a83 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,13 @@ 'use strict'; -const escapeStringRegexp = require('escape-string-regexp'); const ansiStyles = require('ansi-styles'); const {stdout: stdoutColor} = require('supports-color'); const template = require('./templates.js'); +const { + stringReplaceAll, + stringEncaseCRLFWithFirstIndex +} = require('./lib/util'); + // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ 'ansi', @@ -57,22 +61,23 @@ function Chalk(options) { } for (const [styleName, style] of Object.entries(ansiStyles)) { - style.closeRe = new RegExp(escapeStringRegexp(style.close), 'g'); - styles[styleName] = { get() { - return createBuilder(this, [...(this._styles || []), style], this._isEmpty); + const builder = createBuilder(this, createStyler(style.open, style.close, this._styler), this._isEmpty); + Object.defineProperty(this, styleName, {value: builder}); + return builder; } }; } styles.visible = { get() { - return createBuilder(this, this._styles || [], true); + const builder = createBuilder(this, this._styler, true); + Object.defineProperty(this, 'visible', {value: builder}); + return builder; } }; -ansiStyles.color.closeRe = new RegExp(escapeStringRegexp(ansiStyles.color.close), 'g'); for (const model of Object.keys(ansiStyles.color.ansi)) { if (skipModels.has(model)) { continue; @@ -82,19 +87,13 @@ for (const model of Object.keys(ansiStyles.color.ansi)) { get() { const {level} = this; return function (...arguments_) { - const open = ansiStyles.color[levelMapping[level]][model](...arguments_); - const codes = { - open, - close: ansiStyles.color.close, - closeRe: ansiStyles.color.closeRe - }; - return createBuilder(this, [...(this._styles || []), codes], this._isEmpty); + const styler = createStyler(ansiStyles.color[levelMapping[level]][model](...arguments_), ansiStyles.color.close, this._styler); + return createBuilder(this, styler, this._isEmpty); }; } }; } -ansiStyles.bgColor.closeRe = new RegExp(escapeStringRegexp(ansiStyles.bgColor.close), 'g'); for (const model of Object.keys(ansiStyles.bgColor.ansi)) { if (skipModels.has(model)) { continue; @@ -105,72 +104,106 @@ for (const model of Object.keys(ansiStyles.bgColor.ansi)) { get() { const {level} = this; return function (...arguments_) { - const open = ansiStyles.bgColor[levelMapping[level]][model](...arguments_); - const codes = { - open, - close: ansiStyles.bgColor.close, - closeRe: ansiStyles.bgColor.closeRe - }; - return createBuilder(this, [...(this._styles || []), codes], this._isEmpty); + const styler = createStyler(ansiStyles.bgColor[levelMapping[level]][model](...arguments_), ansiStyles.bgColor.close, this._styler); + return createBuilder(this, styler, this._isEmpty); }; } }; } -const proto = Object.defineProperties(() => {}, styles); - -const createBuilder = (self, _styles, _isEmpty) => { - const builder = (...arguments_) => applyStyle(builder, ...arguments_); - builder._styles = _styles; - builder._isEmpty = _isEmpty; - - Object.defineProperty(builder, 'level', { +const proto = Object.defineProperties(() => {}, { + ...styles, + level: { enumerable: true, get() { - return self.level; + return this._generator.level; }, set(level) { - self.level = level; + this._generator.level = level; } - }); - - Object.defineProperty(builder, 'enabled', { + }, + enabled: { enumerable: true, get() { - return self.enabled; + return this._generator.enabled; }, set(enabled) { - self.enabled = enabled; + this._generator.enabled = enabled; } - }); + } +}); + +const createStyler = (open, close, parent) => { + let openAll; + let closeAll; + if (parent === undefined) { + openAll = open; + closeAll = close; + } else { + openAll = parent.openAll + open; + closeAll = close + parent.closeAll; + } + + return { + open, + close, + openAll, + closeAll, + parent + }; +}; + +const createBuilder = (self, _styler, _isEmpty) => { + const 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 // no way to create a function with a different prototype builder.__proto__ = proto; // eslint-disable-line no-proto + builder._generator = self; + builder._styler = _styler; + builder._isEmpty = _isEmpty; + return builder; }; -const applyStyle = (self, ...arguments_) => { - let string = arguments_.join(' '); - +const applyStyle = (self, string) => { if (!self.enabled || self.level <= 0 || !string) { return self._isEmpty ? '' : string; } - for (const code of self._styles.slice().reverse()) { - // Replace any instances already present with a re-opening code - // otherwise only the part of the string until said closing code - // will be colored, and the rest will simply be 'plain'. - string = code.open + string.replace(code.closeRe, code.open) + code.close; + let styler = self._styler; + + if (styler === undefined) { + return string; + } + + const {openAll, closeAll} = styler; + if (string.indexOf('\u001B') !== -1) { + while (styler !== undefined) { + // Replace any instances already present with a re-opening code + // otherwise only the part of the string until said closing code + // will be colored, and the rest will simply be 'plain'. + string = stringReplaceAll(string, styler.close, styler.open); + + styler = styler.parent; + } + } - // Close the styling before a linebreak and reopen - // after next line to fix a bleed issue on macOS - // https://github.com/chalk/chalk/pull/92 - string = string.replace(/\r?\n/g, `${code.close}$&${code.open}`); + // We can move both next actions out of loop, because remaining actions in loop won't have any/visible effect on parts we add here + // Close the styling before a linebreak and reopen + // after next line to fix a bleed issue on macOS + // https://github.com/chalk/chalk/pull/92 + const lfIndex = string.indexOf('\n'); + if (lfIndex !== -1) { + string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex); } - return string; + return openAll + string + closeAll; }; const chalkTag = (chalk, ...strings) => { diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..8ce1166 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,37 @@ +const stringReplaceAll = (string, substring, replacer) => { + let index = string.indexOf(substring); + if (index === -1) { + return string; + } + + const subLen = substring.length; + let end = 0; + let res = ''; + do { + res += string.substr(end, index - end) + replacer; + end = index + subLen; + index = string.indexOf(substring, end); + } while (index !== -1); + + res += string.substr(end); + return res; +}; + +const stringEncaseCRLFWithFirstIndex = (string, prefix, postfix, index) => { + let end = 0; + let res = ''; + do { + const gotCR = string[index - 1] === '\r'; + res += string.substr(end, (gotCR ? index - 1 : index) - end) + prefix + (gotCR ? '\r\n' : '\n') + postfix; + end = index + 1; + index = string.indexOf('\n', end); + } while (index !== -1); + + res += string.substr(end); + return res; +}; + +module.exports = { + stringReplaceAll, + stringEncaseCRLFWithFirstIndex +}; diff --git a/package.json b/package.json index b18d8a3..48712eb 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ ], "dependencies": { "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", "supports-color": "^6.1.0" }, "devDependencies": { diff --git a/test/chalk.js b/test/chalk.js index 240f0f9..3f90ba6 100644 --- a/test/chalk.js +++ b/test/chalk.js @@ -82,6 +82,10 @@ test('line breaks should open and close colors', t => { t.is(chalk.grey('hello\nworld'), '\u001B[90mhello\u001B[39m\n\u001B[90mworld\u001B[39m'); }); +test('line breaks should open and close colors with CRLF', t => { + t.is(chalk.grey('hello\r\nworld'), '\u001B[90mhello\u001B[39m\r\n\u001B[90mworld\u001B[39m'); +}); + test('properly convert RGB to 16 colors on basic color terminals', t => { t.is(new chalk.Instance({level: 1}).hex('#FF0000')('hello'), '\u001B[91mhello\u001B[39m'); t.is(new chalk.Instance({level: 1}).bgHex('#FF0000')('hello'), '\u001B[101mhello\u001B[49m');