mirror of
https://github.com/showdownjs/showdown.git
synced 2024-03-22 13:30:55 +08:00
feature(extensionLoading): refactor extension loading mechanism
This commit is contained in:
parent
c6b60f12fa
commit
33f64f60c9
@ -18,12 +18,14 @@ module.exports = function (grunt) {
|
|||||||
src: [
|
src: [
|
||||||
'src/showdown.js',
|
'src/showdown.js',
|
||||||
'src/helpers.js',
|
'src/helpers.js',
|
||||||
|
'src/converter.js',
|
||||||
'src/subParsers/*.js',
|
'src/subParsers/*.js',
|
||||||
'src/loader.js'
|
'src/loader.js'
|
||||||
],
|
],
|
||||||
dest: 'dist/<%= pkg.name %>.js'
|
dest: 'dist/<%= pkg.name %>.js'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
uglify: {
|
uglify: {
|
||||||
options: {
|
options: {
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
|
667
dist/showdown.js
vendored
667
dist/showdown.js
vendored
@ -1,4 +1,4 @@
|
|||||||
;/*! showdown 28-05-2015 */
|
;/*! showdown 31-05-2015 */
|
||||||
(function(){
|
(function(){
|
||||||
/**
|
/**
|
||||||
* Created by Tivie on 06-01-2015.
|
* Created by Tivie on 06-01-2015.
|
||||||
@ -8,10 +8,11 @@
|
|||||||
var showdown = {},
|
var showdown = {},
|
||||||
parsers = {},
|
parsers = {},
|
||||||
extensions = {},
|
extensions = {},
|
||||||
globalOptions = {
|
defaultOptions = {
|
||||||
omitExtraWLInCodeBlocks: false,
|
omitExtraWLInCodeBlocks: false,
|
||||||
prefixHeaderId: false
|
prefixHeaderId: false
|
||||||
};
|
},
|
||||||
|
globalOptions = JSON.parse(JSON.stringify(defaultOptions)); //clone default options out of laziness =P
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* helper namespace
|
* helper namespace
|
||||||
@ -19,7 +20,10 @@ var showdown = {},
|
|||||||
*/
|
*/
|
||||||
showdown.helper = {};
|
showdown.helper = {};
|
||||||
|
|
||||||
// Public properties
|
/**
|
||||||
|
* TODO LEGACY SUPPORT CODE
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
showdown.extensions = {};
|
showdown.extensions = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,6 +60,11 @@ showdown.getOptions = function () {
|
|||||||
return globalOptions;
|
return globalOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
showdown.resetOptions = function () {
|
||||||
|
'use strict';
|
||||||
|
globalOptions = JSON.parse(JSON.stringify(defaultOptions));
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or set a subParser
|
* Get or set a subParser
|
||||||
*
|
*
|
||||||
@ -81,6 +90,13 @@ showdown.subParser = function (name, func) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or registers an extension
|
||||||
|
* @static
|
||||||
|
* @param {string} name
|
||||||
|
* @param {object|function=} ext
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
showdown.extension = function (name, ext) {
|
showdown.extension = function (name, ext) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@ -90,230 +106,151 @@ showdown.extension = function (name, ext) {
|
|||||||
|
|
||||||
name = showdown.helper.stdExtName(name);
|
name = showdown.helper.stdExtName(name);
|
||||||
|
|
||||||
|
// Getter
|
||||||
if (showdown.helper.isUndefined(ext)) {
|
if (showdown.helper.isUndefined(ext)) {
|
||||||
return getExtension();
|
|
||||||
} else {
|
|
||||||
return setExtension();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function getExtension(name) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (!extensions.hasOwnProperty(name)) {
|
if (!extensions.hasOwnProperty(name)) {
|
||||||
throw Error('Extension named ' + name + ' is not registered!');
|
throw Error('Extension named ' + name + ' is not registered!');
|
||||||
}
|
}
|
||||||
return extensions[name];
|
return extensions[name];
|
||||||
}
|
|
||||||
|
|
||||||
function setExtension(name, ext) {
|
// Setter
|
||||||
|
} else {
|
||||||
|
if (typeof ext === 'function') {
|
||||||
|
ext = ext();
|
||||||
|
}
|
||||||
|
|
||||||
|
var validExtension = validate(ext, name);
|
||||||
|
|
||||||
|
if (validExtension.valid) {
|
||||||
|
extensions[name] = ext;
|
||||||
|
} else {
|
||||||
|
throw Error(validExtension.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all extensions registered
|
||||||
|
* @returns {{}}
|
||||||
|
*/
|
||||||
|
showdown.getAllExtensions = function () {
|
||||||
|
'use strict';
|
||||||
|
return extensions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an extension
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
showdown.removeExtension = function (name) {
|
||||||
|
'use strict';
|
||||||
|
delete extensions[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all extensions
|
||||||
|
*/
|
||||||
|
showdown.resetExtensions = function () {
|
||||||
|
'use strict';
|
||||||
|
extensions = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate extension
|
||||||
|
* @param {object} ext
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {{valid: boolean, error: string}}
|
||||||
|
*/
|
||||||
|
function validate(ext, name) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var baseMsg = (name) ? 'Error in ' + name + ' extension: ' : 'Error in unnamed extension',
|
||||||
|
ret = {
|
||||||
|
valid: true,
|
||||||
|
error: baseMsg
|
||||||
|
};
|
||||||
|
|
||||||
if (typeof ext !== 'object') {
|
if (typeof ext !== 'object') {
|
||||||
throw Error('A Showdown Extension must be an object, ' + typeof ext + ' given');
|
ret.valid = false;
|
||||||
|
ret.error = baseMsg + 'it must be an object, but ' + typeof ext + ' given';
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showdown.helper.isString(ext.type)) {
|
if (!showdown.helper.isString(ext.type)) {
|
||||||
throw Error('When registering a showdown extension, "type" must be a string, ' + typeof ext.type + ' given');
|
ret.valid = false;
|
||||||
|
ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given';
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.type = ext.type.toLowerCase();
|
var type = ext.type = ext.type.toLowerCase();
|
||||||
|
|
||||||
extensions[name] = ext;
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Showdown Converter class
|
* Validate extension
|
||||||
*
|
* @param {object} ext
|
||||||
* @param {object} [converterOptions]
|
* @returns {boolean}
|
||||||
* @returns {{makeHtml: Function}}
|
|
||||||
*/
|
*/
|
||||||
showdown.Converter = function (converterOptions) {
|
showdown.validateExtension = function (ext) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
converterOptions = converterOptions || {};
|
var validateExtension = validate(ext, null);
|
||||||
|
if (!validateExtension.valid) {
|
||||||
var options = {},
|
console.warn(validateExtension.error);
|
||||||
langExtensions = [],
|
return false;
|
||||||
outputModifiers = [],
|
|
||||||
parserOrder = [
|
|
||||||
'githubCodeBlocks',
|
|
||||||
'hashHTMLBlocks',
|
|
||||||
'stripLinkDefinitions',
|
|
||||||
'blockGamut',
|
|
||||||
'unescapeSpecialChars'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (var gOpt in globalOptions) {
|
|
||||||
if (globalOptions.hasOwnProperty(gOpt)) {
|
|
||||||
options[gOpt] = globalOptions[gOpt];
|
|
||||||
}
|
}
|
||||||
}
|
return true;
|
||||||
|
|
||||||
// Merge options
|
|
||||||
if (typeof converterOptions === 'object') {
|
|
||||||
for (var opt in converterOptions) {
|
|
||||||
if (converterOptions.hasOwnProperty(opt)) {
|
|
||||||
options[opt] = converterOptions[opt];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a dirty workaround to maintain backwards extension compatibility
|
|
||||||
// We define a self var (which is a copy of this) and inject the makeHtml function
|
|
||||||
// directly to it. This ensures a full converter object is available when iterating over extensions
|
|
||||||
// We should rewrite the extension loading mechanism and use some kind of interface or decorator pattern
|
|
||||||
// and inject the object reference there instead.
|
|
||||||
var self = this;
|
|
||||||
self.makeHtml = makeHtml;
|
|
||||||
|
|
||||||
// Parse options
|
|
||||||
if (options.extensions) {
|
|
||||||
|
|
||||||
// Iterate over each plugin
|
|
||||||
showdown.helper.forEach(options.extensions, function (plugin) {
|
|
||||||
var pluginName = plugin;
|
|
||||||
|
|
||||||
// Assume it's a bundled plugin if a string is given
|
|
||||||
if (typeof plugin === 'string') {
|
|
||||||
var tPluginName = showdown.helper.stdExtName(plugin);
|
|
||||||
|
|
||||||
if (!showdown.helper.isUndefined(showdown.extensions[tPluginName]) && showdown.extensions[tPluginName]) {
|
|
||||||
//Trigger some kind of deprecated alert
|
|
||||||
plugin = showdown.extensions[tPluginName];
|
|
||||||
|
|
||||||
} else if (!showdown.helper.isUndefined(extensions[tPluginName])) {
|
|
||||||
plugin = extensions[tPluginName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof plugin === 'function') {
|
|
||||||
// Iterate over each extension within that plugin
|
|
||||||
showdown.helper.forEach(plugin(self), function (ext) {
|
|
||||||
// Sort extensions by type
|
|
||||||
if (ext.type) {
|
|
||||||
if (ext.type === 'language' || ext.type === 'lang') {
|
|
||||||
langExtensions.push(ext);
|
|
||||||
} else if (ext.type === 'output' || ext.type === 'html') {
|
|
||||||
outputModifiers.push(ext);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Assume language extension
|
|
||||||
outputModifiers.push(ext);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var errMsg = 'An extension could not be loaded. It was either not found or is not a valid extension.';
|
|
||||||
if (typeof pluginName === 'string') {
|
|
||||||
errMsg = 'Extension "' + pluginName + '" could not be loaded. It was either not found or is not a valid extension.';
|
|
||||||
}
|
|
||||||
throw errMsg;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a markdown string into HTML
|
|
||||||
* @param {string} text
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
function makeHtml(text) {
|
|
||||||
|
|
||||||
//check if text is not falsy
|
|
||||||
if (!text) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
var globals = {
|
|
||||||
gHtmlBlocks: [],
|
|
||||||
gUrls: {},
|
|
||||||
gTitles: {},
|
|
||||||
gListLevel: 0,
|
|
||||||
hashLinkCounts: {},
|
|
||||||
langExtensions: langExtensions,
|
|
||||||
outputModifiers: outputModifiers
|
|
||||||
};
|
|
||||||
|
|
||||||
// attacklab: Replace ~ with ~T
|
|
||||||
// This lets us use tilde as an escape char to avoid md5 hashes
|
|
||||||
// The choice of character is arbitrary; anything that isn't
|
|
||||||
// magic in Markdown will work.
|
|
||||||
text = text.replace(/~/g, '~T');
|
|
||||||
|
|
||||||
// attacklab: Replace $ with ~D
|
|
||||||
// RegExp interprets $ as a special character
|
|
||||||
// when it's in a replacement string
|
|
||||||
text = text.replace(/\$/g, '~D');
|
|
||||||
|
|
||||||
// Standardize line endings
|
|
||||||
text = text.replace(/\r\n/g, '\n'); // DOS to Unix
|
|
||||||
text = text.replace(/\r/g, '\n'); // Mac to Unix
|
|
||||||
|
|
||||||
// Make sure text begins and ends with a couple of newlines:
|
|
||||||
text = '\n\n' + text + '\n\n';
|
|
||||||
|
|
||||||
// detab
|
|
||||||
text = parsers.detab(text, options, globals);
|
|
||||||
|
|
||||||
// stripBlankLines
|
|
||||||
text = parsers.stripBlankLines(text, options, globals);
|
|
||||||
|
|
||||||
//run languageExtensions
|
|
||||||
text = parsers.languageExtensions(text, options, globals);
|
|
||||||
|
|
||||||
// Run all registered parsers
|
|
||||||
for (var i = 0; i < parserOrder.length; ++i) {
|
|
||||||
var name = parserOrder[i];
|
|
||||||
text = parsers[name](text, options, globals);
|
|
||||||
}
|
|
||||||
|
|
||||||
// attacklab: Restore dollar signs
|
|
||||||
text = text.replace(/~D/g, '$$');
|
|
||||||
|
|
||||||
// attacklab: Restore tildes
|
|
||||||
text = text.replace(/~T/g, '~');
|
|
||||||
|
|
||||||
// Run output modifiers
|
|
||||||
showdown.helper.forEach(globals.outputModifiers, function (ext) {
|
|
||||||
text = showdown.subParser('runExtension')(ext, text);
|
|
||||||
});
|
|
||||||
text = parsers.outputModifiers(text, options, globals);
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an option of this Converter instance
|
|
||||||
* @param {string} key
|
|
||||||
* @param {*} value
|
|
||||||
*/
|
|
||||||
function setOption (key, value) {
|
|
||||||
options[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the option of this Converter instance
|
|
||||||
* @param {string} key
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
function getOption(key) {
|
|
||||||
return options[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the options of this Converter instance
|
|
||||||
* @returns {{}}
|
|
||||||
*/
|
|
||||||
function getOptions() {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
makeHtml: makeHtml,
|
|
||||||
setOption: setOption,
|
|
||||||
getOption: getOption,
|
|
||||||
getOptions: getOptions
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -424,6 +361,291 @@ showdown.helper.escapeCharacters = function escapeCharacters(text, charsToEscape
|
|||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POLYFILLS
|
||||||
|
*/
|
||||||
|
if (showdown.helper.isUndefined(console)) {
|
||||||
|
console = {
|
||||||
|
warn: function (msg) {
|
||||||
|
'use strict';
|
||||||
|
alert(msg);
|
||||||
|
},
|
||||||
|
log: function (msg) {
|
||||||
|
'use strict';
|
||||||
|
alert(msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Estevao on 31-05-2015.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Showdown Converter class
|
||||||
|
* @class
|
||||||
|
* @param {object} [converterOptions]
|
||||||
|
* @returns {
|
||||||
|
* {makeHtml: Function},
|
||||||
|
* {setOption: Function},
|
||||||
|
* {getOption: Function},
|
||||||
|
* {getOptions: Function}
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
showdown.Converter = function (converterOptions) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var
|
||||||
|
/**
|
||||||
|
* Options used by this converter
|
||||||
|
* @private
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
options = {
|
||||||
|
omitExtraWLInCodeBlocks: false,
|
||||||
|
prefixHeaderId: false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language extensions used by this converter
|
||||||
|
* @private
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
langExtensions = [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output modifiers extensions used by this converter
|
||||||
|
* @private
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
outputModifiers = [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parser Order
|
||||||
|
* @private
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
parserOrder = [
|
||||||
|
'githubCodeBlocks',
|
||||||
|
'hashHTMLBlocks',
|
||||||
|
'stripLinkDefinitions',
|
||||||
|
'blockGamut',
|
||||||
|
'unescapeSpecialChars'
|
||||||
|
];
|
||||||
|
|
||||||
|
_constructor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converter constructor
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function _constructor() {
|
||||||
|
converterOptions = converterOptions || {};
|
||||||
|
|
||||||
|
for (var gOpt in globalOptions) {
|
||||||
|
if (globalOptions.hasOwnProperty(gOpt)) {
|
||||||
|
options[gOpt] = globalOptions[gOpt];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge options
|
||||||
|
if (typeof converterOptions === 'object') {
|
||||||
|
for (var opt in converterOptions) {
|
||||||
|
if (converterOptions.hasOwnProperty(opt)) {
|
||||||
|
options[opt] = converterOptions[opt];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.extensions) {
|
||||||
|
showdown.helper.forEach(options.extensions, _parseExtension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse extension
|
||||||
|
* @param {*} ext
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function _parseExtension(ext) {
|
||||||
|
|
||||||
|
// If it's a string, the extension was previously loaded
|
||||||
|
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.' +
|
||||||
|
'Please inform the developer that the extension should be updated!');
|
||||||
|
ext = showdown.extensions[ext];
|
||||||
|
// END LEGACY SUPPORT CODE
|
||||||
|
|
||||||
|
} else if (!showdown.helper.isUndefined(extensions[ext])) {
|
||||||
|
ext = extensions[ext];
|
||||||
|
|
||||||
|
} 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') {
|
||||||
|
ext = ext();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showdown.validateExtension(ext)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ext.type) {
|
||||||
|
case 'lang':
|
||||||
|
langExtensions.push(ext);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'output':
|
||||||
|
outputModifiers.push(ext);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// should never reach here
|
||||||
|
throw Error('Extension loader error: Type unrecognized!!!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a markdown string into HTML
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
this.makeHtml = function (text) {
|
||||||
|
//check if text is not falsy
|
||||||
|
if (!text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
var globals = {
|
||||||
|
gHtmlBlocks: [],
|
||||||
|
gUrls: {},
|
||||||
|
gTitles: {},
|
||||||
|
gListLevel: 0,
|
||||||
|
hashLinkCounts: {},
|
||||||
|
langExtensions: langExtensions,
|
||||||
|
outputModifiers: outputModifiers,
|
||||||
|
converter: this
|
||||||
|
};
|
||||||
|
|
||||||
|
// attacklab: Replace ~ with ~T
|
||||||
|
// This lets us use tilde as an escape char to avoid md5 hashes
|
||||||
|
// The choice of character is arbitrary; anything that isn't
|
||||||
|
// magic in Markdown will work.
|
||||||
|
text = text.replace(/~/g, '~T');
|
||||||
|
|
||||||
|
// attacklab: Replace $ with ~D
|
||||||
|
// RegExp interprets $ as a special character
|
||||||
|
// when it's in a replacement string
|
||||||
|
text = text.replace(/\$/g, '~D');
|
||||||
|
|
||||||
|
// Standardize line endings
|
||||||
|
text = text.replace(/\r\n/g, '\n'); // DOS to Unix
|
||||||
|
text = text.replace(/\r/g, '\n'); // Mac to Unix
|
||||||
|
|
||||||
|
// Make sure text begins and ends with a couple of newlines:
|
||||||
|
text = '\n\n' + text + '\n\n';
|
||||||
|
|
||||||
|
// detab
|
||||||
|
text = showdown.subParser('detab')(text, options, globals);
|
||||||
|
|
||||||
|
// stripBlankLines
|
||||||
|
text = showdown.subParser('stripBlankLines')(text, options, globals);
|
||||||
|
|
||||||
|
//run languageExtensions
|
||||||
|
showdown.helper.forEach(langExtensions, function (ext) {
|
||||||
|
text = showdown.subParser('runExtension')(ext, text, options, globals);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run all registered parsers
|
||||||
|
for (var i = 0; i < parserOrder.length; ++i) {
|
||||||
|
var name = parserOrder[i];
|
||||||
|
text = parsers[name](text, options, globals);
|
||||||
|
}
|
||||||
|
|
||||||
|
// attacklab: Restore dollar signs
|
||||||
|
text = text.replace(/~D/g, '$$');
|
||||||
|
|
||||||
|
// attacklab: Restore tildes
|
||||||
|
text = text.replace(/~T/g, '~');
|
||||||
|
|
||||||
|
// Run output modifiers
|
||||||
|
showdown.helper.forEach(outputModifiers, function (ext) {
|
||||||
|
text = showdown.subParser('runExtension')(ext, text, options, globals);
|
||||||
|
});
|
||||||
|
text = parsers.outputModifiers(text, options, globals);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an option of this Converter instance
|
||||||
|
* @param {string} key
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
this.setOption = function (key, value) {
|
||||||
|
options[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the option of this Converter instance
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
this.getOption = function (key) {
|
||||||
|
return options[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the options of this Converter instance
|
||||||
|
* @returns {{}}
|
||||||
|
*/
|
||||||
|
this.getOptions = function () {
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add extension to THIS converter
|
||||||
|
* @param {{}} extension
|
||||||
|
*/
|
||||||
|
this.addExtension = function (extension) {
|
||||||
|
_parseExtension(extension);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an extension from THIS converter
|
||||||
|
* @param {{}} extension
|
||||||
|
*/
|
||||||
|
this.removeExtension = function (extension) {
|
||||||
|
for (var i = 0; i < langExtensions.length; ++i) {
|
||||||
|
if (langExtensions[i] === extension) {
|
||||||
|
langExtensions[i].splice(i, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var ii = 0; ii < outputModifiers.length; ++i) {
|
||||||
|
if (outputModifiers[ii] === extension) {
|
||||||
|
outputModifiers[ii].splice(i, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all extension of THIS converter
|
||||||
|
* @returns {{language: Array, output: Array}}
|
||||||
|
*/
|
||||||
|
this.getAllExtensions = function () {
|
||||||
|
return {
|
||||||
|
language: langExtensions,
|
||||||
|
output: outputModifiers
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turn Markdown link shortcuts into XHTML <a> tags.
|
* Turn Markdown link shortcuts into XHTML <a> tags.
|
||||||
*/
|
*/
|
||||||
@ -1317,18 +1539,6 @@ showdown.subParser('italicsAndBold', function (text) {
|
|||||||
return text;
|
return text;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Run language extensions
|
|
||||||
*/
|
|
||||||
showdown.subParser('languageExtensions', function (text, config, globals) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
showdown.helper.forEach(globals.langExtensions, function (ext) {
|
|
||||||
text = showdown.subParser('runExtension')(ext, text);
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form HTML ordered (numbered) and unordered (bulleted) lists.
|
* Form HTML ordered (numbered) and unordered (bulleted) lists.
|
||||||
*/
|
*/
|
||||||
@ -1545,17 +1755,24 @@ showdown.subParser('paragraphs', function (text, options, globals) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run language extensions
|
* Run extension
|
||||||
*/
|
*/
|
||||||
showdown.subParser('runExtension', function (ext, text) {
|
showdown.subParser('runExtension', function (ext, text, options, globals) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
if (ext.regex) {
|
if (ext.filter) {
|
||||||
var re = new RegExp(ext.regex, 'g');
|
text = ext.filter(text, globals.converter, options);
|
||||||
return text.replace(re, ext.replace);
|
|
||||||
} else if (ext.filter) {
|
} else if (ext.regex) {
|
||||||
return ext.filter(text);
|
// TODO remove this when old extension loading mechanism is deprecated
|
||||||
|
var re = ext.regex;
|
||||||
|
if (!re instanceof RegExp) {
|
||||||
|
re = new RegExp(re, 'g');
|
||||||
}
|
}
|
||||||
|
text = text.replace(re, ext.replace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
2
dist/showdown.js.map
vendored
2
dist/showdown.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/showdown.min.js
vendored
4
dist/showdown.min.js
vendored
File diff suppressed because one or more lines are too long
2
dist/showdown.min.js.map
vendored
2
dist/showdown.min.js.map
vendored
File diff suppressed because one or more lines are too long
268
src/converter.js
Normal file
268
src/converter.js
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* Created by Estevao on 31-05-2015.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Showdown Converter class
|
||||||
|
* @class
|
||||||
|
* @param {object} [converterOptions]
|
||||||
|
* @returns {
|
||||||
|
* {makeHtml: Function},
|
||||||
|
* {setOption: Function},
|
||||||
|
* {getOption: Function},
|
||||||
|
* {getOptions: Function}
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
showdown.Converter = function (converterOptions) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var
|
||||||
|
/**
|
||||||
|
* Options used by this converter
|
||||||
|
* @private
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
options = {
|
||||||
|
omitExtraWLInCodeBlocks: false,
|
||||||
|
prefixHeaderId: false
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language extensions used by this converter
|
||||||
|
* @private
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
langExtensions = [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output modifiers extensions used by this converter
|
||||||
|
* @private
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
outputModifiers = [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parser Order
|
||||||
|
* @private
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
parserOrder = [
|
||||||
|
'githubCodeBlocks',
|
||||||
|
'hashHTMLBlocks',
|
||||||
|
'stripLinkDefinitions',
|
||||||
|
'blockGamut',
|
||||||
|
'unescapeSpecialChars'
|
||||||
|
];
|
||||||
|
|
||||||
|
_constructor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converter constructor
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function _constructor() {
|
||||||
|
converterOptions = converterOptions || {};
|
||||||
|
|
||||||
|
for (var gOpt in globalOptions) {
|
||||||
|
if (globalOptions.hasOwnProperty(gOpt)) {
|
||||||
|
options[gOpt] = globalOptions[gOpt];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge options
|
||||||
|
if (typeof converterOptions === 'object') {
|
||||||
|
for (var opt in converterOptions) {
|
||||||
|
if (converterOptions.hasOwnProperty(opt)) {
|
||||||
|
options[opt] = converterOptions[opt];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.extensions) {
|
||||||
|
showdown.helper.forEach(options.extensions, _parseExtension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse extension
|
||||||
|
* @param {*} ext
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function _parseExtension(ext) {
|
||||||
|
|
||||||
|
// If it's a string, the extension was previously loaded
|
||||||
|
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.' +
|
||||||
|
'Please inform the developer that the extension should be updated!');
|
||||||
|
ext = showdown.extensions[ext];
|
||||||
|
// END LEGACY SUPPORT CODE
|
||||||
|
|
||||||
|
} else if (!showdown.helper.isUndefined(extensions[ext])) {
|
||||||
|
ext = extensions[ext];
|
||||||
|
|
||||||
|
} 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') {
|
||||||
|
ext = ext();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showdown.validateExtension(ext)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ext.type) {
|
||||||
|
case 'lang':
|
||||||
|
langExtensions.push(ext);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'output':
|
||||||
|
outputModifiers.push(ext);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// should never reach here
|
||||||
|
throw Error('Extension loader error: Type unrecognized!!!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a markdown string into HTML
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
this.makeHtml = function (text) {
|
||||||
|
//check if text is not falsy
|
||||||
|
if (!text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
var globals = {
|
||||||
|
gHtmlBlocks: [],
|
||||||
|
gUrls: {},
|
||||||
|
gTitles: {},
|
||||||
|
gListLevel: 0,
|
||||||
|
hashLinkCounts: {},
|
||||||
|
langExtensions: langExtensions,
|
||||||
|
outputModifiers: outputModifiers,
|
||||||
|
converter: this
|
||||||
|
};
|
||||||
|
|
||||||
|
// attacklab: Replace ~ with ~T
|
||||||
|
// This lets us use tilde as an escape char to avoid md5 hashes
|
||||||
|
// The choice of character is arbitrary; anything that isn't
|
||||||
|
// magic in Markdown will work.
|
||||||
|
text = text.replace(/~/g, '~T');
|
||||||
|
|
||||||
|
// attacklab: Replace $ with ~D
|
||||||
|
// RegExp interprets $ as a special character
|
||||||
|
// when it's in a replacement string
|
||||||
|
text = text.replace(/\$/g, '~D');
|
||||||
|
|
||||||
|
// Standardize line endings
|
||||||
|
text = text.replace(/\r\n/g, '\n'); // DOS to Unix
|
||||||
|
text = text.replace(/\r/g, '\n'); // Mac to Unix
|
||||||
|
|
||||||
|
// Make sure text begins and ends with a couple of newlines:
|
||||||
|
text = '\n\n' + text + '\n\n';
|
||||||
|
|
||||||
|
// detab
|
||||||
|
text = showdown.subParser('detab')(text, options, globals);
|
||||||
|
|
||||||
|
// stripBlankLines
|
||||||
|
text = showdown.subParser('stripBlankLines')(text, options, globals);
|
||||||
|
|
||||||
|
//run languageExtensions
|
||||||
|
showdown.helper.forEach(langExtensions, function (ext) {
|
||||||
|
text = showdown.subParser('runExtension')(ext, text, options, globals);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run all registered parsers
|
||||||
|
for (var i = 0; i < parserOrder.length; ++i) {
|
||||||
|
var name = parserOrder[i];
|
||||||
|
text = parsers[name](text, options, globals);
|
||||||
|
}
|
||||||
|
|
||||||
|
// attacklab: Restore dollar signs
|
||||||
|
text = text.replace(/~D/g, '$$');
|
||||||
|
|
||||||
|
// attacklab: Restore tildes
|
||||||
|
text = text.replace(/~T/g, '~');
|
||||||
|
|
||||||
|
// Run output modifiers
|
||||||
|
showdown.helper.forEach(outputModifiers, function (ext) {
|
||||||
|
text = showdown.subParser('runExtension')(ext, text, options, globals);
|
||||||
|
});
|
||||||
|
text = parsers.outputModifiers(text, options, globals);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an option of this Converter instance
|
||||||
|
* @param {string} key
|
||||||
|
* @param {*} value
|
||||||
|
*/
|
||||||
|
this.setOption = function (key, value) {
|
||||||
|
options[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the option of this Converter instance
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
this.getOption = function (key) {
|
||||||
|
return options[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the options of this Converter instance
|
||||||
|
* @returns {{}}
|
||||||
|
*/
|
||||||
|
this.getOptions = function () {
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add extension to THIS converter
|
||||||
|
* @param {{}} extension
|
||||||
|
*/
|
||||||
|
this.addExtension = function (extension) {
|
||||||
|
_parseExtension(extension);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an extension from THIS converter
|
||||||
|
* @param {{}} extension
|
||||||
|
*/
|
||||||
|
this.removeExtension = function (extension) {
|
||||||
|
for (var i = 0; i < langExtensions.length; ++i) {
|
||||||
|
if (langExtensions[i] === extension) {
|
||||||
|
langExtensions[i].splice(i, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var ii = 0; ii < outputModifiers.length; ++i) {
|
||||||
|
if (outputModifiers[ii] === extension) {
|
||||||
|
outputModifiers[ii].splice(i, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all extension of THIS converter
|
||||||
|
* @returns {{language: Array, output: Array}}
|
||||||
|
*/
|
||||||
|
this.getAllExtensions = function () {
|
||||||
|
return {
|
||||||
|
language: langExtensions,
|
||||||
|
output: outputModifiers
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
347
src/showdown.js
347
src/showdown.js
@ -6,10 +6,11 @@
|
|||||||
var showdown = {},
|
var showdown = {},
|
||||||
parsers = {},
|
parsers = {},
|
||||||
extensions = {},
|
extensions = {},
|
||||||
globalOptions = {
|
defaultOptions = {
|
||||||
omitExtraWLInCodeBlocks: false,
|
omitExtraWLInCodeBlocks: false,
|
||||||
prefixHeaderId: false
|
prefixHeaderId: false
|
||||||
};
|
},
|
||||||
|
globalOptions = JSON.parse(JSON.stringify(defaultOptions)); //clone default options out of laziness =P
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* helper namespace
|
* helper namespace
|
||||||
@ -17,7 +18,10 @@ var showdown = {},
|
|||||||
*/
|
*/
|
||||||
showdown.helper = {};
|
showdown.helper = {};
|
||||||
|
|
||||||
// Public properties
|
/**
|
||||||
|
* TODO LEGACY SUPPORT CODE
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
showdown.extensions = {};
|
showdown.extensions = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,6 +58,11 @@ showdown.getOptions = function () {
|
|||||||
return globalOptions;
|
return globalOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
showdown.resetOptions = function () {
|
||||||
|
'use strict';
|
||||||
|
globalOptions = JSON.parse(JSON.stringify(defaultOptions));
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or set a subParser
|
* Get or set a subParser
|
||||||
*
|
*
|
||||||
@ -79,6 +88,13 @@ showdown.subParser = function (name, func) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or registers an extension
|
||||||
|
* @static
|
||||||
|
* @param {string} name
|
||||||
|
* @param {object|function=} ext
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
showdown.extension = function (name, ext) {
|
showdown.extension = function (name, ext) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@ -88,228 +104,149 @@ showdown.extension = function (name, ext) {
|
|||||||
|
|
||||||
name = showdown.helper.stdExtName(name);
|
name = showdown.helper.stdExtName(name);
|
||||||
|
|
||||||
|
// Getter
|
||||||
if (showdown.helper.isUndefined(ext)) {
|
if (showdown.helper.isUndefined(ext)) {
|
||||||
return getExtension();
|
|
||||||
} else {
|
|
||||||
return setExtension();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function getExtension(name) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (!extensions.hasOwnProperty(name)) {
|
if (!extensions.hasOwnProperty(name)) {
|
||||||
throw Error('Extension named ' + name + ' is not registered!');
|
throw Error('Extension named ' + name + ' is not registered!');
|
||||||
}
|
}
|
||||||
return extensions[name];
|
return extensions[name];
|
||||||
}
|
|
||||||
|
|
||||||
function setExtension(name, ext) {
|
// Setter
|
||||||
|
} else {
|
||||||
|
if (typeof ext === 'function') {
|
||||||
|
ext = ext();
|
||||||
|
}
|
||||||
|
|
||||||
|
var validExtension = validate(ext, name);
|
||||||
|
|
||||||
|
if (validExtension.valid) {
|
||||||
|
extensions[name] = ext;
|
||||||
|
} else {
|
||||||
|
throw Error(validExtension.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all extensions registered
|
||||||
|
* @returns {{}}
|
||||||
|
*/
|
||||||
|
showdown.getAllExtensions = function () {
|
||||||
|
'use strict';
|
||||||
|
return extensions;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an extension
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
showdown.removeExtension = function (name) {
|
||||||
|
'use strict';
|
||||||
|
delete extensions[name];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all extensions
|
||||||
|
*/
|
||||||
|
showdown.resetExtensions = function () {
|
||||||
|
'use strict';
|
||||||
|
extensions = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate extension
|
||||||
|
* @param {object} ext
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {{valid: boolean, error: string}}
|
||||||
|
*/
|
||||||
|
function validate(ext, name) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var baseMsg = (name) ? 'Error in ' + name + ' extension: ' : 'Error in unnamed extension',
|
||||||
|
ret = {
|
||||||
|
valid: true,
|
||||||
|
error: baseMsg
|
||||||
|
};
|
||||||
|
|
||||||
if (typeof ext !== 'object') {
|
if (typeof ext !== 'object') {
|
||||||
throw Error('A Showdown Extension must be an object, ' + typeof ext + ' given');
|
ret.valid = false;
|
||||||
|
ret.error = baseMsg + 'it must be an object, but ' + typeof ext + ' given';
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showdown.helper.isString(ext.type)) {
|
if (!showdown.helper.isString(ext.type)) {
|
||||||
throw Error('When registering a showdown extension, "type" must be a string, ' + typeof ext.type + ' given');
|
ret.valid = false;
|
||||||
|
ret.error = baseMsg + 'property "type" must be a string, but ' + typeof ext.type + ' given';
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.type = ext.type.toLowerCase();
|
var type = ext.type = ext.type.toLowerCase();
|
||||||
|
|
||||||
extensions[name] = ext;
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Showdown Converter class
|
* Validate extension
|
||||||
*
|
* @param {object} ext
|
||||||
* @param {object} [converterOptions]
|
* @returns {boolean}
|
||||||
* @returns {{makeHtml: Function}}
|
|
||||||
*/
|
*/
|
||||||
showdown.Converter = function (converterOptions) {
|
showdown.validateExtension = function (ext) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
converterOptions = converterOptions || {};
|
var validateExtension = validate(ext, null);
|
||||||
|
if (!validateExtension.valid) {
|
||||||
var options = {},
|
console.warn(validateExtension.error);
|
||||||
langExtensions = [],
|
return false;
|
||||||
outputModifiers = [],
|
|
||||||
parserOrder = [
|
|
||||||
'githubCodeBlocks',
|
|
||||||
'hashHTMLBlocks',
|
|
||||||
'stripLinkDefinitions',
|
|
||||||
'blockGamut',
|
|
||||||
'unescapeSpecialChars'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (var gOpt in globalOptions) {
|
|
||||||
if (globalOptions.hasOwnProperty(gOpt)) {
|
|
||||||
options[gOpt] = globalOptions[gOpt];
|
|
||||||
}
|
}
|
||||||
}
|
return true;
|
||||||
|
|
||||||
// Merge options
|
|
||||||
if (typeof converterOptions === 'object') {
|
|
||||||
for (var opt in converterOptions) {
|
|
||||||
if (converterOptions.hasOwnProperty(opt)) {
|
|
||||||
options[opt] = converterOptions[opt];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a dirty workaround to maintain backwards extension compatibility
|
|
||||||
// We define a self var (which is a copy of this) and inject the makeHtml function
|
|
||||||
// directly to it. This ensures a full converter object is available when iterating over extensions
|
|
||||||
// We should rewrite the extension loading mechanism and use some kind of interface or decorator pattern
|
|
||||||
// and inject the object reference there instead.
|
|
||||||
var self = this;
|
|
||||||
self.makeHtml = makeHtml;
|
|
||||||
|
|
||||||
// Parse options
|
|
||||||
if (options.extensions) {
|
|
||||||
|
|
||||||
// Iterate over each plugin
|
|
||||||
showdown.helper.forEach(options.extensions, function (plugin) {
|
|
||||||
var pluginName = plugin;
|
|
||||||
|
|
||||||
// Assume it's a bundled plugin if a string is given
|
|
||||||
if (typeof plugin === 'string') {
|
|
||||||
var tPluginName = showdown.helper.stdExtName(plugin);
|
|
||||||
|
|
||||||
if (!showdown.helper.isUndefined(showdown.extensions[tPluginName]) && showdown.extensions[tPluginName]) {
|
|
||||||
//Trigger some kind of deprecated alert
|
|
||||||
plugin = showdown.extensions[tPluginName];
|
|
||||||
|
|
||||||
} else if (!showdown.helper.isUndefined(extensions[tPluginName])) {
|
|
||||||
plugin = extensions[tPluginName];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof plugin === 'function') {
|
|
||||||
// Iterate over each extension within that plugin
|
|
||||||
showdown.helper.forEach(plugin(self), function (ext) {
|
|
||||||
// Sort extensions by type
|
|
||||||
if (ext.type) {
|
|
||||||
if (ext.type === 'language' || ext.type === 'lang') {
|
|
||||||
langExtensions.push(ext);
|
|
||||||
} else if (ext.type === 'output' || ext.type === 'html') {
|
|
||||||
outputModifiers.push(ext);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Assume language extension
|
|
||||||
outputModifiers.push(ext);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var errMsg = 'An extension could not be loaded. It was either not found or is not a valid extension.';
|
|
||||||
if (typeof pluginName === 'string') {
|
|
||||||
errMsg = 'Extension "' + pluginName + '" could not be loaded. It was either not found or is not a valid extension.';
|
|
||||||
}
|
|
||||||
throw errMsg;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a markdown string into HTML
|
|
||||||
* @param {string} text
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
function makeHtml(text) {
|
|
||||||
|
|
||||||
//check if text is not falsy
|
|
||||||
if (!text) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
var globals = {
|
|
||||||
gHtmlBlocks: [],
|
|
||||||
gUrls: {},
|
|
||||||
gTitles: {},
|
|
||||||
gListLevel: 0,
|
|
||||||
hashLinkCounts: {},
|
|
||||||
langExtensions: langExtensions,
|
|
||||||
outputModifiers: outputModifiers
|
|
||||||
};
|
|
||||||
|
|
||||||
// attacklab: Replace ~ with ~T
|
|
||||||
// This lets us use tilde as an escape char to avoid md5 hashes
|
|
||||||
// The choice of character is arbitrary; anything that isn't
|
|
||||||
// magic in Markdown will work.
|
|
||||||
text = text.replace(/~/g, '~T');
|
|
||||||
|
|
||||||
// attacklab: Replace $ with ~D
|
|
||||||
// RegExp interprets $ as a special character
|
|
||||||
// when it's in a replacement string
|
|
||||||
text = text.replace(/\$/g, '~D');
|
|
||||||
|
|
||||||
// Standardize line endings
|
|
||||||
text = text.replace(/\r\n/g, '\n'); // DOS to Unix
|
|
||||||
text = text.replace(/\r/g, '\n'); // Mac to Unix
|
|
||||||
|
|
||||||
// Make sure text begins and ends with a couple of newlines:
|
|
||||||
text = '\n\n' + text + '\n\n';
|
|
||||||
|
|
||||||
// detab
|
|
||||||
text = parsers.detab(text, options, globals);
|
|
||||||
|
|
||||||
// stripBlankLines
|
|
||||||
text = parsers.stripBlankLines(text, options, globals);
|
|
||||||
|
|
||||||
//run languageExtensions
|
|
||||||
text = parsers.languageExtensions(text, options, globals);
|
|
||||||
|
|
||||||
// Run all registered parsers
|
|
||||||
for (var i = 0; i < parserOrder.length; ++i) {
|
|
||||||
var name = parserOrder[i];
|
|
||||||
text = parsers[name](text, options, globals);
|
|
||||||
}
|
|
||||||
|
|
||||||
// attacklab: Restore dollar signs
|
|
||||||
text = text.replace(/~D/g, '$$');
|
|
||||||
|
|
||||||
// attacklab: Restore tildes
|
|
||||||
text = text.replace(/~T/g, '~');
|
|
||||||
|
|
||||||
// Run output modifiers
|
|
||||||
showdown.helper.forEach(globals.outputModifiers, function (ext) {
|
|
||||||
text = showdown.subParser('runExtension')(ext, text);
|
|
||||||
});
|
|
||||||
text = parsers.outputModifiers(text, options, globals);
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an option of this Converter instance
|
|
||||||
* @param {string} key
|
|
||||||
* @param {*} value
|
|
||||||
*/
|
|
||||||
function setOption (key, value) {
|
|
||||||
options[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the option of this Converter instance
|
|
||||||
* @param {string} key
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
function getOption(key) {
|
|
||||||
return options[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the options of this Converter instance
|
|
||||||
* @returns {{}}
|
|
||||||
*/
|
|
||||||
function getOptions() {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
makeHtml: makeHtml,
|
|
||||||
setOption: setOption,
|
|
||||||
getOption: getOption,
|
|
||||||
getOptions: getOptions
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* Run language extensions
|
|
||||||
*/
|
|
||||||
showdown.subParser('languageExtensions', function (text, config, globals) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
showdown.helper.forEach(globals.langExtensions, function (ext) {
|
|
||||||
text = showdown.subParser('runExtension')(ext, text);
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
});
|
|
@ -1,13 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* Run language extensions
|
* Run extension
|
||||||
*/
|
*/
|
||||||
showdown.subParser('runExtension', function (ext, text) {
|
showdown.subParser('runExtension', function (ext, text, options, globals) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
if (ext.regex) {
|
if (ext.filter) {
|
||||||
var re = new RegExp(ext.regex, 'g');
|
text = ext.filter(text, globals.converter, options);
|
||||||
return text.replace(re, ext.replace);
|
|
||||||
} else if (ext.filter) {
|
} else if (ext.regex) {
|
||||||
return ext.filter(text);
|
// TODO remove this when old extension loading mechanism is deprecated
|
||||||
|
var re = ext.regex;
|
||||||
|
if (!re instanceof RegExp) {
|
||||||
|
re = new RegExp(re, 'g');
|
||||||
}
|
}
|
||||||
|
text = text.replace(re, ext.replace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
});
|
});
|
||||||
|
61
test/node/showdown.Converter.js
Normal file
61
test/node/showdown.Converter.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Created by Estevao on 31-05-2015.
|
||||||
|
*/
|
||||||
|
require('source-map-support').install();
|
||||||
|
require('chai').should();
|
||||||
|
require('sinon');
|
||||||
|
var showdown = require('../../dist/showdown.js');
|
||||||
|
|
||||||
|
describe('showdown.Converter', function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('option methods', function () {
|
||||||
|
var converter = new showdown.Converter();
|
||||||
|
|
||||||
|
it('setOption() should set option foo=baz', function () {
|
||||||
|
converter.setOption('foo', 'baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getOption() should get option foo to equal baz', function () {
|
||||||
|
converter.getOption('foo').should.equal('baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getOptions() should contain foo=baz', function () {
|
||||||
|
var options = converter.getOptions();
|
||||||
|
options.should.have.ownProperty('foo');
|
||||||
|
options.foo.should.equal('baz');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extension methods', function () {
|
||||||
|
var extObjMock = {
|
||||||
|
type: 'lang',
|
||||||
|
filter: function () {}
|
||||||
|
},
|
||||||
|
extObjFunc = function () {
|
||||||
|
return extObjMock;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('addExtension() should add an extension Object', function () {
|
||||||
|
var converter = new showdown.Converter();
|
||||||
|
converter.addExtension(extObjMock);
|
||||||
|
converter.getAllExtensions().language.should.contain(extObjMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('addExtension() should unwrap an extension wrapped in a function', function () {
|
||||||
|
var converter = new showdown.Converter();
|
||||||
|
|
||||||
|
converter.addExtension(extObjFunc);
|
||||||
|
converter.getAllExtensions().language.should.contain(extObjMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('addExtension() should add a previous registered extension in showdown', function () {
|
||||||
|
showdown.extension('foo', extObjMock);
|
||||||
|
var converter = new showdown.Converter();
|
||||||
|
|
||||||
|
converter.addExtension('foo');
|
||||||
|
converter.getAllExtensions().language.should.contain(extObjMock);
|
||||||
|
showdown.resetExtensions();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
83
test/node/showdown.js
Normal file
83
test/node/showdown.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
require('source-map-support').install();
|
||||||
|
require('chai').should();
|
||||||
|
var expect = require('chai').expect,
|
||||||
|
showdown = require('../../dist/showdown.js');
|
||||||
|
|
||||||
|
describe('showdown.options', function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('setOption() and getOption()', function () {
|
||||||
|
it('should set option foo=bar', function () {
|
||||||
|
showdown.setOption('foo', 'bar');
|
||||||
|
showdown.getOption('foo').should.equal('bar');
|
||||||
|
showdown.resetOptions();
|
||||||
|
expect(showdown.getOption('foo')).to.be.undefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showdown.extension', function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var extObjMock = {
|
||||||
|
type: 'lang',
|
||||||
|
filter: function () {}
|
||||||
|
},
|
||||||
|
extObjFunc = function () {
|
||||||
|
return extObjMock;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('should register', function () {
|
||||||
|
it('should register an extension object', function () {
|
||||||
|
showdown.extension('foo', extObjMock);
|
||||||
|
showdown.extension('foo').should.equal(extObjMock);
|
||||||
|
showdown.resetExtensions();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register an extension function', function () {
|
||||||
|
showdown.extension('foo', extObjFunc);
|
||||||
|
showdown.extension('foo').should.equal(extObjMock);
|
||||||
|
showdown.resetExtensions();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should refuse to register', function () {
|
||||||
|
it('a generic object', function () {
|
||||||
|
var fn = function () {
|
||||||
|
showdown.extension('foo', {});
|
||||||
|
};
|
||||||
|
expect(fn).to.throw();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('an extension with invalid type', function () {
|
||||||
|
var fn = function () {
|
||||||
|
showdown.extension('foo', {
|
||||||
|
type: 'foo'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
expect(fn).to.throw(/type .+? is not recognized\. Valid values: "lang" or "output"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('an extension without regex or filter', function () {
|
||||||
|
var fn = function () {
|
||||||
|
showdown.extension('foo', {
|
||||||
|
type: 'lang'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
expect(fn).to.throw(/extensions must define either a "regex" property or a "filter" method/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showdown.getAllExtensions()', function () {
|
||||||
|
'use strict';
|
||||||
|
var extObjMock = {
|
||||||
|
type: 'lang',
|
||||||
|
filter: function () {}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return all extensions', function () {
|
||||||
|
showdown.extension('bar', extObjMock);
|
||||||
|
showdown.getAllExtensions().should.eql({bar: extObjMock});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user