/** * 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 = {}, /** * The flavor set in this converter */ setConvFlavor = setFlavor; _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 * @param {string} [name=''] * @private */ function _parseExtension (ext, name) { name = name || null; // If it's a string, the extension was previously loaded if (showdown.helper.isString(ext)) { ext = showdown.helper.stdExtName(ext); 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]; } 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); } function rTrimInputText (text) { var rsp = text.match(/^\s*/)[0].length, rgx = new RegExp('^\\s{0,' + rsp + '}', 'gm'); return text.replace(rgx, ''); } /** * Dispatch an event * @private * @param {string} evtName Event name * @param {string} text Text * @param {{}} options Converter Options * @param {{}} globals * @returns {string} */ this._dispatch = function dispatch (evtName, text, options, globals) { if (listeners.hasOwnProperty(evtName)) { for (var ei = 0; ei < listeners[evtName].length; ++ei) { var nText = listeners[evtName][ei](evtName, text, this, options, globals); 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: [] }; // This lets us use ¨ trema 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'); // 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 // Stardardize line spaces (nbsp causes trouble in older browsers and some regex flavors) text = text.replace(/\u00A0/g, ' '); if (options.smartIndentationFix) { text = rTrimInputText(text); } // 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); /** * Strip any lines consisting only of spaces and tabs. * This makes subsequent regexs easier to write, because we can * match consecutive blank lines with /\n+/ instead of something * contorted like /[ \t]*\n+/ */ text = text.replace(/^[ \t]+$/mg, ''); //run languageExtensions showdown.helper.forEach(langExtensions, function (ext) { text = showdown.subParser('runExtension')(ext, text, options, globals); }); // run the sub parsers text = showdown.subParser('hashPreCodeTags')(text, options, globals); text = showdown.subParser('githubCodeBlocks')(text, options, globals); text = showdown.subParser('hashHTMLBlocks')(text, options, globals); text = showdown.subParser('hashCodeTags')(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 tremas text = text.replace(/¨T/g, '¨'); // render a complete html document instead of a partial if the option is enabled text = showdown.subParser('completeHTMLOutput')(text, options, globals); // 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 * @param {string} [name=null] */ 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)) { throw Error(name + ' flavor was not found'); } var preset = flavor[name]; setConvFlavor = name; for (var option in preset) { if (preset.hasOwnProperty(option)) { options[option] = preset[option]; } } }; /** * Get the currently set flavor of this converter * @returns {string} */ this.getFlavor = function () { return setConvFlavor; }; /** * 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 }; }; };