From b722f22a9be9c2d0c755cedc1c9064c3e088580c Mon Sep 17 00:00:00 2001 From: Andreas Lind Date: Tue, 21 Feb 2017 00:18:33 +0100 Subject: [PATCH] Change behavior of MagicPen#clone so it's more compatible with expect.child() * Fix inheritance of the styles property in clones * Fix inheritance of the _themes property in clones * Allow installing plugins in clones that are already installed in the parent, while allowing a plugin requirement to be fulfilled by a plugin installed in the parent * Instate a more complex detection of whether a built-in style is being redefined * Prohibit definition of styles for which the corresponding property has been set to false (maintaining Unexpected's ability to prohibit the definition of styles named "inline" and "diff" in clones). --- lib/MagicPen.js | 74 +++++++++++++++++++++++++++++-------------- test/magicpen.spec.js | 70 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 118 insertions(+), 26 deletions(-) diff --git a/lib/MagicPen.js b/lib/MagicPen.js index 5dffbf7..9d8e85e 100644 --- a/lib/MagicPen.js +++ b/lib/MagicPen.js @@ -5,6 +5,14 @@ var duplicateText = require('./duplicateText'); var rgbRegexp = require('./rgbRegexp'); var cssStyles = require('./cssStyles'); +var builtInStyleNames = [ + 'bold', 'dim', 'italic', 'underline', 'inverse', 'hidden', + 'strikeThrough', 'black', 'red', 'green', 'yellow', 'blue', + 'magenta', 'cyan', 'white', 'gray', 'bgBlack', 'bgRed', + 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta', 'bgCyan', + 'bgWhite' +]; + function MagicPen(options) { if (!(this instanceof MagicPen)) { return new MagicPen(options); @@ -24,7 +32,12 @@ function MagicPen(options) { this.output = [[]]; this.styles = Object.create(null); this.installedPlugins = []; - this._themes = {}; + // Ready to be cloned individually: + this._themes = { + html: { styles: {} }, + ansi: { styles: {} }, + text: { styles: {} } + }; this.preferredWidth = (!process.browser && process.stdout.columns) || 80; if (options.format) { this.format = options.format; @@ -145,19 +158,22 @@ MagicPen.prototype.outdentLines = function () { }; MagicPen.prototype.addStyle = function (style, handler, allowRedefinition) { - var existingType = typeof this[style]; - if (existingType === 'function') { - if (!allowRedefinition) { - throw new Error('"' + style + '" style is already defined, set 3rd arg (allowRedefinition) to true to define it anyway'); - } - } else if (existingType !== 'undefined') { + if (this[style] === false || ((this.hasOwnProperty(style) || MagicPen.prototype[style]) && !Object.prototype.hasOwnProperty.call(this.styles, style) && builtInStyleNames.indexOf(style) === -1)) { throw new Error('"' + style + '" style cannot be defined, it clashes with a built-in attribute'); } - var styles = this.styles; - this.styles = Object.create(null); - for (var p in styles) { - this.styles[p] = styles[p]; + // Refuse to redefine a built-in style or a style already defined directly on this pen unless allowRedefinition is true: + if (this.hasOwnProperty(style) || builtInStyleNames.indexOf(style) !== -1) { + var existingType = typeof this[style]; + if (existingType === 'function') { + if (!allowRedefinition) { + throw new Error('"' + style + '" style is already defined, set 3rd arg (allowRedefinition) to true to define it anyway'); + } + } + } + if (this._stylesHaveNotBeenClonedYet) { + this.styles = Object.create(this.styles); + this._stylesHaveNotBeenClonedYet = false; } this.styles[style] = handler; @@ -437,13 +453,7 @@ MagicPen.prototype.space = MagicPen.prototype.sp = function (count) { return this.text(duplicateText(' ', count)); }; -[ - 'bold', 'dim', 'italic', 'underline', 'inverse', 'hidden', - 'strikeThrough', 'black', 'red', 'green', 'yellow', 'blue', - 'magenta', 'cyan', 'white', 'gray', 'bgBlack', 'bgRed', - 'bgGreen', 'bgYellow', 'bgBlue', 'bgMagenta', 'bgCyan', - 'bgWhite' -].forEach(function (textStyle) { +builtInStyleNames.forEach(function (textStyle) { MagicPen.prototype[textStyle] = MagicPen.prototype[textStyle.toLowerCase()] = function (content) { return this.text(content, textStyle); }; @@ -458,11 +468,14 @@ MagicPen.prototype.clone = function (format) { MagicPenClone.prototype = this; var clonedPen = new MagicPenClone(); clonedPen.styles = this.styles; + clonedPen._stylesHaveNotBeenClonedYet = true; clonedPen.indentationLevel = 0; clonedPen.output = [[]]; - clonedPen.installedPlugins = this.installedPlugins; + clonedPen.installedPlugins = []; clonedPen._themes = this._themes; + clonedPen._themesHaveNotBeenClonedYet = true; clonedPen.format = format || this.format; + clonedPen.parent = this; return clonedPen; }; @@ -513,10 +526,17 @@ MagicPen.prototype.use = function (plugin) { } if (plugin.dependencies) { - var installedPlugins = this.installedPlugins; + var instance = this; + var thisAndParents = []; + do { + thisAndParents.push(instance); + instance = instance.parent; + } while (instance); var unfulfilledDependencies = plugin.dependencies.filter(function (dependency) { - return !installedPlugins.some(function (plugin) { - return plugin.name === dependency; + return !thisAndParents.some(function (instance) { + return instance.installedPlugins.some(function (plugin) { + return plugin.name === dependency; + }); }); }); @@ -667,6 +687,15 @@ MagicPen.prototype.installTheme = function (formats, theme) { }; } + if (that._themesHaveNotBeenClonedYet) { + var clonedThemes = {}; + Object.keys(that._themes).forEach(function (format) { + clonedThemes[format] = Object.create(that._themes[format]); + }); + that._themes = clonedThemes; + that._themesHaveNotBeenClonedYet = false; + } + Object.keys(theme.styles).forEach(function (themeKey) { if (rgbRegexp.test(themeKey) || cssStyles[themeKey]) { throw new Error("Invalid theme key: '" + themeKey + "' you can't map build styles."); @@ -679,7 +708,6 @@ MagicPen.prototype.installTheme = function (formats, theme) { } }); - that._themes = extend({}, that._themes); formats.forEach(function (format) { var baseTheme = that._themes[format] || { styles: {} }; var extendedTheme = extend({}, baseTheme, theme); diff --git a/test/magicpen.spec.js b/test/magicpen.spec.js index 24b6a7f..e78ea5b 100644 --- a/test/magicpen.spec.js +++ b/test/magicpen.spec.js @@ -46,11 +46,17 @@ describe('magicpen', function () { return pen; } - it('throws when creating a custom style with a name that already exists', function () { - forEach(['red', 'write', 'addStyle'], function (name) { + it('throws when creating a custom style with a name that already exists as a built-in style', function () { + expect(function () { + magicpen().addStyle('red', function () {}); + }, 'to throw', '"red" style is already defined, set 3rd arg (allowRedefinition) to true to define it anyway'); + }); + + it('throws when creating a custom style that clashes with a built-in one', function () { + forEach(['write', 'addStyle'], function (name) { expect(function () { magicpen().addStyle(name, function () {}); - }, 'to throw', '"' + name + '" style is already defined, set 3rd arg (allowRedefinition) to true to define it anyway'); + }, 'to throw', '"' + name + '" style cannot be defined, it clashes with a built-in attribute'); }); }); @@ -1751,4 +1757,62 @@ describe('magicpen', function () { expect(magicpen().text('foo').isAtStartOfLine(), 'to be false'); }); }); + + // Unexpected uses this trick to prevent 'diff' and 'inline' styles from being added: + describe('when setting a property to false', function () { + it('prohibits definition of a style of that name', function () { + pen.foo = false; + expect(function () { + pen.addStyle('foo', function () {}); + }, 'to throw', '"foo" style cannot be defined, it clashes with a built-in attribute'); + }); + + it('prohibits definition of a style of that name on a clone', function () { + pen.foo = false; + expect(function () { + pen.clone().addStyle('foo', function () {}); + }, 'to throw', '"foo" style cannot be defined, it clashes with a built-in attribute'); + }); + }); + + describe('when redefining styles in a clone', function () { + var clone; + beforeEach(function () { + pen.addStyle('someStyle', function () { + this.text('parent'); + }); + clone = pen.clone(); + }); + + it('should allow redefining a style in a clone without requiring the allowRedefinition to be true', function () { + clone.addStyle('someStyle', function () { + this.text('clone'); + }); + expect(clone.someStyle().toString(), 'to equal', 'clone'); + }); + + it('should not affect the parent', function () { + clone.addStyle('someStyle', function () { + this.text('clone'); + }); + expect(clone.someStyle().toString(), 'to equal', 'clone'); + expect(pen.someStyle().toString(), 'to equal', 'parent'); + }); + + it('should lazy-clone the styles object', function () { + expect(clone.styles, 'to be', pen.styles); + clone.addStyle('someStyle', function () { + this.text('clone'); + }); + expect(clone.styles, 'not to be', pen.styles); + }); + + it('should lazy-clone the _themes object', function () { + expect(clone._themes, 'to be', pen._themes); + expect(clone._themes.html, 'to be', pen._themes.html); + clone.installTheme({}); + expect(clone._themes, 'not to be', pen._themes); + expect(clone._themes.html, 'not to be', pen._themes.html); + }); + }); });