feature(extensionLoading): add support to legacy extensions in the new extension mechanism

Old extensions that register themselves in `showdown.extensions` can be loaded and validated using the new extension loading mechanism.
However, a warn is issued, alerting users and developers that the extension should be updated to use the new mechanism

BREAKING CHANGE: Deprecates `showdown.extensions` property. To migrate, you should use the new method `showdown.extension(<ext name>, <extension>)` to register the extension.
This commit is contained in:
Estevao Soares dos Santos 2015-06-07 19:02:45 +01:00
parent e7cb15f1e9
commit 4ebd0caa27
12 changed files with 185 additions and 104 deletions

View File

@ -108,10 +108,11 @@ module.exports = function (grunt) {
require('load-grunt-tasks')(grunt); require('load-grunt-tasks')(grunt);
grunt.registerTask('concatenate', ['concat']);
grunt.registerTask('lint', ['jshint', 'jscs']); grunt.registerTask('lint', ['jshint', 'jscs']);
grunt.registerTask('test', ['lint', 'concat', 'simplemocha']); grunt.registerTask('test', ['lint', 'concat', 'simplemocha']);
grunt.registerTask('test-without-building', ['simplemocha']); grunt.registerTask('test-without-building', ['simplemocha']);
grunt.registerTask('build', ['lint', 'test', 'uglify']); grunt.registerTask('build', ['test', 'uglify']);
grunt.registerTask('prep-release', ['build', 'changelog']); grunt.registerTask('prep-release', ['build', 'changelog']);
// Default task(s). // Default task(s).

BIN
dist/showdown.js vendored

Binary file not shown.

BIN
dist/showdown.js.map vendored

Binary file not shown.

BIN
dist/showdown.min.js vendored

Binary file not shown.

Binary file not shown.

View File

@ -30,7 +30,7 @@
"url": "https://github.com/showdownjs/showdown.git", "url": "https://github.com/showdownjs/showdown.git",
"web": "https://github.com/showdownjs/showdown" "web": "https://github.com/showdownjs/showdown"
}, },
"license": "BSD", "license": "BSD-2-Clause",
"main": "./dist/showdown.js", "main": "./dist/showdown.js",
"scripts": { "scripts": {
"test": "grunt test" "test": "grunt test"

View File

@ -97,11 +97,12 @@ showdown.Converter = function (converterOptions) {
if (showdown.helper.isString(ext)) { if (showdown.helper.isString(ext)) {
ext = showdown.helper.stdExtName(ext); ext = showdown.helper.stdExtName(ext);
// TODO LEGACY SUPPORT CODE // LEGACY_SUPPORT CODE
if (!showdown.helper.isUndefined(showdown.extensions[ext]) && showdown.extensions[ext]) { if (showdown.extensions[ext]) {
console.warn(ext + ' is an old extension that uses a deprecated loading method.' + console.warn('DEPRECATION WARNING: ' + ext + ' is an old extension that uses a deprecated loading method.' +
'Please inform the developer that the extension should be updated!'); 'Please inform the developer that the extension should be updated!');
ext = showdown.extensions[ext]; legacyExtensionLoading(showdown.extensions[ext], ext);
return;
// END LEGACY SUPPORT CODE // END LEGACY SUPPORT CODE
} else if (!showdown.helper.isUndefined(extensions[ext])) { } else if (!showdown.helper.isUndefined(extensions[ext])) {
@ -110,26 +111,66 @@ showdown.Converter = function (converterOptions) {
} else { } else {
throw Error('Extension "' + ext + '" could not be loaded. It was either not found or is not a valid extension.'); throw Error('Extension "' + ext + '" could not be loaded. It was either not found or is not a valid extension.');
} }
} else if (typeof ext === 'function') { }
if (typeof ext === 'function') {
ext = ext(); ext = ext();
} }
if (!showdown.helper.isArray(ext)) {
ext = [ext];
}
if (!showdown.validateExtension(ext)) { if (!showdown.validateExtension(ext)) {
return; return;
} }
switch (ext.type) { for (var i = 0; i < ext.length; ++i) {
case 'lang': switch (ext[i].type) {
langExtensions.push(ext); case 'lang':
break; langExtensions.push(ext[i]);
break;
case 'output': case 'output':
outputModifiers.push(ext); outputModifiers.push(ext[i]);
break; break;
default: default:
// should never reach here // should never reach here
throw Error('Extension loader error: Type unrecognized!!!'); throw Error('Extension loader error: Type unrecognized!!!');
}
}
}
/**
* LEGACY_SUPPORT
* @param {*} ext
* @param {string} name
*/
function legacyExtensionLoading(ext, name) {
if (typeof ext === 'function') {
ext = ext(new showdown.Converter());
}
if (!showdown.helper.isArray(ext)) {
ext = [ext];
}
var valid = validate(ext, name);
if (!valid.valid) {
throw Error(valid.error);
}
for (var i = 0; i < ext.length; ++i) {
switch (ext[i].type) {
case 'lang':
langExtensions.push(ext[i]);
break;
case 'output':
outputModifiers.push(ext[i]);
break;
default:// should never reach here
throw Error('Extension loader error: Type unrecognized!!!');
}
} }
} }
@ -200,7 +241,6 @@ showdown.Converter = function (converterOptions) {
showdown.helper.forEach(outputModifiers, function (ext) { showdown.helper.forEach(outputModifiers, function (ext) {
text = showdown.subParser('runExtension')(ext, text, options, globals); text = showdown.subParser('runExtension')(ext, text, options, globals);
}); });
text = parsers.outputModifiers(text, options, globals);
return text; return text;
}; };
@ -240,20 +280,34 @@ showdown.Converter = function (converterOptions) {
}; };
/** /**
* Remove an extension from THIS converter * Use a global registered extension with THIS converter
* @param {{}} extension * @param {string} extensionName Name of the previously registered extension
*/
this.useExtension = function (extensionName) {
_parseExtension(extensionName);
};
/**
* Remove an extension from THIS converter.
* Note: This is a costly operation. It's better to initialize a new converter
* and specify the extensions you wish to use
* @param {Array} extension
*/ */
this.removeExtension = function (extension) { this.removeExtension = function (extension) {
for (var i = 0; i < langExtensions.length; ++i) { if (!showdown.helper.isArray(extension)) {
if (langExtensions[i] === extension) { extension = [extension];
langExtensions[i].splice(i, 1);
return;
}
} }
for (var ii = 0; ii < outputModifiers.length; ++i) { for (var a = 0; a < extension.length; ++a) {
if (outputModifiers[ii] === extension) { var ext = extension[a];
outputModifiers[ii].splice(i, 1); for (var i = 0; i < langExtensions.length; ++i) {
return; if (langExtensions[i] === ext) {
langExtensions[i].splice(i, 1);
}
}
for (var ii = 0; ii < outputModifiers.length; ++i) {
if (outputModifiers[ii] === ext) {
outputModifiers[ii].splice(i, 1);
}
} }
} }
}; };

View File

@ -113,10 +113,16 @@ showdown.extension = function (name, ext) {
// Setter // Setter
} else { } else {
// Expand extension if it's wrapped in a function
if (typeof ext === 'function') { if (typeof ext === 'function') {
ext = ext(); ext = ext();
} }
// Ensure extension is an array
if (!showdown.helper.isArray(ext)) {
ext = [ext];
}
var validExtension = validate(ext, name); var validExtension = validate(ext, name);
if (validExtension.valid) { if (validExtension.valid) {
@ -155,83 +161,90 @@ showdown.resetExtensions = function () {
/** /**
* Validate extension * Validate extension
* @param {object} ext * @param {array} extension
* @param {string} name * @param {string} name
* @returns {{valid: boolean, error: string}} * @returns {{valid: boolean, error: string}}
*/ */
function validate(ext, name) { function validate(extension, name) {
'use strict'; 'use strict';
var baseMsg = (name) ? 'Error in ' + name + ' extension: ' : 'Error in unnamed extension', var errMsg = (name) ? 'Error in ' + name + ' extension->' : 'Error in unnamed extension',
ret = { ret = {
valid: true, valid: true,
error: baseMsg error: ''
}; };
if (typeof ext !== 'object') { if (!showdown.helper.isArray(extension)) {
ret.valid = false; extension = [extension];
ret.error = baseMsg + 'it must be an object, but ' + typeof ext + ' given';
return ret;
} }
if (!showdown.helper.isString(ext.type)) { for (var i = 0; i < extension.length; ++i) {
ret.valid = false; var baseMsg = errMsg + 'sub-extension ' + i + ': ',
ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given'; ext = extension[i];
return ret; if (typeof ext !== 'object') {
}
var type = ext.type = ext.type.toLowerCase();
// normalize extension type
if (type === 'language') {
type = ext.type = 'lang';
}
if (type === 'html') {
type = ext.type = 'output';
}
if (type !== 'lang' && type !== 'output') {
ret.valid = false;
ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang" or "output"';
return ret;
}
if (ext.filter) {
if (typeof ext.filter !== 'function') {
ret.valid = false; ret.valid = false;
ret.error = baseMsg + '"filter" must be a function, but ' + typeof ext.filter + ' given'; ret.error = baseMsg + 'must be an object, but ' + typeof ext + ' given';
return ret; return ret;
} }
} else if (ext.regex) { if (!showdown.helper.isString(ext.type)) {
if (showdown.helper.isString(ext.regex)) {
ext.regex = new RegExp(ext.regex, 'g');
}
if (!ext.regex instanceof RegExp) {
ret.valid = false; ret.valid = false;
ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' + ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given';
typeof ext.regex + ' given';
return ret;
}
if (showdown.helper.isUndefined(ext.replace)) {
ret.valid = false;
ret.error = baseMsg + '"regex" extensions must implement a replace string or function';
return ret; return ret;
} }
} else { var type = ext.type = ext.type.toLowerCase();
ret.valid = false;
ret.error = baseMsg + 'extensions must define either a "regex" property or a "filter" method';
return ret;
}
if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) { // normalize extension type
ret.valid = false; if (type === 'language') {
ret.error = baseMsg + 'output extensions must define a filter property'; type = ext.type = 'lang';
return ret; }
}
if (type === 'html') {
type = ext.type = 'output';
}
if (type !== 'lang' && type !== 'output') {
ret.valid = false;
ret.error = baseMsg + 'type ' + type + ' is not recognized. Valid values: "lang" or "output"';
return ret;
}
if (ext.filter) {
if (typeof ext.filter !== 'function') {
ret.valid = false;
ret.error = baseMsg + '"filter" must be a function, but ' + typeof ext.filter + ' given';
return ret;
}
} else if (ext.regex) {
if (showdown.helper.isString(ext.regex)) {
ext.regex = new RegExp(ext.regex, 'g');
}
if (!ext.regex instanceof RegExp) {
ret.valid = false;
ret.error = baseMsg + '"regex" property must either be a string or a RegExp object, but ' +
typeof ext.regex + ' given';
return ret;
}
if (showdown.helper.isUndefined(ext.replace)) {
ret.valid = false;
ret.error = baseMsg + '"regex" extensions must implement a replace string or function';
return ret;
}
} else {
ret.valid = false;
ret.error = baseMsg + 'extensions must define either a "regex" property or a "filter" method';
return ret;
}
if (showdown.helper.isUndefined(ext.filter) && showdown.helper.isUndefined(ext.regex)) {
ret.valid = false;
ret.error = baseMsg + 'output extensions must define a filter property';
return ret;
}
}
return ret; return ret;
} }

View File

@ -1,11 +0,0 @@
/**
* Run language extensions
*/
showdown.subParser('outputModifiers', function (text, config, globals) {
'use strict';
showdown.helper.forEach(globals.outputModifiers, function (ext) {
text = showdown.subParser('runExtension')(ext, text);
});
return text;
});

View File

@ -0,0 +1,24 @@
/**
* Created by Estevao on 06-06-2015.
*/
require('source-map-support').install();
var expect = require('chai').expect,
showdown = require('../../dist/showdown.js');
describe('showdown legacy extension support', function () {
'use strict';
var extObjMock =
{
type: 'lang',
filter: function () {}
},
extFunc = function () {
return [extObjMock];
};
it('accept extensions loaded by the old mechanism', function () {
showdown.extensions.bazinga = extFunc;
var cnv = new showdown.Converter({extensions: ['bazinga']});
expect(cnv.getAllExtensions().language).to.eql([extObjMock]);
});
});

View File

@ -49,11 +49,11 @@ describe('showdown.Converter', function () {
converter.getAllExtensions().language.should.contain(extObjMock); converter.getAllExtensions().language.should.contain(extObjMock);
}); });
it('addExtension() should add a previous registered extension in showdown', function () { it('useExtension() should use a previous registered extension in showdown', function () {
showdown.extension('foo', extObjMock); showdown.extension('foo', extObjMock);
var converter = new showdown.Converter(); var converter = new showdown.Converter();
converter.addExtension('foo'); converter.useExtension('foo');
converter.getAllExtensions().language.should.contain(extObjMock); converter.getAllExtensions().language.should.contain(extObjMock);
showdown.resetExtensions(); showdown.resetExtensions();
}); });

View File

@ -16,7 +16,7 @@ describe('showdown.options', function () {
}); });
}); });
describe('showdown.extension', function () { describe('showdown.extension()', function () {
'use strict'; 'use strict';
var extObjMock = { var extObjMock = {
@ -28,15 +28,15 @@ describe('showdown.extension', function () {
}; };
describe('should register', function () { describe('should register', function () {
it('should register an extension object', function () { it('an extension object', function () {
showdown.extension('foo', extObjMock); showdown.extension('foo', extObjMock);
showdown.extension('foo').should.equal(extObjMock); showdown.extension('foo').should.eql([extObjMock]);
showdown.resetExtensions(); showdown.resetExtensions();
}); });
it('should register an extension function', function () { it('an extension function', function () {
showdown.extension('foo', extObjFunc); showdown.extension('foo', extObjFunc);
showdown.extension('foo').should.equal(extObjMock); showdown.extension('foo').should.eql([extObjMock]);
showdown.resetExtensions(); showdown.resetExtensions();
}); });
}); });
@ -78,6 +78,6 @@ describe('showdown.getAllExtensions()', function () {
it('should return all extensions', function () { it('should return all extensions', function () {
showdown.extension('bar', extObjMock); showdown.extension('bar', extObjMock);
showdown.getAllExtensions().should.eql({bar: extObjMock}); showdown.getAllExtensions().should.eql({bar: [extObjMock]});
}); });
}); });