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);
grunt.registerTask('concatenate', ['concat']);
grunt.registerTask('lint', ['jshint', 'jscs']);
grunt.registerTask('test', ['lint', 'concat', 'simplemocha']);
grunt.registerTask('test-without-building', ['simplemocha']);
grunt.registerTask('build', ['lint', 'test', 'uglify']);
grunt.registerTask('build', ['test', 'uglify']);
grunt.registerTask('prep-release', ['build', 'changelog']);
// 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",
"web": "https://github.com/showdownjs/showdown"
},
"license": "BSD",
"license": "BSD-2-Clause",
"main": "./dist/showdown.js",
"scripts": {
"test": "grunt test"

View File

@ -97,11 +97,12 @@ showdown.Converter = function (converterOptions) {
if (showdown.helper.isString(ext)) {
ext = showdown.helper.stdExtName(ext);
// TODO LEGACY SUPPORT CODE
if (!showdown.helper.isUndefined(showdown.extensions[ext]) && showdown.extensions[ext]) {
console.warn(ext + ' is an old extension that uses a deprecated loading method.' +
// LEGACY_SUPPORT CODE
if (showdown.extensions[ext]) {
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!');
ext = showdown.extensions[ext];
legacyExtensionLoading(showdown.extensions[ext], ext);
return;
// END LEGACY SUPPORT CODE
} else if (!showdown.helper.isUndefined(extensions[ext])) {
@ -110,26 +111,66 @@ showdown.Converter = function (converterOptions) {
} else {
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();
}
if (!showdown.helper.isArray(ext)) {
ext = [ext];
}
if (!showdown.validateExtension(ext)) {
return;
}
switch (ext.type) {
case 'lang':
langExtensions.push(ext);
break;
for (var i = 0; i < ext.length; ++i) {
switch (ext[i].type) {
case 'lang':
langExtensions.push(ext[i]);
break;
case 'output':
outputModifiers.push(ext);
break;
case 'output':
outputModifiers.push(ext[i]);
break;
default:
// should never reach here
throw Error('Extension loader error: Type unrecognized!!!');
default:
// should never reach here
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) {
text = showdown.subParser('runExtension')(ext, text, options, globals);
});
text = parsers.outputModifiers(text, options, globals);
return text;
};
@ -240,20 +280,34 @@ showdown.Converter = function (converterOptions) {
};
/**
* Remove an extension from THIS converter
* @param {{}} extension
* Use a global registered extension with THIS converter
* @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) {
for (var i = 0; i < langExtensions.length; ++i) {
if (langExtensions[i] === extension) {
langExtensions[i].splice(i, 1);
return;
}
if (!showdown.helper.isArray(extension)) {
extension = [extension];
}
for (var ii = 0; ii < outputModifiers.length; ++i) {
if (outputModifiers[ii] === extension) {
outputModifiers[ii].splice(i, 1);
return;
for (var a = 0; a < extension.length; ++a) {
var ext = extension[a];
for (var i = 0; i < langExtensions.length; ++i) {
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
} else {
// Expand extension if it's wrapped in a function
if (typeof ext === 'function') {
ext = ext();
}
// Ensure extension is an array
if (!showdown.helper.isArray(ext)) {
ext = [ext];
}
var validExtension = validate(ext, name);
if (validExtension.valid) {
@ -155,83 +161,90 @@ showdown.resetExtensions = function () {
/**
* Validate extension
* @param {object} ext
* @param {array} extension
* @param {string} name
* @returns {{valid: boolean, error: string}}
*/
function validate(ext, name) {
function validate(extension, name) {
'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 = {
valid: true,
error: baseMsg
error: ''
};
if (typeof ext !== 'object') {
ret.valid = false;
ret.error = baseMsg + 'it must be an object, but ' + typeof ext + ' given';
return ret;
if (!showdown.helper.isArray(extension)) {
extension = [extension];
}
if (!showdown.helper.isString(ext.type)) {
ret.valid = false;
ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given';
return ret;
}
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') {
for (var i = 0; i < extension.length; ++i) {
var baseMsg = errMsg + 'sub-extension ' + i + ': ',
ext = extension[i];
if (typeof ext !== 'object') {
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;
}
} else if (ext.regex) {
if (showdown.helper.isString(ext.regex)) {
ext.regex = new RegExp(ext.regex, 'g');
}
if (!ext.regex instanceof RegExp) {
if (!showdown.helper.isString(ext.type)) {
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';
ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given';
return ret;
}
} else {
ret.valid = false;
ret.error = baseMsg + 'extensions must define either a "regex" property or a "filter" method';
return ret;
}
var type = ext.type = ext.type.toLowerCase();
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;
}
// 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.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;
}

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);
});
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);
var converter = new showdown.Converter();
converter.addExtension('foo');
converter.useExtension('foo');
converter.getAllExtensions().language.should.contain(extObjMock);
showdown.resetExtensions();
});

View File

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