showdown/src/converter.js

396 lines
9.9 KiB
JavaScript
Raw Normal View History

/**
* Created by Estevao on 31-05-2015.
*/
/**
* Showdown Converter class
* @class
* @param {object} [converterOptions]
* @returns {Converter}
*/
showdown.Converter = function (converterOptions) {
'use strict';
var
/**
* Options used by this converter
* @private
* @type {{}}
*/
options = {},
/**
* Language extensions used by this converter
* @private
* @type {Array}
*/
langExtensions = [],
/**
* Output modifiers extensions used by this converter
* @private
* @type {Array}
*/
outputModifiers = [],
/**
* Event listeners
* @private
* @type {{}}
*/
listeners = {};
_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];
}
}
} else {
throw Error('Converter expects the passed parameter to be an object, but ' + typeof converterOptions +
' was passed instead.');
}
if (options.extensions) {
showdown.helper.forEach(options.extensions, _parseExtension);
}
}
/**
* Parse extension
* @param {*} ext
2015-07-13 12:09:03 +08:00
* @param {string} [name='']
* @private
*/
2015-07-13 12:09:03 +08:00
function _parseExtension(ext, name) {
2015-07-13 12:09:03 +08:00
name = name || null;
// If it's a string, the extension was previously loaded
if (showdown.helper.isString(ext)) {
ext = showdown.helper.stdExtName(ext);
2015-07-13 12:09:03 +08:00
name = ext;
// 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!');
legacyExtensionLoading(showdown.extensions[ext], ext);
return;
// 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.');
}
}
if (typeof ext === 'function') {
ext = ext();
}
if (!showdown.helper.isArray(ext)) {
ext = [ext];
}
2015-07-13 12:09:03 +08:00
var validExt = validate(ext, name);
if (!validExt.valid) {
throw Error(validExt.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;
}
if (ext[i].hasOwnProperty(listeners)) {
for (var ln in ext[i].listeners) {
if (ext[i].listeners.hasOwnProperty(ln)) {
listen(ln, ext[i].listeners[ln]);
}
}
}
}
}
/**
* 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!!!');
}
}
}
/**
* Listen to an event
* @param {string} name
* @param {function} callback
*/
function listen(name, callback) {
if (!showdown.helper.isString(name)) {
throw Error('Invalid argument in converter.listen() method: name must be a string, but ' + typeof name + ' given');
}
if (typeof callback !== 'function') {
throw Error('Invalid argument in converter.listen() method: callback must be a function, but ' + typeof callback + ' given');
}
if (!listeners.hasOwnProperty(name)) {
listeners[name] = [];
}
listeners[name].push(callback);
}
/**
* Dispatch an event
* @private
* @param {string} evtName Event name
* @param {string} text Text
* @param {{}} options Converter Options
* @returns {string}
*/
this._dispatch = function dispatch (evtName, text, options) {
if (listeners.hasOwnProperty(evtName)) {
for (var ei = 0; ei < listeners[evtName].length; ++ei) {
var nText = listeners[evtName][ei](evtName, text, this, options);
if (nText && typeof nText !== 'undefined') {
text = nText;
}
}
}
return text;
};
/**
* Listen to an event
* @param {string} name
* @param {function} callback
* @returns {showdown.Converter}
*/
this.listen = function (name, callback) {
listen(name, callback);
return this;
};
/**
* 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: [],
gHtmlMdBlocks: [],
gHtmlSpans: [],
gUrls: {},
gTitles: {},
gDimensions: {},
gListLevel: 0,
hashLinkCounts: {},
langExtensions: langExtensions,
outputModifiers: outputModifiers,
converter: this,
ghCodeBlocks: []
};
// 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 the sub parsers
text = showdown.subParser('githubCodeBlocks')(text, options, globals);
text = showdown.subParser('hashHTMLBlocks')(text, options, globals);
text = showdown.subParser('hashHTMLSpans')(text, options, globals);
text = showdown.subParser('stripLinkDefinitions')(text, options, globals);
text = showdown.subParser('blockGamut')(text, options, globals);
text = showdown.subParser('unhashHTMLSpans')(text, options, globals);
text = showdown.subParser('unescapeSpecialChars')(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);
});
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
2015-07-13 12:09:03 +08:00
* @param {string} [name=null]
*/
2015-07-13 12:09:03 +08:00
this.addExtension = function (extension, name) {
name = name || null;
_parseExtension(extension, name);
};
/**
* Use a global registered extension with THIS converter
* @param {string} extensionName Name of the previously registered extension
*/
this.useExtension = function (extensionName) {
_parseExtension(extensionName);
};
/**
* Set the flavor THIS converter should use
* @param {string} name
*/
this.setFlavor = function (name) {
if (flavor.hasOwnProperty(name)) {
var preset = flavor[name];
for (var option in preset) {
if (preset.hasOwnProperty(option)) {
options[option] = preset[option];
}
}
}
};
/**
* 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) {
if (!showdown.helper.isArray(extension)) {
extension = [extension];
}
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);
}
}
}
};
/**
* Get all extension of THIS converter
* @returns {{language: Array, output: Array}}
*/
this.getAllExtensions = function () {
return {
language: langExtensions,
output: outputModifiers
};
};
};